Skip to content

Commit

Permalink
Multiple collection filters (#963)
Browse files Browse the repository at this point in the history
* feat(backend): allow multiple collections in filters

* refactor(frontend): extract zod utility

* feat(frontend): allow selecting multiple collections

* chore(backend): change name of variables

* refactor(frontend): extract into common component

* feat(backend): support for multiple collection ids

* ci: Run CI
  • Loading branch information
IgnisDa committed Aug 14, 2024
1 parent 2caea29 commit 171f7d1
Show file tree
Hide file tree
Showing 9 changed files with 94 additions and 95 deletions.
8 changes: 4 additions & 4 deletions apps/backend/src/miscellaneous.rs
Original file line number Diff line number Diff line change
Expand Up @@ -529,7 +529,7 @@ enum MediaGeneralFilter {
#[derive(Debug, Serialize, Deserialize, InputObject, Clone)]
struct MediaFilter {
general: Option<MediaGeneralFilter>,
collection: Option<String>,
collections: Option<Vec<String>>,
}

#[derive(Debug, Serialize, Deserialize, InputObject, Clone)]
Expand Down Expand Up @@ -2297,7 +2297,7 @@ impl MiscellaneousService {
)
})
.apply_if(
input.filter.clone().and_then(|f| f.collection),
input.filter.clone().and_then(|f| f.collections),
|query, v| {
apply_collection_filter(
query,
Expand Down Expand Up @@ -6344,7 +6344,7 @@ impl MiscellaneousService {
)
})
.apply_if(
input.filter.clone().and_then(|f| f.collection),
input.filter.clone().and_then(|f| f.collections),
|query, v| {
apply_collection_filter(
query,
Expand Down Expand Up @@ -6411,7 +6411,7 @@ impl MiscellaneousService {
)
})
.apply_if(
input.filter.clone().and_then(|f| f.collection),
input.filter.clone().and_then(|f| f.collections),
|query, v| {
apply_collection_filter(
query,
Expand Down
20 changes: 18 additions & 2 deletions apps/backend/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use axum::{
Extension, RequestPartsExt,
};
use chrono::{NaiveDate, Utc};
use database::AliasedCollectionToEntity;
use itertools::Itertools;
use openidconnect::{
core::{CoreClient, CoreProviderMetadata},
Expand All @@ -24,6 +25,7 @@ use sea_orm::{
ActiveModelTrait, ActiveValue, ColumnTrait, ConnectionTrait, DatabaseConnection, EntityTrait,
QueryFilter, QuerySelect, QueryTrait, Select,
};
use sea_query::{Expr, PgFunc};

use crate::{
background::{ApplicationJob, CoreApplicationJob},
Expand Down Expand Up @@ -371,7 +373,7 @@ pub async fn add_entity_to_collection(

pub fn apply_collection_filter<E, C, D>(
query: Select<E>,
collection_id: Option<String>,
collection_id: Option<Vec<String>>,
invert_collection: Option<bool>,
entity_column: C,
id_column: D,
Expand All @@ -382,11 +384,25 @@ where
D: ColumnTrait,
{
query.apply_if(collection_id, |query, v| {
let unique_collections = v.into_iter().unique().collect_vec();
let count = unique_collections.len() as i32;
let subquery = CollectionToEntity::find()
.select_only()
.column(id_column)
.filter(collection_to_entity::Column::CollectionId.eq(v))
.filter(
Expr::col((
AliasedCollectionToEntity::Table,
collection_to_entity::Column::CollectionId,
))
.eq(PgFunc::any(unique_collections)),
)
.filter(id_column.is_not_null())
.group_by(id_column)
.having(
collection_to_entity::Column::CollectionId
.count()
.eq(Expr::val(count)),
)
.into_query();
if invert_collection.unwrap_or_default() {
query.filter(entity_column.not_in_subquery(subquery))
Expand Down
29 changes: 29 additions & 0 deletions apps/frontend/app/components/common.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
Group,
Image,
Modal,
MultiSelect,
SimpleGrid,
Stack,
Text,
Expand Down Expand Up @@ -38,6 +39,7 @@ import {
useAppSearchParam,
useCoreDetails,
useFallbackImageUrl,
useUserCollections,
} from "~/lib/hooks";
import classes from "~/styles/common.module.css";

Expand Down Expand Up @@ -215,3 +217,30 @@ export const FiltersModal = (props: {
</Modal>
);
};

export const CollectionsFilter = (props: {
cookieName: string;
collections?: string[];
}) => {
const collections = useUserCollections();
const [_, { setP }] = useAppSearchParam(props.cookieName);

return (
<MultiSelect
placeholder="Select a collection"
defaultValue={props.collections}
data={[
{
group: "My collections",
items: collections.map((c) => ({
value: c.id.toString(),
label: c.name,
})),
},
]}
onChange={(v) => setP("collections", v.join(","))}
clearable
searchable
/>
);
};
23 changes: 15 additions & 8 deletions apps/frontend/app/lib/generals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
SetLot,
UserMetadataDetailsDocument,
} from "@ryot/generated/graphql/backend/graphql";
import { isString } from "@ryot/ts-utils";
import {
IconBook,
IconBook2,
Expand All @@ -31,13 +32,27 @@ import relativeTime from "dayjs/plugin/relativeTime";
import { GraphQLClient } from "graphql-request";
import Cookies from "js-cookie";
import { match } from "ts-pattern";
import { z } from "zod";

declare global {
interface Window {
umami?: {
track: typeof Umami.trackEvent;
};
}
}

dayjs.extend(relativeTime);
dayjs.extend(duration);
dayjs.extend(localizedFormat);

export { dayjs as dayjsLib };

export const commaDelimitedString = z
.string()
.optional()
.transform((v) => (isString(v) ? v.split(",") : undefined));

export const CurrentWorkoutKey = "CurrentWorkout";
export const LOGO_IMAGE_URL =
"https://github.com/IgnisDa/ryot/main/libs/assets/icon-512x512.png";
Expand All @@ -49,14 +64,6 @@ export const queryClient = new QueryClient({
defaultOptions: { queries: { staleTime: Number.POSITIVE_INFINITY } },
});

declare global {
interface Window {
umami?: {
track: typeof Umami.trackEvent;
};
}
}

export const getSetColor = (l: SetLot) =>
match(l)
.with(SetLot.WarmUp, () => "yellow")
Expand Down
30 changes: 8 additions & 22 deletions apps/frontend/app/routes/_dashboard.media.$action.$lot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,16 +55,16 @@ import { z } from "zod";
import { zx } from "zodix";
import {
ApplicationGrid,
CollectionsFilter,
DebouncedSearchInput,
FiltersModal,
} from "~/components/common";
import { BaseMediaDisplayItem, MetadataDisplayItem } from "~/components/media";
import { Verb, getLot, getVerb } from "~/lib/generals";
import { Verb, commaDelimitedString, getLot, getVerb } from "~/lib/generals";
import {
useAppSearchParam,
useApplicationEvents,
useCoreDetails,
useUserCollections,
useUserDetails,
useUserPreferences,
} from "~/lib/hooks";
Expand Down Expand Up @@ -135,7 +135,7 @@ export const loader = unstable_defineLoader(async ({ request, params }) => {
generalFilter: z
.nativeEnum(MediaGeneralFilter)
.default(defaultFilters.mineGeneralFilter),
collection: z.string().optional(),
collections: commaDelimitedString,
invertCollection: zx.BoolAsString.optional(),
});
const { metadataList } = await serverGqlService.authenticatedRequest(
Expand All @@ -148,7 +148,7 @@ export const loader = unstable_defineLoader(async ({ request, params }) => {
sort: { order: urlParse.sortOrder, by: urlParse.sortBy },
filter: {
general: urlParse.generalFilter,
collection: urlParse.collection,
collections: urlParse.collections,
},
invertCollection: urlParse.invertCollection,
},
Expand Down Expand Up @@ -221,7 +221,7 @@ export default function Page() {
defaultFilters.mineGeneralFilter ||
loaderData.mediaList?.url.sortOrder !== defaultFilters.mineSortOrder ||
loaderData.mediaList?.url.sortBy !== defaultFilters.mineSortBy ||
loaderData.mediaList?.url.collection !== defaultFilters.mineCollection;
loaderData.mediaList?.url.collections !== defaultFilters.mineCollection;

return (
<Container>
Expand Down Expand Up @@ -524,7 +524,6 @@ const MediaSearchItem = (props: {

const FiltersModalForm = () => {
const loaderData = useLoaderData<typeof loader>();
const collections = useUserCollections();
const [_, { setP }] = useAppSearchParam(loaderData.cookieName);

if (!loaderData.mediaList) return null;
Expand Down Expand Up @@ -578,22 +577,9 @@ const FiltersModalForm = () => {
</ActionIcon>
</Flex>
<Flex gap="xs" align="center">
<Select
flex={1}
placeholder="Select a collection"
defaultValue={loaderData.mediaList.url.collection?.toString()}
data={[
{
group: "My collections",
items: collections.map((c) => ({
value: c.id.toString(),
label: c.name,
})),
},
]}
onChange={(v) => setP("collection", v)}
clearable
searchable
<CollectionsFilter
cookieName={loaderData.cookieName}
collections={loaderData.mediaList.url.collections}
/>
<Checkbox
label="Invert"
Expand Down
34 changes: 9 additions & 25 deletions apps/frontend/app/routes/_dashboard.media.groups.$action.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,18 +46,16 @@ import { z } from "zod";
import { zx } from "zodix";
import {
ApplicationGrid,
CollectionsFilter,
DebouncedSearchInput,
FiltersModal,
} from "~/components/common";
import {
BaseMediaDisplayItem,
MetadataGroupDisplayItem,
} from "~/components/media";
import {
useAppSearchParam,
useCoreDetails,
useUserCollections,
} from "~/lib/hooks";
import { commaDelimitedString } from "~/lib/generals";
import { useAppSearchParam, useCoreDetails } from "~/lib/hooks";
import {
getEnhancedCookieName,
serverGqlService,
Expand Down Expand Up @@ -94,7 +92,7 @@ export const loader = unstable_defineLoader(async ({ request, params }) => {
const urlParse = zx.parseQuery(request, {
sortBy: z.nativeEnum(PersonSortBy).default(defaultFilters.sortBy),
orderBy: z.nativeEnum(GraphqlSortOrder).default(defaultFilters.orderBy),
collection: z.string().optional(),
collections: commaDelimitedString,
invertCollection: zx.BoolAsString.optional(),
});
const { metadataGroupsList } =
Expand All @@ -105,7 +103,7 @@ export const loader = unstable_defineLoader(async ({ request, params }) => {
input: {
search: { page, query },
sort: { by: urlParse.sortBy, order: urlParse.orderBy },
filter: { collection: urlParse.collection },
filter: { collections: urlParse.collections },
invertCollection: urlParse.invertCollection,
},
},
Expand Down Expand Up @@ -188,7 +186,7 @@ export default function Page() {
color={
loaderData.list?.url.orderBy !== defaultFilters.orderBy ||
loaderData.list?.url.sortBy !== defaultFilters.sortBy ||
isString(loaderData.list?.url.collection)
isString(loaderData.list?.url.collections)
? "blue"
: "gray"
}
Expand Down Expand Up @@ -343,7 +341,6 @@ const commitGroup = async (

const FiltersModalForm = () => {
const loaderData = useLoaderData<typeof loader>();
const collections = useUserCollections();
const [_, { setP }] = useAppSearchParam(loaderData.cookieName);

if (!loaderData.list) return null;
Expand Down Expand Up @@ -375,22 +372,9 @@ const FiltersModalForm = () => {
</ActionIcon>
</Flex>
<Flex gap="xs" align="center">
<Select
flex={1}
placeholder="Select a collection"
defaultValue={loaderData.list.url.collection?.toString()}
data={[
{
group: "My collections",
items: collections.map((c) => ({
value: c.id.toString(),
label: c.name,
})),
},
]}
onChange={(v) => setP("collection", v)}
clearable
searchable
<CollectionsFilter
cookieName={loaderData.cookieName}
collections={loaderData.list.url.collections}
/>
<Checkbox
label="Invert"
Expand Down
Loading

0 comments on commit 171f7d1

Please sign in to comment.