diff --git a/api/src/base.ts b/api/src/base.ts
index b6cf5e8b..d6db13a7 100644
--- a/api/src/base.ts
+++ b/api/src/base.ts
@@ -8,6 +8,7 @@ import { nextup } from "./controllers/profiles/nextup";
import { watchlistH } from "./controllers/profiles/watchlist";
import { seasonsH } from "./controllers/seasons";
import { seed } from "./controllers/seed";
+import { videoLinkH } from "./controllers/seed/video-links";
import { videosWriteH } from "./controllers/seed/videos";
import { collections } from "./controllers/shows/collections";
import { movies } from "./controllers/shows/movies";
@@ -140,5 +141,5 @@ export const handlers = new Elysia({ prefix })
},
permissions: ["core.write"],
},
- (app) => app.use(videosWriteH).use(seed),
+ (app) => app.use(videosWriteH).use(videoLinkH).use(seed),
);
diff --git a/api/src/controllers/videos.ts b/api/src/controllers/videos.ts
index 983e91c7..dd67d3b5 100644
--- a/api/src/controllers/videos.ts
+++ b/api/src/controllers/videos.ts
@@ -79,7 +79,7 @@ const videoSort = Sort(
],
},
{
- default: ["entry"],
+ default: ["entry", "path"],
tablePk: videos.pk,
},
);
diff --git a/front/src/primitives/combobox.tsx b/front/src/primitives/combobox.tsx
index fd6a38f2..cc9f58ad 100644
--- a/front/src/primitives/combobox.tsx
+++ b/front/src/primitives/combobox.tsx
@@ -91,6 +91,7 @@ export const ComboBox = ({
{(multiple ? !values : !value)
? label
: (multiple ? values : [value!])
+ .sort((a, b) => getKey(a).localeCompare(getKey(b)))
.map(getSmallLabel ?? getLabel)
.join(", ")}
diff --git a/front/src/primitives/combobox.web.tsx b/front/src/primitives/combobox.web.tsx
index f1f794b5..822d0e47 100644
--- a/front/src/primitives/combobox.web.tsx
+++ b/front/src/primitives/combobox.web.tsx
@@ -70,6 +70,7 @@ export const ComboBox = ({
{(multiple ? !values : !value)
? label
: (multiple ? values : [value!])
+ .sort((a, b) => getKey(a).localeCompare(getKey(b)))
.map(getSmallLabel ?? getLabel)
.join(", ")}
diff --git a/front/src/query/fetch-infinite.tsx b/front/src/query/fetch-infinite.tsx
index 1ebd45e1..a3a0fbdd 100644
--- a/front/src/query/fetch-infinite.tsx
+++ b/front/src/query/fetch-infinite.tsx
@@ -68,8 +68,8 @@ export const InfiniteFetch = ({
: placeholderCount;
const placeholders = [...Array(count === 0 ? numColumns : count)].fill(0);
if (!items) return placeholders;
- return isFetching ? [...items, ...placeholders] : items;
- }, [items, isFetching, placeholderCount, numColumns]);
+ return isFetching && !isRefetching ? [...items, ...placeholders] : items;
+ }, [items, isFetching, isRefetching, placeholderCount, numColumns]);
if (!data.length && Empty) return Empty;
diff --git a/front/src/query/query.tsx b/front/src/query/query.tsx
index 705970fd..ca402a50 100644
--- a/front/src/query/query.tsx
+++ b/front/src/query/query.tsx
@@ -322,10 +322,12 @@ export const useMutation = ({
compute,
invalidate,
optimistic,
+ optimisticKey,
...queryParams
}: MutationParams & {
compute?: (param: T) => MutationParams;
optimistic?: (param: T, previous?: QueryRet) => QueryRet | undefined;
+ optimisticKey?: QueryIdentifier;
invalidate: string[] | null;
}) => {
const { apiUrl, authToken } = useContext(AccountContext);
@@ -348,7 +350,11 @@ export const useMutation = ({
...(invalidate && optimistic
? {
onMutate: async (params) => {
- const queryKey = toQueryKey({ apiUrl, path: invalidate });
+ const queryKey = toQueryKey({
+ apiUrl,
+ path: optimisticKey?.path ?? invalidate,
+ params: optimisticKey?.params,
+ });
await queryClient.cancelQueries({
queryKey,
});
@@ -361,7 +367,11 @@ export const useMutation = ({
},
onError: (_, __, context) => {
queryClient.setQueryData(
- toQueryKey({ apiUrl, path: invalidate }),
+ toQueryKey({
+ apiUrl,
+ path: optimisticKey?.path ?? invalidate,
+ params: optimisticKey?.params,
+ }),
context!.previous,
);
},
diff --git a/front/src/ui/admin/videos-modal.tsx b/front/src/ui/admin/videos-modal.tsx
index a92b9719..967ddf4f 100644
--- a/front/src/ui/admin/videos-modal.tsx
+++ b/front/src/ui/admin/videos-modal.tsx
@@ -1,9 +1,14 @@
import { useTranslation } from "react-i18next";
import { View } from "react-native";
import { entryDisplayNumber } from "~/components/entries";
-import { Entry, FullVideo } from "~/models";
+import { Entry, FullVideo, type Page } from "~/models";
import { ComboBox, Modal, P, Skeleton } from "~/primitives";
-import { InfiniteFetch, type QueryIdentifier, useFetch } from "~/query";
+import {
+ InfiniteFetch,
+ type QueryIdentifier,
+ useFetch,
+ useMutation,
+} from "~/query";
import { useQueryState } from "~/utils";
import { Header } from "../details/header";
@@ -12,6 +17,32 @@ export const VideosModal = () => {
const { data } = useFetch(Header.query("serie", slug));
const { t } = useTranslation();
+ const { mutateAsync } = useMutation({
+ method: "PUT",
+ path: ["api", "videos", "link"],
+ compute: ({
+ video,
+ entries,
+ }: {
+ video: string;
+ entries: Omit[];
+ }) => ({
+ body: [{ id: video, for: entries.map((x) => ({ slug: x.slug })) }],
+ }),
+ invalidate: ["api", "series", slug],
+ optimisticKey: VideosModal.query(slug),
+ optimistic: (params, prev?: { pages: Page[] }) => ({
+ ...prev!,
+ pages: prev!.pages.map((p) => ({
+ ...p,
+ items: p!.items.map((x) => {
+ if (x.id !== params.video) return x;
+ return { ...x, entries: params.entries };
+ }) as FullVideo[],
+ })),
+ }),
+ });
+
return (
{
getKey={(x) => x.id}
getLabel={(x) => `${entryDisplayNumber(x)} - ${x.name}`}
getSmallLabel={entryDisplayNumber}
- onValueChange={(x) => {}}
+ onValueChange={async (entries) => {
+ await mutateAsync({
+ video: item.id,
+ entries,
+ });
+ }}
/>
)}
@@ -49,5 +85,8 @@ export const VideosModal = () => {
VideosModal.query = (slug: string): QueryIdentifier => ({
parser: FullVideo,
path: ["api", "series", slug, "videos"],
+ params: {
+ sort: "path",
+ },
infinite: true,
});