diff options
Diffstat (limited to 'frontend/src/pages/views/MassEditor.tsx')
-rw-r--r-- | frontend/src/pages/views/MassEditor.tsx | 110 |
1 files changed, 82 insertions, 28 deletions
diff --git a/frontend/src/pages/views/MassEditor.tsx b/frontend/src/pages/views/MassEditor.tsx index b15a55e83..48068d1c6 100644 --- a/frontend/src/pages/views/MassEditor.tsx +++ b/frontend/src/pages/views/MassEditor.tsx @@ -1,18 +1,17 @@ -import { useIsAnyMutationRunning, useLanguageProfiles } from "@/apis/hooks"; -import { SimpleTable, Toolbox } from "@/components"; -import { Selector, SelectorOption } from "@/components/inputs"; -import { useCustomSelection } from "@/components/tables/plugins"; -import { GetItemId, useSelectorOptions } from "@/utilities"; +import { useCallback, useMemo, useRef, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { Box, Container, useCombobox } from "@mantine/core"; import { faCheck, faUndo } from "@fortawesome/free-solid-svg-icons"; -import { Box, Container } from "@mantine/core"; +import { UseMutationResult } from "@tanstack/react-query"; +import { ColumnDef, Table } from "@tanstack/react-table"; import { uniqBy } from "lodash"; -import { useCallback, useMemo, useState } from "react"; -import { UseMutationResult } from "react-query"; -import { useNavigate } from "react-router-dom"; -import { Column, useRowSelect } from "react-table"; +import { useIsAnyMutationRunning, useLanguageProfiles } from "@/apis/hooks"; +import { GroupedSelector, GroupedSelectorOptions, Toolbox } from "@/components"; +import SimpleTable from "@/components/tables/SimpleTable"; +import { GetItemId, useSelectorOptions } from "@/utilities"; interface MassEditorProps<T extends Item.Base = Item.Base> { - columns: Column<T>[]; + columns: ColumnDef<T>[]; data: T[]; mutation: UseMutationResult<void, unknown, FormType.ModifyItem>; } @@ -24,6 +23,7 @@ function MassEditor<T extends Item.Base>(props: MassEditorProps<T>) { const [dirties, setDirties] = useState<T[]>([]); const hasTask = useIsAnyMutationRunning(); const { data: profiles } = useLanguageProfiles(); + const tableRef = useRef<Table<T>>(null); const navigate = useNavigate(); @@ -37,14 +37,25 @@ function MassEditor<T extends Item.Base>(props: MassEditorProps<T>) { const profileOptions = useSelectorOptions(profiles ?? [], (v) => v.name); const profileOptionsWithAction = useMemo< - SelectorOption<Language.Profile | null>[] - >( - () => [ - { label: "Clear", value: null, group: "Action" }, - ...profileOptions.options, - ], - [profileOptions.options], - ); + GroupedSelectorOptions<string>[] + >(() => { + return [ + { + group: "Actions", + items: [{ label: "Clear", value: "", profileId: null }], + }, + { + group: "Profiles", + items: profileOptions.options.map((a) => { + return { + value: a.value.profileId.toString(), + label: a.label, + profileId: a.value.profileId, + }; + }), + }, + ]; + }, [profileOptions.options]); const getKey = useCallback((value: Language.Profile | null) => { if (value) { @@ -56,11 +67,20 @@ function MassEditor<T extends Item.Base>(props: MassEditorProps<T>) { const { mutateAsync } = mutation; + /** + * Submit the form that contains the series id and the respective profile id set in chunks to prevent payloads too + * large when we have a high amount of series or movies being applied the profile. The chunks are executed in order + * since there are no much benefit on executing in parallel, also parallelism could result in high load on the server + * side if not throttled properly. + */ const save = useCallback(() => { + const chunkSize = 1000; + const form: FormType.ModifyItem = { id: [], profileid: [], }; + dirties.forEach((v) => { const id = GetItemId(v); if (id) { @@ -68,32 +88,63 @@ function MassEditor<T extends Item.Base>(props: MassEditorProps<T>) { form.profileid.push(v.profileId); } }); - return mutateAsync(form); + + const mutateInChunks = async ( + ids: number[], + profileIds: (number | null)[], + ) => { + if (ids.length === 0) return; + + const chunkIds = ids.slice(0, chunkSize); + const chunkProfileIds = profileIds.slice(0, chunkSize); + + await mutateAsync({ + id: chunkIds, + profileid: chunkProfileIds, + }); + + await mutateInChunks(ids.slice(chunkSize), profileIds.slice(chunkSize)); + }; + + return mutateInChunks(form.id, form.profileid); }, [dirties, mutateAsync]); const setProfiles = useCallback( - (profile: Language.Profile | null) => { - const id = profile?.profileId ?? null; + (id: number | null) => { const newItems = selections.map((v) => ({ ...v, profileId: id })); setDirties((dirty) => { return uniqBy([...newItems, ...dirty], GetItemId); }); + + tableRef.current?.toggleAllRowsSelected(false); }, [selections], ); + + const combobox = useCombobox(); + return ( <Container fluid px={0}> <Toolbox> <Box> - <Selector - allowDeselect + <GroupedSelector + onClick={() => combobox.openDropdown()} + onDropdownClose={() => { + combobox.resetSelectedOption(); + }} placeholder="Change Profile" + withCheckIcon={false} options={profileOptionsWithAction} getkey={getKey} disabled={selections.length === 0} - onChange={setProfiles} - ></Selector> + comboboxProps={{ + store: combobox, + onOptionSubmit: (value) => { + setProfiles(value ? +value : null); + }, + }} + ></GroupedSelector> </Box> <Box> <Toolbox.Button icon={faUndo} onClick={onEnded}> @@ -110,10 +161,13 @@ function MassEditor<T extends Item.Base>(props: MassEditorProps<T>) { </Box> </Toolbox> <SimpleTable + instanceRef={tableRef} columns={columns} data={data} - onSelect={setSelections} - plugins={[useRowSelect, useCustomSelection]} + enableRowSelection + onRowSelectionChanged={(row) => { + setSelections(row.map((r) => r.original)); + }} ></SimpleTable> </Container> ); |