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, });