Repository: IgnisDa/ryot (language: rust)

Issue:
[FEATURE REQUEST] Bulk remove collection contents
After import movies and tv shows from Trakt, I find all tv series I have watched are put into system created collections `Monitor` and `In Progress`

This makes some trouble:
Some tv series I have dropped appear in calendar, dashbaord `upcoming` and `In Process`. For example, I watched "The Boys" Season 1 and Season 2, and I have gave up watching the other seasons. "The Boys" will still be shown in my dashbaord. For these tv sereis, I have to remove them from system created collections one by one.

Expect:
Don't put the imported tv series into `Monitor` and `In Progress` collections. Or add batch delete item in the collections function so they can be removed from the collection easily.


Discussion / hints:
Bulk delete is something i plan to work on soon.

Fix patch:
diff --git a/apps/backend/src/fitness/resolver.rs b/apps/backend/src/fitness/resolver.rs
index c1df3426e3..0c2f79f745 100644
--- a/apps/backend/src/fitness/resolver.rs
+++ b/apps/backend/src/fitness/resolver.rs
@@ -58,10 +58,10 @@ struct ExerciseListFilter {
 
 #[derive(Debug, Serialize, Deserialize, Enum, Clone, PartialEq, Eq, Copy, Default)]
 enum ExerciseSortBy {
+    Name,
     #[default]
     LastPerformed,
-    NumTimesPerformed,
-    Name,
+    TimesPerformed,
 }
 
 #[derive(Debug, Serialize, Deserialize, InputObject, Clone)]
@@ -478,7 +478,7 @@ impl ExerciseService {
                 // DEV: This is just a small hack to reduce duplicated code. We
                 // are ordering by name for the other `sort_by` anyway.
                 ExerciseSortBy::Name => Expr::val("1"),
-                ExerciseSortBy::NumTimesPerformed => Expr::expr(Func::coalesce([
+                ExerciseSortBy::TimesPerformed => Expr::expr(Func::coalesce([
                     Expr::col((
                         etu.clone(),
                         user_to_entity::Column::ExerciseNumTimesInteracted,
diff --git a/apps/frontend/app/components/common.tsx b/apps/frontend/app/components/common.tsx
index f1ff5b2e02..88b4241e25 100644
--- a/apps/frontend/app/components/common.tsx
+++ b/apps/frontend/app/components/common.tsx
@@ -7,20 +7,30 @@ import {
 	Badge,
 	Box,
 	Flex,
+	Group,
 	Image,
+	Modal,
 	SimpleGrid,
 	Stack,
 	Text,
 	TextInput,
+	Title,
 	Tooltip,
 } from "@mantine/core";
 import { useDebouncedValue, useDidUpdate } from "@mantine/hooks";
+import { useNavigate } from "@remix-run/react";
 import type {
 	MediaLot,
 	MediaSource,
 } from "@ryot/generated/graphql/backend/graphql";
 import { snakeCase } from "@ryot/ts-utils";
-import { IconExternalLink, IconSearch, IconX } from "@tabler/icons-react";
+import {
+	IconExternalLink,
+	IconFilterOff,
+	IconSearch,
+	IconX,
+} from "@tabler/icons-react";
+import Cookies from "js-cookie";
 import type { ReactNode } from "react";
 import { useState } from "react";
 import { withFragment, withoutHost } from "ufo";
@@ -189,3 +199,38 @@ export const ProRequiredAlert = (props: { tooltipLabel?: string }) => {
 		</Alert>
 	) : null;
 };
+
+export const FiltersModal = (props: {
+	opened: boolean;
+	cookieName: string;
+	children: ReactNode;
+	closeFiltersModal: () => void;
+	title?: string;
+}) => {
+	const navigate = useNavigate();
+
+	return (
+		<Modal
+			onClose={props.closeFiltersModal}
+			opened={props.opened}
+			withCloseButton={false}
+			centered
+		>
+			<Stack>
+				<Group justify="space-between">
+					<Title order={3}>{props.title || "Filters"}</Title>
+					<ActionIcon
+						onClick={() => {
+							navigate(".");
+							props.closeFiltersModal();
+							Cookies.remove(props.cookieName);
+						}}
+					>
+						<IconFilterOff size={24} />
+					</ActionIcon>
+				</Group>
+				{props.children}
+			</Stack>
+		</Modal>
+	);
+};
diff --git a/apps/frontend/app/components/fitness.tsx b/apps/frontend/app/components/fitness.tsx
index c3936bd82a..cfc8ed70cc 100644
--- a/apps/frontend/app/components/fitness.tsx
+++ b/apps/frontend/app/components/fitness.tsx
@@ -95,7 +95,7 @@ export const ExerciseHistory = (props: {
 				component={Link}
 				to={withFragment(
 					$path("/fitness/workouts/:id", { id: props.history.workoutId }),
-					`${props.exerciseId}__${props.history.index}`,
+					props.history.index.toString(),
 				)}
 				fw="bold"
 			>
diff --git a/apps/frontend/app/lib/generals.ts b/apps/frontend/app/lib/generals.ts
index 8ede4e7b2f..64f6317508 100644
--- a/apps/frontend/app/lib/generals.ts
+++ b/apps/frontend/app/lib/generals.ts
@@ -3,6 +3,7 @@ import {
 	mergeQueryKeys,
 } from "@lukemorales/query-key-factory";
 import {
+	EntityLot,
 	MediaLot,
 	MediaSource,
 	SetLot,
@@ -243,8 +244,8 @@ const collectionQueryKeys = createQueryKeys("collections", {
 	userList: (userId: string) => ({
 		queryKey: ["userCollectionsList", userId],
 	}),
-	details: (collectionId: string) => ({
-		queryKey: ["collectionDetails", collectionId],
+	details: (collectionId: string, take?: number) => ({
+		queryKey: ["collectionDetails", collectionId, take],
 	}),
 });
 
@@ -262,3 +263,15 @@ export const queryFactory = mergeQueryKeys(
 	collectionQueryKeys,
 	fitnessQueryKeys,
 );
+
+export const convertEntityToIndividualId = (
+	entityId: string,
+	entityLot: EntityLot,
+) => {
+	const metadataId = entityLot === EntityLot.Metadata ? entityId : undefined;
+	const metadataGroupId =
+		entityLot === EntityLot.MetadataGroup ? entityId : undefined;
+	const personId = entityLot === EntityLot.Person ? entityId : undefined;
+	const exerciseId = entityLot === EntityLot.Exercise ? entityId : undefined;
+	return { metadataId, metadataGroupId, personId, exerciseId };
+};
diff --git a/apps/frontend/app/lib/hooks.ts b/apps/frontend/app/lib/hooks.ts
index 943549b609..fbd060188c 100644
--- a/apps/frontend/app/lib/hooks.ts
+++ b/apps/frontend/app/lib/hooks.ts
@@ -39,34 +39,43 @@ export const useFallbackImageUrl = (text = "No Image") => {
 	}/${colorScheme === "dark" ? "FFF" : "121211"}?text=${text}`;
 };
 
-export const useSearchParam = () => {
+export const useSearchParam = (replace?: boolean) => {
 	const [searchParams, setSearchParams] = useSearchParams();
 
 	const delP = (key: string) => {
-		setSearchParams((prev) => {
-			prev.delete(key);
-			return prev;
-		});
+		setSearchParams(
+			(prev) => {
+				prev.delete(key);
+				return prev;
+			},
+			{ replace },
+		);
 	};
 
 	const setP = (key: string, value?: string | null) => {
-		setSearchParams((prev) => {
-			if (!value) delP(key);
-			else prev.set(key, value);
-			return prev;
-		});
+		setSearchParams(
+			(prev) => {
+				if (!value) delP(key);
+				else prev.set(key, value);
+				return prev;
+			},
+			{ replace },
+		);
 	};
 
 	return [searchParams, { setP, delP }] as const;
 };
 
 export const useCookieEnhancedSearchParam = (cookieKey: string) => {
-	const [searchParams, { setP, delP }] = useSearchParam();
+	const userPreferences = useUserPreferences();
+	const [searchParams, { setP, delP }] = useSearchParam(
+		userPreferences.general.persistQueries,
+	);
 
 	const updateCookieP = (key: string, value?: string | null) => {
 		const cookieValue = Cookies.get(cookieKey);
 		const cookieSearchParams = new URLSearchParams(cookieValue);
-		if (value === undefined || value === null) cookieSearchParams.delete(key);
+		if (!value) cookieSearchParams.delete(key);
 		else cookieSearchParams.set(key, value);
 		Cookies.set(cookieKey, cookieSearchParams.toString());
 	};
diff --git a/apps/frontend/app/lib/state/fitness.ts b/apps/frontend/app/lib/state/fitness.ts
index 7ea1fa4ff6..0557f7f4be 100644
--- a/apps/frontend/app/lib/state/fitness.ts
+++ b/apps/frontend/app/lib/state/fitness.ts
@@ -1,4 +1,5 @@
 import { $path } from "@ignisda/remix-routes";
+import type { NavigateFunction } from "@remix-run/react";
 import {
 	type CreateUserWorkoutMutationVariables,
 	ExerciseDetailsDocument,
@@ -14,6 +15,7 @@ import type { Dayjs } from "dayjs";
 import { createDraft, finishDraft } from "immer";
 import { atom, useAtom } from "jotai";
 import { atomWithReset, atomWithStorage } from "jotai/utils";
+import { withFragment } from "ufo";
 import { v4 as randomUUID } from "uuid";
 import {
 	CurrentWorkoutKey,
@@ -174,9 +176,10 @@ export const addExerciseToWorkout = async (
 	currentWorkout: InProgressWorkout,
 	setCurrentWorkout: (v: InProgressWorkout) => void,
 	selectedExercises: Array<{ name: string; lot: ExerciseLot }>,
-	navigate: (path: string) => void,
+	navigate: NavigateFunction,
 ) => {
 	const draft = createDraft(currentWorkout);
+	const idxOfNextExercise = draft.exercises.length;
 	for (const [_exerciseIdx, ex] of selectedExercises.entries()) {
 		const exerciseDetails = await getExerciseDetails(ex.name);
 		draft.exercises.push({
@@ -201,7 +204,12 @@ export const addExerciseToWorkout = async (
 	}
 	const finishedDraft = finishDraft(draft);
 	setCurrentWorkout(finishedDraft);
-	navigate($path("/fitness/workouts/current"));
+	navigate(
+		withFragment(
+			$path("/fitness/workouts/current"),
+			idxOfNextExercise.toString(),
+		),
+	);
 };
 
 export const currentWorkoutToCreateWorkoutInput = (
diff --git a/apps/frontend/app/routes/_dashboard.collections.$id._index.tsx b/apps/frontend/app/routes/_dashboard.collections.$id._index.tsx
index 3c2ed34c3d..35b47501bd 100644
--- a/apps/frontend/app/routes/_dashboard.collections.$id._index.tsx
+++ b/apps/frontend/app/routes/_dashboard.collections.$id._index.tsx
@@ -1,26 +1,28 @@
 import {
 	ActionIcon,
+	Affix,
 	Box,
 	Button,
 	Center,
 	Container,
 	Flex,
 	Group,
-	Modal,
 	Pagination,
+	Paper,
 	Select,
 	SimpleGrid,
 	Stack,
 	Tabs,
 	Text,
 	Title,
+	rem,
 } from "@mantine/core";
-import { useDisclosure } from "@mantine/hooks";
-import { unstable_defineLoader } from "@remix-run/node";
+import { useDisclosure, useListState } from "@mantine/hooks";
+import { unstable_defineAction, unstable_defineLoader } from "@remix-run/node";
 import {
+	Form,
 	type MetaArgs_SingleFetch,
 	useLoaderData,
-	useNavigate,
 } from "@remix-run/react";
 import {
 	CollectionContentsDocument,
@@ -28,27 +30,41 @@ import {
 	EntityLot,
 	GraphqlSortOrder,
 	MediaLot,
+	RemoveEntityFromCollectionDocument,
 } from "@ryot/generated/graphql/backend/graphql";
 import { startCase } from "@ryot/ts-utils";
 import {
 	IconBucketDroplet,
 	IconFilter,
-	IconFilterOff,
 	IconMessageCircle2,
 	IconSortAscending,
 	IconSortDescending,
+	IconTrashFilled,
 	IconUser,
 } from "@tabler/icons-react";
-import Cookies from "js-cookie";
+import { Fragment, useState } from "react";
+import { namedAction } from "remix-utils/named-action";
 import invariant from "tiny-invariant";
+import { withQuery } from "ufo";
 import { z } from "zod";
 import { zx } from "zodix";
-import { ApplicationGrid, DebouncedSearchInput } from "~/components/common";
+import {
+	ApplicationGrid,
+	DebouncedSearchInput,
+	FiltersModal,
+} from "~/components/common";
 import {
 	MediaItemWithoutUpdateModal,
 	ReviewItemDisplay,
 } from "~/components/media";
-import { dayjsLib, enhancedCookieName } from "~/lib/generals";
+import {
+	clientGqlService,
+	convertEntityToIndividualId,
+	dayjsLib,
+	enhancedCookieName,
+	queryClient,
+	queryFactory,
+} from "~/lib/generals";
 import {
 	useCookieEnhancedSearchParam,
 	useCoreDetails,
@@ -57,10 +73,14 @@ import {
 import { useReviewEntity } from "~/lib/state/media";
 import {
 	getAuthorizationHeader,
+	processSubmission,
 	redirectUsingEnhancedCookieSearchParams,
+	removeCachedUserCollectionsList,
 	serverGqlService,
 } from "~/lib/utilities.server";
 
+const DEFAULT_TAB = "contents";
+
 const defaultFiltersValue = {
 	sort: CollectionContentsSortBy.LastUpdatedOn,
 	order: GraphqlSortOrder.Desc,
@@ -110,20 +130,151 @@ export const meta = ({ data }: MetaArgs_SingleFetch<typeof loader>) => {
 	return [{ title: `${data?.collectionContents.details.name} | Ryot` }];
 };
 
+export const action = unstable_defineAction(async ({ request }) => {
+	const formData = await request.clone().formData();
+	return namedAction(request, {
+		bulkRemove: async () => {
+			const submission = processSubmission(formData, bulkRemoveSchema);
+			for (const item of submission.items) {
+				const input = convertEntityToIndividualId(
+					item.entityId,
+					item.entityLot,
+				);
+				await serverGqlService.request(
+					RemoveEntityFromCollectionDocument,
+					{
+						input: {
+							collectionName: submission.collectionName,
+							creatorUserId: submission.creatorUserId,
+							...input,
+						},
+					},
+					getAuthorizationHeader(request),
+				);
+			}
+			await removeCachedUserCollectionsList(request);
+			return Response.json({});
+		},
+	});
+});
+
+const bulkRemoveSchema = z.object({
+	collectionName: z.string(),
+	creatorUserId: z.string(),
+	items: z.array(
+		z.object({
+			entityId: z.string(),
+			entityLot: z.nativeEnum(EntityLot),
+		}),
+	),
+});
+
 export default function Page() {
 	const loaderData = useLoaderData<typeof loader>();
 	const userPreferences = useUserPreferences();
 	const coreDetails = useCoreDetails();
-	const navigate = useNavigate();
-	const [_, { setP }] = useCookieEnhancedSearchParam(loaderData.cookieName);
+	const [tab, setTab] = useState<string | null>(
+		loaderData.query.defaultTab || DEFAULT_TAB,
+	);
+	const [isBulkRemoving, setIsBulkRemoving] = useState(false);
+	const [isSelectAllLoading, setIsSelectAllLoading] = useState(false);
+	const [_e, { setP }] = useCookieEnhancedSearchParam(loaderData.cookieName);
 	const [_r, setEntityToReview] = useReviewEntity();
 	const [
 		filtersModalOpened,
 		{ open: openFiltersModal, close: closeFiltersModal },
 	] = useDisclosure(false);
+	const [bulkRemoveItems, bulkRemoveItemsHandler] = useListState<{
+		entityId: string;
+		entityLot: EntityLot;
+	}>([]);
+
+	const addBulkRemoveItem = (entityId: string, entityLot: EntityLot) => {
+		if (!bulkRemoveItems.includes({ entityId, entityLot }))
+			bulkRemoveItemsHandler.append({ entityId, entityLot });
+	};
 
 	return (
 		<Container>
+			{isBulkRemoving ? (
+				<Affix position={{ bottom: rem(30) }} w="100%" px="sm">
+					<Form
+						method="POST"
+						reloadDocument
+						action={withQuery(".", { intent: "bulkRemove" })}
+					>
+						<input
+							type="hidden"
+							name="collectionName"
+							defaultValue={loaderData.collectionContents.details.name}
+						/>
+						<input
+							type="hidden"
+							name="creatorUserId"
+							defaultValue={loaderData.collectionContents.user.id}
+						/>
+						{bulkRemoveItems.map((item, index) => (
+							<Fragment key={item.entityId}>
+								<input
+									type="hidden"
+									readOnly
+									name={`items[${index}].entityId`}
+									value={item.entityId}
+								/>
+								<input
+									type="hidden"
+									readOnly
+									name={`items[${index}].entityLot`}
+									value={item.entityLot}
+								/>
+							</Fragment>
+						))}
+						<Paper withBorder shadow="xl" p="md" w={{ md: "40%" }} mx="auto">
+							<Group wrap="nowrap" justify="space-between">
+								<Text>{bulkRemoveItems.length} items selected</Text>
+								<Group wrap="nowrap">
+									<Button
+										color="blue"
+										loading={isSelectAllLoading}
+										onClick={async () => {
+											setIsSelectAllLoading(true);
+											const { collectionContents } =
+												await queryClient.ensureQueryData({
+													queryKey: queryFactory.collections.details(
+														loaderData.collectionId,
+														Number.MAX_SAFE_INTEGER,
+													).queryKey,
+													queryFn: () =>
+														clientGqlService.request(
+															CollectionContentsDocument,
+															{
+																input: {
+																	collectionId: loaderData.collectionId,
+																	take: Number.MAX_SAFE_INTEGER,
+																},
+															},
+														),
+												});
+											for (const lm of collectionContents.results.items)
+												addBulkRemoveItem(lm.details.identifier, lm.entityLot);
+											setIsSelectAllLoading(false);
+										}}
+									>
+										Select all items
+									</Button>
+									<Button
+										color="red"
+										type="submit"
+										disabled={bulkRemoveItems.length === 0}
+									>
+										Remove
+									</Button>
+								</Group>
+							</Group>
+						</Paper>
+					</Form>
+				</Affix>
+			) : null}
 			<Stack>
 				<Box>
 					<Title>{loaderData.collectionContents.details.name}</Title>{" "}
@@ -136,7 +287,7 @@ export default function Page() {
 					</Text>
 				</Box>
 				<Text>{loaderData.collectionContents.details.description}</Text>
-				<Tabs defaultValue={loaderData.query.defaultTab || "contents"}>
+				<Tabs value={tab} onChange={setTab}>
 					<Tabs.List mb="xs">
 						<Tabs.Tab
 							value="contents"
@@ -165,7 +316,7 @@ export default function Page() {
 									enhancedQueryParams={loaderData.cookieName}
 								/>
 								<ActionIcon
-									onClick={openFiltersModal}
+									onClick={() => openFiltersModal()}
 									color={
 										loaderData.query.entityLot !== undefined ||
 										loaderData.query.metadataLot !== undefined ||
@@ -177,98 +328,54 @@ export default function Page() {
 								>
 									<IconFilter size={24} />
 								</ActionIcon>
-								<Modal
+								<FiltersModal
+									closeFiltersModal={closeFiltersModal}
+									cookieName={loaderData.cookieName}
 									opened={filtersModalOpened}
-									onClose={closeFiltersModal}
-									centered
-									withCloseButton={false}
 								>
-									<Stack>
-										<Group justify="space-between">
-											<Title order={3}>Filters</Title>
-											<ActionIcon
-												onClick={() => {
-													navigate(".");
-													closeFiltersModal();
-													Cookies.remove(loaderData.cookieName);
-												}}
-											>
-												<IconFilterOff size={24} />
-											</ActionIcon>
-										</Group>
-										<Flex gap="xs" align="center">
-											<Select
-												w="100%"
-												data={[
-													{
-														group: "Sort by",
-														items: Object.values(CollectionContentsSortBy).map(
-															(o) => ({
-																value: o.toString(),
-																label: startCase(o.toLowerCase()),
-															}),
-														),
-													},
-												]}
-												defaultValue={loaderData.query.sortBy}
-												onChange={(v) => setP("sortBy", v)}
-											/>
-											<ActionIcon
-												onClick={() => {
-													if (loaderData.query.orderBy === GraphqlSortOrder.Asc)
-														setP("orderBy", GraphqlSortOrder.Desc);
-													else setP("orderBy", GraphqlSortOrder.Asc);
-												}}
-											>
-												{loaderData.query.orderBy === GraphqlSortOrder.Asc ? (
-													<IconSortAscending />
-												) : (
-													<IconSortDescending />
-												)}
-											</ActionIcon>
-										</Flex>
-										<Select
-											placeholder="Select an entity type"
-											defaultValue={loaderData.query.entityLot}
-											data={Object.values(EntityLot)
-												.filter((o) => o !== EntityLot.Collection)
-												.map((o) => ({
-													value: o.toString(),
-													label: startCase(o.toLowerCase()),
-												}))}
-											onChange={(v) => setP("entityLot", v)}
-											clearable
-										/>
-										{loaderData.query.entityLot === EntityLot.Metadata ||
-										loaderData.query.entityLot === EntityLot.MetadataGroup ? (
-											<Select
-												placeholder="Select a media type"
-												defaultValue={loaderData.query.metadataLot}
-												data={Object.values(MediaLot).map((o) => ({
-													value: o.toString(),
-													label: startCase(o.toLowerCase()),
-												}))}
-												onChange={(v) => setP("metadataLot", v)}
-												clearable
-											/>
-										) : null}
-									</Stack>
-								</Modal>
+									<FiltersModalForm />
+								</FiltersModal>
 							</Group>
 							{loaderData.collectionContents.results.items.length > 0 ? (
 								<ApplicationGrid>
-									{loaderData.collectionContents.results.items.map((lm) => (
-										<MediaItemWithoutUpdateModal
-											key={lm.details.identifier}
-											item={{
-												...lm.details,
-												publishYear: lm.details.publishYear?.toString(),
-											}}
-											lot={lm.metadataLot}
-											entityLot={lm.entityLot}
-											reviewScale={userPreferences.general.reviewScale}
-										/>
-									))}
+									{loaderData.collectionContents.results.items.map((lm) => {
+										const atIndex = bulkRemoveItems.findIndex(
+											(i) => i.entityId === lm.details.identifier,
+										);
+										return (
+											<MediaItemWithoutUpdateModal
+												key={lm.details.identifier}
+												item={{
+													...lm.details,
+													publishYear: lm.details.publishYear?.toString(),
+												}}
+												lot={lm.metadataLot}
+												entityLot={lm.entityLot}
+												reviewScale={userPreferences.general.reviewScale}
+												topRight={
+													isBulkRemoving ? (
+														<ActionIcon
+															variant={
+																atIndex !== -1 ? "filled" : "transparent"
+															}
+															color="red"
+															onClick={(e) => {
+																e.preventDefault();
+																if (atIndex === -1)
+																	addBulkRemoveItem(
+																		lm.details.identifier,
+																		lm.entityLot,
+																	);
+																else bulkRemoveItemsHandler.remove(atIndex);
+															}}
+														>
+															<IconTrashFilled size={18} />
+														</ActionIcon>
+													) : null
+												}
+											/>
+										);
+									})}
 								</ApplicationGrid>
 							) : (
 								<Text>You have not added anything this collection</Text>
@@ -303,6 +410,16 @@ export default function Page() {
 							>
 								Post a review
 							</Button>
+							<Button
+								variant="outline"
+								w="100%"
+								onClick={() => {
+									setIsBulkRemoving(true);
+									setTab("contents");
+								}}
+							>
+								Bulk remove
+							</Button>
 						</SimpleGrid>
 					</Tabs.Panel>
 					{!userPreferences.general.disableReviews ? (
@@ -329,3 +446,67 @@ export default function Page() {
 		</Container>
 	);
 }
+
+const FiltersModalForm = () => {
+	const loaderData = useLoaderData<typeof loader>();
+	const [_, { setP }] = useCookieEnhancedSearchParam(loaderData.cookieName);
+
+	return (
+		<>
+			<Flex gap="xs" align="center">
+				<Select
+					w="100%"
+					data={[
+						{
+							group: "Sort by",
+							items: Object.values(CollectionContentsSortBy).map((o) => ({
+								value: o.toString(),
+								label: startCase(o.toLowerCase()),
+							})),
+						},
+					]}
+					defaultValue={loaderData.query.sortBy}
+					onChange={(v) => setP("sortBy", v)}
+				/>
+				<ActionIcon
+					onClick={() => {
+						if (loaderData.query.orderBy === GraphqlSortOrder.Asc)
+							setP("orderBy", GraphqlSortOrder.Desc);
+						else setP("orderBy", GraphqlSortOrder.Asc);
+					}}
+				>
+					{loaderData.query.orderBy === GraphqlSortOrder.Asc ? (
+						<IconSortAscending />
+					) : (
+						<IconSortDescending />
+					)}
+				</ActionIcon>
+			</Flex>
+			<Select
+				placeholder="Select an entity type"
+				defaultValue={loaderData.query.entityLot}
+				data={Object.values(EntityLot)
+					.filter((o) => o !== EntityLot.Collection)
+					.map((o) => ({
+						value: o.toString(),
+						label: startCase(o.toLowerCase()),
+					}))}
+				onChange={(v) => setP("entityLot", v)}
+				clearable
+			/>
+			{loaderData.query.entityLot === EntityLot.Metadata ||
+			loaderData.query.entityLot === EntityLot.MetadataGroup ? (
+				<Select
+					placeholder="Select a media type"
+					defaultValue={loaderData.query.metadataLot}
+					data={Object.values(MediaLot).map((o) => ({
+						value: o.toString(),
+						label: startCase(o.toLowerCase()),
+					}))}
+					onChange={(v) => setP("metadataLot", v)}
+					clearable
+				/>
+			) : null}
+		</>
+	);
+};
diff --git a/apps/frontend/app/routes/_dashboard.fitness.exercises.item.$id._index.tsx b/apps/frontend/app/routes/_dashboard.fitness.exercises.item.$id._index.tsx
index 76c4662167..f4069d7fd7 100644
--- a/apps/frontend/app/routes/_dashboard.fitness.exercises.item.$id._index.tsx
+++ b/apps/frontend/app/routes/_dashboard.fitness.exercises.item.$id._index.tsx
@@ -346,7 +346,7 @@ export default function Page() {
 																			$path("/fitness/workouts/:id", {
 																				id: s.workoutId,
 																			}),
-																			`${loaderData.exerciseDetails.id}__${s.exerciseIdx}`,
+																			s.exerciseIdx.toString(),
 																		)}
 																		fw="bold"
 																	>
diff --git a/apps/frontend/app/routes/_dashboard.fitness.exercises.list.tsx b/apps/frontend/app/routes/_dashboard.fitness.exercises.list.tsx
index 65176a3860..506a2a6d62 100644
--- a/apps/frontend/app/routes/_dashboard.fitness.exercises.list.tsx
+++ b/apps/frontend/app/routes/_dashboard.fitness.exercises.list.tsx
@@ -13,7 +13,6 @@ import {
 	Group,
 	Indicator,
 	MantineThemeProvider,
-	Modal,
 	Pagination,
 	Select,
 	SimpleGrid,
@@ -46,13 +45,11 @@ import {
 	IconAlertCircle,
 	IconCheck,
 	IconFilter,
-	IconFilterOff,
 	IconPlus,
 } from "@tabler/icons-react";
-import Cookies from "js-cookie";
 import { z } from "zod";
 import { zx } from "zodix";
-import { DebouncedSearchInput } from "~/components/common";
+import { DebouncedSearchInput, FiltersModal } from "~/components/common";
 import { dayjsLib, enhancedCookieName } from "~/lib/generals";
 import {
 	useCookieEnhancedSearchParam,
@@ -73,7 +70,7 @@ const defaultFiltersValue = {
 	force: undefined,
 	level: undefined,
 	mechanic: undefined,
-	sortBy: ExerciseSortBy.NumTimesPerformed,
+	sortBy: ExerciseSortBy.TimesPerformed,
 	collection: undefined,
 };
 
@@ -96,7 +93,7 @@ export const loader = unstable_defineLoader(async ({ request }) => {
 	const cookieName = enhancedCookieName("exercises.list");
 	await redirectUsingEnhancedCookieSearchParams(request, cookieName);
 	const query = zx.parseQuery(request, searchParamsSchema);
-	query.sortBy = query.sortBy ?? ExerciseSortBy.NumTimesPerformed;
+	query.sortBy = query.sortBy ?? defaultFiltersValue.sortBy;
 	query.page = query.page ?? 1;
 	const [{ exerciseParameters }, { exercisesList }] = await Promise.all([
 		serverGqlService.request(ExerciseParametersDocument, {}),
@@ -133,7 +130,6 @@ export const meta = (_args: MetaArgs_SingleFetch<typeof loader>) => {
 export default function Page() {
 	const loaderData = useLoaderData<typeof loader>();
 	const coreDetails = useCoreDetails();
-	const collections = useUserCollections();
 	const navigate = useNavigate();
 	const [_, { setP }] = useCookieEnhancedSearchParam(loaderData.cookieName);
 	const [selectedExercises, setSelectedExercises] = useListState<{
@@ -194,86 +190,13 @@ export default function Page() {
 							>
 								<IconFilter size={24} />
 							</ActionIcon>
-							<Modal
+							<FiltersModal
+								closeFiltersModal={closeFiltersModal}
+								cookieName={loaderData.cookieName}
 								opened={filtersModalOpened}
-								onClose={closeFiltersModal}
-								centered
-								withCloseButton={false}
 							>
-								<MantineThemeProvider
-									theme={{
-										components: {
-											Select: Select.extend({ defaultProps: { size: "xs" } }),
-										},
-									}}
-								>
-									<Stack gap={4}>
-										<Group justify="space-between">
-											<Title order={3}>Filters</Title>
-											<ActionIcon
-												onClick={() => {
-													navigate(".");
-													closeFiltersModal();
-													Cookies.remove(loaderData.cookieName);
-												}}
-											>
-												<IconFilterOff size={24} />
-											</ActionIcon>
-										</Group>
-										<Select
-											data={Object.values(ExerciseSortBy).map((v) => ({
-												label: startCase(snakeCase(v)),
-												value: v,
-											}))}
-											label="Sort by"
-											defaultValue={loaderData.query.sortBy}
-											onChange={(v) => setP("sortBy", v)}
-										/>
-										{Object.keys(defaultFiltersValue)
-											.filter(
-												(f) =>
-													f !== "sortBy" && f !== "order" && f !== "collection",
-											)
-											.map((f) => (
-												<Select
-													key={f}
-													clearable
-													// biome-ignore lint/suspicious/noExplicitAny: required here
-													data={(loaderData.exerciseParameters.filters as any)[
-														f
-													].map(
-														// biome-ignore lint/suspicious/noExplicitAny: required here
-														(v: any) => ({
-															label: startCase(snakeCase(v)),
-															value: v,
-														}),
-													)}
-													label={startCase(f)}
-													// biome-ignore lint/suspicious/noExplicitAny: required here
-													defaultValue={(loaderData.query as any)[f]}
-													onChange={(v) => setP(f, v)}
-												/>
-											))}
-										{collections.length > 0 ? (
-											<Select
-												label="Collection"
-												defaultValue={loaderData.query.collection?.toString()}
-												data={[
-													{
-														group: "My collections",
-														items: collections.map((c) => ({
-															value: c.id.toString(),
-															label: c.name,
-														})),
-													},
-												]}
-												onChange={(v) => setP("collection", v)}
-												clearable
-											/>
-										) : null}
-									</Stack>
-								</MantineThemeProvider>
-							</Modal>
+								<FiltersModalForm />
+							</FiltersModal>
 						</Group>
 						{loaderData.exercisesList.details.total > 0 ? (
 							<>
@@ -403,3 +326,68 @@ export default function Page() {
 		</Container>
 	);
 }
+
+const FiltersModalForm = () => {
+	const loaderData = useLoaderData<typeof loader>();
+	const collections = useUserCollections();
+	const [_, { setP }] = useCookieEnhancedSearchParam(loaderData.cookieName);
+
+	return (
+		<MantineThemeProvider
+			theme={{
+				components: {
+					Select: Select.extend({ defaultProps: { size: "xs" } }),
+				},
+			}}
+		>
+			<Stack gap={4}>
+				<Select
+					data={Object.values(ExerciseSortBy).map((v) => ({
+						label: startCase(snakeCase(v)),
+						value: v,
+					}))}
+					label="Sort by"
+					defaultValue={loaderData.query.sortBy}
+					onChange={(v) => setP("sortBy", v)}
+				/>
+				{Object.keys(defaultFiltersValue)
+					.filter((f) => f !== "sortBy" && f !== "order" && f !== "collection")
+					.map((f) => (
+						<Select
+							key={f}
+							clearable
+							// biome-ignore lint/suspicious/noExplicitAny: required here
+							data={(loaderData.exerciseParameters.filters as any)[f].map(
+								// biome-ignore lint/suspicious/noExplicitAny: required here
+								(v: any) => ({
+									label: startCase(snakeCase(v)),
+									value: v,
+								}),
+							)}
+							label={startCase(f)}
+							// biome-ignore lint/suspicious/noExplicitAny: required here
+							defaultValue={(loaderData.query as any)[f]}
+							onChange={(v) => setP(f, v)}
+						/>
+					))}
+				{collections.length > 0 ? (
+					<Select
+						label="Collection"
+						defaultValue={loaderData.query.collection?.toString()}
+						data={[
+							{
+								group: "My collections",
+								items: collections.map((c) => ({
+									value: c.id.toString(),
+									label: c.name,
+								})),
+							},
+						]}
+						onChange={(v) => setP("collection", v)}
+						clearable
+					/>
+				) : null}
+			</Stack>
+		</MantineThemeProvider>
+	);
+};
diff --git a/apps/frontend/app/routes/_dashboard.fitness.workouts.$id._index.tsx b/apps/frontend/app/routes/_dashboard.fitness.workouts.$id._index.tsx
index 72214f5b25..15a53dca0c 100644
--- a/apps/frontend/app/routes/_dashboard.fitness.workouts.$id._index.tsx
+++ b/apps/frontend/app/routes/_dashboard.fitness.workouts.$id._index.tsx
@@ -402,7 +402,7 @@ const DisplayExercise = (props: { exercise: Exercise; idx: number }) => {
 			<Stack mb="xs" gap="xs" ref={parent}>
 				<Group justify="space-between" wrap="nowrap">
 					<Anchor
-						id={`${props.exercise.name}__${props.idx}`}
+						id={props.idx.toString()}
 						component={Link}
 						to={$path("/fitness/exercises/item/:id", {
 							id: props.exercise.name,
diff --git a/apps/frontend/app/routes/_dashboard.fitness.workouts.current.tsx b/apps/frontend/app/routes/_dashboard.fitness.workouts.current.tsx
index 6987cc58c3..a4e5daf9c8 100644
--- a/apps/frontend/app/routes/_dashboard.fitness.workouts.current.tsx
+++ b/apps/frontend/app/routes/_dashboard.fitness.workouts.current.tsx
@@ -290,7 +290,7 @@ export default function Page() {
 										value={`${
 											currentWorkout.exercises
 												.map((e) => e.sets.every((s) => s.confirmedAt))
-												.filter((e) => e !== null).length
+												.filter(Boolean).length
 										}/${currentWorkout.exercises.length}`}
 									/>
 									<StatDisplay
@@ -862,7 +862,10 @@ const ExerciseDisplay = (props: {
 					)}
 				</Stack>
 			</Modal>
-			<Paper px={{ base: 4, md: "xs", lg: "sm" }}>
+			<Paper
+				id={props.exerciseIdx.toString()}
+				px={{ base: 4, md: "xs", lg: "sm" }}
+			>
 				<Stack>
 					<Menu shadow="md" width={200} position="left-end">
 						<Stack ref={parent}>
diff --git a/apps/frontend/app/routes/_dashboard.media.$action.$lot.tsx b/apps/frontend/app/routes/_dashboard.media.$action.$lot.tsx
index f39c2cd0c7..ff76ac39b6 100644
--- a/apps/frontend/app/routes/_dashboard.media.$action.$lot.tsx
+++ b/apps/frontend/app/routes/_dashboard.media.$action.$lot.tsx
@@ -8,13 +8,11 @@ import {
 	Flex,
 	Group,
 	Menu,
-	Modal,
 	Pagination,
 	Select,
 	Stack,
 	Tabs,
 	Text,
-	Title,
 } from "@mantine/core";
 import { useDisclosure } from "@mantine/hooks";
 import { unstable_defineLoader } from "@remix-run/node";
@@ -42,7 +40,6 @@ import {
 	IconBoxMultiple,
 	IconDotsVertical,
 	IconFilter,
-	IconFilterOff,
 	IconListCheck,
 	IconPhotoPlus,
 	IconSearch,
@@ -50,14 +47,17 @@ import {
 	IconSortDescending,
 	IconStarFilled,
 } from "@tabler/icons-react";
-import Cookies from "js-cookie";
 import { useState } from "react";
 import invariant from "tiny-invariant";
 import { match } from "ts-pattern";
 import { withoutHost } from "ufo";
 import { z } from "zod";
 import { zx } from "zodix";
-import { ApplicationGrid, DebouncedSearchInput } from "~/components/common";
+import {
+	ApplicationGrid,
+	DebouncedSearchInput,
+	FiltersModal,
+} from "~/components/common";
 import {
 	type Item,
 	MediaItemWithoutUpdateModal,
@@ -219,7 +219,6 @@ export default function Page() {
 	const loaderData = useLoaderData<typeof loader>();
 	const userPreferences = useUserPreferences();
 	const coreDetails = useCoreDetails();
-	const collections = useUserCollections();
 	const [_, { setP }] = useCookieEnhancedSearchParam(loaderData.cookieName);
 	const [_r, setEntityToReview] = useReviewEntity();
 	const [
@@ -290,95 +289,13 @@ export default function Page() {
 							>
 								<IconFilter size={24} />
 							</ActionIcon>
-							<Modal
+							<FiltersModal
+								closeFiltersModal={closeFiltersModal}
+								cookieName={loaderData.cookieName}
 								opened={filtersModalOpened}
-								onClose={closeFiltersModal}
-								centered
-								withCloseButton={false}
 							>
-								<Stack>
-									<Group justify="space-between">
-										<Title order={3}>Filters</Title>
-										<ActionIcon
-											onClick={() => {
-												navigate(".");
-												closeFiltersModal();
-												Cookies.remove(loaderData.cookieName);
-											}}
-										>
-											<IconFilterOff size={24} />
-										</ActionIcon>
-									</Group>
-									<Select
-										defaultValue={loaderData.mediaList.url.generalFilter}
-										data={[
-											{
-												group: "General filters",
-												items: Object.values(MediaGeneralFilter).map((o) => ({
-													value: o.toString(),
-													label: startCase(o.toLowerCase()),
-												})),
-											},
-										]}
-										onChange={(v) => {
-											if (v) setP("generalFilter", v);
-										}}
-									/>
-									<Flex gap="xs" align="center">
-										<Select
-											w="100%"
-											data={[
-												{
-													group: "Sort by",
-													items: Object.values(MediaSortBy).map((o) => ({
-														value: o.toString(),
-														label: startCase(o.toLowerCase()),
-													})),
-												},
-											]}
-											defaultValue={loaderData.mediaList.url.sortBy}
-											onChange={(v) => {
-												if (v) setP("sortBy", v);
-											}}
-										/>
-										<ActionIcon
-											onClick={() => {
-												if (
-													loaderData.mediaList?.url.sortOrder ===
-													GraphqlSortOrder.Asc
-												)
-													setP("sortOrder", GraphqlSortOrder.Desc);
-												else setP("sortOrder", GraphqlSortOrder.Asc);
-											}}
-										>
-											{loaderData.mediaList.url.sortOrder ===
-											GraphqlSortOrder.Asc ? (
-												<IconSortAscending />
-											) : (
-												<IconSortDescending />
-											)}
-										</ActionIcon>
-									</Flex>
-									{collections.length > 0 ? (
-										<Select
-											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
-										/>
-									) : null}
-								</Stack>
-							</Modal>
+								<FiltersModalForm />
+							</FiltersModal>
 						</Group>
 						{loaderData.mediaList.list.details.total > 0 ? (
 							<>
@@ -650,3 +567,80 @@ const MediaSearchItem = (props: {
 		</MediaItemWithoutUpdateModal>
 	);
 };
+
+const FiltersModalForm = () => {
+	const loaderData = useLoaderData<typeof loader>();
+	const collections = useUserCollections();
+	const [_, { setP }] = useCookieEnhancedSearchParam(loaderData.cookieName);
+
+	if (!loaderData.mediaList) return null;
+
+	return (
+		<>
+			<Select
+				defaultValue={loaderData.mediaList.url.generalFilter}
+				data={[
+					{
+						group: "General filters",
+						items: Object.values(MediaGeneralFilter).map((o) => ({
+							value: o.toString(),
+							label: startCase(o.toLowerCase()),
+						})),
+					},
+				]}
+				onChange={(v) => {
+					if (v) setP("generalFilter", v);
+				}}
+			/>
+			<Flex gap="xs" align="center">
+				<Select
+					w="100%"
+					data={[
+						{
+							group: "Sort by",
+							items: Object.values(MediaSortBy).map((o) => ({
+								value: o.toString(),
+								label: startCase(o.toLowerCase()),
+							})),
+						},
+					]}
+					defaultValue={loaderData.mediaList.url.sortBy}
+					onChange={(v) => {
+						if (v) setP("sortBy", v);
+					}}
+				/>
+				<ActionIcon
+					onClick={() => {
+						if (loaderData.mediaList?.url.sortOrder === GraphqlSortOrder.Asc)
+							setP("sortOrder", GraphqlSortOrder.Desc);
+						else setP("sortOrder", GraphqlSortOrder.Asc);
+					}}
+				>
+					{loaderData.mediaList.url.sortOrder === GraphqlSortOrder.Asc ? (
+						<IconSortAscending />
+					) : (
+						<IconSortDescending />
+					)}
+				</ActionIcon>
+			</Flex>
+			{collections.length > 0 ? (
+				<Select
+					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
+				/>
+			) : null}
+		</>
+	);
+};
diff --git a/apps/frontend/app/routes/_dashboard.media.people.$action.tsx b/apps/frontend/app/routes/_dashboard.media.people.$action.tsx
index 6de5320baa..94c236bb7b 100644
--- a/apps/frontend/app/routes/_dashboard.media.people.$action.tsx
+++ b/apps/frontend/app/routes/_dashboard.media.people.$action.tsx
@@ -7,7 +7,6 @@ import {
 	Container,
 	Flex,
 	Group,
-	Modal,
 	Pagination,
 	Select,
 	Stack,
@@ -32,19 +31,21 @@ import {
 import { changeCase, getInitials, startCase } from "@ryot/ts-utils";
 import {
 	IconFilter,
-	IconFilterOff,
 	IconListCheck,
 	IconSearch,
 	IconSortAscending,
 	IconSortDescending,
 } from "@tabler/icons-react";
-import Cookies from "js-cookie";
 import { useState } from "react";
 import { match } from "ts-pattern";
 import { withoutHost } from "ufo";
 import { z } from "zod";
 import { zx } from "zodix";
-import { ApplicationGrid, DebouncedSearchInput } from "~/components/common";
+import {
+	ApplicationGrid,
+	DebouncedSearchInput,
+	FiltersModal,
+} from "~/components/common";
 import {
 	BaseDisplayItem,
 	type Item,
@@ -216,55 +217,13 @@ export default function Page() {
 							>
 								<IconFilter size={24} />
 							</ActionIcon>
-							<Modal
+							<FiltersModal
+								closeFiltersModal={closeFiltersModal}
+								cookieName={loaderData.cookieName}
 								opened={filtersModalOpened}
-								onClose={closeFiltersModal}
-								centered
-								withCloseButton={false}
 							>
-								<Stack>
-									<Group justify="space-between">
-										<Title order={3}>Sort by</Title>
-										<ActionIcon
-											onClick={() => {
-												navigate(".");
-												closeFiltersModal();
-												Cookies.remove(loaderData.cookieName);
-											}}
-										>
-											<IconFilterOff size={24} />
-										</ActionIcon>
-									</Group>
-									<Flex gap="xs" align="center">
-										<Select
-											w="100%"
-											data={Object.values(PersonSortBy).map((o) => ({
-												value: o.toString(),
-												label: startCase(o.toLowerCase()),
-											}))}
-											defaultValue={loaderData.peopleList?.url.sortBy}
-											onChange={(v) => setEnhancedP("sortBy", v)}
-										/>
-										<ActionIcon
-											onClick={() => {
-												if (
-													loaderData.peopleList?.url.orderBy ===
-													GraphqlSortOrder.Asc
-												)
-													setEnhancedP("orderBy", GraphqlSortOrder.Desc);
-												else setEnhancedP("orderBy", GraphqlSortOrder.Asc);
-											}}
-										>
-											{loaderData.peopleList?.url.orderBy ===
-											GraphqlSortOrder.Asc ? (
-												<IconSortAscending />
-											) : (
-												<IconSortDescending />
-											)}
-										</ActionIcon>
-									</Flex>
-								</Stack>
-							</Modal>
+								<FiltersModalForm />
+							</FiltersModal>
 						</>
 					) : null}
 					{loaderData.action === Action.Search ? (
@@ -435,3 +394,35 @@ const commitPerson = async (
 	const json = await resp.json();
 	return json.commitPerson.id;
 };
+
+const FiltersModalForm = () => {
+	const loaderData = useLoaderData<typeof loader>();
+	const [_, { setP }] = useCookieEnhancedSearchParam(loaderData.cookieName);
+
+	return (
+		<Flex gap="xs" align="center">
+			<Select
+				w="100%"
+				data={Object.values(PersonSortBy).map((o) => ({
+					value: o.toString(),
+					label: startCase(o.toLowerCase()),
+				}))}
+				defaultValue={loaderData.peopleList?.url.sortBy}
+				onChange={(v) => setP("sortBy", v)}
+			/>
+			<ActionIcon
+				onClick={() => {
+					if (loaderData.peopleList?.url.orderBy === GraphqlSortOrder.Asc)
+						setP("orderBy", GraphqlSortOrder.Desc);
+					else setP("orderBy", GraphqlSortOrder.Asc);
+				}}
+			>
+				{loaderData.peopleList?.url.orderBy === GraphqlSortOrder.Asc ? (
+					<IconSortAscending />
+				) : (
+					<IconSortDescending />
+				)}
+			</ActionIcon>
+		</Flex>
+	);
+};
diff --git a/apps/frontend/app/routes/_dashboard.settings.preferences.tsx b/apps/frontend/app/routes/_dashboard.settings.preferences.tsx
index ded3d9fe14..940aa24acd 100644
--- a/apps/frontend/app/routes/_dashboard.settings.preferences.tsx
+++ b/apps/frontend/app/routes/_dashboard.settings.preferences.tsx
@@ -60,6 +60,7 @@ import {
 	getAuthorizationCookie,
 	getAuthorizationHeader,
 	getCookiesForApplication,
+	isWorkoutActive,
 	serverGqlService,
 } from "~/lib/utilities.server";
 import classes from "~/styles/preferences.module.css";
@@ -70,7 +71,8 @@ const searchSchema = z.object({
 
 export const loader = unstable_defineLoader(async ({ request }) => {
 	const query = zx.parseQuery(request, searchSchema);
-	return { query };
+	const workoutInProgress = isWorkoutActive(request);
+	return { query, workoutInProgress };
 });
 
 export const meta = (_args: MetaArgs_SingleFetch<typeof loader>) => {
@@ -139,7 +141,12 @@ export default function Page() {
 	return (
 		<Container size="xs">
 			{toUpdatePreferences.length > 0 ? (
-				<Affix position={{ bottom: rem(40), right: rem(30) }}>
+				<Affix
+					position={{
+						bottom: rem(45),
+						right: rem(loaderData.workoutInProgress ? 100 : 40),
+					}}
+				>
 					<Form method="POST" action={`?defaultTab=${defaultTab}`} replace>
 						{toUpdatePreferences.map((pref) => (
 							<input
diff --git a/apps/frontend/app/routes/actions.tsx b/apps/frontend/app/routes/actions.tsx
index 206ee59918..6ed8faa10d 100644
--- a/apps/frontend/app/routes/actions.tsx
+++ b/apps/frontend/app/routes/actions.tsx
@@ -27,7 +27,10 @@ import invariant from "tiny-invariant";
 import { match } from "ts-pattern";
 import { z } from "zod";
 import { zx } from "zodix";
-import { redirectToQueryParam } from "~/lib/generals";
+import {
+	convertEntityToIndividualId,
+	redirectToQueryParam,
+} from "~/lib/generals";
 import {
 	MetadataIdSchema,
 	MetadataSpecificsSchema,
@@ -322,6 +325,7 @@ export const action = unstable_defineAction(async ({ request, response }) => {
 				getAuthorizationHeader(request),
 			);
 			await sleepForHalfSecond();
+			await removeCachedUserCollectionsList(request);
 			response.headers = extendResponseHeaders(
 				response.headers,
 				await createToastHeaders({
@@ -341,6 +345,7 @@ export const action = unstable_defineAction(async ({ request, response }) => {
 				getAuthorizationHeader(request),
 			);
 			await sleepForHalfSecond();
+			await removeCachedUserCollectionsList(request);
 			response.headers = extendResponseHeaders(
 				response.headers,
 				await createToastHeaders({
@@ -427,24 +432,11 @@ const getChangeCollectionToEntityVariables = (formData: FormData) => {
 		formData,
 		changeCollectionToEntitySchema.passthrough(),
 	);
-	const metadataId =
-		submission.entityLot === EntityLot.Metadata
-			? submission.entityId
-			: undefined;
-	const metadataGroupId =
-		submission.entityLot === EntityLot.MetadataGroup
-			? submission.entityId
-			: undefined;
-	const personId =
-		submission.entityLot === EntityLot.Person ? submission.entityId : undefined;
-	const exerciseId =
-		submission.entityLot === EntityLot.Exercise
-			? submission.entityId
-			: undefined;
-	return [
-		submission,
-		{ metadataId, metadataGroupId, exerciseId, personId },
-	] as const;
+	const individualIds = convertEntityToIndividualId(
+		submission.entityId,
+		submission.entityLot,
+	);
+	return [submission, individualIds] as const;
 };
 
 const progressUpdateSchema = z
diff --git a/libs/generated/src/graphql/backend/gql.ts b/libs/generated/src/graphql/backend/gql.ts
index e376876769..571c08f076 100644
--- a/libs/generated/src/graphql/backend/gql.ts
+++ b/libs/generated/src/graphql/backend/gql.ts
@@ -16,7 +16,7 @@ const documents = {
     "mutation LoginUser($input: AuthUserInput!) {\n  loginUser(input: $input) {\n    __typename\n    ... on LoginError {\n      error\n    }\n    ... on LoginResponse {\n      apiKey\n    }\n  }\n}": types.LoginUserDocument,
     "mutation RegisterUser($input: AuthUserInput!) {\n  registerUser(input: $input) {\n    __typename\n    ... on RegisterError {\n      error\n    }\n    ... on StringIdObject {\n      id\n    }\n  }\n}": types.RegisterUserDocument,
     "mutation AddEntityToCollection($input: ChangeCollectionToEntityInput!) {\n  addEntityToCollection(input: $input)\n}\n\nmutation CommitMetadata($input: CommitMediaInput!) {\n  commitMetadata(input: $input) {\n    id\n  }\n}\n\nmutation CommitMetadataGroup($input: CommitMediaInput!) {\n  commitMetadataGroup(input: $input) {\n    id\n  }\n}\n\nmutation CommitPerson($input: CommitPersonInput!) {\n  commitPerson(input: $input) {\n    id\n  }\n}\n\nmutation CreateCustomExercise($input: ExerciseInput!) {\n  createCustomExercise(input: $input)\n}\n\nmutation EditCustomExercise($input: EditCustomExerciseInput!) {\n  editCustomExercise(input: $input)\n}\n\nmutation CreateCustomMetadata($input: CreateCustomMetadataInput!) {\n  createCustomMetadata(input: $input) {\n    id\n  }\n}\n\nmutation CreateOrUpdateCollection($input: CreateOrUpdateCollectionInput!) {\n  createOrUpdateCollection(input: $input) {\n    id\n  }\n}\n\nmutation CreateReviewComment($input: CreateReviewCommentInput!) {\n  createReviewComment(input: $input)\n}\n\nmutation CreateUserMeasurement($input: UserMeasurementInput!) {\n  createUserMeasurement(input: $input)\n}\n\nmutation CreateUserNotificationPlatform($input: CreateUserNotificationPlatformInput!) {\n  createUserNotificationPlatform(input: $input)\n}\n\nmutation CreateUserIntegration($input: CreateIntegrationInput!) {\n  createUserIntegration(input: $input) {\n    id\n  }\n}\n\nmutation CreateUserWorkout($input: UserWorkoutInput!) {\n  createUserWorkout(input: $input)\n}\n\nmutation DeleteCollection($collectionName: String!) {\n  deleteCollection(collectionName: $collectionName)\n}\n\nmutation DeleteReview($reviewId: String!) {\n  deleteReview(reviewId: $reviewId)\n}\n\nmutation DeleteS3Object($key: String!) {\n  deleteS3Object(key: $key)\n}\n\nmutation DeleteSeenItem($seenId: String!) {\n  deleteSeenItem(seenId: $seenId) {\n    id\n  }\n}\n\nmutation DeleteUser($toDeleteUserId: String!) {\n  deleteUser(toDeleteUserId: $toDeleteUserId)\n}\n\nmutation DeleteUserIntegration($integrationId: String!) {\n  deleteUserIntegration(integrationId: $integrationId)\n}\n\nmutation DeleteUserMeasurement($timestamp: DateTime!) {\n  deleteUserMeasurement(timestamp: $timestamp)\n}\n\nmutation DeleteUserNotificationPlatform($notificationId: Int!) {\n  deleteUserNotificationPlatform(notificationId: $notificationId)\n}\n\nmutation DeleteUserWorkout($workoutId: String!) {\n  deleteUserWorkout(workoutId: $workoutId)\n}\n\nmutation DeployBackgroundJob($jobName: BackgroundJob!) {\n  deployBackgroundJob(jobName: $jobName)\n}\n\nmutation DeployBulkProgressUpdate($input: [ProgressUpdateInput!]!) {\n  deployBulkProgressUpdate(input: $input)\n}\n\nmutation DeployExportJob($toExport: [ExportItem!]!) {\n  deployExportJob(toExport: $toExport)\n}\n\nmutation DeployImportJob($input: DeployImportJobInput!) {\n  deployImportJob(input: $input)\n}\n\nmutation DeployUpdateMetadataJob($metadataId: String!) {\n  deployUpdateMetadataJob(metadataId: $metadataId)\n}\n\nmutation DeployUpdatePersonJob($personId: String!) {\n  deployUpdatePersonJob(personId: $personId)\n}\n\nmutation EditSeenItem($input: EditSeenItemInput!) {\n  editSeenItem(input: $input)\n}\n\nmutation EditUserWorkout($input: EditUserWorkoutInput!) {\n  editUserWorkout(input: $input)\n}\n\nmutation GenerateAuthToken {\n  generateAuthToken\n}\n\nmutation MergeMetadata($mergeFrom: String!, $mergeInto: String!) {\n  mergeMetadata(mergeFrom: $mergeFrom, mergeInto: $mergeInto)\n}\n\nmutation PostReview($input: PostReviewInput!) {\n  postReview(input: $input) {\n    id\n  }\n}\n\nmutation PresignedPutS3Url($input: PresignedPutUrlInput!) {\n  presignedPutS3Url(input: $input) {\n    key\n    uploadUrl\n  }\n}\n\nmutation RemoveEntityFromCollection($input: ChangeCollectionToEntityInput!) {\n  removeEntityFromCollection(input: $input) {\n    id\n  }\n}\n\nmutation TestUserNotificationPlatforms {\n  testUserNotificationPlatforms\n}\n\nmutation UpdateUser($input: UpdateUserInput!) {\n  updateUser(input: $input) {\n    id\n  }\n}\n\nmutation UpdateUserPreference($input: UpdateUserPreferenceInput!) {\n  updateUserPreference(input: $input)\n}": types.AddEntityToCollectionDocument,
-    "query CollectionContents($input: CollectionContentsInput!) {\n  collectionContents(input: $input) {\n    user {\n      name\n    }\n    reviews {\n      ...ReviewItemPart\n    }\n    results {\n      details {\n        total\n        nextPage\n      }\n      items {\n        metadataLot\n        entityLot\n        details {\n          ...MetadataSearchItemPart\n        }\n      }\n    }\n    details {\n      name\n      description\n      createdOn\n    }\n  }\n}": types.CollectionContentsDocument,
+    "query CollectionContents($input: CollectionContentsInput!) {\n  collectionContents(input: $input) {\n    user {\n      id\n      name\n    }\n    reviews {\n      ...ReviewItemPart\n    }\n    results {\n      details {\n        total\n        nextPage\n      }\n      items {\n        metadataLot\n        entityLot\n        details {\n          ...MetadataSearchItemPart\n        }\n      }\n    }\n    details {\n      name\n      description\n      createdOn\n    }\n  }\n}": types.CollectionContentsDocument,
     "query CoreDetails {\n  coreDetails {\n    isPro\n    timezone\n    docsLink\n    pageLimit\n    websiteUrl\n    authorName\n    oidcEnabled\n    repositoryLink\n    localAuthDisabled\n    tokenValidForDays\n  }\n}": types.CoreDetailsDocument,
     "query ExerciseDetails($exerciseId: String!) {\n  exerciseDetails(exerciseId: $exerciseId) {\n    id\n    lot\n    source\n    level\n    force\n    mechanic\n    equipment\n    muscles\n    createdByUserId\n    attributes {\n      instructions\n      images\n    }\n  }\n}": types.ExerciseDetailsDocument,
     "query ExerciseParameters {\n  exerciseParameters {\n    filters {\n      type\n      level\n      force\n      mechanic\n      equipment\n      muscle\n    }\n    downloadRequired\n  }\n}": types.ExerciseParametersDocument,
@@ -76,7 +76,7 @@ export function graphql(source: "mutation AddEntityToCollection($input: ChangeCo
 /**
  * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
  */
-export function graphql(source: "query CollectionContents($input: CollectionContentsInput!) {\n  collectionContents(input: $input) {\n    user {\n      name\n    }\n    reviews {\n      ...ReviewItemPart\n    }\n    results {\n      details {\n        total\n        nextPage\n      }\n      items {\n        metadataLot\n        entityLot\n        details {\n          ...MetadataSearchItemPart\n        }\n      }\n    }\n    details {\n      name\n      description\n      createdOn\n    }\n  }\n}"): (typeof documents)["query CollectionContents($input: CollectionContentsInput!) {\n  collectionContents(input: $input) {\n    user {\n      name\n    }\n    reviews {\n      ...ReviewItemPart\n    }\n    results {\n      details {\n        total\n        nextPage\n      }\n      items {\n        metadataLot\n        entityLot\n        details {\n          ...MetadataSearchItemPart\n        }\n      }\n    }\n    details {\n      name\n      description\n      createdOn\n    }\n  }\n}"];
+export function graphql(source: "query CollectionContents($input: CollectionContentsInput!) {\n  collectionContents(input: $input) {\n    user {\n      id\n      name\n    }\n    reviews {\n      ...ReviewItemPart\n    }\n    results {\n      details {\n        total\n        nextPage\n      }\n      items {\n        metadataLot\n        entityLot\n        details {\n          ...MetadataSearchItemPart\n        }\n      }\n    }\n    details {\n      name\n      description\n      createdOn\n    }\n  }\n}"): (typeof documents)["query CollectionContents($input: CollectionContentsInput!) {\n  collectionContents(input: $input) {\n    user {\n      id\n      name\n    }\n    reviews {\n      ...ReviewItemPart\n    }\n    results {\n      details {\n        total\n        nextPage\n      }\n      items {\n        metadataLot\n        entityLot\n        details {\n          ...MetadataSearchItemPart\n        }\n      }\n    }\n    details {\n      name\n      description\n      createdOn\n    }\n  }\n}"];
 /**
  * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
  */
diff --git a/libs/generated/src/graphql/backend/graphql.ts b/libs/generated/src/graphql/backend/graphql.ts
index f6dd50cace..cd99df39ef 100644
--- a/libs/generated/src/graphql/backend/graphql.ts
+++ b/libs/generated/src/graphql/backend/graphql.ts
@@ -506,7 +506,7 @@ export type ExerciseParameters = {
 export enum ExerciseSortBy {
   LastPerformed = 'LAST_PERFORMED',
   Name = 'NAME',
-  NumTimesPerformed = 'NUM_TIMES_PERFORMED'
+  TimesPerformed = 'TIMES_PERFORMED'
 }
 
 export enum ExerciseSource {
@@ -2711,7 +2711,7 @@ export type CollectionContentsQueryVariables = Exact<{
 }>;
 
 
-export type CollectionContentsQuery = { collectionContents: { user: { name: string }, reviews: Array<{ id: string, rating?: string | null, textOriginal?: string | null, textRendered?: string | null, isSpoiler: boolean, visibility: Visibility, postedOn: string, postedBy: { id: string, name: string }, comments: Array<{ id: string, text: string, createdOn: string, likedBy: Array<string>, user: { id: string, name: string } }>, showExtraInformation?: { episode: number, season: number } | null, podcastExtraInformation?: { episode: number } | null, animeExtraInformation?: { episode?: number | null } | null, mangaExtraInformation?: { chapter?: number | null, volume?: number | null } | null }>, results: { details: { total: number, nextPage?: number | null }, items: Array<{ metadataLot?: MediaLot | null, entityLot: EntityLot, details: { identifier: string, title: string, image?: string | null, publishYear?: number | null } }> }, details: { name: string, description?: string | null, createdOn: string } } };
+export type CollectionContentsQuery = { collectionContents: { user: { id: string, name: string }, reviews: Array<{ id: string, rating?: string | null, textOriginal?: string | null, textRendered?: string | null, isSpoiler: boolean, visibility: Visibility, postedOn: string, postedBy: { id: string, name: string }, comments: Array<{ id: string, text: string, createdOn: string, likedBy: Array<string>, user: { id: string, name: string } }>, showExtraInformation?: { episode: number, season: number } | null, podcastExtraInformation?: { episode: number } | null, animeExtraInformation?: { episode?: number | null } | null, mangaExtraInformation?: { chapter?: number | null, volume?: number | null } | null }>, results: { details: { total: number, nextPage?: number | null }, items: Array<{ metadataLot?: MediaLot | null, entityLot: EntityLot, details: { identifier: string, title: string, image?: string | null, publishYear?: number | null } }> }, details: { name: string, description?: string | null, createdOn: string } } };
 
 export type CoreDetailsQueryVariables = Exact<{ [key: string]: never; }>;
 
@@ -3037,7 +3037,7 @@ export const RemoveEntityFromCollectionDocument = {"kind":"Document","definition
 export const TestUserNotificationPlatformsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"TestUserNotificationPlatforms"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"testUserNotificationPlatforms"}}]}}]} as unknown as DocumentNode<TestUserNotificationPlatformsMutation, TestUserNotificationPlatformsMutationVariables>;
 export const UpdateUserDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateUser"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateUserInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateUser"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode<UpdateUserMutation, UpdateUserMutationVariables>;
 export const UpdateUserPreferenceDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateUserPreference"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateUserPreferenceInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateUserPreference"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]} as unknown as DocumentNode<UpdateUserPreferenceMutation, UpdateUserPreferenceMutationVariables>;
-export const CollectionContentsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"CollectionContents"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CollectionContentsInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"collectionContents"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"reviews"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ReviewItemPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"results"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"details"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"}},{"kind":"Field","name":{"kind":"Name","value":"nextPage"}}]}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"metadataLot"}},{"kind":"Field","name":{"kind":"Name","value":"entityLot"}},{"kind":"Field","name":{"kind":"Name","value":"details"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"MetadataSearchItemPart"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"details"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"createdOn"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenShowExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenShowExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}},{"kind":"Field","name":{"kind":"Name","value":"season"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenPodcastExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenPodcastExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenAnimeExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenAnimeExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenMangaExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenMangaExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"chapter"}},{"kind":"Field","name":{"kind":"Name","value":"volume"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ReviewItemPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ReviewItem"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"rating"}},{"kind":"Field","name":{"kind":"Name","value":"textOriginal"}},{"kind":"Field","name":{"kind":"Name","value":"textRendered"}},{"kind":"Field","name":{"kind":"Name","value":"isSpoiler"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"postedOn"}},{"kind":"Field","name":{"kind":"Name","value":"postedBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"comments"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"text"}},{"kind":"Field","name":{"kind":"Name","value":"createdOn"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"likedBy"}}]}},{"kind":"Field","name":{"kind":"Name","value":"showExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenShowExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"podcastExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenPodcastExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"animeExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenAnimeExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"mangaExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenMangaExtraInformationPart"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"MetadataSearchItemPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"MetadataSearchItem"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"identifier"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"image"}},{"kind":"Field","name":{"kind":"Name","value":"publishYear"}}]}}]} as unknown as DocumentNode<CollectionContentsQuery, CollectionContentsQueryVariables>;
+export const CollectionContentsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"CollectionContents"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CollectionContentsInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"collectionContents"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"reviews"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ReviewItemPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"results"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"details"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"}},{"kind":"Field","name":{"kind":"Name","value":"nextPage"}}]}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"metadataLot"}},{"kind":"Field","name":{"kind":"Name","value":"entityLot"}},{"kind":"Field","name":{"kind":"Name","value":"details"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"MetadataSearchItemPart"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"details"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"createdOn"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenShowExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenShowExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}},{"kind":"Field","name":{"kind":"Name","value":"season"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenPodcastExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenPodcastExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenAnimeExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenAnimeExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episode"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SeenMangaExtraInformationPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SeenMangaExtraInformation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"chapter"}},{"kind":"Field","name":{"kind":"Name","value":"volume"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ReviewItemPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ReviewItem"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"rating"}},{"kind":"Field","name":{"kind":"Name","value":"textOriginal"}},{"kind":"Field","name":{"kind":"Name","value":"textRendered"}},{"kind":"Field","name":{"kind":"Name","value":"isSpoiler"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"postedOn"}},{"kind":"Field","name":{"kind":"Name","value":"postedBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"comments"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"text"}},{"kind":"Field","name":{"kind":"Name","value":"createdOn"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"likedBy"}}]}},{"kind":"Field","name":{"kind":"Name","value":"showExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenShowExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"podcastExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenPodcastExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"animeExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenAnimeExtraInformationPart"}}]}},{"kind":"Field","name":{"kind":"Name","value":"mangaExtraInformation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SeenMangaExtraInformationPart"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"MetadataSearchItemPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"MetadataSearchItem"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"identifier"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"image"}},{"kind":"Field","name":{"kind":"Name","value":"publishYear"}}]}}]} as unknown as DocumentNode<CollectionContentsQuery, CollectionContentsQueryVariables>;
 export const CoreDetailsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"CoreDetails"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"coreDetails"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"isPro"}},{"kind":"Field","name":{"kind":"Name","value":"timezone"}},{"kind":"Field","name":{"kind":"Name","value":"docsLink"}},{"kind":"Field","name":{"kind":"Name","value":"pageLimit"}},{"kind":"Field","name":{"kind":"Name","value":"websiteUrl"}},{"kind":"Field","name":{"kind":"Name","value":"authorName"}},{"kind":"Field","name":{"kind":"Name","value":"oidcEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"repositoryLink"}},{"kind":"Field","name":{"kind":"Name","value":"localAuthDisabled"}},{"kind":"Field","name":{"kind":"Name","value":"tokenValidForDays"}}]}}]}}]} as unknown as DocumentNode<CoreDetailsQuery, CoreDetailsQueryVariables>;
 export const ExerciseDetailsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ExerciseDetails"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"exerciseId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"exerciseDetails"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"exerciseId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"exerciseId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"lot"}},{"kind":"Field","name":{"kind":"Name","value":"source"}},{"kind":"Field","name":{"kind":"Name","value":"level"}},{"kind":"Field","name":{"kind":"Name","value":"force"}},{"kind":"Field","name":{"kind":"Name","value":"mechanic"}},{"kind":"Field","name":{"kind":"Name","value":"equipment"}},{"kind":"Field","name":{"kind":"Name","value":"muscles"}},{"kind":"Field","name":{"kind":"Name","value":"createdByUserId"}},{"kind":"Field","name":{"kind":"Name","value":"attributes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"instructions"}},{"kind":"Field","name":{"kind":"Name","value":"images"}}]}}]}}]}}]} as unknown as DocumentNode<ExerciseDetailsQuery, ExerciseDetailsQueryVariables>;
 export const ExerciseParametersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ExerciseParameters"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"exerciseParameters"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"filters"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"level"}},{"kind":"Field","name":{"kind":"Name","value":"force"}},{"kind":"Field","name":{"kind":"Name","value":"mechanic"}},{"kind":"Field","name":{"kind":"Name","value":"equipment"}},{"kind":"Field","name":{"kind":"Name","value":"muscle"}}]}},{"kind":"Field","name":{"kind":"Name","value":"downloadRequired"}}]}}]}}]} as unknown as DocumentNode<ExerciseParametersQuery, ExerciseParametersQueryVariables>;
diff --git a/libs/graphql/src/backend/queries/CollectionContents.gql b/libs/graphql/src/backend/queries/CollectionContents.gql
index d2789924b8..4c99ed83cb 100644
--- a/libs/graphql/src/backend/queries/CollectionContents.gql
+++ b/libs/graphql/src/backend/queries/CollectionContents.gql
@@ -1,6 +1,7 @@
 query CollectionContents($input: CollectionContentsInput!) {
   collectionContents(input: $input) {
     user {
+      id
       name
     }
     reviews {


Test patch: