Updated tanstack table to v8.x (#2564)

pull/2567/head
Anderson Shindy Oki 3 months ago committed by GitHub
parent a4527a7942
commit 8beebce2e4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -16,6 +16,7 @@
"@mantine/modals": "^7.11.0", "@mantine/modals": "^7.11.0",
"@mantine/notifications": "^7.11.0", "@mantine/notifications": "^7.11.0",
"@tanstack/react-query": "^5.40.1", "@tanstack/react-query": "^5.40.1",
"@tanstack/react-table": "^8.19.2",
"axios": "^1.6.8", "axios": "^1.6.8",
"braces": "^3.0.3", "braces": "^3.0.3",
"react": "^18.3.1", "react": "^18.3.1",
@ -39,7 +40,6 @@
"@types/node": "^20.12.6", "@types/node": "^20.12.6",
"@types/react": "^18.3.3", "@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.0",
"@types/react-table": "^7.7.20",
"@vitejs/plugin-react": "^4.2.1", "@vitejs/plugin-react": "^4.2.1",
"@vitest/coverage-v8": "^1.4.0", "@vitest/coverage-v8": "^1.4.0",
"@vitest/ui": "^1.2.2", "@vitest/ui": "^1.2.2",
@ -57,7 +57,6 @@
"prettier": "^3.2.5", "prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^3.2.4", "prettier-plugin-organize-imports": "^3.2.4",
"pretty-quick": "^4.0.0", "pretty-quick": "^4.0.0",
"react-table": "^7.8.0",
"recharts": "^2.12.6", "recharts": "^2.12.6",
"sass": "^1.74.1", "sass": "^1.74.1",
"typescript": "^5.4.4", "typescript": "^5.4.4",
@ -3381,6 +3380,39 @@
"react": "^18 || ^19" "react": "^18 || ^19"
} }
}, },
"node_modules/@tanstack/react-table": {
"version": "8.19.2",
"resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.19.2.tgz",
"integrity": "sha512-itoSIAkA/Vsg+bjY23FSemcTyPhc5/1YjYyaMsr9QSH/cdbZnQxHVWrpWn0Sp2BWN71qkzR7e5ye8WuMmwyOjg==",
"license": "MIT",
"dependencies": {
"@tanstack/table-core": "8.19.2"
},
"engines": {
"node": ">=12"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": ">=16.8",
"react-dom": ">=16.8"
}
},
"node_modules/@tanstack/table-core": {
"version": "8.19.2",
"resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.19.2.tgz",
"integrity": "sha512-KpRjhgehIhbfH78ARm/GJDXGnpdw4bCg3qas6yjWSi7czJhI/J6pWln7NHtmBkGE9ZbohiiNtLqwGzKmBfixig==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@testing-library/dom": { "node_modules/@testing-library/dom": {
"version": "10.0.0", "version": "10.0.0",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.0.0.tgz", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.0.0.tgz",
@ -3714,13 +3746,13 @@
"version": "15.7.12", "version": "15.7.12",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz",
"integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==",
"devOptional": true "dev": true
}, },
"node_modules/@types/react": { "node_modules/@types/react": {
"version": "18.3.3", "version": "18.3.3",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz",
"integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==",
"devOptional": true, "dev": true,
"dependencies": { "dependencies": {
"@types/prop-types": "*", "@types/prop-types": "*",
"csstype": "^3.0.2" "csstype": "^3.0.2"
@ -3735,15 +3767,6 @@
"@types/react": "*" "@types/react": "*"
} }
}, },
"node_modules/@types/react-table": {
"version": "7.7.20",
"resolved": "https://registry.npmjs.org/@types/react-table/-/react-table-7.7.20.tgz",
"integrity": "sha512-ahMp4pmjVlnExxNwxyaDrFgmKxSbPwU23sGQw2gJK4EhCvnvmib2s/O/+y1dfV57dXOwpr2plfyBol+vEHbi2w==",
"dev": true,
"dependencies": {
"@types/react": "*"
}
},
"node_modules/@types/semver": { "node_modules/@types/semver": {
"version": "7.5.8", "version": "7.5.8",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
@ -8958,19 +8981,6 @@
} }
} }
}, },
"node_modules/react-table": {
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/react-table/-/react-table-7.8.0.tgz",
"integrity": "sha512-hNaz4ygkZO4bESeFfnfOft73iBUj8K5oKi1EcSHPAibEydfsX2MyU6Z8KCr3mv3C9Kqqh71U+DhZkFvibbnPbA==",
"dev": true,
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": "^16.8.3 || ^17.0.0-0 || ^18.0.0"
}
},
"node_modules/react-textarea-autosize": { "node_modules/react-textarea-autosize": {
"version": "8.5.3", "version": "8.5.3",
"resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.5.3.tgz", "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.5.3.tgz",

@ -20,6 +20,7 @@
"@mantine/modals": "^7.11.0", "@mantine/modals": "^7.11.0",
"@mantine/notifications": "^7.11.0", "@mantine/notifications": "^7.11.0",
"@tanstack/react-query": "^5.40.1", "@tanstack/react-query": "^5.40.1",
"@tanstack/react-table": "^8.19.2",
"axios": "^1.6.8", "axios": "^1.6.8",
"braces": "^3.0.3", "braces": "^3.0.3",
"react": "^18.3.1", "react": "^18.3.1",
@ -43,7 +44,6 @@
"@types/node": "^20.12.6", "@types/node": "^20.12.6",
"@types/react": "^18.3.3", "@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.0",
"@types/react-table": "^7.7.20",
"@vitejs/plugin-react": "^4.2.1", "@vitejs/plugin-react": "^4.2.1",
"@vitest/coverage-v8": "^1.4.0", "@vitest/coverage-v8": "^1.4.0",
"@vitest/ui": "^1.2.2", "@vitest/ui": "^1.2.2",
@ -61,7 +61,6 @@
"prettier": "^3.2.5", "prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^3.2.4", "prettier-plugin-organize-imports": "^3.2.4",
"pretty-quick": "^4.0.0", "pretty-quick": "^4.0.0",
"react-table": "^7.8.0",
"recharts": "^2.12.6", "recharts": "^2.12.6",
"sass": "^1.74.1", "sass": "^1.74.1",
"typescript": "^5.4.4", "typescript": "^5.4.4",

@ -1,5 +1,4 @@
import { FunctionComponent, useEffect, useMemo } from "react"; import { FunctionComponent, useEffect, useMemo } from "react";
import { Column } from "react-table";
import { import {
Button, Button,
Checkbox, Checkbox,
@ -17,10 +16,11 @@ import {
faTrash, faTrash,
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ColumnDef } from "@tanstack/react-table";
import { isString } from "lodash"; import { isString } from "lodash";
import { useMovieSubtitleModification } from "@/apis/hooks"; import { useMovieSubtitleModification } from "@/apis/hooks";
import { Action, Selector } from "@/components/inputs"; import { Action, Selector } from "@/components/inputs";
import { SimpleTable } from "@/components/tables"; import SimpleTable from "@/components/tables/SimpleTable";
import TextPopover from "@/components/TextPopover"; import TextPopover from "@/components/TextPopover";
import { useModals, withModal } from "@/modules/modals"; import { useModals, withModal } from "@/modules/modals";
import { task, TaskGroup } from "@/modules/task"; import { task, TaskGroup } from "@/modules/task";
@ -143,61 +143,77 @@ const MovieUploadForm: FunctionComponent<Props> = ({
}); });
}); });
const columns = useMemo<Column<SubtitleFile>[]>( const ValidateResultCell = ({
() => [ validateResult,
{ }: {
accessor: "validateResult", validateResult: SubtitleValidateResult | undefined;
Cell: ({ cell: { value } }) => { }) => {
const icon = useMemo(() => { const icon = useMemo(() => {
switch (value?.state) { switch (validateResult?.state) {
case "valid": case "valid":
return faCheck; return faCheck;
case "warning": case "warning":
return faInfoCircle; return faInfoCircle;
case "error": case "error":
return faTimes; return faTimes;
default: default:
return faCircleNotch; return faCircleNotch;
} }
}, [value?.state]); }, [validateResult?.state]);
const color = useMemo<MantineColor | undefined>(() => { const color = useMemo<MantineColor | undefined>(() => {
switch (value?.state) { switch (validateResult?.state) {
case "valid": case "valid":
return "green"; return "green";
case "warning": case "warning":
return "yellow"; return "yellow";
case "error": case "error":
return "red"; return "red";
default: default:
return undefined; return undefined;
} }
}, [value?.state]); }, [validateResult?.state]);
return ( return (
<TextPopover text={value?.messages}> <TextPopover text={validateResult?.messages}>
<Text c={color} inline> <Text c={color} inline>
<FontAwesomeIcon icon={icon}></FontAwesomeIcon> <FontAwesomeIcon icon={icon} />
</Text> </Text>
</TextPopover> </TextPopover>
); );
};
const columns = useMemo<ColumnDef<SubtitleFile>[]>(
() => [
{
id: "validateResult",
cell: ({
row: {
original: { validateResult },
},
}) => {
return <ValidateResultCell validateResult={validateResult} />;
}, },
}, },
{ {
Header: "File", header: "File",
id: "filename", id: "filename",
accessor: "file", accessorKey: "file",
Cell: ({ value }) => { cell: ({
return <Text className="table-primary">{value.name}</Text>; row: {
original: { file },
},
}) => {
return <Text className="table-primary">{file.name}</Text>;
}, },
}, },
{ {
Header: "Forced", header: "Forced",
accessor: "forced", accessorKey: "forced",
Cell: ({ row: { original, index }, value }) => { cell: ({ row: { original, index } }) => {
return ( return (
<Checkbox <Checkbox
checked={value} checked={original.forced}
onChange={({ currentTarget: { checked } }) => { onChange={({ currentTarget: { checked } }) => {
action.mutate(index, { ...original, forced: checked }); action.mutate(index, { ...original, forced: checked });
}} }}
@ -206,12 +222,12 @@ const MovieUploadForm: FunctionComponent<Props> = ({
}, },
}, },
{ {
Header: "HI", header: "HI",
accessor: "hi", accessorKey: "hi",
Cell: ({ row: { original, index }, value }) => { cell: ({ row: { original, index } }) => {
return ( return (
<Checkbox <Checkbox
checked={value} checked={original.hi}
onChange={({ currentTarget: { checked } }) => { onChange={({ currentTarget: { checked } }) => {
action.mutate(index, { ...original, hi: checked }); action.mutate(index, { ...original, hi: checked });
}} }}
@ -220,14 +236,14 @@ const MovieUploadForm: FunctionComponent<Props> = ({
}, },
}, },
{ {
Header: "Language", header: "Language",
accessor: "language", accessorKey: "language",
Cell: ({ row: { original, index }, value }) => { cell: ({ row: { original, index } }) => {
return ( return (
<Selector <Selector
{...languageOptions} {...languageOptions}
className="table-long-break" className="table-long-break"
value={value} value={original.language}
onChange={(item) => { onChange={(item) => {
action.mutate(index, { ...original, language: item }); action.mutate(index, { ...original, language: item });
}} }}
@ -237,8 +253,7 @@ const MovieUploadForm: FunctionComponent<Props> = ({
}, },
{ {
id: "action", id: "action",
accessor: "file", cell: ({ row: { index } }) => {
Cell: ({ row: { index } }) => {
return ( return (
<Action <Action
label="Remove" label="Remove"

@ -1,5 +1,4 @@
import { FunctionComponent, useCallback, useMemo } from "react"; import React, { FunctionComponent, useCallback, useMemo } from "react";
import { Column } from "react-table";
import { import {
Accordion, Accordion,
Button, Button,
@ -12,8 +11,10 @@ import {
} from "@mantine/core"; } from "@mantine/core";
import { useForm } from "@mantine/form"; import { useForm } from "@mantine/form";
import { faTrash } from "@fortawesome/free-solid-svg-icons"; import { faTrash } from "@fortawesome/free-solid-svg-icons";
import { Action, Selector, SelectorOption, SimpleTable } from "@/components"; import { ColumnDef } from "@tanstack/react-table";
import { Action, Selector, SelectorOption } from "@/components";
import ChipInput from "@/components/inputs/ChipInput"; import ChipInput from "@/components/inputs/ChipInput";
import SimpleTable from "@/components/tables/SimpleTable";
import { useModals, withModal } from "@/modules/modals"; import { useModals, withModal } from "@/modules/modals";
import { useArrayAction, useSelectorOptions } from "@/utilities"; import { useArrayAction, useSelectorOptions } from "@/utilities";
import { LOG } from "@/utilities/console"; import { LOG } from "@/utilities/console";
@ -145,76 +146,88 @@ const ProfileEditForm: FunctionComponent<Props> = ({
} }
}, [form, languages]); }, [form, languages]);
const columns = useMemo<Column<Language.ProfileItem>[]>( const LanguageCell = React.memo(
({ item, index }: { item: Language.ProfileItem; index: number }) => {
const code = useMemo(
() =>
languageOptions.options.find((l) => l.value.code2 === item.language)
?.value ?? null,
[item.language],
);
return (
<Selector
{...languageOptions}
className="table-select"
value={code}
onChange={(value) => {
if (value) {
item.language = value.code2;
action.mutate(index, { ...item, language: value.code2 });
}
}}
></Selector>
);
},
);
const SubtitleTypeCell = React.memo(
({ item, index }: { item: Language.ProfileItem; index: number }) => {
const selectValue = useMemo(() => {
if (item.forced === "True") {
return "forced";
} else if (item.hi === "True") {
return "hi";
} else {
return "normal";
}
}, [item.forced, item.hi]);
return (
<Select
value={selectValue}
data={subtitlesTypeOptions}
onChange={(value) => {
if (value) {
action.mutate(index, {
...item,
hi: value === "hi" ? "True" : "False",
forced: value === "forced" ? "True" : "False",
});
}
}}
></Select>
);
},
);
const columns = useMemo<ColumnDef<Language.ProfileItem>[]>(
() => [ () => [
{ {
Header: "ID", header: "ID",
accessor: "id", accessorKey: "id",
}, },
{ {
Header: "Language", header: "Language",
accessor: "language", accessorKey: "language",
Cell: ({ value: code, row: { original: item, index } }) => { cell: ({ row: { original: item, index } }) => {
const language = useMemo( return <LanguageCell item={item} index={index} />;
() =>
languageOptions.options.find((l) => l.value.code2 === code)
?.value ?? null,
[code],
);
return (
<Selector
{...languageOptions}
className="table-select"
value={language}
onChange={(value) => {
if (value) {
item.language = value.code2;
action.mutate(index, { ...item, language: value.code2 });
}
}}
></Selector>
);
}, },
}, },
{ {
Header: "Subtitles Type", header: "Subtitles Type",
accessor: "forced", accessorKey: "forced",
Cell: ({ row: { original: item, index }, value }) => { cell: ({ row: { original: item, index } }) => {
const selectValue = useMemo(() => { return <SubtitleTypeCell item={item} index={index} />;
if (item.forced === "True") {
return "forced";
} else if (item.hi === "True") {
return "hi";
} else {
return "normal";
}
}, [item.forced, item.hi]);
return (
<Select
value={selectValue}
data={subtitlesTypeOptions}
onChange={(value) => {
if (value) {
action.mutate(index, {
...item,
hi: value === "hi" ? "True" : "False",
forced: value === "forced" ? "True" : "False",
});
}
}}
></Select>
);
}, },
}, },
{ {
Header: "Exclude If Matching Audio", header: "Exclude If Matching Audio",
accessor: "audio_exclude", accessorKey: "audio_exclude",
Cell: ({ row: { original: item, index }, value }) => { cell: ({ row: { original: item, index } }) => {
return ( return (
<Checkbox <Checkbox
checked={value === "True"} checked={item.audio_exclude === "True"}
onChange={({ currentTarget: { checked } }) => { onChange={({ currentTarget: { checked } }) => {
action.mutate(index, { action.mutate(index, {
...item, ...item,
@ -228,8 +241,7 @@ const ProfileEditForm: FunctionComponent<Props> = ({
}, },
{ {
id: "action", id: "action",
accessor: "id", cell: ({ row }) => {
Cell: ({ row }) => {
return ( return (
<Action <Action
label="Remove" label="Remove"
@ -241,7 +253,7 @@ const ProfileEditForm: FunctionComponent<Props> = ({
}, },
}, },
], ],
[action, languageOptions], [action, LanguageCell, SubtitleTypeCell],
); );
return ( return (

@ -1,5 +1,4 @@
import { FunctionComponent, useEffect, useMemo } from "react"; import { FunctionComponent, useEffect, useMemo } from "react";
import { Column } from "react-table";
import { import {
Button, Button,
Checkbox, Checkbox,
@ -17,6 +16,7 @@ import {
faTrash, faTrash,
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ColumnDef } from "@tanstack/react-table";
import { isString } from "lodash"; import { isString } from "lodash";
import { import {
useEpisodesBySeriesId, useEpisodesBySeriesId,
@ -24,7 +24,7 @@ import {
useSubtitleInfos, useSubtitleInfos,
} from "@/apis/hooks"; } from "@/apis/hooks";
import { Action, Selector } from "@/components/inputs"; import { Action, Selector } from "@/components/inputs";
import { SimpleTable } from "@/components/tables"; import SimpleTable from "@/components/tables/SimpleTable";
import TextPopover from "@/components/TextPopover"; import TextPopover from "@/components/TextPopover";
import { useModals, withModal } from "@/modules/modals"; import { useModals, withModal } from "@/modules/modals";
import { task, TaskGroup } from "@/modules/task"; import { task, TaskGroup } from "@/modules/task";
@ -169,61 +169,79 @@ const SeriesUploadForm: FunctionComponent<Props> = ({
} }
}, [action, episodes.data, infos.data]); }, [action, episodes.data, infos.data]);
const columns = useMemo<Column<SubtitleFile>[]>( const ValidateResultCell = ({
() => [ validateResult,
{ }: {
accessor: "validateResult", validateResult: SubtitleValidateResult | undefined;
Cell: ({ cell: { value } }) => { }) => {
const icon = useMemo(() => { const icon = useMemo(() => {
switch (value?.state) { switch (validateResult?.state) {
case "valid": case "valid":
return faCheck; return faCheck;
case "warning": case "warning":
return faInfoCircle; return faInfoCircle;
case "error": case "error":
return faTimes; return faTimes;
default: default:
return faCircleNotch; return faCircleNotch;
} }
}, [value?.state]); }, [validateResult?.state]);
const color = useMemo<MantineColor | undefined>(() => { const color = useMemo<MantineColor | undefined>(() => {
switch (value?.state) { switch (validateResult?.state) {
case "valid": case "valid":
return "green"; return "green";
case "warning": case "warning":
return "yellow"; return "yellow";
case "error": case "error":
return "red"; return "red";
default: default:
return undefined; return undefined;
} }
}, [value?.state]); }, [validateResult?.state]);
return ( return (
<TextPopover text={value?.messages}> <TextPopover text={validateResult?.messages}>
<Text color={color} inline> <Text c={color} inline>
<FontAwesomeIcon icon={icon}></FontAwesomeIcon> <FontAwesomeIcon icon={icon}></FontAwesomeIcon>
</Text> </Text>
</TextPopover> </TextPopover>
); );
};
const columns = useMemo<ColumnDef<SubtitleFile>[]>(
() => [
{
id: "validateResult",
cell: ({
row: {
original: { validateResult },
},
}) => {
return <ValidateResultCell validateResult={validateResult} />;
}, },
}, },
{ {
Header: "File", header: "File",
id: "filename", id: "filename",
accessor: "file", accessorKey: "file",
Cell: ({ value: { name } }) => { cell: ({
row: {
original: {
file: { name },
},
},
}) => {
return <Text className="table-primary">{name}</Text>; return <Text className="table-primary">{name}</Text>;
}, },
}, },
{ {
Header: "Forced", header: "Forced",
accessor: "forced", accessorKey: "forced",
Cell: ({ row: { original, index }, value }) => { cell: ({ row: { original, index } }) => {
return ( return (
<Checkbox <Checkbox
checked={value} checked={original.forced}
onChange={({ currentTarget: { checked } }) => { onChange={({ currentTarget: { checked } }) => {
action.mutate(index, { action.mutate(index, {
...original, ...original,
@ -236,12 +254,12 @@ const SeriesUploadForm: FunctionComponent<Props> = ({
}, },
}, },
{ {
Header: "HI", header: "HI",
accessor: "hi", accessorKey: "hi",
Cell: ({ row: { original, index }, value }) => { cell: ({ row: { original, index } }) => {
return ( return (
<Checkbox <Checkbox
checked={value} checked={original.hi}
onChange={({ currentTarget: { checked } }) => { onChange={({ currentTarget: { checked } }) => {
action.mutate(index, { action.mutate(index, {
...original, ...original,
@ -254,7 +272,7 @@ const SeriesUploadForm: FunctionComponent<Props> = ({
}, },
}, },
{ {
Header: ( header: () => (
<Selector <Selector
{...languageOptions} {...languageOptions}
value={null} value={null}
@ -269,13 +287,13 @@ const SeriesUploadForm: FunctionComponent<Props> = ({
}} }}
></Selector> ></Selector>
), ),
accessor: "language", accessorKey: "language",
Cell: ({ row: { original, index }, value }) => { cell: ({ row: { original, index } }) => {
return ( return (
<Selector <Selector
{...languageOptions} {...languageOptions}
className="table-select" className="table-select"
value={value} value={original.language}
onChange={(item) => { onChange={(item) => {
action.mutate(index, { ...original, language: item }); action.mutate(index, { ...original, language: item });
}} }}
@ -285,17 +303,17 @@ const SeriesUploadForm: FunctionComponent<Props> = ({
}, },
{ {
id: "episode", id: "episode",
Header: "Episode", header: "Episode",
accessor: "episode", accessorKey: "episode",
Cell: ({ value, row }) => { cell: ({ row: { original, index } }) => {
return ( return (
<Selector <Selector
{...episodeOptions} {...episodeOptions}
searchable searchable
className="table-select" className="table-select"
value={value} value={original.episode}
onChange={(item) => { onChange={(item) => {
action.mutate(row.index, { ...row.original, episode: item }); action.mutate(index, { ...original, episode: item });
}} }}
></Selector> ></Selector>
); );
@ -303,8 +321,7 @@ const SeriesUploadForm: FunctionComponent<Props> = ({
}, },
{ {
id: "action", id: "action",
accessor: "file", cell: ({ row: { index } }) => {
Cell: ({ row: { index } }) => {
return ( return (
<Action <Action
label="Remove" label="Remove"

@ -1,21 +1,21 @@
/* eslint-disable camelcase */ /* eslint-disable camelcase */
import { FunctionComponent, useMemo } from "react"; import { FunctionComponent, useMemo } from "react";
import { Column } from "react-table";
import { Badge, Center, Text } from "@mantine/core"; import { Badge, Center, Text } from "@mantine/core";
import { faFileExcel, faInfoCircle } from "@fortawesome/free-solid-svg-icons"; import { faFileExcel, faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ColumnDef } from "@tanstack/react-table";
import { import {
useEpisodeAddBlacklist, useEpisodeAddBlacklist,
useEpisodeHistory, useEpisodeHistory,
useMovieAddBlacklist, useMovieAddBlacklist,
useMovieHistory, useMovieHistory,
} from "@/apis/hooks"; } from "@/apis/hooks";
import { PageTable } from "@/components";
import MutateAction from "@/components/async/MutateAction"; import MutateAction from "@/components/async/MutateAction";
import QueryOverlay from "@/components/async/QueryOverlay"; import QueryOverlay from "@/components/async/QueryOverlay";
import { HistoryIcon } from "@/components/bazarr"; import { HistoryIcon } from "@/components/bazarr";
import Language from "@/components/bazarr/Language"; import Language from "@/components/bazarr/Language";
import StateIcon from "@/components/StateIcon"; import StateIcon from "@/components/StateIcon";
import PageTable from "@/components/tables/PageTable";
import TextPopover from "@/components/TextPopover"; import TextPopover from "@/components/TextPopover";
import { withModal } from "@/modules/modals"; import { withModal } from "@/modules/modals";
@ -30,24 +30,34 @@ const MovieHistoryView: FunctionComponent<MovieHistoryViewProps> = ({
const { data } = history; const { data } = history;
const columns = useMemo<Column<History.Movie>[]>( const addMovieToBlacklist = useMovieAddBlacklist();
const columns = useMemo<ColumnDef<History.Movie>[]>(
() => [ () => [
{ {
accessor: "action", id: "action",
Cell: (row) => ( cell: ({
row: {
original: { action },
},
}) => (
<Center> <Center>
<HistoryIcon action={row.value}></HistoryIcon> <HistoryIcon action={action}></HistoryIcon>
</Center> </Center>
), ),
}, },
{ {
Header: "Language", header: "Language",
accessor: "language", accessorKey: "language",
Cell: ({ value }) => { cell: ({
if (value) { row: {
original: { language },
},
}) => {
if (language) {
return ( return (
<Badge> <Badge>
<Language.Text value={value} long></Language.Text> <Language.Text value={language} long></Language.Text>
</Badge> </Badge>
); );
} else { } else {
@ -56,17 +66,20 @@ const MovieHistoryView: FunctionComponent<MovieHistoryViewProps> = ({
}, },
}, },
{ {
Header: "Provider", header: "Provider",
accessor: "provider", accessorKey: "provider",
}, },
{ {
Header: "Score", header: "Score",
accessor: "score", accessorKey: "score",
}, },
{ {
accessor: "matches", id: "matches",
Cell: (row) => { cell: ({
const { matches, dont_matches: dont } = row.row.original; row: {
original: { matches, dont_matches: dont },
},
}) => {
if (matches.length || dont.length) { if (matches.length || dont.length) {
return ( return (
<StateIcon <StateIcon
@ -81,31 +94,42 @@ const MovieHistoryView: FunctionComponent<MovieHistoryViewProps> = ({
}, },
}, },
{ {
Header: "Date", header: "Date",
accessor: "timestamp", accessorKey: "timestamp",
Cell: ({ value, row }) => { cell: ({
row: {
original: { timestamp, parsed_timestamp: parsedTimestamp },
},
}) => {
return ( return (
<TextPopover text={row.original.parsed_timestamp}> <TextPopover text={parsedTimestamp}>
<Text>{value}</Text> <Text>{timestamp}</Text>
</TextPopover> </TextPopover>
); );
}, },
}, },
{ {
// Actions // Actions
accessor: "blacklisted", id: "blacklisted",
Cell: ({ row, value }) => { cell: ({
const add = useMovieAddBlacklist(); row: {
const { radarrId, provider, subs_id, language, subtitles_path } = original: {
row.original; blacklisted,
radarrId,
provider,
subs_id,
language,
subtitles_path,
},
},
}) => {
if (subs_id && provider && language) { if (subs_id && provider && language) {
return ( return (
<MutateAction <MutateAction
label="Add to Blacklist" label="Add to Blacklist"
disabled={value} disabled={blacklisted}
icon={faFileExcel} icon={faFileExcel}
mutation={add} mutation={addMovieToBlacklist}
args={() => ({ args={() => ({
id: radarrId, id: radarrId,
form: { form: {
@ -123,7 +147,7 @@ const MovieHistoryView: FunctionComponent<MovieHistoryViewProps> = ({
}, },
}, },
], ],
[], [addMovieToBlacklist],
); );
return ( return (
@ -153,24 +177,34 @@ const EpisodeHistoryView: FunctionComponent<EpisodeHistoryViewProps> = ({
const { data } = history; const { data } = history;
const columns = useMemo<Column<History.Episode>[]>( const addEpisodeToBlacklist = useEpisodeAddBlacklist();
const columns = useMemo<ColumnDef<History.Episode>[]>(
() => [ () => [
{ {
accessor: "action", id: "action",
Cell: (row) => ( cell: ({
row: {
original: { action },
},
}) => (
<Center> <Center>
<HistoryIcon action={row.value}></HistoryIcon> <HistoryIcon action={action}></HistoryIcon>
</Center> </Center>
), ),
}, },
{ {
Header: "Language", header: "Language",
accessor: "language", accessorKey: "language",
Cell: ({ value }) => { cell: ({
if (value) { row: {
original: { language },
},
}) => {
if (language) {
return ( return (
<Badge> <Badge>
<Language.Text value={value} long></Language.Text> <Language.Text value={language} long></Language.Text>
</Badge> </Badge>
); );
} else { } else {
@ -179,16 +213,16 @@ const EpisodeHistoryView: FunctionComponent<EpisodeHistoryViewProps> = ({
}, },
}, },
{ {
Header: "Provider", header: "Provider",
accessor: "provider", accessorKey: "provider",
}, },
{ {
Header: "Score", header: "Score",
accessor: "score", accessorKey: "score",
}, },
{ {
accessor: "matches", id: "matches",
Cell: (row) => { cell: (row) => {
const { matches, dont_matches: dont } = row.row.original; const { matches, dont_matches: dont } = row.row.original;
if (matches.length || dont.length) { if (matches.length || dont.length) {
return ( return (
@ -204,21 +238,29 @@ const EpisodeHistoryView: FunctionComponent<EpisodeHistoryViewProps> = ({
}, },
}, },
{ {
Header: "Date", header: "Date",
accessor: "timestamp", accessorKey: "timestamp",
Cell: ({ row, value }) => { cell: ({
row: {
original: { timestamp, parsed_timestamp: parsedTimestamp },
},
}) => {
return ( return (
<TextPopover text={row.original.parsed_timestamp}> <TextPopover text={parsedTimestamp}>
<Text>{value}</Text> <Text>{timestamp}</Text>
</TextPopover> </TextPopover>
); );
}, },
}, },
{ {
accessor: "description", id: "description",
Cell: ({ value }) => { cell: ({
row: {
original: { description },
},
}) => {
return ( return (
<TextPopover text={value}> <TextPopover text={description}>
<FontAwesomeIcon size="sm" icon={faInfoCircle}></FontAwesomeIcon> <FontAwesomeIcon size="sm" icon={faInfoCircle}></FontAwesomeIcon>
</TextPopover> </TextPopover>
); );
@ -226,25 +268,27 @@ const EpisodeHistoryView: FunctionComponent<EpisodeHistoryViewProps> = ({
}, },
{ {
// Actions // Actions
accessor: "blacklisted", id: "blacklisted",
Cell: ({ row, value }) => { cell: ({
const { row: {
sonarrEpisodeId, original: {
sonarrSeriesId, blacklisted,
provider, sonarrEpisodeId,
subs_id, sonarrSeriesId,
language, provider,
subtitles_path, subs_id,
} = row.original; language,
const add = useEpisodeAddBlacklist(); subtitles_path,
},
},
}) => {
if (subs_id && provider && language) { if (subs_id && provider && language) {
return ( return (
<MutateAction <MutateAction
label="Add to Blacklist" label="Add to Blacklist"
disabled={value} disabled={blacklisted}
icon={faFileExcel} icon={faFileExcel}
mutation={add} mutation={addEpisodeToBlacklist}
args={() => ({ args={() => ({
seriesId: sonarrSeriesId, seriesId: sonarrSeriesId,
episodeId: sonarrEpisodeId, episodeId: sonarrEpisodeId,
@ -263,7 +307,7 @@ const EpisodeHistoryView: FunctionComponent<EpisodeHistoryViewProps> = ({
}, },
}, },
], ],
[], [addEpisodeToBlacklist],
); );
return ( return (

@ -1,5 +1,4 @@
import { useCallback, useMemo, useState } from "react"; import React, { useCallback, useMemo, useState } from "react";
import { Column } from "react-table";
import { import {
Alert, Alert,
Anchor, Anchor,
@ -18,10 +17,12 @@ import {
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { UseQueryResult } from "@tanstack/react-query"; import { UseQueryResult } from "@tanstack/react-query";
import { ColumnDef } from "@tanstack/react-table";
import { isString } from "lodash"; import { isString } from "lodash";
import { Action, PageTable } from "@/components"; import { Action } from "@/components";
import Language from "@/components/bazarr/Language"; import Language from "@/components/bazarr/Language";
import StateIcon from "@/components/StateIcon"; import StateIcon from "@/components/StateIcon";
import PageTable from "@/components/tables/PageTable";
import { withModal } from "@/modules/modals"; import { withModal } from "@/modules/modals";
import { task, TaskGroup } from "@/modules/task"; import { task, TaskGroup } from "@/modules/task";
import { GetItemId } from "@/utilities"; import { GetItemId } from "@/utilities";
@ -51,23 +52,63 @@ function ManualSearchView<T extends SupportType>(props: Props<T>) {
void results.refetch(); void results.refetch();
}, [results]); }, [results]);
const columns = useMemo<Column<SearchResultType>[]>( const ReleaseInfoCell = React.memo(
({ releaseInfo }: { releaseInfo: string[] }) => {
const [open, setOpen] = useState(false);
const items = useMemo(
() => releaseInfo.slice(1).map((v, idx) => <Text key={idx}>{v}</Text>),
[releaseInfo],
);
if (releaseInfo.length === 0) {
return <Text c="dimmed">Cannot get release info</Text>;
}
return (
<Stack gap={0} onClick={() => setOpen((o) => !o)}>
<Text className="table-primary" span>
{releaseInfo[0]}
{releaseInfo.length > 1 && (
<FontAwesomeIcon
icon={faCaretDown}
rotation={open ? 180 : undefined}
></FontAwesomeIcon>
)}
</Text>
<Collapse in={open}>
<>{items}</>
</Collapse>
</Stack>
);
},
);
const columns = useMemo<ColumnDef<SearchResultType>[]>(
() => [ () => [
{ {
Header: "Score", header: "Score",
accessor: "score", accessorKey: "score",
Cell: ({ value }) => { cell: ({
return <Text className="table-no-wrap">{value}%</Text>; row: {
original: { score },
},
}) => {
return <Text className="table-no-wrap">{score}%</Text>;
}, },
}, },
{ {
Header: "Language", header: "Language",
accessor: "language", accessorKey: "language",
Cell: ({ row: { original }, value }) => { cell: ({
row: {
original: { language, hearing_impaired: hi, forced },
},
}) => {
const lang: Language.Info = { const lang: Language.Info = {
code2: value, code2: language,
hi: original.hearing_impaired === "True", hi: hi === "True",
forced: original.forced === "True", forced: forced === "True",
name: "", name: "",
}; };
return ( return (
@ -78,11 +119,15 @@ function ManualSearchView<T extends SupportType>(props: Props<T>) {
}, },
}, },
{ {
Header: "Provider", header: "Provider",
accessor: "provider", accessorKey: "provider",
Cell: (row) => { cell: ({
const value = row.value; row: {
const { url } = row.row.original; original: { provider, url },
},
}) => {
const value = provider;
if (url) { if (url) {
return ( return (
<Anchor <Anchor
@ -100,49 +145,31 @@ function ManualSearchView<T extends SupportType>(props: Props<T>) {
}, },
}, },
{ {
Header: "Release", header: "Release",
accessor: "release_info", accessorKey: "release_info",
Cell: ({ value }) => { cell: ({
const [open, setOpen] = useState(false); row: {
original: { release_info: releaseInfo },
const items = useMemo( },
() => value.slice(1).map((v, idx) => <Text key={idx}>{v}</Text>), }) => {
[value], return <ReleaseInfoCell releaseInfo={releaseInfo} />;
);
if (value.length === 0) {
return <Text c="dimmed">Cannot get release info</Text>;
}
return (
<Stack gap={0} onClick={() => setOpen((o) => !o)}>
<Text className="table-primary" span>
{value[0]}
{value.length > 1 && (
<FontAwesomeIcon
icon={faCaretDown}
rotation={open ? 180 : undefined}
></FontAwesomeIcon>
)}
</Text>
<Collapse in={open}>
<>{items}</>
</Collapse>
</Stack>
);
}, },
}, },
{ {
Header: "Uploader", header: "Uploader",
accessor: "uploader", accessorKey: "uploader",
Cell: ({ value }) => { cell: ({
return <Text className="table-no-wrap">{value ?? "-"}</Text>; row: {
original: { uploader },
},
}) => {
return <Text className="table-no-wrap">{uploader ?? "-"}</Text>;
}, },
}, },
{ {
Header: "Match", header: "Match",
accessor: "matches", accessorKey: "matches",
Cell: (row) => { cell: (row) => {
const { matches, dont_matches: dont } = row.row.original; const { matches, dont_matches: dont } = row.row.original;
return ( return (
<StateIcon <StateIcon
@ -154,9 +181,9 @@ function ManualSearchView<T extends SupportType>(props: Props<T>) {
}, },
}, },
{ {
Header: "Get", header: "Get",
accessor: "subtitle", accessorKey: "subtitle",
Cell: ({ row }) => { cell: ({ row }) => {
const result = row.original; const result = row.original;
return ( return (
<Action <Action
@ -180,7 +207,7 @@ function ManualSearchView<T extends SupportType>(props: Props<T>) {
}, },
}, },
], ],
[download, item], [download, item, ReleaseInfoCell],
); );
const bSceneNameAvailable = const bSceneNameAvailable =

@ -1,10 +1,17 @@
import { FunctionComponent, useMemo, useState } from "react"; import { FunctionComponent, useMemo, useState } from "react";
import { Column, useRowSelect } from "react-table"; import {
import { Badge, Button, Divider, Group, Stack, Text } from "@mantine/core"; Badge,
Button,
Checkbox,
Divider,
Group,
Stack,
Text,
} from "@mantine/core";
import { ColumnDef } from "@tanstack/react-table";
import Language from "@/components/bazarr/Language"; import Language from "@/components/bazarr/Language";
import SubtitleToolsMenu from "@/components/SubtitleToolsMenu"; import SubtitleToolsMenu from "@/components/SubtitleToolsMenu";
import { SimpleTable } from "@/components/tables"; import SimpleTable from "@/components/tables/SimpleTable";
import { useCustomSelection } from "@/components/tables/plugins";
import { withModal } from "@/modules/modals"; import { withModal } from "@/modules/modals";
import { isMovie } from "@/utilities"; import { isMovie } from "@/utilities";
@ -35,24 +42,53 @@ const SubtitleToolView: FunctionComponent<SubtitleToolViewProps> = ({
}) => { }) => {
const [selections, setSelections] = useState<TableColumnType[]>([]); const [selections, setSelections] = useState<TableColumnType[]>([]);
const columns: Column<TableColumnType>[] = useMemo<Column<TableColumnType>[]>( const columns = useMemo<ColumnDef<TableColumnType>[]>(
() => [ () => [
{ {
Header: "Language", id: "selection",
accessor: "raw_language", header: ({ table }) => {
Cell: ({ value }) => ( return (
<Checkbox
id="table-header-selection"
indeterminate={table.getIsSomeRowsSelected()}
checked={table.getIsAllRowsSelected()}
onChange={table.getToggleAllRowsSelectedHandler()}
></Checkbox>
);
},
cell: ({ row: { index, getIsSelected, getToggleSelectedHandler } }) => {
return (
<Checkbox
id={`table-cell-${index}`}
checked={getIsSelected()}
onChange={getToggleSelectedHandler()}
onClick={getToggleSelectedHandler()}
></Checkbox>
);
},
},
{
header: "Language",
accessorKey: "raw_language",
cell: ({
row: {
original: { raw_language: rawLanguage },
},
}) => (
<Badge color="secondary"> <Badge color="secondary">
<Language.Text value={value} long></Language.Text> <Language.Text value={rawLanguage} long></Language.Text>
</Badge> </Badge>
), ),
}, },
{ {
id: "file", id: "file",
Header: "File", header: "File",
accessor: "path", accessorKey: "path",
Cell: ({ value }) => { cell: ({
const path = value; row: {
original: { path },
},
}) => {
let idx = path.lastIndexOf("/"); let idx = path.lastIndexOf("/");
if (idx === -1) { if (idx === -1) {
@ -94,16 +130,15 @@ const SubtitleToolView: FunctionComponent<SubtitleToolViewProps> = ({
[payload], [payload],
); );
const plugins = [useRowSelect, useCustomSelection];
return ( return (
<Stack> <Stack>
<SimpleTable <SimpleTable
tableStyles={{ emptyText: "No external subtitles found" }} tableStyles={{ emptyText: "No external subtitles found" }}
plugins={plugins} enableRowSelection={(row) => CanSelectSubtitle(row.original)}
onRowSelectionChanged={(rows) =>
setSelections(rows.map((r) => r.original))
}
columns={columns} columns={columns}
onSelect={setSelections}
canSelect={CanSelectSubtitle}
data={data} data={data}
></SimpleTable> ></SimpleTable>
<Divider></Divider> <Divider></Divider>

@ -1,11 +1,17 @@
import { ReactNode, useMemo } from "react"; import React, { ReactNode, useMemo } from "react";
import { HeaderGroup, Row, TableInstance } from "react-table";
import { Box, Skeleton, Table, Text } from "@mantine/core"; import { Box, Skeleton, Table, Text } from "@mantine/core";
import {
flexRender,
Header,
Row,
Table as TableInstance,
} from "@tanstack/react-table";
import { useIsLoading } from "@/contexts"; import { useIsLoading } from "@/contexts";
import { usePageSize } from "@/utilities/storage"; import { usePageSize } from "@/utilities/storage";
import styles from "./BaseTable.module.scss"; import styles from "@/components/tables/BaseTable.module.scss";
export type BaseTableProps<T extends object> = TableInstance<T> & { export type BaseTableProps<T extends object> = {
instance: TableInstance<T>;
tableStyles?: TableStyleProps<T>; tableStyles?: TableStyleProps<T>;
}; };
@ -15,60 +21,57 @@ export interface TableStyleProps<T extends object> {
placeholder?: number; placeholder?: number;
hideHeader?: boolean; hideHeader?: boolean;
fixHeader?: boolean; fixHeader?: boolean;
headersRenderer?: (headers: HeaderGroup<T>[]) => JSX.Element[]; headersRenderer?: (headers: Header<T, unknown>[]) => React.JSX.Element[];
rowRenderer?: (row: Row<T>) => Nullable<JSX.Element>; rowRenderer?: (row: Row<T>) => Nullable<React.JSX.Element>;
} }
function DefaultHeaderRenderer<T extends object>( function DefaultHeaderRenderer<T extends object>(
headers: HeaderGroup<T>[], headers: Header<T, unknown>[],
): JSX.Element[] { ): React.JSX.Element[] {
return headers.map((col) => ( return headers.map((header) => (
<Table.Th style={{ whiteSpace: "nowrap" }} {...col.getHeaderProps()}> <Table.Th style={{ whiteSpace: "nowrap" }} key={header.id}>
{col.render("Header")} {flexRender(header.column.columnDef.header, header.getContext())}
</Table.Th> </Table.Th>
)); ));
} }
function DefaultRowRenderer<T extends object>(row: Row<T>): JSX.Element | null { function DefaultRowRenderer<T extends object>(
row: Row<T>,
): React.JSX.Element | null {
return ( return (
<Table.Tr {...row.getRowProps()}> <Table.Tr key={row.id}>
{row.cells.map((cell) => ( {row.getVisibleCells().map((cell) => (
<Table.Td {...cell.getCellProps()}>{cell.render("Cell")}</Table.Td> <Table.Td key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</Table.Td>
))} ))}
</Table.Tr> </Table.Tr>
); );
} }
export default function BaseTable<T extends object>(props: BaseTableProps<T>) { export default function BaseTable<T extends object>(props: BaseTableProps<T>) {
const { const { instance, tableStyles } = props;
headerGroups,
rows: tableRows,
page: tablePages,
prepareRow,
getTableProps,
getTableBodyProps,
tableStyles,
} = props;
const headersRenderer = tableStyles?.headersRenderer ?? DefaultHeaderRenderer; const headersRenderer = tableStyles?.headersRenderer ?? DefaultHeaderRenderer;
const rowRenderer = tableStyles?.rowRenderer ?? DefaultRowRenderer; const rowRenderer = tableStyles?.rowRenderer ?? DefaultRowRenderer;
const colCount = useMemo(() => { const colCount = useMemo(() => {
return headerGroups.reduce( return instance
(prev, curr) => (curr.headers.length > prev ? curr.headers.length : prev), .getHeaderGroups()
0, .reduce(
); (prev, curr) =>
}, [headerGroups]); curr.headers.length > prev ? curr.headers.length : prev,
0,
);
}, [instance]);
// Switch to usePagination plugin if enabled const empty = instance.getRowCount() === 0;
const rows = tablePages ?? tableRows;
const empty = rows.length === 0;
const pageSize = usePageSize(); const pageSize = usePageSize();
const isLoading = useIsLoading(); const isLoading = useIsLoading();
let body: ReactNode; let body: ReactNode;
if (isLoading) { if (isLoading) {
body = Array(tableStyles?.placeholder ?? pageSize) body = Array(tableStyles?.placeholder ?? pageSize)
.fill(0) .fill(0)
@ -88,27 +91,22 @@ export default function BaseTable<T extends object>(props: BaseTableProps<T>) {
</Table.Tr> </Table.Tr>
); );
} else { } else {
body = rows.map((row) => { body = instance.getRowModel().rows.map((row) => {
prepareRow(row);
return rowRenderer(row); return rowRenderer(row);
}); });
} }
return ( return (
<Box className={styles.container}> <Box className={styles.container}>
<Table <Table className={styles.table} striped={tableStyles?.striped ?? true}>
className={styles.table}
striped={tableStyles?.striped ?? true}
{...getTableProps()}
>
<Table.Thead hidden={tableStyles?.hideHeader}> <Table.Thead hidden={tableStyles?.hideHeader}>
{headerGroups.map((headerGroup) => ( {instance.getHeaderGroups().map((headerGroup) => (
<Table.Tr {...headerGroup.getHeaderGroupProps()}> <Table.Tr key={headerGroup.id}>
{headersRenderer(headerGroup.headers)} {headersRenderer(headerGroup.headers)}
</Table.Tr> </Table.Tr>
))} ))}
</Table.Thead> </Table.Thead>
<Table.Tbody {...getTableBodyProps()}>{body}</Table.Tbody> <Table.Tbody>{body}</Table.Tbody>
</Table> </Table>
</Box> </Box>
); );

@ -1,38 +1,44 @@
import { import React, { Fragment } from "react";
Cell,
HeaderGroup,
Row,
useExpanded,
useGroupBy,
useSortBy,
} from "react-table";
import { Box, Table, Text } from "@mantine/core"; import { Box, Table, Text } from "@mantine/core";
import { faChevronCircleRight } from "@fortawesome/free-solid-svg-icons"; import { faChevronCircleRight } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import SimpleTable, { SimpleTableProps } from "./SimpleTable"; import {
Cell,
flexRender,
getExpandedRowModel,
getGroupedRowModel,
Header,
Row,
} from "@tanstack/react-table";
import SimpleTable, { SimpleTableProps } from "@/components/tables/SimpleTable";
function renderCell<T extends object = object>(cell: Cell<T>, row: Row<T>) { function renderCell<T extends object = object>(
if (cell.isGrouped) { cell: Cell<T, unknown>,
row: Row<T>,
) {
if (cell.getIsGrouped()) {
return ( return (
<div {...row.getToggleRowExpandedProps()}>{cell.render("Cell")}</div> <div>{flexRender(cell.column.columnDef.cell, cell.getContext())}</div>
); );
} else if (row.canExpand || cell.isAggregated) { } else if (row.getCanExpand() || cell.getIsAggregated()) {
return null; return null;
} else { } else {
return cell.render("Cell"); return flexRender(cell.column.columnDef.cell, cell.getContext());
} }
} }
function renderRow<T extends object>(row: Row<T>) { function renderRow<T extends object>(row: Row<T>) {
if (row.canExpand) { if (row.getCanExpand()) {
const cell = row.cells.find((cell) => cell.isGrouped); const cell = row.getVisibleCells().find((cell) => cell.getIsGrouped());
if (cell) { if (cell) {
const rotation = row.isExpanded ? 90 : undefined; const rotation = row.getIsExpanded() ? 90 : undefined;
return ( return (
<Table.Tr {...row.getRowProps()}> <Table.Tr key={row.id} style={{ cursor: "pointer" }}>
<Table.Td {...cell.getCellProps()} colSpan={row.cells.length}> <Table.Td key={cell.id} colSpan={row.getVisibleCells().length}>
<Text {...row.getToggleRowExpandedProps()} p={2}> <Text p={2} onClick={() => row.toggleExpanded()}>
{cell.render("Cell")} {flexRender(cell.column.columnDef.cell, cell.getContext())}
<Box component="span" mx={12}> <Box component="span" mx={12}>
<FontAwesomeIcon <FontAwesomeIcon
icon={faChevronCircleRight} icon={faChevronCircleRight}
@ -48,13 +54,12 @@ function renderRow<T extends object>(row: Row<T>) {
} }
} else { } else {
return ( return (
<Table.Tr {...row.getRowProps()}> <Table.Tr key={row.id}>
{row.cells {row
.filter((cell) => !cell.isPlaceholder) .getVisibleCells()
.filter((cell) => !cell.getIsPlaceholder())
.map((cell) => ( .map((cell) => (
<Table.Td {...cell.getCellProps()}> <Table.Td key={cell.id}>{renderCell(cell, row)}</Table.Td>
{renderCell(cell, row)}
</Table.Td>
))} ))}
</Table.Tr> </Table.Tr>
); );
@ -62,27 +67,34 @@ function renderRow<T extends object>(row: Row<T>) {
} }
function renderHeaders<T extends object>( function renderHeaders<T extends object>(
headers: HeaderGroup<T>[], headers: Header<T, unknown>[],
): JSX.Element[] { ): React.JSX.Element[] {
return headers return headers.map((header) => {
.filter((col) => !col.isGrouped) if (header.column.getIsGrouped()) {
.map((col) => ( return <Fragment key={header.id}></Fragment>;
<Table.Th {...col.getHeaderProps()}>{col.render("Header")}</Table.Th> }
));
return (
<Table.Th key={header.id} colSpan={header.colSpan}>
{flexRender(header.column.columnDef.header, header.getContext())}
</Table.Th>
);
});
} }
type Props<T extends object> = Omit< type Props<T extends object> = Omit<
SimpleTableProps<T>, SimpleTableProps<T>,
"plugins" | "headersRenderer" | "rowRenderer" "headersRenderer" | "rowRenderer"
>; >;
const plugins = [useGroupBy, useSortBy, useExpanded];
function GroupTable<T extends object = object>(props: Props<T>) { function GroupTable<T extends object = object>(props: Props<T>) {
return ( return (
<SimpleTable <SimpleTable
{...props} {...props}
plugins={plugins} enableGrouping
enableExpanding
getGroupedRowModel={getGroupedRowModel()}
getExpandedRowModel={getExpandedRowModel()}
tableStyles={{ headersRenderer: renderHeaders, rowRenderer: renderRow }} tableStyles={{ headersRenderer: renderHeaders, rowRenderer: renderRow }}
></SimpleTable> ></SimpleTable>
); );

@ -1,55 +1,62 @@
import { useEffect } from "react"; import { MutableRefObject, useEffect } from "react";
import { usePagination, useTable } from "react-table"; import {
getCoreRowModel,
getPaginationRowModel,
Table,
TableOptions,
useReactTable,
} from "@tanstack/react-table";
import BaseTable, { TableStyleProps } from "@/components/tables/BaseTable";
import { ScrollToTop } from "@/utilities"; import { ScrollToTop } from "@/utilities";
import { usePageSize } from "@/utilities/storage"; import { usePageSize } from "@/utilities/storage";
import BaseTable from "./BaseTable";
import PageControl from "./PageControl"; import PageControl from "./PageControl";
import { useDefaultSettings } from "./plugins";
import { SimpleTableProps } from "./SimpleTable";
type Props<T extends object> = SimpleTableProps<T> & { type Props<T extends object> = Omit<TableOptions<T>, "getCoreRowModel"> & {
instanceRef?: MutableRefObject<Table<T> | null>;
tableStyles?: TableStyleProps<T>;
autoScroll?: boolean; autoScroll?: boolean;
}; };
const tablePlugins = [useDefaultSettings, usePagination];
export default function PageTable<T extends object>(props: Props<T>) { export default function PageTable<T extends object>(props: Props<T>) {
const { autoScroll = true, plugins, instanceRef, ...options } = props; const { instanceRef, autoScroll, ...options } = props;
const instance = useTable( const pageSize = usePageSize();
options,
useDefaultSettings,
...tablePlugins,
...(plugins ?? []),
);
// use page size as specified in UI settings const instance = useReactTable({
instance.state.pageSize = usePageSize(); ...options,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
initialState: {
pagination: {
pageSize: pageSize,
},
},
});
if (instanceRef) { if (instanceRef) {
instanceRef.current = instance; instanceRef.current = instance;
} }
const pageIndex = instance.getState().pagination.pageIndex;
// Scroll to top when page is changed // Scroll to top when page is changed
useEffect(() => { useEffect(() => {
if (autoScroll) { if (autoScroll) {
ScrollToTop(); ScrollToTop();
} }
}, [instance.state.pageIndex, autoScroll]); }, [pageIndex, autoScroll]);
const state = instance.getState();
return ( return (
<> <>
<BaseTable <BaseTable {...options} instance={instance}></BaseTable>
{...options}
{...instance}
plugins={[...tablePlugins, ...(plugins ?? [])]}
></BaseTable>
<PageControl <PageControl
count={instance.pageCount} count={instance.getPageCount()}
index={instance.state.pageIndex} index={state.pagination.pageIndex}
size={instance.state.pageSize} size={pageSize}
total={instance.rows.length} total={instance.getRowCount()}
goto={instance.gotoPage} goto={instance.setPageIndex}
></PageControl> ></PageControl>
</> </>
); );

@ -1,9 +1,9 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { UsePaginationQueryResult } from "@/apis/queries/hooks"; import { UsePaginationQueryResult } from "@/apis/queries/hooks";
import SimpleTable, { SimpleTableProps } from "@/components/tables/SimpleTable";
import { LoadingProvider } from "@/contexts"; import { LoadingProvider } from "@/contexts";
import { ScrollToTop } from "@/utilities"; import { ScrollToTop } from "@/utilities";
import PageControl from "./PageControl"; import PageControl from "./PageControl";
import SimpleTable, { SimpleTableProps } from "./SimpleTable";
type Props<T extends object> = Omit<SimpleTableProps<T>, "data"> & { type Props<T extends object> = Omit<SimpleTableProps<T>, "data"> & {
query: UsePaginationQueryResult<T>; query: UsePaginationQueryResult<T>;

@ -1,23 +1,65 @@
import { PluginHook, TableInstance, TableOptions, useTable } from "react-table"; import { MutableRefObject, useEffect, useMemo } from "react";
import BaseTable, { TableStyleProps } from "./BaseTable"; import {
import { useDefaultSettings } from "./plugins"; getCoreRowModel,
Row,
Table,
TableOptions,
useReactTable,
} from "@tanstack/react-table";
import BaseTable, { TableStyleProps } from "@/components/tables/BaseTable";
import { usePageSize } from "@/utilities/storage";
export type SimpleTableProps<T extends object> = TableOptions<T> & { export type SimpleTableProps<T extends object> = Omit<
plugins?: PluginHook<T>[]; TableOptions<T>,
instanceRef?: React.MutableRefObject<TableInstance<T> | null>; "getCoreRowModel"
> & {
instanceRef?: MutableRefObject<Table<T> | null>;
tableStyles?: TableStyleProps<T>; tableStyles?: TableStyleProps<T>;
onRowSelectionChanged?: (selectedRows: Row<T>[]) => void;
onAllRowsExpandedChanged?: (isAllRowsExpanded: boolean) => void;
}; };
export default function SimpleTable<T extends object>( export default function SimpleTable<T extends object>(
props: SimpleTableProps<T>, props: SimpleTableProps<T>,
) { ) {
const { plugins, instanceRef, tableStyles, ...options } = props; const {
instanceRef,
tableStyles,
onRowSelectionChanged,
onAllRowsExpandedChanged,
...options
} = props;
const instance = useTable(options, useDefaultSettings, ...(plugins ?? [])); const pageSize = usePageSize();
const instance = useReactTable({
...options,
getCoreRowModel: getCoreRowModel(),
autoResetPageIndex: false,
autoResetExpanded: false,
pageCount: pageSize,
});
if (instanceRef) { if (instanceRef) {
instanceRef.current = instance; instanceRef.current = instance;
} }
return <BaseTable tableStyles={tableStyles} {...instance}></BaseTable>; const selectedRows = instance.getSelectedRowModel().rows;
const memoizedRows = useMemo(() => selectedRows, [selectedRows]);
// eslint-disable-next-line react-hooks/exhaustive-deps
const memoizedRowSelectionChanged = useMemo(() => onRowSelectionChanged, []);
const isAllRowsExpanded = instance.getIsAllRowsExpanded();
useEffect(() => {
memoizedRowSelectionChanged?.(memoizedRows);
}, [memoizedRowSelectionChanged, memoizedRows]);
useEffect(() => {
onAllRowsExpandedChanged?.(isAllRowsExpanded);
}, [onAllRowsExpandedChanged, isAllRowsExpanded]);
return <BaseTable tableStyles={tableStyles} instance={instance}></BaseTable>;
} }

@ -1,2 +0,0 @@
export { default as useCustomSelection } from "./useCustomSelection";
export { default as useDefaultSettings } from "./useDefaultSettings";

@ -1,113 +0,0 @@
import { forwardRef, useEffect, useRef } from "react";
import {
CellProps,
Column,
ColumnInstance,
ensurePluginOrder,
HeaderProps,
Hooks,
MetaBase,
TableInstance,
TableToggleCommonProps,
} from "react-table";
import { Checkbox as MantineCheckbox } from "@mantine/core";
const pluginName = "useCustomSelection";
const checkboxId = "---selection---";
interface CheckboxProps {
idIn: string;
disabled?: boolean;
}
const Checkbox = forwardRef<
HTMLInputElement,
TableToggleCommonProps & CheckboxProps
>(({ indeterminate, checked, disabled, idIn, ...rest }, ref) => {
const defaultRef = useRef<HTMLInputElement>(null);
const resolvedRef = ref || defaultRef;
useEffect(() => {
if (typeof resolvedRef === "object" && resolvedRef.current) {
resolvedRef.current.indeterminate = indeterminate ?? false;
if (disabled) {
resolvedRef.current.checked = false;
} else {
resolvedRef.current.checked = checked ?? false;
}
}
}, [resolvedRef, indeterminate, checked, disabled]);
return (
<MantineCheckbox
key={idIn}
disabled={disabled}
ref={resolvedRef}
{...rest}
></MantineCheckbox>
);
});
function useCustomSelection<T extends object>(hooks: Hooks<T>) {
hooks.visibleColumns.push(visibleColumns);
hooks.useInstance.push(useInstance);
}
useCustomSelection.pluginName = pluginName;
function useInstance<T extends object>(instance: TableInstance<T>) {
const {
plugins,
rows,
onSelect,
canSelect,
state: { selectedRowIds },
} = instance;
ensurePluginOrder(plugins, ["useRowSelect"], pluginName);
useEffect(() => {
// Performance
let items = Object.keys(selectedRowIds).flatMap(
(v) => rows.find((n) => n.id === v)?.original ?? [],
);
if (canSelect) {
items = items.filter((v) => canSelect(v));
}
onSelect && onSelect(items);
}, [selectedRowIds, onSelect, rows, canSelect]);
}
function visibleColumns<T extends object>(
columns: ColumnInstance<T>[],
meta: MetaBase<T>,
): Column<T>[] {
const { instance } = meta;
const checkbox: Column<T> = {
id: checkboxId,
Header: ({ getToggleAllRowsSelectedProps }: HeaderProps<T>) => (
<Checkbox
idIn="table-header-selection"
{...getToggleAllRowsSelectedProps()}
></Checkbox>
),
Cell: ({ row }: CellProps<T>) => {
const canSelect = instance.canSelect;
const disabled = (canSelect && !canSelect(row.original)) ?? false;
return (
<Checkbox
idIn={`table-cell-${row.index}`}
disabled={disabled}
{...row.getToggleRowSelectedProps()}
></Checkbox>
);
},
};
return [checkbox, ...columns];
}
export default useCustomSelection;

@ -1,33 +0,0 @@
import { Hooks, TableOptions } from "react-table";
import { usePageSize } from "@/utilities/storage";
const pluginName = "useLocalSettings";
function useDefaultSettings<T extends object>(hooks: Hooks<T>) {
hooks.useOptions.push(useOptions);
}
useDefaultSettings.pluginName = pluginName;
function useOptions<T extends object>(options: TableOptions<T>) {
const pageSize = usePageSize();
if (options.autoResetPage === undefined) {
options.autoResetPage = false;
}
if (options.autoResetExpanded === undefined) {
options.autoResetExpanded = false;
}
if (options.initialState === undefined) {
options.initialState = {};
}
if (options.initialState.pageSize === undefined) {
options.initialState.pageSize = pageSize;
}
return options;
}
export default useDefaultSettings;

@ -1,56 +1,70 @@
import { FunctionComponent, useMemo } from "react"; import { FunctionComponent, useMemo } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { Column } from "react-table";
import { Anchor, Text } from "@mantine/core"; import { Anchor, Text } from "@mantine/core";
import { faTrash } from "@fortawesome/free-solid-svg-icons"; import { faTrash } from "@fortawesome/free-solid-svg-icons";
import { ColumnDef } from "@tanstack/react-table";
import { useMovieDeleteBlacklist } from "@/apis/hooks"; import { useMovieDeleteBlacklist } from "@/apis/hooks";
import { PageTable } from "@/components";
import MutateAction from "@/components/async/MutateAction"; import MutateAction from "@/components/async/MutateAction";
import Language from "@/components/bazarr/Language"; import Language from "@/components/bazarr/Language";
import PageTable from "@/components/tables/PageTable";
import TextPopover from "@/components/TextPopover"; import TextPopover from "@/components/TextPopover";
interface Props { interface Props {
blacklist: readonly Blacklist.Movie[]; blacklist: Blacklist.Movie[];
} }
const Table: FunctionComponent<Props> = ({ blacklist }) => { const Table: FunctionComponent<Props> = ({ blacklist }) => {
const columns = useMemo<Column<Blacklist.Movie>[]>( const remove = useMovieDeleteBlacklist();
const columns = useMemo<ColumnDef<Blacklist.Movie>[]>(
() => [ () => [
{ {
Header: "Name", header: "Name",
accessor: "title", accessorKey: "title",
Cell: (row) => { cell: ({
const target = `/movies/${row.row.original.radarrId}`; row: {
original: { radarrId },
},
}) => {
const target = `/movies/${radarrId}`;
return ( return (
<Anchor className="table-primary" component={Link} to={target}> <Anchor className="table-primary" component={Link} to={target}>
{row.value} {radarrId}
</Anchor> </Anchor>
); );
}, },
}, },
{ {
Header: "Language", header: "Language",
accessor: "language", accessorKey: "language",
Cell: ({ value }) => { cell: ({
if (value) { row: {
return <Language.Text value={value} long></Language.Text>; original: { language },
},
}) => {
if (language) {
return <Language.Text value={language} long></Language.Text>;
} else { } else {
return null; return null;
} }
}, },
}, },
{ {
Header: "Provider", header: "Provider",
accessor: "provider", accessorKey: "provider",
}, },
{ {
Header: "Date", header: "Date",
accessor: "timestamp", accessorKey: "timestamp",
Cell: (row) => { cell: ({
if (row.value) { row: {
original: { timestamp, parsed_timestamp: parsedTimestamp },
},
}) => {
if (timestamp) {
return ( return (
<TextPopover text={row.row.original.parsed_timestamp}> <TextPopover text={parsedTimestamp}>
<Text>{row.value}</Text> <Text>{timestamp}</Text>
</TextPopover> </TextPopover>
); );
} else { } else {
@ -59,10 +73,12 @@ const Table: FunctionComponent<Props> = ({ blacklist }) => {
}, },
}, },
{ {
accessor: "subs_id", id: "subs_id",
Cell: ({ row, value }) => { cell: ({
const remove = useMovieDeleteBlacklist(); row: {
original: { subs_id: subsId, provider },
},
}) => {
return ( return (
<MutateAction <MutateAction
label="Remove from Blacklist" label="Remove from Blacklist"
@ -72,9 +88,9 @@ const Table: FunctionComponent<Props> = ({ blacklist }) => {
args={() => ({ args={() => ({
all: false, all: false,
form: { form: {
provider: row.original.provider, provider: provider,
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
subs_id: value, subs_id: subsId,
}, },
})} })}
></MutateAction> ></MutateAction>
@ -82,7 +98,7 @@ const Table: FunctionComponent<Props> = ({ blacklist }) => {
}, },
}, },
], ],
[], [remove],
); );
return ( return (
<PageTable <PageTable

@ -1,63 +1,77 @@
import { FunctionComponent, useMemo } from "react"; import { FunctionComponent, useMemo } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { Column } from "react-table";
import { Anchor, Text } from "@mantine/core"; import { Anchor, Text } from "@mantine/core";
import { faTrash } from "@fortawesome/free-solid-svg-icons"; import { faTrash } from "@fortawesome/free-solid-svg-icons";
import { ColumnDef } from "@tanstack/react-table";
import { useEpisodeDeleteBlacklist } from "@/apis/hooks"; import { useEpisodeDeleteBlacklist } from "@/apis/hooks";
import { PageTable } from "@/components";
import MutateAction from "@/components/async/MutateAction"; import MutateAction from "@/components/async/MutateAction";
import Language from "@/components/bazarr/Language"; import Language from "@/components/bazarr/Language";
import PageTable from "@/components/tables/PageTable";
import TextPopover from "@/components/TextPopover"; import TextPopover from "@/components/TextPopover";
interface Props { interface Props {
blacklist: readonly Blacklist.Episode[]; blacklist: Blacklist.Episode[];
} }
const Table: FunctionComponent<Props> = ({ blacklist }) => { const Table: FunctionComponent<Props> = ({ blacklist }) => {
const columns = useMemo<Column<Blacklist.Episode>[]>( const removeFromBlacklist = useEpisodeDeleteBlacklist();
const columns = useMemo<ColumnDef<Blacklist.Episode>[]>(
() => [ () => [
{ {
Header: "Series", header: "Series",
accessor: "seriesTitle", accessorKey: "seriesTitle",
Cell: (row) => { cell: ({
const target = `/series/${row.row.original.sonarrSeriesId}`; row: {
original: { sonarrSeriesId, seriesTitle },
},
}) => {
const target = `/series/${sonarrSeriesId}`;
return ( return (
<Anchor className="table-primary" component={Link} to={target}> <Anchor className="table-primary" component={Link} to={target}>
{row.value} {seriesTitle}
</Anchor> </Anchor>
); );
}, },
}, },
{ {
Header: "Episode", header: "Episode",
accessor: "episode_number", accessorKey: "episode_number",
}, },
{ {
accessor: "episodeTitle", id: "episodeTitle",
}, },
{ {
Header: "Language", header: "Language",
accessor: "language", accessorKey: "language",
Cell: ({ value }) => { cell: ({
if (value) { row: {
return <Language.Text value={value} long></Language.Text>; original: { language },
},
}) => {
if (language) {
return <Language.Text value={language} long></Language.Text>;
} else { } else {
return null; return null;
} }
}, },
}, },
{ {
Header: "Provider", header: "Provider",
accessor: "provider", accessorKey: "provider",
}, },
{ {
Header: "Date", header: "Date",
accessor: "timestamp", accessorKey: "timestamp",
Cell: (row) => { cell: ({
if (row.value) { row: {
original: { timestamp, parsed_timestamp: parsedTimestamp },
},
}) => {
if (timestamp) {
return ( return (
<TextPopover text={row.row.original.parsed_timestamp}> <TextPopover text={parsedTimestamp}>
<Text>{row.value}</Text> <Text>{timestamp}</Text>
</TextPopover> </TextPopover>
); );
} else { } else {
@ -66,22 +80,24 @@ const Table: FunctionComponent<Props> = ({ blacklist }) => {
}, },
}, },
{ {
accessor: "subs_id", id: "subs_id",
Cell: ({ row, value }) => { cell: ({
const remove = useEpisodeDeleteBlacklist(); row: {
original: { subs_id: subsId, provider },
},
}) => {
return ( return (
<MutateAction <MutateAction
label="Remove from Blacklist" label="Remove from Blacklist"
noReset noReset
icon={faTrash} icon={faTrash}
mutation={remove} mutation={removeFromBlacklist}
args={() => ({ args={() => ({
all: false, all: false,
form: { form: {
provider: row.original.provider, provider: provider,
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
subs_id: value, subs_id: subsId,
}, },
})} })}
></MutateAction> ></MutateAction>
@ -89,7 +105,7 @@ const Table: FunctionComponent<Props> = ({ blacklist }) => {
}, },
}, },
], ],
[], [removeFromBlacklist],
); );
return ( return (
<PageTable <PageTable

@ -21,6 +21,7 @@ import {
faSync, faSync,
faWrench, faWrench,
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { Table as TableInstance } from "@tanstack/table-core/build/lib/types";
import { import {
useEpisodesBySeriesId, useEpisodesBySeriesId,
useIsAnyActionRunning, useIsAnyActionRunning,
@ -41,12 +42,6 @@ import { useLanguageProfileBy } from "@/utilities/languages";
import Table from "./table"; import Table from "./table";
const SeriesEpisodesView: FunctionComponent = () => { const SeriesEpisodesView: FunctionComponent = () => {
const [state, setState] = useState({
expand: false,
buttonText: "Expand All",
initial: true,
});
const params = useParams(); const params = useParams();
const id = Number.parseInt(params.id as string); const id = Number.parseInt(params.id as string);
@ -102,18 +97,18 @@ const SeriesEpisodesView: FunctionComponent = () => {
useDocumentTitle(`${series?.title ?? "Unknown Series"} - Bazarr (Series)`); useDocumentTitle(`${series?.title ?? "Unknown Series"} - Bazarr (Series)`);
const tableRef = useRef<TableInstance<Item.Episode> | null>(null);
const [isAllRowExpanded, setIsAllRowExpanded] = useState(
tableRef?.current?.getIsAllRowsExpanded(),
);
const openDropzone = useRef<VoidFunction>(null); const openDropzone = useRef<VoidFunction>(null);
if (isNaN(id) || (isFetched && !series)) { if (isNaN(id) || (isFetched && !series)) {
return <Navigate to={RouterNames.NotFound}></Navigate>; return <Navigate to={RouterNames.NotFound}></Navigate>;
} }
const toggleState = () => {
state.expand
? setState({ expand: false, buttonText: "Expand All", initial: false })
: setState({ expand: true, buttonText: "Collapse All", initial: false });
};
return ( return (
<Container px={0} fluid> <Container px={0} fluid>
<QueryOverlay result={seriesQuery}> <QueryOverlay result={seriesQuery}>
@ -210,12 +205,14 @@ const SeriesEpisodesView: FunctionComponent = () => {
Edit Series Edit Series
</Toolbox.Button> </Toolbox.Button>
<Toolbox.Button <Toolbox.Button
icon={state.expand ? faCircleChevronRight : faCircleChevronDown} icon={
isAllRowExpanded ? faCircleChevronRight : faCircleChevronDown
}
onClick={() => { onClick={() => {
toggleState(); tableRef.current?.toggleAllRowsExpanded();
}} }}
> >
{state.buttonText} {isAllRowExpanded ? "Collapse All" : "Expand All"}
</Toolbox.Button> </Toolbox.Button>
</Group> </Group>
</Toolbox> </Toolbox>
@ -223,11 +220,11 @@ const SeriesEpisodesView: FunctionComponent = () => {
<ItemOverview item={series ?? null} details={details}></ItemOverview> <ItemOverview item={series ?? null} details={details}></ItemOverview>
<QueryOverlay result={episodesQuery}> <QueryOverlay result={episodesQuery}>
<Table <Table
expand={state.expand} ref={tableRef}
initial={state.initial}
episodes={episodes ?? null} episodes={episodes ?? null}
profile={profile} profile={profile}
disabled={hasTask || !series || series.profileId === null} disabled={hasTask || !series || series.profileId === null}
onAllRowsExpandedChanged={setIsAllRowExpanded}
></Table> ></Table>
</QueryOverlay> </QueryOverlay>
</Stack> </Stack>

@ -1,11 +1,4 @@
import { import React, { forwardRef, useCallback, useEffect, useMemo } from "react";
FunctionComponent,
useCallback,
useEffect,
useMemo,
useRef,
} from "react";
import { Column, TableInstance } from "react-table";
import { Group, Text } from "@mantine/core"; import { Group, Text } from "@mantine/core";
import { faBookmark as farBookmark } from "@fortawesome/free-regular-svg-icons"; import { faBookmark as farBookmark } from "@fortawesome/free-regular-svg-icons";
import { import {
@ -14,6 +7,7 @@ import {
faUser, faUser,
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ColumnDef, Table as TableInstance } from "@tanstack/react-table";
import { useDownloadEpisodeSubtitles, useEpisodesProvider } from "@/apis/hooks"; import { useDownloadEpisodeSubtitles, useEpisodesProvider } from "@/apis/hooks";
import { useShowOnlyDesired } from "@/apis/hooks/site"; import { useShowOnlyDesired } from "@/apis/hooks/site";
import { Action, GroupTable } from "@/components"; import { Action, GroupTable } from "@/components";
@ -30,219 +24,227 @@ interface Props {
episodes: Item.Episode[] | null; episodes: Item.Episode[] | null;
disabled?: boolean; disabled?: boolean;
profile?: Language.Profile; profile?: Language.Profile;
expand?: boolean; onAllRowsExpandedChanged: (isAllRowsExpanded: boolean) => void;
initial?: boolean;
} }
const Table: FunctionComponent<Props> = ({ const Table = forwardRef<TableInstance<Item.Episode> | null, Props>(
episodes, ({ episodes, profile, disabled, onAllRowsExpandedChanged }, ref) => {
profile, const onlyDesired = useShowOnlyDesired();
disabled,
expand, const tableRef =
initial, ref as React.MutableRefObject<TableInstance<Item.Episode> | null>;
}) => {
const onlyDesired = useShowOnlyDesired(); const profileItems = useProfileItemsToLanguages(profile);
const profileItems = useProfileItemsToLanguages(profile); const { mutateAsync } = useDownloadEpisodeSubtitles();
const { mutateAsync } = useDownloadEpisodeSubtitles();
const modals = useModals();
const download = useCallback(
(item: Item.Episode, result: SearchResultType) => { const download = useCallback(
const { (item: Item.Episode, result: SearchResultType) => {
language, const {
hearing_impaired: hi,
forced,
provider,
subtitle,
original_format: originalFormat,
} = result;
const { sonarrSeriesId: seriesId, sonarrEpisodeId: episodeId } = item;
return mutateAsync({
seriesId,
episodeId,
form: {
language, language,
hi, hearing_impaired: hi,
forced, forced,
provider, provider,
subtitle, subtitle,
// eslint-disable-next-line camelcase
original_format: originalFormat, original_format: originalFormat,
} = result;
const { sonarrSeriesId: seriesId, sonarrEpisodeId: episodeId } = item;
return mutateAsync({
seriesId,
episodeId,
form: {
language,
hi,
forced,
provider,
subtitle,
// eslint-disable-next-line camelcase
original_format: originalFormat,
},
});
},
[mutateAsync],
);
const SubtitlesCell = React.memo(
({ episode }: { episode: Item.Episode }) => {
const seriesId = episode.sonarrSeriesId;
const elements = useMemo(() => {
const episodeId = episode.sonarrEpisodeId;
const missing = episode.missing_subtitles.map((val, idx) => (
<Subtitle
missing
key={BuildKey(idx, val.code2, "missing")}
seriesId={seriesId}
episodeId={episodeId}
subtitle={val}
></Subtitle>
));
let rawSubtitles = episode.subtitles;
if (onlyDesired) {
rawSubtitles = filterSubtitleBy(rawSubtitles, profileItems);
}
const subtitles = rawSubtitles.map((val, idx) => (
<Subtitle
key={BuildKey(idx, val.code2, "valid")}
seriesId={seriesId}
episodeId={episodeId}
subtitle={val}
></Subtitle>
));
return [...missing, ...subtitles];
}, [episode, seriesId]);
return (
<Group gap="xs" wrap="nowrap">
{elements}
</Group>
);
},
);
const columns = useMemo<ColumnDef<Item.Episode>[]>(
() => [
{
id: "monitored",
cell: ({
row: {
original: { monitored },
},
}) => {
return (
<FontAwesomeIcon
title={monitored ? "monitored" : "unmonitored"}
icon={monitored ? faBookmark : farBookmark}
></FontAwesomeIcon>
);
},
}, },
}); {
}, header: "",
[mutateAsync], accessorKey: "season",
); cell: ({
row: {
const columns: Column<Item.Episode>[] = useMemo<Column<Item.Episode>[]>( original: { season },
() => [ },
{ }) => {
accessor: "monitored", return <Text span>Season {season}</Text>;
Cell: (row) => { },
return (
<FontAwesomeIcon
title={row.value ? "monitored" : "unmonitored"}
icon={row.value ? faBookmark : farBookmark}
></FontAwesomeIcon>
);
}, },
}, {
{ header: "Episode",
accessor: "season", accessorKey: "episode",
Cell: (row) => {
return <Text span>Season {row.value}</Text>;
}, },
}, {
{ header: "Title",
Header: "Episode", accessorKey: "title",
accessor: "episode", cell: ({
}, row: {
{ original: { sceneName, title },
Header: "Title", },
accessor: "title", }) => {
Cell: ({ value, row }) => { return (
return ( <TextPopover text={sceneName}>
<TextPopover text={row.original.sceneName}> <Text className="table-primary">{title}</Text>
<Text className="table-primary">{value}</Text> </TextPopover>
</TextPopover> );
); },
}, },
}, {
{ header: "Audio",
Header: "Audio", accessorKey: "audio_language",
accessor: "audio_language", cell: ({
Cell: ({ value }) => <AudioList audios={value}></AudioList>, row: {
}, original: { audio_language: audioLanguage },
{ },
Header: "Subtitles", }) => <AudioList audios={audioLanguage}></AudioList>,
accessor: "missing_subtitles",
Cell: ({ row }) => {
const episode = row.original;
const seriesId = episode.sonarrSeriesId;
const elements = useMemo(() => {
const episodeId = episode.sonarrEpisodeId;
const missing = episode.missing_subtitles.map((val, idx) => (
<Subtitle
missing
key={BuildKey(idx, val.code2, "missing")}
seriesId={seriesId}
episodeId={episodeId}
subtitle={val}
></Subtitle>
));
let rawSubtitles = episode.subtitles;
if (onlyDesired) {
rawSubtitles = filterSubtitleBy(rawSubtitles, profileItems);
}
const subtitles = rawSubtitles.map((val, idx) => (
<Subtitle
key={BuildKey(idx, val.code2, "valid")}
seriesId={seriesId}
episodeId={episodeId}
subtitle={val}
></Subtitle>
));
return [...missing, ...subtitles];
}, [episode, seriesId]);
return (
<Group gap="xs" wrap="nowrap">
{elements}
</Group>
);
}, },
}, {
{ header: "Subtitles",
Header: "Actions", accessorKey: "missing_subtitles",
accessor: "sonarrEpisodeId", cell: ({ row: { original } }) => {
Cell: ({ row }) => { return <SubtitlesCell episode={original} />;
const modals = useModals(); },
return (
<Group gap="xs" wrap="nowrap">
<Action
label="Manual Search"
disabled={disabled}
onClick={() => {
modals.openContextModal(EpisodeSearchModal, {
item: row.original,
download,
query: useEpisodesProvider,
});
}}
icon={faUser}
></Action>
<Action
label="History"
disabled={disabled}
onClick={() => {
modals.openContextModal(
EpisodeHistoryModal,
{
episode: row.original,
},
{
title: `History - ${row.original.title}`,
},
);
}}
icon={faHistory}
></Action>
</Group>
);
}, },
}, {
], header: "Actions",
[onlyDesired, profileItems, disabled, download], cell: ({ row }) => {
); return (
<Group gap="xs" wrap="nowrap">
const maxSeason = useMemo( <Action
() => label="Manual Search"
episodes?.reduce<number>( disabled={disabled}
(prev, curr) => Math.max(prev, curr.season), onClick={() => {
0, modals.openContextModal(EpisodeSearchModal, {
) ?? 0, item: row.original,
[episodes], download,
); query: useEpisodesProvider,
});
const instance = useRef<TableInstance<Item.Episode> | null>(null); }}
icon={faUser}
useEffect(() => { ></Action>
if (instance.current) { <Action
if (initial) { label="History"
// start with all rows collapsed disabled={disabled}
instance.current.toggleAllRowsExpanded(false); onClick={() => {
// expand the last/current season on initial display modals.openContextModal(
instance.current.toggleRowExpanded([`season:${maxSeason}`], true); EpisodeHistoryModal,
} else { {
if (expand !== undefined) { episode: row.original,
instance.current.toggleAllRowsExpanded(expand); },
} {
} title: `History - ${row.original.title}`,
} },
}, [maxSeason, expand, initial]); );
}}
return ( icon={faHistory}
<GroupTable ></Action>
columns={columns} </Group>
data={episodes ?? []} );
instanceRef={instance} },
initialState={{ },
sortBy: [ ],
{ id: "season", desc: true }, [disabled, download, modals, SubtitlesCell],
{ id: "episode", desc: true }, );
],
groupBy: ["season"], const maxSeason = useMemo(
}} () =>
tableStyles={{ emptyText: "No Episode Found For This Series" }} episodes?.reduce<number>(
></GroupTable> (prev, curr) => Math.max(prev, curr.season),
); 0,
}; ) ?? 0,
[episodes],
);
useEffect(() => {
tableRef?.current?.setExpanded(() => ({ [`season:${maxSeason}`]: true }));
}, [tableRef, maxSeason]);
return (
<GroupTable
columns={columns}
data={episodes ?? []}
instanceRef={tableRef}
onAllRowsExpandedChanged={onAllRowsExpandedChanged}
initialState={{
sorting: [
{ id: "season", desc: true },
{ id: "episode", desc: true },
],
grouping: ["season"],
}}
tableStyles={{ emptyText: "No Episode Found For This Series" }}
></GroupTable>
);
},
);
export default Table; export default Table;

@ -1,7 +1,6 @@
/* eslint-disable camelcase */ /* eslint-disable camelcase */
import { FunctionComponent, useMemo } from "react"; import { FunctionComponent, useMemo } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { Column } from "react-table";
import { Anchor, Badge, Text } from "@mantine/core"; import { Anchor, Badge, Text } from "@mantine/core";
import { import {
faFileExcel, faFileExcel,
@ -9,6 +8,7 @@ import {
faRecycle, faRecycle,
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ColumnDef } from "@tanstack/react-table";
import { useMovieAddBlacklist, useMovieHistoryPagination } from "@/apis/hooks"; import { useMovieAddBlacklist, useMovieHistoryPagination } from "@/apis/hooks";
import { MutateAction } from "@/components/async"; import { MutateAction } from "@/components/async";
import { HistoryIcon } from "@/components/bazarr"; import { HistoryIcon } from "@/components/bazarr";
@ -18,32 +18,40 @@ import TextPopover from "@/components/TextPopover";
import HistoryView from "@/pages/views/HistoryView"; import HistoryView from "@/pages/views/HistoryView";
const MoviesHistoryView: FunctionComponent = () => { const MoviesHistoryView: FunctionComponent = () => {
const columns: Column<History.Movie>[] = useMemo<Column<History.Movie>[]>( const addToBlacklist = useMovieAddBlacklist();
const columns = useMemo<ColumnDef<History.Movie>[]>(
() => [ () => [
{ {
accessor: "action", id: "action",
Cell: (row) => <HistoryIcon action={row.value}></HistoryIcon>, cell: ({ row }) => (
<HistoryIcon action={row.original.action}></HistoryIcon>
),
}, },
{ {
Header: "Name", header: "Name",
accessor: "title", accessorKey: "title",
Cell: ({ row, value }) => { cell: ({ row }) => {
const target = `/movies/${row.original.radarrId}`; const target = `/movies/${row.original.radarrId}`;
return ( return (
<Anchor className="table-primary" component={Link} to={target}> <Anchor className="table-primary" component={Link} to={target}>
{value} {row.original.title}
</Anchor> </Anchor>
); );
}, },
}, },
{ {
Header: "Language", header: "Language",
accessor: "language", accessorKey: "language",
Cell: ({ value }) => { cell: ({
if (value) { row: {
original: { language },
},
}) => {
if (language) {
return ( return (
<Badge> <Badge>
<Language.Text value={value} long></Language.Text> <Language.Text value={language} long></Language.Text>
</Badge> </Badge>
); );
} else { } else {
@ -52,13 +60,13 @@ const MoviesHistoryView: FunctionComponent = () => {
}, },
}, },
{ {
Header: "Score", header: "Score",
accessor: "score", accessorKey: "score",
}, },
{ {
Header: "Match", header: "Match",
accessor: "matches", accessorKey: "matches",
Cell: (row) => { cell: (row) => {
const { matches, dont_matches: dont } = row.row.original; const { matches, dont_matches: dont } = row.row.original;
if (matches.length || dont.length) { if (matches.length || dont.length) {
return ( return (
@ -74,13 +82,17 @@ const MoviesHistoryView: FunctionComponent = () => {
}, },
}, },
{ {
Header: "Date", header: "Date",
accessor: "timestamp", accessorKey: "timestamp",
Cell: (row) => { cell: ({
if (row.value) { row: {
original: { timestamp, parsed_timestamp },
},
}) => {
if (timestamp) {
return ( return (
<TextPopover text={row.row.original.parsed_timestamp}> <TextPopover text={parsed_timestamp}>
<Text>{row.value}</Text> <Text>{timestamp}</Text>
</TextPopover> </TextPopover>
); );
} else { } else {
@ -89,21 +101,29 @@ const MoviesHistoryView: FunctionComponent = () => {
}, },
}, },
{ {
Header: "Info", header: "Info",
accessor: "description", accessorKey: "description",
Cell: ({ value }) => { cell: ({
row: {
original: { description },
},
}) => {
return ( return (
<TextPopover text={value}> <TextPopover text={description}>
<FontAwesomeIcon size="sm" icon={faInfoCircle}></FontAwesomeIcon> <FontAwesomeIcon size="sm" icon={faInfoCircle}></FontAwesomeIcon>
</TextPopover> </TextPopover>
); );
}, },
}, },
{ {
Header: "Upgrade", header: "Upgrade",
accessor: "upgradable", accessorKey: "upgradable",
Cell: (row) => { cell: ({
if (row.value) { row: {
original: { upgradable },
},
}) => {
if (upgradable) {
return ( return (
<TextPopover text="This Subtitle File Is Eligible For An Upgrade."> <TextPopover text="This Subtitle File Is Eligible For An Upgrade.">
<FontAwesomeIcon size="sm" icon={faRecycle}></FontAwesomeIcon> <FontAwesomeIcon size="sm" icon={faRecycle}></FontAwesomeIcon>
@ -115,20 +135,25 @@ const MoviesHistoryView: FunctionComponent = () => {
}, },
}, },
{ {
Header: "Blacklist", header: "Blacklist",
accessor: "blacklisted", accessorKey: "blacklisted",
Cell: ({ row, value }) => { cell: ({ row }) => {
const add = useMovieAddBlacklist(); const {
const { radarrId, provider, subs_id, language, subtitles_path } = blacklisted,
row.original; radarrId,
provider,
subs_id,
language,
subtitles_path,
} = row.original;
if (subs_id && provider && language) { if (subs_id && provider && language) {
return ( return (
<MutateAction <MutateAction
label="Add to Blacklist" label="Add to Blacklist"
disabled={value} disabled={blacklisted}
icon={faFileExcel} icon={faFileExcel}
mutation={add} mutation={addToBlacklist}
args={() => ({ args={() => ({
id: radarrId, id: radarrId,
form: { form: {
@ -146,7 +171,7 @@ const MoviesHistoryView: FunctionComponent = () => {
}, },
}, },
], ],
[], [addToBlacklist],
); );
const query = useMovieHistoryPagination(); const query = useMovieHistoryPagination();

@ -1,7 +1,6 @@
/* eslint-disable camelcase */ /* eslint-disable camelcase */
import { FunctionComponent, useMemo } from "react"; import { FunctionComponent, useMemo } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { Column } from "react-table";
import { Anchor, Badge, Text } from "@mantine/core"; import { Anchor, Badge, Text } from "@mantine/core";
import { import {
faFileExcel, faFileExcel,
@ -9,6 +8,7 @@ import {
faRecycle, faRecycle,
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ColumnDef } from "@tanstack/react-table";
import { import {
useEpisodeAddBlacklist, useEpisodeAddBlacklist,
useEpisodeHistoryPagination, useEpisodeHistoryPagination,
@ -21,44 +21,60 @@ import TextPopover from "@/components/TextPopover";
import HistoryView from "@/pages/views/HistoryView"; import HistoryView from "@/pages/views/HistoryView";
const SeriesHistoryView: FunctionComponent = () => { const SeriesHistoryView: FunctionComponent = () => {
const columns: Column<History.Episode>[] = useMemo<Column<History.Episode>[]>( const addToBlacklist = useEpisodeAddBlacklist();
const columns = useMemo<ColumnDef<History.Episode>[]>(
() => [ () => [
{ {
accessor: "action", id: "action",
Cell: ({ value }) => <HistoryIcon action={value}></HistoryIcon>, cell: ({ row: { original } }) => (
<HistoryIcon action={original.action}></HistoryIcon>
),
}, },
{ {
Header: "Series", header: "Series",
accessor: "seriesTitle", accessorKey: "seriesTitle",
Cell: (row) => { cell: ({
const target = `/series/${row.row.original.sonarrSeriesId}`; row: {
original: { seriesTitle, sonarrSeriesId },
},
}) => {
const target = `/series/${sonarrSeriesId}`;
return ( return (
<Anchor className="table-primary" component={Link} to={target}> <Anchor className="table-primary" component={Link} to={target}>
{row.value} {seriesTitle}
</Anchor> </Anchor>
); );
}, },
}, },
{ {
Header: "Episode", header: "Episode",
accessor: "episode_number", accessorKey: "episode_number",
}, },
{ {
Header: "Title", header: "Title",
accessor: "episodeTitle", accessorKey: "episodeTitle",
Cell: ({ value }) => { cell: ({
return <Text className="table-no-wrap">{value}</Text>; row: {
original: { episodeTitle },
},
}) => {
return <Text className="table-no-wrap">{episodeTitle}</Text>;
}, },
}, },
{ {
Header: "Language", header: "Language",
accessor: "language", accessorKey: "language",
Cell: ({ value }) => { cell: ({
if (value) { row: {
original: { language },
},
}) => {
if (language) {
return ( return (
<Badge color="secondary"> <Badge color="secondary">
<Language.Text value={value} long></Language.Text> <Language.Text value={language} long></Language.Text>
</Badge> </Badge>
); );
} else { } else {
@ -67,13 +83,13 @@ const SeriesHistoryView: FunctionComponent = () => {
}, },
}, },
{ {
Header: "Score", header: "Score",
accessor: "score", accessorKey: "score",
}, },
{ {
Header: "Match", header: "Match",
accessor: "matches", accessorKey: "matches",
Cell: (row) => { cell: (row) => {
const { matches, dont_matches: dont } = row.row.original; const { matches, dont_matches: dont } = row.row.original;
if (matches.length || dont.length) { if (matches.length || dont.length) {
return ( return (
@ -89,13 +105,17 @@ const SeriesHistoryView: FunctionComponent = () => {
}, },
}, },
{ {
Header: "Date", header: "Date",
accessor: "timestamp", accessorKey: "timestamp",
Cell: (row) => { cell: ({
if (row.value) { row: {
original: { timestamp, parsed_timestamp },
},
}) => {
if (timestamp) {
return ( return (
<TextPopover text={row.row.original.parsed_timestamp}> <TextPopover text={parsed_timestamp}>
<Text>{row.value}</Text> <Text>{timestamp}</Text>
</TextPopover> </TextPopover>
); );
} else { } else {
@ -104,21 +124,29 @@ const SeriesHistoryView: FunctionComponent = () => {
}, },
}, },
{ {
Header: "Info", header: "Info",
accessor: "description", accessorKey: "description",
Cell: ({ row, value }) => { cell: ({
row: {
original: { description },
},
}) => {
return ( return (
<TextPopover text={value}> <TextPopover text={description}>
<FontAwesomeIcon size="sm" icon={faInfoCircle}></FontAwesomeIcon> <FontAwesomeIcon size="sm" icon={faInfoCircle}></FontAwesomeIcon>
</TextPopover> </TextPopover>
); );
}, },
}, },
{ {
Header: "Upgrade", header: "Upgrade",
accessor: "upgradable", accessorKey: "upgradable",
Cell: (row) => { cell: ({
if (row.value) { row: {
original: { upgradable },
},
}) => {
if (upgradable) {
return ( return (
<TextPopover text="This Subtitle File Is Eligible For An Upgrade."> <TextPopover text="This Subtitle File Is Eligible For An Upgrade.">
<FontAwesomeIcon size="sm" icon={faRecycle}></FontAwesomeIcon> <FontAwesomeIcon size="sm" icon={faRecycle}></FontAwesomeIcon>
@ -130,9 +158,9 @@ const SeriesHistoryView: FunctionComponent = () => {
}, },
}, },
{ {
Header: "Blacklist", header: "Blacklist",
accessor: "blacklisted", accessorKey: "blacklisted",
Cell: ({ row, value }) => { cell: ({ row }) => {
const { const {
sonarrEpisodeId, sonarrEpisodeId,
sonarrSeriesId, sonarrSeriesId,
@ -140,16 +168,15 @@ const SeriesHistoryView: FunctionComponent = () => {
subs_id, subs_id,
language, language,
subtitles_path, subtitles_path,
blacklisted,
} = row.original; } = row.original;
const add = useEpisodeAddBlacklist();
if (subs_id && provider && language) { if (subs_id && provider && language) {
return ( return (
<MutateAction <MutateAction
label="Add to Blacklist" label="Add to Blacklist"
disabled={value} disabled={blacklisted}
icon={faFileExcel} icon={faFileExcel}
mutation={add} mutation={addToBlacklist}
args={() => ({ args={() => ({
seriesId: sonarrSeriesId, seriesId: sonarrSeriesId,
episodeId: sonarrEpisodeId, episodeId: sonarrEpisodeId,
@ -168,7 +195,7 @@ const SeriesHistoryView: FunctionComponent = () => {
}, },
}, },
], ],
[], [addToBlacklist],
); );
const query = useEpisodeHistoryPagination(); const query = useEpisodeHistoryPagination();

@ -1,13 +1,14 @@
import { FunctionComponent, useMemo } from "react"; import React, { FunctionComponent, useMemo } from "react";
import { Column } from "react-table";
import { Badge, Text, TextProps } from "@mantine/core"; import { Badge, Text, TextProps } from "@mantine/core";
import { faEllipsis, faSearch } from "@fortawesome/free-solid-svg-icons"; import { faEllipsis, faSearch } from "@fortawesome/free-solid-svg-icons";
import { ColumnDef } from "@tanstack/react-table";
import { isString } from "lodash"; import { isString } from "lodash";
import { useMovieSubtitleModification } from "@/apis/hooks"; import { useMovieSubtitleModification } from "@/apis/hooks";
import { useShowOnlyDesired } from "@/apis/hooks/site"; import { useShowOnlyDesired } from "@/apis/hooks/site";
import { Action, SimpleTable } from "@/components"; import { Action } from "@/components";
import Language from "@/components/bazarr/Language"; import Language from "@/components/bazarr/Language";
import SubtitleToolsMenu from "@/components/SubtitleToolsMenu"; import SubtitleToolsMenu from "@/components/SubtitleToolsMenu";
import SimpleTable from "@/components/tables/SimpleTable";
import { task, TaskGroup } from "@/modules/task"; import { task, TaskGroup } from "@/modules/task";
import { filterSubtitleBy, toPython } from "@/utilities"; import { filterSubtitleBy, toPython } from "@/utilities";
import { useProfileItemsToLanguages } from "@/utilities/languages"; import { useProfileItemsToLanguages } from "@/utilities/languages";
@ -33,35 +34,125 @@ const Table: FunctionComponent<Props> = ({ movie, profile, disabled }) => {
const profileItems = useProfileItemsToLanguages(profile); const profileItems = useProfileItemsToLanguages(profile);
const columns: Column<Subtitle>[] = useMemo<Column<Subtitle>[]>( const { download, remove } = useMovieSubtitleModification();
const CodeCell = React.memo(({ item }: { item: Subtitle }) => {
const { code2, path, hi, forced } = item;
const selections = useMemo(() => {
const list: FormType.ModifySubtitle[] = [];
if (path && !isSubtitleMissing(path) && movie !== null) {
list.push({
type: "movie",
path,
id: movie.radarrId,
language: code2,
forced: toPython(forced),
hi: toPython(hi),
});
}
return list;
}, [code2, path, forced, hi]);
if (movie === null) {
return null;
}
const { radarrId } = movie;
if (isSubtitleMissing(path)) {
return (
<Action
label="Search Subtitle"
icon={faSearch}
disabled={disabled}
onClick={() => {
task.create(
movie.title,
TaskGroup.SearchSubtitle,
download.mutateAsync,
{
radarrId,
form: {
language: code2,
forced,
hi,
},
},
);
}}
></Action>
);
}
return (
<SubtitleToolsMenu
selections={selections}
onAction={(action) => {
if (action === "delete" && path) {
task.create(
movie.title,
TaskGroup.DeleteSubtitle,
remove.mutateAsync,
{
radarrId,
form: {
language: code2,
forced,
hi,
path,
},
},
);
} else if (action === "search") {
throw new Error("This shouldn't happen, please report the bug");
}
}}
>
<Action
label="Subtitle Actions"
disabled={isSubtitleTrack(path)}
icon={faEllipsis}
></Action>
</SubtitleToolsMenu>
);
});
const columns = useMemo<ColumnDef<Subtitle>[]>(
() => [ () => [
{ {
Header: "Subtitle Path", header: "Subtitle Path",
accessor: "path", accessorKey: "path",
Cell: ({ value }) => { cell: ({
row: {
original: { path },
},
}) => {
const props: TextProps = { const props: TextProps = {
className: "table-primary", className: "table-primary",
}; };
if (isSubtitleTrack(value)) { if (isSubtitleTrack(path)) {
return ( return (
<Text className="table-primary">Video File Subtitle Track</Text> <Text className="table-primary">Video File Subtitle Track</Text>
); );
} else if (isSubtitleMissing(value)) { } else if (isSubtitleMissing(path)) {
return ( return (
<Text {...props} c="dimmed"> <Text {...props} c="dimmed">
{value} {path}
</Text> </Text>
); );
} else { } else {
return <Text {...props}>{value}</Text>; return <Text {...props}>{path}</Text>;
} }
}, },
}, },
{ {
Header: "Language", header: "Language",
accessor: "name", accessorKey: "name",
Cell: ({ row }) => { cell: ({ row }) => {
if (row.original.path === missingText) { if (row.original.path === missingText) {
return ( return (
<Badge color="primary"> <Badge color="primary">
@ -78,99 +169,13 @@ const Table: FunctionComponent<Props> = ({ movie, profile, disabled }) => {
}, },
}, },
{ {
accessor: "code2", id: "code2",
Cell: ({ row }) => { cell: ({ row: { original } }) => {
const { return <CodeCell item={original} />;
original: { code2, path, hi, forced },
} = row;
const { download, remove } = useMovieSubtitleModification();
const selections = useMemo(() => {
const list: FormType.ModifySubtitle[] = [];
if (path && !isSubtitleMissing(path) && movie !== null) {
list.push({
type: "movie",
path,
id: movie.radarrId,
language: code2,
forced: toPython(forced),
hi: toPython(hi),
});
}
return list;
}, [code2, path, forced, hi]);
if (movie === null) {
return null;
}
const { radarrId } = movie;
if (isSubtitleMissing(path)) {
return (
<Action
label="Search Subtitle"
icon={faSearch}
disabled={disabled}
onClick={() => {
task.create(
movie.title,
TaskGroup.SearchSubtitle,
download.mutateAsync,
{
radarrId,
form: {
language: code2,
forced,
hi,
},
},
);
}}
></Action>
);
}
return (
<SubtitleToolsMenu
selections={selections}
onAction={(action) => {
if (action === "delete" && path) {
task.create(
movie.title,
TaskGroup.DeleteSubtitle,
remove.mutateAsync,
{
radarrId,
form: {
language: code2,
forced,
hi,
path,
},
},
);
} else if (action === "search") {
throw new Error(
"This shouldn't happen, please report the bug",
);
}
}}
>
<Action
label="Subtitle Actions"
disabled={isSubtitleTrack(path)}
icon={faEllipsis}
></Action>
</SubtitleToolsMenu>
);
}, },
}, },
], ],
[movie, disabled], [CodeCell],
); );
const data: Subtitle[] = useMemo(() => { const data: Subtitle[] = useMemo(() => {

@ -1,6 +1,7 @@
import { FunctionComponent, useMemo } from "react"; import { FunctionComponent, useMemo } from "react";
import { Column } from "react-table"; import { Checkbox } from "@mantine/core";
import { useDocumentTitle } from "@mantine/hooks"; import { useDocumentTitle } from "@mantine/hooks";
import { ColumnDef } from "@tanstack/react-table";
import { useMovieModification, useMovies } from "@/apis/hooks"; import { useMovieModification, useMovies } from "@/apis/hooks";
import { QueryOverlay } from "@/components/async"; import { QueryOverlay } from "@/components/async";
import { AudioList } from "@/components/bazarr"; import { AudioList } from "@/components/bazarr";
@ -11,32 +12,63 @@ const MovieMassEditor: FunctionComponent = () => {
const query = useMovies(); const query = useMovies();
const mutation = useMovieModification(); const mutation = useMovieModification();
const columns = useMemo<Column<Item.Movie>[]>( useDocumentTitle("Movies - Bazarr (Mass Editor)");
const columns = useMemo<ColumnDef<Item.Movie>[]>(
() => [ () => [
{ {
Header: "Name", id: "selection",
accessor: "title", header: ({ table }) => {
return (
<Checkbox
id="table-header-selection"
indeterminate={table.getIsSomeRowsSelected()}
checked={table.getIsAllRowsSelected()}
onChange={table.getToggleAllRowsSelectedHandler()}
></Checkbox>
);
},
cell: ({ row: { index, getIsSelected, getToggleSelectedHandler } }) => {
return (
<Checkbox
id={`table-cell-${index}`}
checked={getIsSelected()}
onChange={getToggleSelectedHandler()}
onClick={getToggleSelectedHandler()}
></Checkbox>
);
},
},
{
header: "Name",
accessorKey: "title",
}, },
{ {
Header: "Audio", header: "Audio",
accessor: "audio_language", accessorKey: "audio_language",
Cell: ({ value }) => { cell: ({
return <AudioList audios={value}></AudioList>; row: {
original: { audio_language: audioLanguage },
},
}) => {
return <AudioList audios={audioLanguage}></AudioList>;
}, },
}, },
{ {
Header: "Languages Profile", header: "Languages Profile",
accessor: "profileId", accessorKey: "profileId",
Cell: ({ value }) => { cell: ({
return <LanguageProfileName index={value}></LanguageProfileName>; row: {
original: { profileId },
},
}) => {
return <LanguageProfileName index={profileId}></LanguageProfileName>;
}, },
}, },
], ],
[], [],
); );
useDocumentTitle("Movies - Bazarr (Mass Editor)");
return ( return (
<QueryOverlay result={query}> <QueryOverlay result={query}>
<MassEditor <MassEditor

@ -1,11 +1,11 @@
import { FunctionComponent, useMemo } from "react"; import { FunctionComponent, useMemo } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { Column } from "react-table";
import { Anchor, Badge, Container } from "@mantine/core"; import { Anchor, Badge, Container } from "@mantine/core";
import { useDocumentTitle } from "@mantine/hooks"; import { useDocumentTitle } from "@mantine/hooks";
import { faBookmark as farBookmark } from "@fortawesome/free-regular-svg-icons"; import { faBookmark as farBookmark } from "@fortawesome/free-regular-svg-icons";
import { faBookmark, faWrench } from "@fortawesome/free-solid-svg-icons"; import { faBookmark, faWrench } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ColumnDef } from "@tanstack/react-table";
import { useMovieModification, useMoviesPagination } from "@/apis/hooks"; import { useMovieModification, useMoviesPagination } from "@/apis/hooks";
import { Action } from "@/components"; import { Action } from "@/components";
import { AudioList } from "@/components/bazarr"; import { AudioList } from "@/components/bazarr";
@ -17,55 +17,81 @@ import ItemView from "@/pages/views/ItemView";
import { BuildKey } from "@/utilities"; import { BuildKey } from "@/utilities";
const MovieView: FunctionComponent = () => { const MovieView: FunctionComponent = () => {
const modifyMovie = useMovieModification();
const modals = useModals();
const query = useMoviesPagination(); const query = useMoviesPagination();
const columns: Column<Item.Movie>[] = useMemo<Column<Item.Movie>[]>( const columns = useMemo<ColumnDef<Item.Movie>[]>(
() => [ () => [
{ {
accessor: "monitored", id: "monitored",
Cell: ({ value }) => ( cell: ({
row: {
original: { monitored },
},
}) => (
<FontAwesomeIcon <FontAwesomeIcon
title={value ? "monitored" : "unmonitored"} title={monitored ? "monitored" : "unmonitored"}
icon={value ? faBookmark : farBookmark} icon={monitored ? faBookmark : farBookmark}
></FontAwesomeIcon> ></FontAwesomeIcon>
), ),
}, },
{ {
Header: "Name", header: "Name",
accessor: "title", accessorKey: "title",
Cell: ({ row, value }) => { cell: ({
const target = `/movies/${row.original.radarrId}`; row: {
original: { title, radarrId },
},
}) => {
const target = `/movies/${radarrId}`;
return ( return (
<Anchor className="table-primary" component={Link} to={target}> <Anchor className="table-primary" component={Link} to={target}>
{value} {title}
</Anchor> </Anchor>
); );
}, },
}, },
{ {
Header: "Audio", header: "Audio",
accessor: "audio_language", accessorKey: "audio_language",
Cell: ({ value }) => { cell: ({
return <AudioList audios={value}></AudioList>; row: {
original: { audio_language: audioLanguage },
},
}) => {
return <AudioList audios={audioLanguage}></AudioList>;
}, },
}, },
{ {
Header: "Languages Profile", header: "Languages Profile",
accessor: "profileId", accessorKey: "profileId",
Cell: ({ value }) => { cell: ({
row: {
original: { profileId },
},
}) => {
return ( return (
<LanguageProfileName index={value} empty=""></LanguageProfileName> <LanguageProfileName
index={profileId}
empty=""
></LanguageProfileName>
); );
}, },
}, },
{ {
Header: "Missing Subtitles", header: "Missing Subtitles",
accessor: "missing_subtitles", accessorKey: "missing_subtitles",
Cell: (row) => { cell: ({
const missing = row.value; row: {
original: { missing_subtitles: missingSubtitles },
},
}) => {
return ( return (
<> <>
{missing.map((v) => ( {missingSubtitles.map((v) => (
<Badge <Badge
mr="xs" mr="xs"
color="yellow" color="yellow"
@ -79,10 +105,8 @@ const MovieView: FunctionComponent = () => {
}, },
}, },
{ {
accessor: "radarrId", id: "radarrId",
Cell: ({ row }) => { cell: ({ row }) => {
const modals = useModals();
const mutation = useMovieModification();
return ( return (
<Action <Action
label="Edit Movie" label="Edit Movie"
@ -91,7 +115,7 @@ const MovieView: FunctionComponent = () => {
modals.openContextModal( modals.openContextModal(
ItemEditModal, ItemEditModal,
{ {
mutation, mutation: modifyMovie,
item: row.original, item: row.original,
}, },
{ {
@ -105,7 +129,7 @@ const MovieView: FunctionComponent = () => {
}, },
}, },
], ],
[], [modals, modifyMovie],
); );
useDocumentTitle("Movies - Bazarr"); useDocumentTitle("Movies - Bazarr");

@ -1,6 +1,7 @@
import { FunctionComponent, useMemo } from "react"; import { FunctionComponent, useMemo } from "react";
import { Column } from "react-table"; import { Checkbox } from "@mantine/core";
import { useDocumentTitle } from "@mantine/hooks"; import { useDocumentTitle } from "@mantine/hooks";
import { ColumnDef } from "@tanstack/react-table";
import { useSeries, useSeriesModification } from "@/apis/hooks"; import { useSeries, useSeriesModification } from "@/apis/hooks";
import { QueryOverlay } from "@/components/async"; import { QueryOverlay } from "@/components/async";
import { AudioList } from "@/components/bazarr"; import { AudioList } from "@/components/bazarr";
@ -11,24 +12,55 @@ const SeriesMassEditor: FunctionComponent = () => {
const query = useSeries(); const query = useSeries();
const mutation = useSeriesModification(); const mutation = useSeriesModification();
const columns = useMemo<Column<Item.Series>[]>( const columns = useMemo<ColumnDef<Item.Series>[]>(
() => [ () => [
{ {
Header: "Name", id: "selection",
accessor: "title", header: ({ table }) => {
return (
<Checkbox
id="table-header-selection"
indeterminate={table.getIsSomeRowsSelected()}
checked={table.getIsAllRowsSelected()}
onChange={table.getToggleAllRowsSelectedHandler()}
></Checkbox>
);
},
cell: ({ row: { index, getIsSelected, getToggleSelectedHandler } }) => {
return (
<Checkbox
id={`table-cell-${index}`}
checked={getIsSelected()}
onChange={getToggleSelectedHandler()}
onClick={getToggleSelectedHandler()}
></Checkbox>
);
},
},
{
header: "Name",
accessorKey: "title",
}, },
{ {
Header: "Audio", header: "Audio",
accessor: "audio_language", accessorKey: "audio_language",
Cell: ({ value }) => { cell: ({
return <AudioList audios={value}></AudioList>; row: {
original: { audio_language: audioLanguage },
},
}) => {
return <AudioList audios={audioLanguage}></AudioList>;
}, },
}, },
{ {
Header: "Languages Profile", header: "Languages Profile",
accessor: "profileId", accessorKey: "profileId",
Cell: ({ value }) => { cell: ({
return <LanguageProfileName index={value}></LanguageProfileName>; row: {
original: { profileId },
},
}) => {
return <LanguageProfileName index={profileId}></LanguageProfileName>;
}, },
}, },
], ],

@ -1,11 +1,11 @@
import { FunctionComponent, useMemo } from "react"; import { FunctionComponent, useMemo } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { Column } from "react-table";
import { Anchor, Container, Progress } from "@mantine/core"; import { Anchor, Container, Progress } from "@mantine/core";
import { useDocumentTitle } from "@mantine/hooks"; import { useDocumentTitle } from "@mantine/hooks";
import { faBookmark as farBookmark } from "@fortawesome/free-regular-svg-icons"; import { faBookmark as farBookmark } from "@fortawesome/free-regular-svg-icons";
import { faBookmark, faWrench } from "@fortawesome/free-solid-svg-icons"; import { faBookmark, faWrench } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ColumnDef } from "@tanstack/react-table";
import { useSeriesModification, useSeriesPagination } from "@/apis/hooks"; import { useSeriesModification, useSeriesPagination } from "@/apis/hooks";
import { Action } from "@/components"; import { Action } from "@/components";
import LanguageProfileName from "@/components/bazarr/LanguageProfile"; import LanguageProfileName from "@/components/bazarr/LanguageProfile";
@ -18,42 +18,51 @@ const SeriesView: FunctionComponent = () => {
const query = useSeriesPagination(); const query = useSeriesPagination();
const columns: Column<Item.Series>[] = useMemo<Column<Item.Series>[]>( const modals = useModals();
const columns = useMemo<ColumnDef<Item.Series>[]>(
() => [ () => [
{ {
accessor: "monitored", id: "monitored",
Cell: ({ value }) => ( cell: ({
row: {
original: { monitored },
},
}) => (
<FontAwesomeIcon <FontAwesomeIcon
title={value ? "monitored" : "unmonitored"} title={monitored ? "monitored" : "unmonitored"}
icon={value ? faBookmark : farBookmark} icon={monitored ? faBookmark : farBookmark}
></FontAwesomeIcon> ></FontAwesomeIcon>
), ),
}, },
{ {
Header: "Name", header: "Name",
accessor: "title", accessorKey: "title",
Cell: ({ row, value }) => { cell: ({ row: { original } }) => {
const target = `/series/${row.original.sonarrSeriesId}`; const target = `/series/${original.sonarrSeriesId}`;
return ( return (
<Anchor className="table-primary" component={Link} to={target}> <Anchor className="table-primary" component={Link} to={target}>
{value} {original.title}
</Anchor> </Anchor>
); );
}, },
}, },
{ {
Header: "Languages Profile", header: "Languages Profile",
accessor: "profileId", accessorKey: "profileId",
Cell: ({ value }) => { cell: ({ row: { original } }) => {
return ( return (
<LanguageProfileName index={value} empty=""></LanguageProfileName> <LanguageProfileName
index={original.profileId}
empty=""
></LanguageProfileName>
); );
}, },
}, },
{ {
Header: "Episodes", header: "Episodes",
accessor: "episodeFileCount", accessorKey: "episodeFileCount",
Cell: (row) => { cell: (row) => {
const { episodeFileCount, episodeMissingCount, profileId, title } = const { episodeFileCount, episodeMissingCount, profileId, title } =
row.row.original; row.row.original;
let progress = 0; let progress = 0;
@ -80,9 +89,8 @@ const SeriesView: FunctionComponent = () => {
}, },
}, },
{ {
accessor: "sonarrSeriesId", id: "sonarrSeriesId",
Cell: ({ row: { original } }) => { cell: ({ row: { original } }) => {
const modals = useModals();
return ( return (
<Action <Action
label="Edit Series" label="Edit Series"
@ -105,7 +113,7 @@ const SeriesView: FunctionComponent = () => {
}, },
}, },
], ],
[mutation], [mutation, modals],
); );
useDocumentTitle("Series - Bazarr"); useDocumentTitle("Series - Bazarr");

@ -1,11 +1,12 @@
import { FunctionComponent, useCallback, useMemo } from "react"; import { FunctionComponent, useCallback, useMemo } from "react";
import { Column } from "react-table";
import { Button, Checkbox } from "@mantine/core"; import { Button, Checkbox } from "@mantine/core";
import { faEquals, faTrash } from "@fortawesome/free-solid-svg-icons"; import { faEquals, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ColumnDef } from "@tanstack/react-table";
import { useLanguages } from "@/apis/hooks"; import { useLanguages } from "@/apis/hooks";
import { Action, SimpleTable } from "@/components"; import { Action } from "@/components";
import LanguageSelector from "@/components/bazarr/LanguageSelector"; import LanguageSelector from "@/components/bazarr/LanguageSelector";
import SimpleTable from "@/components/tables/SimpleTable";
import { languageEqualsKey } from "@/pages/Settings/keys"; import { languageEqualsKey } from "@/pages/Settings/keys";
import { useFormActions } from "@/pages/Settings/utilities/FormValues"; import { useFormActions } from "@/pages/Settings/utilities/FormValues";
import { useSettingValue } from "@/pages/Settings/utilities/hooks"; import { useSettingValue } from "@/pages/Settings/utilities/hooks";
@ -196,22 +197,22 @@ const EqualsTable: FunctionComponent<EqualsTableProps> = () => {
[equals, setEquals], [equals, setEquals],
); );
const columns = useMemo<Column<LanguageEqualData>[]>( const columns = useMemo<ColumnDef<LanguageEqualData>[]>(
() => [ () => [
{ {
Header: "Source", header: "Source",
id: "source-lang", id: "source-lang",
accessor: "source", accessorKey: "source",
Cell: ({ value: { content }, row }) => { cell: ({ row: { original, index } }) => {
return ( return (
<LanguageSelector <LanguageSelector
enabled enabled
value={content} value={original.source.content}
onChange={(result) => { onChange={(result) => {
if (result !== null) { if (result !== null) {
update(row.index, { update(index, {
...row.original, ...original,
source: { ...row.original.source, content: result }, source: { ...original.source, content: result },
}); });
} }
}} }}
@ -221,12 +222,11 @@ const EqualsTable: FunctionComponent<EqualsTableProps> = () => {
}, },
{ {
id: "source-hi", id: "source-hi",
accessor: "source", cell: ({ row }) => {
Cell: ({ value: { hi }, row }) => {
return ( return (
<Checkbox <Checkbox
label="HI" label="HI"
checked={hi} checked={row.original.source.hi}
onChange={({ currentTarget: { checked } }) => { onChange={({ currentTarget: { checked } }) => {
update(row.index, { update(row.index, {
...row.original, ...row.original,
@ -243,12 +243,11 @@ const EqualsTable: FunctionComponent<EqualsTableProps> = () => {
}, },
{ {
id: "source-forced", id: "source-forced",
accessor: "source", cell: ({ row }) => {
Cell: ({ value: { forced }, row }) => {
return ( return (
<Checkbox <Checkbox
label="Forced" label="Forced"
checked={forced} checked={row.original.source.forced}
onChange={({ currentTarget: { checked } }) => { onChange={({ currentTarget: { checked } }) => {
update(row.index, { update(row.index, {
...row.original, ...row.original,
@ -265,19 +264,18 @@ const EqualsTable: FunctionComponent<EqualsTableProps> = () => {
}, },
{ {
id: "equal-icon", id: "equal-icon",
Cell: () => { cell: () => {
return <FontAwesomeIcon icon={faEquals} />; return <FontAwesomeIcon icon={faEquals} />;
}, },
}, },
{ {
Header: "Target", header: "Target",
id: "target-lang", id: "target-lang",
accessor: "target", cell: ({ row }) => {
Cell: ({ value: { content }, row }) => {
return ( return (
<LanguageSelector <LanguageSelector
enabled enabled
value={content} value={row.original.target.content}
onChange={(result) => { onChange={(result) => {
if (result !== null) { if (result !== null) {
update(row.index, { update(row.index, {
@ -292,12 +290,11 @@ const EqualsTable: FunctionComponent<EqualsTableProps> = () => {
}, },
{ {
id: "target-hi", id: "target-hi",
accessor: "target", cell: ({ row }) => {
Cell: ({ value: { hi }, row }) => {
return ( return (
<Checkbox <Checkbox
label="HI" label="HI"
checked={hi} checked={row.original.target.hi}
onChange={({ currentTarget: { checked } }) => { onChange={({ currentTarget: { checked } }) => {
update(row.index, { update(row.index, {
...row.original, ...row.original,
@ -314,12 +311,11 @@ const EqualsTable: FunctionComponent<EqualsTableProps> = () => {
}, },
{ {
id: "target-forced", id: "target-forced",
accessor: "target", cell: ({ row }) => {
Cell: ({ value: { forced }, row }) => {
return ( return (
<Checkbox <Checkbox
label="Forced" label="Forced"
checked={forced} checked={row.original.target.forced}
onChange={({ currentTarget: { checked } }) => { onChange={({ currentTarget: { checked } }) => {
update(row.index, { update(row.index, {
...row.original, ...row.original,
@ -336,8 +332,7 @@ const EqualsTable: FunctionComponent<EqualsTableProps> = () => {
}, },
{ {
id: "action", id: "action",
accessor: "target", cell: ({ row }) => {
Cell: ({ row }) => {
return ( return (
<Action <Action
label="Remove" label="Remove"

@ -1,13 +1,14 @@
import { FunctionComponent, useCallback, useMemo } from "react"; import { FunctionComponent, useCallback, useMemo } from "react";
import { Column } from "react-table";
import { Badge, Button, Group } from "@mantine/core"; import { Badge, Button, Group } from "@mantine/core";
import { faTrash, faWrench } from "@fortawesome/free-solid-svg-icons"; import { faTrash, faWrench } from "@fortawesome/free-solid-svg-icons";
import { ColumnDef } from "@tanstack/react-table";
import { cloneDeep } from "lodash"; import { cloneDeep } from "lodash";
import { Action, SimpleTable } from "@/components"; import { Action } from "@/components";
import { import {
anyCutoff, anyCutoff,
ProfileEditModal, ProfileEditModal,
} from "@/components/forms/ProfileEditForm"; } from "@/components/forms/ProfileEditForm";
import SimpleTable from "@/components/tables/SimpleTable";
import { useModals } from "@/modules/modals"; import { useModals } from "@/modules/modals";
import { languageProfileKey } from "@/pages/Settings/keys"; import { languageProfileKey } from "@/pages/Settings/keys";
import { useFormActions } from "@/pages/Settings/utilities/FormValues"; import { useFormActions } from "@/pages/Settings/utilities/FormValues";
@ -40,6 +41,7 @@ const Table: FunctionComponent = () => {
const updateProfile = useCallback( const updateProfile = useCallback(
(profile: Language.Profile) => { (profile: Language.Profile) => {
const list = [...profiles]; const list = [...profiles];
const idx = list.findIndex((v) => v.profileId === profile.profileId); const idx = list.findIndex((v) => v.profileId === profile.profileId);
if (idx !== -1) { if (idx !== -1) {
@ -57,18 +59,20 @@ const Table: FunctionComponent = () => {
submitProfiles(fn(list)); submitProfiles(fn(list));
}); });
const columns = useMemo<Column<Language.Profile>[]>( const columns = useMemo<ColumnDef<Language.Profile>[]>(
() => [ () => [
{ {
Header: "Name", header: "Name",
accessor: "name", accessorKey: "name",
}, },
{ {
Header: "Languages", header: "Languages",
accessor: "items", accessorKey: "items",
Cell: (row) => { cell: ({
const items = row.value; row: {
const cutoff = row.row.original.cutoff; original: { items, cutoff },
},
}) => {
return ( return (
<Group gap="xs" wrap="nowrap"> <Group gap="xs" wrap="nowrap">
{items.map((v) => { {items.map((v) => {
@ -82,16 +86,19 @@ const Table: FunctionComponent = () => {
}, },
}, },
{ {
Header: "Must contain", header: "Must contain",
accessor: "mustContain", accessorKey: "mustContain",
Cell: (row) => { cell: ({
const items = row.value; row: {
if (!items) { original: { mustContain },
},
}) => {
if (!mustContain) {
return null; return null;
} }
return ( return (
<> <>
{items.map((v, idx) => { {mustContain.map((v, idx) => {
return ( return (
<Badge key={BuildKey(idx, v)} color="gray"> <Badge key={BuildKey(idx, v)} color="gray">
{v} {v}
@ -103,16 +110,19 @@ const Table: FunctionComponent = () => {
}, },
}, },
{ {
Header: "Must not contain", header: "Must not contain",
accessor: "mustNotContain", accessorKey: "mustNotContain",
Cell: (row) => { cell: ({
const items = row.value; row: {
if (!items) { original: { mustNotContain },
},
}) => {
if (!mustNotContain) {
return null; return null;
} }
return ( return (
<> <>
{items.map((v, idx) => { {mustNotContain.map((v, idx) => {
return ( return (
<Badge key={BuildKey(idx, v)} color="gray"> <Badge key={BuildKey(idx, v)} color="gray">
{v} {v}
@ -124,8 +134,8 @@ const Table: FunctionComponent = () => {
}, },
}, },
{ {
accessor: "profileId", id: "profileId",
Cell: ({ row }) => { cell: ({ row }) => {
const profile = row.original; const profile = row.original;
return ( return (
<Group gap="xs" wrap="nowrap"> <Group gap="xs" wrap="nowrap">
@ -160,7 +170,7 @@ const Table: FunctionComponent = () => {
return ( return (
<> <>
<SimpleTable columns={columns} data={profiles}></SimpleTable> <SimpleTable columns={columns} data={[...profiles]}></SimpleTable>
<Button <Button
fullWidth fullWidth
disabled={!canAdd} disabled={!canAdd}

@ -1,10 +1,11 @@
import { FunctionComponent, useCallback, useMemo } from "react"; import { FunctionComponent, useCallback, useMemo } from "react";
import { Column } from "react-table";
import { Button } from "@mantine/core"; import { Button } from "@mantine/core";
import { faArrowCircleRight, faTrash } from "@fortawesome/free-solid-svg-icons"; import { faArrowCircleRight, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ColumnDef } from "@tanstack/react-table";
import { capitalize } from "lodash"; import { capitalize } from "lodash";
import { Action, FileBrowser, SimpleTable } from "@/components"; import { Action, FileBrowser } from "@/components";
import SimpleTable from "@/components/tables/SimpleTable";
import { import {
moviesEnabledKey, moviesEnabledKey,
pathMappingsKey, pathMappingsKey,
@ -78,16 +79,16 @@ export const PathMappingTable: FunctionComponent<TableProps> = ({ type }) => {
updateRow(fn(data)); updateRow(fn(data));
}); });
const columns = useMemo<Column<PathMappingItem>[]>( const columns = useMemo<ColumnDef<PathMappingItem>[]>(
() => [ () => [
{ {
Header: capitalize(type), header: capitalize(type),
accessor: "from", accessorKey: "from",
Cell: ({ value, row: { original, index } }) => { cell: ({ row: { original, index } }) => {
return ( return (
<FileBrowser <FileBrowser
type={type} type={type}
defaultValue={value} defaultValue={original.from}
onChange={(path) => { onChange={(path) => {
action.mutate(index, { ...original, from: path }); action.mutate(index, { ...original, from: path });
}} }}
@ -97,17 +98,17 @@ export const PathMappingTable: FunctionComponent<TableProps> = ({ type }) => {
}, },
{ {
id: "arrow", id: "arrow",
Cell: () => ( cell: () => (
<FontAwesomeIcon icon={faArrowCircleRight}></FontAwesomeIcon> <FontAwesomeIcon icon={faArrowCircleRight}></FontAwesomeIcon>
), ),
}, },
{ {
Header: "Bazarr", header: "Bazarr",
accessor: "to", accessorKey: "to",
Cell: ({ value, row: { original, index } }) => { cell: ({ row: { original, index } }) => {
return ( return (
<FileBrowser <FileBrowser
defaultValue={value} defaultValue={original.to}
type="bazarr" type="bazarr"
onChange={(path) => { onChange={(path) => {
action.mutate(index, { ...original, to: path }); action.mutate(index, { ...original, to: path });
@ -118,8 +119,8 @@ export const PathMappingTable: FunctionComponent<TableProps> = ({ type }) => {
}, },
{ {
id: "action", id: "action",
accessor: "to", accessorKey: "to",
Cell: ({ row: { index } }) => { cell: ({ row: { index } }) => {
return ( return (
<Action <Action
label="Remove" label="Remove"

@ -1,65 +1,82 @@
import { FunctionComponent, useMemo } from "react"; import { FunctionComponent, useMemo } from "react";
import { Column } from "react-table";
import { Anchor, Text } from "@mantine/core"; import { Anchor, Text } from "@mantine/core";
import { faWindowClose } from "@fortawesome/free-solid-svg-icons"; import { faWindowClose } from "@fortawesome/free-solid-svg-icons";
import { ColumnDef } from "@tanstack/react-table";
import { useSystemAnnouncementsAddDismiss } from "@/apis/hooks"; import { useSystemAnnouncementsAddDismiss } from "@/apis/hooks";
import { SimpleTable } from "@/components";
import { MutateAction } from "@/components/async"; import { MutateAction } from "@/components/async";
import SimpleTable from "@/components/tables/SimpleTable";
interface Props { interface Props {
announcements: readonly System.Announcements[]; announcements: System.Announcements[];
} }
const Table: FunctionComponent<Props> = ({ announcements }) => { const Table: FunctionComponent<Props> = ({ announcements }) => {
const columns: Column<System.Announcements>[] = useMemo< const addDismiss = useSystemAnnouncementsAddDismiss();
Column<System.Announcements>[]
const columns: ColumnDef<System.Announcements>[] = useMemo<
ColumnDef<System.Announcements>[]
>( >(
() => [ () => [
{ {
Header: "Since", header: "Since",
accessor: "timestamp", accessor: "timestamp",
Cell: ({ value }) => { cell: ({
return <Text className="table-primary">{value}</Text>; row: {
original: { timestamp },
},
}) => {
return <Text className="table-primary">{timestamp}</Text>;
}, },
}, },
{ {
Header: "Announcement", header: "Announcement",
accessor: "text", accessor: "text",
Cell: ({ value }) => { cell: ({
return <Text className="table-primary">{value}</Text>; row: {
original: { text },
},
}) => {
return <Text className="table-primary">{text}</Text>;
}, },
}, },
{ {
Header: "More Info", header: "More Info",
accessor: "link", accessor: "link",
Cell: ({ value }) => { cell: ({
if (value) { row: {
return <Label link={value}>Link</Label>; original: { link },
},
}) => {
if (link) {
return <Label link={link}>Link</Label>;
} else { } else {
return <Text>n/a</Text>; return <Text>n/a</Text>;
} }
}, },
}, },
{ {
Header: "Dismiss", header: "Dismiss",
accessor: "hash", accessor: "hash",
Cell: ({ row, value }) => { cell: ({
const add = useSystemAnnouncementsAddDismiss(); row: {
original: { dismissible, hash },
},
}) => {
return ( return (
<MutateAction <MutateAction
label="Dismiss announcement" label="Dismiss announcement"
disabled={!row.original.dismissible} disabled={!dismissible}
icon={faWindowClose} icon={faWindowClose}
mutation={add} mutation={addDismiss}
args={() => ({ args={() => ({
hash: value, hash: hash,
})} })}
></MutateAction> ></MutateAction>
); );
}, },
}, },
], ],
[], [addDismiss],
); );
return ( return (

@ -1,53 +1,74 @@
import { FunctionComponent, useMemo } from "react"; import { FunctionComponent, useMemo } from "react";
import { Column } from "react-table";
import { Anchor, Text } from "@mantine/core"; import { Anchor, Text } from "@mantine/core";
import { faHistory, faTrash } from "@fortawesome/free-solid-svg-icons"; import { faHistory, faTrash } from "@fortawesome/free-solid-svg-icons";
import { ColumnDef } from "@tanstack/react-table";
import { useDeleteBackups, useRestoreBackups } from "@/apis/hooks"; import { useDeleteBackups, useRestoreBackups } from "@/apis/hooks";
import { Action, PageTable } from "@/components"; import { Action } from "@/components";
import PageTable from "@/components/tables/PageTable";
import { useModals } from "@/modules/modals"; import { useModals } from "@/modules/modals";
import { Environment } from "@/utilities"; import { Environment } from "@/utilities";
interface Props { interface Props {
backups: readonly System.Backups[]; backups: System.Backups[];
} }
const Table: FunctionComponent<Props> = ({ backups }) => { const Table: FunctionComponent<Props> = ({ backups }) => {
const columns: Column<System.Backups>[] = useMemo<Column<System.Backups>[]>( const modals = useModals();
const restore = useRestoreBackups();
const remove = useDeleteBackups();
const columns = useMemo<ColumnDef<System.Backups>[]>(
() => [ () => [
{ {
Header: "Name", header: "Name",
accessor: "filename", accessorKey: "filename",
Cell: ({ value }) => { cell: ({
row: {
original: { filename },
},
}) => {
return ( return (
<Anchor <Anchor
href={`${Environment.baseUrl}/system/backup/download/${value}`} href={`${Environment.baseUrl}/system/backup/download/${filename}`}
> >
{value} {filename}
</Anchor> </Anchor>
); );
}, },
}, },
{ {
Header: "Size", header: "Size",
accessor: "size", accessorKey: "size",
Cell: ({ value }) => { cell: ({
return <Text className="table-no-wrap">{value}</Text>; row: {
original: { size },
},
}) => {
return <Text className="table-no-wrap">{size}</Text>;
}, },
}, },
{ {
Header: "Time", header: "Time",
accessor: "date", accessorKey: "date",
Cell: ({ value }) => { cell: ({
return <Text className="table-no-wrap">{value}</Text>; row: {
original: { date },
},
}) => {
return <Text className="table-no-wrap">{date}</Text>;
}, },
}, },
{ {
id: "restore", id: "restore",
Header: "Restore", header: "Restore",
accessor: "filename", accessorKey: "filename",
Cell: ({ value }) => { cell: ({
const modals = useModals(); row: {
const restore = useRestoreBackups(); original: { filename },
},
}) => {
return ( return (
<Action <Action
label="Restore" label="Restore"
@ -56,14 +77,14 @@ const Table: FunctionComponent<Props> = ({ backups }) => {
title: "Restore Backup", title: "Restore Backup",
children: ( children: (
<Text size="sm"> <Text size="sm">
Are you sure you want to restore the backup ({value})? Are you sure you want to restore the backup ({filename})?
Bazarr will automatically restart and reload the UI during Bazarr will automatically restart and reload the UI during
the restore process. the restore process.
</Text> </Text>
), ),
labels: { confirm: "Restore", cancel: "Cancel" }, labels: { confirm: "Restore", cancel: "Cancel" },
confirmProps: { color: "red" }, confirmProps: { color: "red" },
onConfirm: () => restore.mutate(value), onConfirm: () => restore.mutate(filename),
}) })
} }
icon={faHistory} icon={faHistory}
@ -72,12 +93,14 @@ const Table: FunctionComponent<Props> = ({ backups }) => {
}, },
}, },
{ {
id: "delet4", id: "delete",
Header: "Delete", header: "Delete",
accessor: "filename", accessorKey: "filename",
Cell: ({ value }) => { cell: ({
const modals = useModals(); row: {
const remove = useDeleteBackups(); original: { filename },
},
}) => {
return ( return (
<Action <Action
label="Delete" label="Delete"
@ -87,12 +110,12 @@ const Table: FunctionComponent<Props> = ({ backups }) => {
title: "Delete Backup", title: "Delete Backup",
children: ( children: (
<Text size="sm"> <Text size="sm">
Are you sure you want to delete the backup ({value})? Are you sure you want to delete the backup ({filename})?
</Text> </Text>
), ),
labels: { confirm: "Delete", cancel: "Cancel" }, labels: { confirm: "Delete", cancel: "Cancel" },
confirmProps: { color: "red" }, confirmProps: { color: "red" },
onConfirm: () => remove.mutate(value), onConfirm: () => remove.mutate(filename),
}) })
} }
icon={faTrash} icon={faTrash}
@ -101,7 +124,7 @@ const Table: FunctionComponent<Props> = ({ backups }) => {
}, },
}, },
], ],
[], [modals, remove, restore],
); );
return <PageTable columns={columns} data={backups}></PageTable>; return <PageTable columns={columns} data={backups}></PageTable>;

@ -1,5 +1,4 @@
import { FunctionComponent, useMemo } from "react"; import { FunctionComponent, useMemo } from "react";
import { Column } from "react-table";
import { IconDefinition } from "@fortawesome/fontawesome-svg-core"; import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
import { import {
faBug, faBug,
@ -10,12 +9,14 @@ import {
faQuestion, faQuestion,
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Action, PageTable } from "@/components"; import { ColumnDef } from "@tanstack/react-table";
import { Action } from "@/components";
import PageTable from "@/components/tables/PageTable";
import { useModals } from "@/modules/modals"; import { useModals } from "@/modules/modals";
import SystemLogModal from "./modal"; import SystemLogModal from "./modal";
interface Props { interface Props {
logs: readonly System.Log[]; logs: System.Log[];
} }
function mapTypeToIcon(type: System.LogType): IconDefinition { function mapTypeToIcon(type: System.LogType): IconDefinition {
@ -34,33 +35,40 @@ function mapTypeToIcon(type: System.LogType): IconDefinition {
} }
const Table: FunctionComponent<Props> = ({ logs }) => { const Table: FunctionComponent<Props> = ({ logs }) => {
const columns: Column<System.Log>[] = useMemo<Column<System.Log>[]>( const modals = useModals();
const columns = useMemo<ColumnDef<System.Log>[]>(
() => [ () => [
{ {
accessor: "type", accessorKey: "type",
Cell: (row) => ( cell: ({
<FontAwesomeIcon icon={mapTypeToIcon(row.value)}></FontAwesomeIcon> row: {
), original: { type },
},
}) => <FontAwesomeIcon icon={mapTypeToIcon(type)}></FontAwesomeIcon>,
}, },
{ {
Header: "Message", Header: "Message",
accessor: "message", accessorKey: "message",
}, },
{ {
Header: "Date", Header: "Date",
accessor: "timestamp", accessorKey: "timestamp",
}, },
{ {
accessor: "exception", accessorKey: "exception",
Cell: ({ value }) => { cell: ({
const modals = useModals(); row: {
if (value) { original: { exception },
},
}) => {
if (exception) {
return ( return (
<Action <Action
label="Detail" label="Detail"
icon={faLayerGroup} icon={faLayerGroup}
onClick={() => onClick={() =>
modals.openContextModal(SystemLogModal, { stack: value }) modals.openContextModal(SystemLogModal, { stack: exception })
} }
></Action> ></Action>
); );
@ -70,7 +78,7 @@ const Table: FunctionComponent<Props> = ({ logs }) => {
}, },
}, },
], ],
[], [modals],
); );
return ( return (

@ -1,25 +1,25 @@
import { FunctionComponent, useMemo } from "react"; import { FunctionComponent, useMemo } from "react";
import { Column } from "react-table"; import { ColumnDef } from "@tanstack/react-table";
import { SimpleTable } from "@/components"; import SimpleTable from "@/components/tables/SimpleTable";
interface Props { interface Props {
providers: readonly System.Provider[]; providers: System.Provider[];
} }
const Table: FunctionComponent<Props> = (props) => { const Table: FunctionComponent<Props> = (props) => {
const columns: Column<System.Provider>[] = useMemo<Column<System.Provider>[]>( const columns = useMemo<ColumnDef<System.Provider>[]>(
() => [ () => [
{ {
Header: "Name", header: "Name",
accessor: "name", accessorKey: "name",
}, },
{ {
Header: "Status", header: "Status",
accessor: "status", accessorKey: "status",
}, },
{ {
Header: "Next Retry", header: "Next Retry",
accessor: "retry", accessorKey: "retry",
}, },
], ],
[], [],

@ -1,27 +1,35 @@
import { FunctionComponent, useMemo } from "react"; import { FunctionComponent, useMemo } from "react";
import { Column } from "react-table";
import { Text } from "@mantine/core"; import { Text } from "@mantine/core";
import { SimpleTable } from "@/components"; import { ColumnDef } from "@tanstack/react-table";
import SimpleTable from "@/components/tables/SimpleTable";
interface Props { interface Props {
health: readonly System.Health[]; health: System.Health[];
} }
const Table: FunctionComponent<Props> = ({ health }) => { const Table: FunctionComponent<Props> = ({ health }) => {
const columns: Column<System.Health>[] = useMemo<Column<System.Health>[]>( const columns = useMemo<ColumnDef<System.Health>[]>(
() => [ () => [
{ {
Header: "Object", header: "Object",
accessor: "object", accessorKey: "object",
Cell: ({ value }) => { cell: ({
return <Text className="table-no-wrap">{value}</Text>; row: {
original: { object },
},
}) => {
return <Text className="table-no-wrap">{object}</Text>;
}, },
}, },
{ {
Header: "Issue", header: "Issue",
accessor: "issue", accessorKey: "issue",
Cell: ({ value }) => { cell: ({
return <Text className="table-primary">{value}</Text>; row: {
original: { issue },
},
}) => {
return <Text className="table-primary">{issue}</Text>;
}, },
}, },
], ],

@ -1,48 +1,59 @@
import { FunctionComponent, useMemo } from "react"; import { FunctionComponent, useMemo } from "react";
import { Column, useSortBy } from "react-table";
import { Text } from "@mantine/core"; import { Text } from "@mantine/core";
import { faPlay } from "@fortawesome/free-solid-svg-icons"; import { faPlay } from "@fortawesome/free-solid-svg-icons";
import { ColumnDef, getSortedRowModel } from "@tanstack/react-table";
import { useRunTask } from "@/apis/hooks"; import { useRunTask } from "@/apis/hooks";
import { SimpleTable } from "@/components";
import MutateAction from "@/components/async/MutateAction"; import MutateAction from "@/components/async/MutateAction";
import SimpleTable from "@/components/tables/SimpleTable";
interface Props { interface Props {
tasks: readonly System.Task[]; tasks: System.Task[];
} }
const Table: FunctionComponent<Props> = ({ tasks }) => { const Table: FunctionComponent<Props> = ({ tasks }) => {
const columns: Column<System.Task>[] = useMemo<Column<System.Task>[]>( const runTask = useRunTask();
const columns: ColumnDef<System.Task>[] = useMemo<ColumnDef<System.Task>[]>(
() => [ () => [
{ {
Header: "Name", header: "Name",
accessor: "name", accessor: "name",
Cell: ({ value }) => { cell: ({
return <Text className="table-primary">{value}</Text>; row: {
original: { name },
},
}) => {
return <Text className="table-primary">{name}</Text>;
}, },
}, },
{ {
Header: "Interval", header: "Interval",
accessor: "interval", accessor: "interval",
Cell: ({ value }) => { cell: ({
return <Text className="table-no-wrap">{value}</Text>; row: {
original: { interval },
},
}) => {
return <Text className="table-no-wrap">{interval}</Text>;
}, },
}, },
{ {
Header: "Next Execution", header: "Next Execution",
accessor: "next_run_in", accessor: "next_run_in",
}, },
{ {
Header: "Run", header: "Run",
accessor: "job_running", accessor: "job_running",
Cell: ({ row, value }) => { cell: ({
const { job_id: jobId } = row.original; row: {
const runTask = useRunTask(); original: { job_id: jobId, job_running: jobRunning },
},
}) => {
return ( return (
<MutateAction <MutateAction
label="Run Job" label="Run Job"
icon={faPlay} icon={faPlay}
iconProps={{ spin: value }} iconProps={{ spin: jobRunning }}
mutation={runTask} mutation={runTask}
args={() => jobId} args={() => jobId}
></MutateAction> ></MutateAction>
@ -50,15 +61,16 @@ const Table: FunctionComponent<Props> = ({ tasks }) => {
}, },
}, },
], ],
[], [runTask],
); );
return ( return (
<SimpleTable <SimpleTable
initialState={{ sortBy: [{ id: "name", desc: false }] }} initialState={{ sorting: [{ id: "name", desc: false }] }}
columns={columns} columns={columns}
data={tasks} data={tasks}
plugins={[useSortBy]} enableSorting
getSortedRowModel={getSortedRowModel()}
></SimpleTable> ></SimpleTable>
); );
}; };

@ -1,9 +1,9 @@
import { FunctionComponent, useMemo } from "react"; import { FunctionComponent, useMemo } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { Column } from "react-table";
import { Anchor, Badge, Group } from "@mantine/core"; import { Anchor, Badge, Group } from "@mantine/core";
import { faSearch } from "@fortawesome/free-solid-svg-icons"; import { faSearch } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ColumnDef } from "@tanstack/react-table";
import { import {
useMovieAction, useMovieAction,
useMovieSubtitleModification, useMovieSubtitleModification,
@ -15,32 +15,37 @@ import WantedView from "@/pages/views/WantedView";
import { BuildKey } from "@/utilities"; import { BuildKey } from "@/utilities";
const WantedMoviesView: FunctionComponent = () => { const WantedMoviesView: FunctionComponent = () => {
const columns: Column<Wanted.Movie>[] = useMemo<Column<Wanted.Movie>[]>( const { download } = useMovieSubtitleModification();
const columns = useMemo<ColumnDef<Wanted.Movie>[]>(
() => [ () => [
{ {
Header: "Name", header: "Name",
accessor: "title", accessor: "title",
Cell: (row) => { cell: ({
const target = `/movies/${row.row.original.radarrId}`; row: {
original: { title, radarrId },
},
}) => {
const target = `/movies/${radarrId}`;
return ( return (
<Anchor component={Link} to={target}> <Anchor component={Link} to={target}>
{row.value} {title}
</Anchor> </Anchor>
); );
}, },
}, },
{ {
Header: "Missing", header: "Missing",
accessor: "missing_subtitles", accessor: "missing_subtitles",
Cell: ({ row, value }) => { cell: ({
const wanted = row.original; row: {
const { radarrId } = wanted; original: { radarrId, missing_subtitles: missingSubtitles },
},
const { download } = useMovieSubtitleModification(); }) => {
return ( return (
<Group gap="sm"> <Group gap="sm">
{value.map((item, idx) => ( {missingSubtitles.map((item, idx) => (
<Badge <Badge
color={download.isPending ? "gray" : undefined} color={download.isPending ? "gray" : undefined}
leftSection={<FontAwesomeIcon icon={faSearch} />} leftSection={<FontAwesomeIcon icon={faSearch} />}
@ -70,7 +75,7 @@ const WantedMoviesView: FunctionComponent = () => {
}, },
}, },
], ],
[], [download],
); );
const { mutateAsync } = useMovieAction(); const { mutateAsync } = useMovieAction();

@ -1,9 +1,9 @@
import { FunctionComponent, useMemo } from "react"; import { FunctionComponent, useMemo } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { Column } from "react-table";
import { Anchor, Badge, Group } from "@mantine/core"; import { Anchor, Badge, Group } from "@mantine/core";
import { faSearch } from "@fortawesome/free-solid-svg-icons"; import { faSearch } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ColumnDef } from "@tanstack/react-table";
import { import {
useEpisodeSubtitleModification, useEpisodeSubtitleModification,
useEpisodeWantedPagination, useEpisodeWantedPagination,
@ -15,40 +15,51 @@ import WantedView from "@/pages/views/WantedView";
import { BuildKey } from "@/utilities"; import { BuildKey } from "@/utilities";
const WantedSeriesView: FunctionComponent = () => { const WantedSeriesView: FunctionComponent = () => {
const columns: Column<Wanted.Episode>[] = useMemo<Column<Wanted.Episode>[]>( const { download } = useEpisodeSubtitleModification();
const columns = useMemo<ColumnDef<Wanted.Episode>[]>(
() => [ () => [
{ {
Header: "Name", header: "Name",
accessor: "seriesTitle", accessorKey: "seriesTitle",
Cell: (row) => { cell: ({
const target = `/series/${row.row.original.sonarrSeriesId}`; row: {
original: { sonarrSeriesId, seriesTitle },
},
}) => {
const target = `/series/${sonarrSeriesId}`;
return ( return (
<Anchor className="table-primary" component={Link} to={target}> <Anchor className="table-primary" component={Link} to={target}>
{row.value} {seriesTitle}
</Anchor> </Anchor>
); );
}, },
}, },
{ {
Header: "Episode", header: "Episode",
accessor: "episode_number", accessorKey: "episode_number",
}, },
{ {
accessor: "episodeTitle", accessorKey: "episodeTitle",
}, },
{ {
Header: "Missing", header: "Missing",
accessor: "missing_subtitles", accessorKey: "missing_subtitles",
Cell: ({ row, value }) => { cell: ({
const wanted = row.original; row: {
const seriesId = wanted.sonarrSeriesId; original: {
const episodeId = wanted.sonarrEpisodeId; sonarrSeriesId,
sonarrEpisodeId,
const { download } = useEpisodeSubtitleModification(); missing_subtitles: missingSubtitles,
},
},
}) => {
const seriesId = sonarrSeriesId;
const episodeId = sonarrEpisodeId;
return ( return (
<Group gap="sm"> <Group gap="sm">
{value.map((item, idx) => ( {missingSubtitles.map((item, idx) => (
<Badge <Badge
color={download.isPending ? "gray" : undefined} color={download.isPending ? "gray" : undefined}
leftSection={<FontAwesomeIcon icon={faSearch} />} leftSection={<FontAwesomeIcon icon={faSearch} />}
@ -79,7 +90,7 @@ const WantedSeriesView: FunctionComponent = () => {
}, },
}, },
], ],
[], [download],
); );
const { mutateAsync } = useSeriesAction(); const { mutateAsync } = useSeriesAction();

@ -1,13 +1,13 @@
import { Column } from "react-table";
import { Container } from "@mantine/core"; import { Container } from "@mantine/core";
import { useDocumentTitle } from "@mantine/hooks"; import { useDocumentTitle } from "@mantine/hooks";
import { ColumnDef } from "@tanstack/react-table";
import { UsePaginationQueryResult } from "@/apis/queries/hooks"; import { UsePaginationQueryResult } from "@/apis/queries/hooks";
import { QueryPageTable } from "@/components"; import { QueryPageTable } from "@/components";
interface Props<T extends History.Base> { interface Props<T extends History.Base> {
name: string; name: string;
query: UsePaginationQueryResult<T>; query: UsePaginationQueryResult<T>;
columns: Column<T>[]; columns: ColumnDef<T>[];
} }
function HistoryView<T extends History.Base = History.Base>({ function HistoryView<T extends History.Base = History.Base>({

@ -1,12 +1,12 @@
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { Column } from "react-table";
import { faList } from "@fortawesome/free-solid-svg-icons"; import { faList } from "@fortawesome/free-solid-svg-icons";
import { ColumnDef } from "@tanstack/react-table";
import { UsePaginationQueryResult } from "@/apis/queries/hooks"; import { UsePaginationQueryResult } from "@/apis/queries/hooks";
import { QueryPageTable, Toolbox } from "@/components"; import { QueryPageTable, Toolbox } from "@/components";
interface Props<T extends Item.Base = Item.Base> { interface Props<T extends Item.Base = Item.Base> {
query: UsePaginationQueryResult<T>; query: UsePaginationQueryResult<T>;
columns: Column<T>[]; columns: ColumnDef<T>[];
} }
function ItemView<T extends Item.Base>({ query, columns }: Props<T>) { function ItemView<T extends Item.Base>({ query, columns }: Props<T>) {

@ -1,22 +1,17 @@
import { useCallback, useMemo, useState } from "react"; import { useCallback, useMemo, useRef, useState } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { Column, useRowSelect } from "react-table";
import { Box, Container, useCombobox } from "@mantine/core"; import { Box, Container, useCombobox } from "@mantine/core";
import { faCheck, faUndo } from "@fortawesome/free-solid-svg-icons"; import { faCheck, faUndo } from "@fortawesome/free-solid-svg-icons";
import { UseMutationResult } from "@tanstack/react-query"; import { UseMutationResult } from "@tanstack/react-query";
import { ColumnDef, Table } from "@tanstack/react-table";
import { uniqBy } from "lodash"; import { uniqBy } from "lodash";
import { useIsAnyMutationRunning, useLanguageProfiles } from "@/apis/hooks"; import { useIsAnyMutationRunning, useLanguageProfiles } from "@/apis/hooks";
import { import { GroupedSelector, GroupedSelectorOptions, Toolbox } from "@/components";
GroupedSelector, import SimpleTable from "@/components/tables/SimpleTable";
GroupedSelectorOptions,
SimpleTable,
Toolbox,
} from "@/components";
import { useCustomSelection } from "@/components/tables/plugins";
import { GetItemId, useSelectorOptions } from "@/utilities"; import { GetItemId, useSelectorOptions } from "@/utilities";
interface MassEditorProps<T extends Item.Base = Item.Base> { interface MassEditorProps<T extends Item.Base = Item.Base> {
columns: Column<T>[]; columns: ColumnDef<T>[];
data: T[]; data: T[];
mutation: UseMutationResult<void, unknown, FormType.ModifyItem>; mutation: UseMutationResult<void, unknown, FormType.ModifyItem>;
} }
@ -28,6 +23,7 @@ function MassEditor<T extends Item.Base>(props: MassEditorProps<T>) {
const [dirties, setDirties] = useState<T[]>([]); const [dirties, setDirties] = useState<T[]>([]);
const hasTask = useIsAnyMutationRunning(); const hasTask = useIsAnyMutationRunning();
const { data: profiles } = useLanguageProfiles(); const { data: profiles } = useLanguageProfiles();
const tableRef = useRef<Table<T>>(null);
const navigate = useNavigate(); const navigate = useNavigate();
@ -120,6 +116,8 @@ function MassEditor<T extends Item.Base>(props: MassEditorProps<T>) {
setDirties((dirty) => { setDirties((dirty) => {
return uniqBy([...newItems, ...dirty], GetItemId); return uniqBy([...newItems, ...dirty], GetItemId);
}); });
tableRef.current?.toggleAllRowsSelected(false);
}, },
[selections], [selections],
); );
@ -163,10 +161,13 @@ function MassEditor<T extends Item.Base>(props: MassEditorProps<T>) {
</Box> </Box>
</Toolbox> </Toolbox>
<SimpleTable <SimpleTable
instanceRef={tableRef}
columns={columns} columns={columns}
data={data} data={data}
onSelect={setSelections} enableRowSelection
plugins={[useRowSelect, useCustomSelection]} onRowSelectionChanged={(row) => {
setSelections(row.map((r) => r.original));
}}
></SimpleTable> ></SimpleTable>
</Container> </Container>
); );

@ -1,14 +1,14 @@
import { Column } from "react-table";
import { Container } from "@mantine/core"; import { Container } from "@mantine/core";
import { useDocumentTitle } from "@mantine/hooks"; import { useDocumentTitle } from "@mantine/hooks";
import { faSearch } from "@fortawesome/free-solid-svg-icons"; import { faSearch } from "@fortawesome/free-solid-svg-icons";
import { ColumnDef } from "@tanstack/react-table";
import { useIsAnyActionRunning } from "@/apis/hooks"; import { useIsAnyActionRunning } from "@/apis/hooks";
import { UsePaginationQueryResult } from "@/apis/queries/hooks"; import { UsePaginationQueryResult } from "@/apis/queries/hooks";
import { QueryPageTable, Toolbox } from "@/components"; import { QueryPageTable, Toolbox } from "@/components";
interface Props<T extends Wanted.Base> { interface Props<T extends Wanted.Base> {
name: string; name: string;
columns: Column<T>[]; columns: ColumnDef<T>[];
query: UsePaginationQueryResult<T>; query: UsePaginationQueryResult<T>;
searchAll: () => Promise<void>; searchAll: () => Promise<void>;
} }

Loading…
Cancel
Save