summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--frontend/package-lock.json62
-rw-r--r--frontend/package.json3
-rw-r--r--frontend/src/components/forms/MovieUploadForm.tsx127
-rw-r--r--frontend/src/components/forms/ProfileEditForm.tsx140
-rw-r--r--frontend/src/components/forms/SeriesUploadForm.tsx137
-rw-r--r--frontend/src/components/modals/HistoryModal.tsx184
-rw-r--r--frontend/src/components/modals/ManualSearchModal.tsx147
-rw-r--r--frontend/src/components/modals/SubtitleToolsModal.tsx73
-rw-r--r--frontend/src/components/tables/BaseTable.tsx84
-rw-r--r--frontend/src/components/tables/GroupTable.tsx88
-rw-r--r--frontend/src/components/tables/PageTable.tsx63
-rw-r--r--frontend/src/components/tables/QueryPageTable.tsx2
-rw-r--r--frontend/src/components/tables/SimpleTable.tsx60
-rw-r--r--frontend/src/components/tables/plugins/index.ts2
-rw-r--r--frontend/src/components/tables/plugins/useCustomSelection.tsx113
-rw-r--r--frontend/src/components/tables/plugins/useDefaultSettings.tsx33
-rw-r--r--frontend/src/pages/Blacklist/Movies/table.tsx74
-rw-r--r--frontend/src/pages/Blacklist/Series/table.tsx82
-rw-r--r--frontend/src/pages/Episodes/index.tsx31
-rw-r--r--frontend/src/pages/Episodes/table.tsx420
-rw-r--r--frontend/src/pages/History/Movies/index.tsx107
-rw-r--r--frontend/src/pages/History/Series/index.tsx121
-rw-r--r--frontend/src/pages/Movies/Details/table.tsx213
-rw-r--r--frontend/src/pages/Movies/Editor.tsx60
-rw-r--r--frontend/src/pages/Movies/index.tsx84
-rw-r--r--frontend/src/pages/Series/Editor.tsx56
-rw-r--r--frontend/src/pages/Series/index.tsx52
-rw-r--r--frontend/src/pages/Settings/Languages/equals.tsx53
-rw-r--r--frontend/src/pages/Settings/Languages/table.tsx60
-rw-r--r--frontend/src/pages/Settings/components/pathMapper.tsx29
-rw-r--r--frontend/src/pages/System/Announcements/table.tsx61
-rw-r--r--frontend/src/pages/System/Backups/table.tsx89
-rw-r--r--frontend/src/pages/System/Logs/table.tsx40
-rw-r--r--frontend/src/pages/System/Providers/table.tsx20
-rw-r--r--frontend/src/pages/System/Status/table.tsx32
-rw-r--r--frontend/src/pages/System/Tasks/table.tsx52
-rw-r--r--frontend/src/pages/Wanted/Movies/index.tsx35
-rw-r--r--frontend/src/pages/Wanted/Series/index.tsx51
-rw-r--r--frontend/src/pages/views/HistoryView.tsx4
-rw-r--r--frontend/src/pages/views/ItemView.tsx4
-rw-r--r--frontend/src/pages/views/MassEditor.tsx25
-rw-r--r--frontend/src/pages/views/WantedView.tsx4
42 files changed, 1761 insertions, 1416 deletions
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index b1064664b..981b8acbd 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -16,6 +16,7 @@
"@mantine/modals": "^7.11.0",
"@mantine/notifications": "^7.11.0",
"@tanstack/react-query": "^5.40.1",
+ "@tanstack/react-table": "^8.19.2",
"axios": "^1.6.8",
"braces": "^3.0.3",
"react": "^18.3.1",
@@ -39,7 +40,6 @@
"@types/node": "^20.12.6",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
- "@types/react-table": "^7.7.20",
"@vitejs/plugin-react": "^4.2.1",
"@vitest/coverage-v8": "^1.4.0",
"@vitest/ui": "^1.2.2",
@@ -57,7 +57,6 @@
"prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^3.2.4",
"pretty-quick": "^4.0.0",
- "react-table": "^7.8.0",
"recharts": "^2.12.6",
"sass": "^1.74.1",
"typescript": "^5.4.4",
@@ -3381,6 +3380,39 @@
"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": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.0.0.tgz",
@@ -3714,13 +3746,13 @@
"version": "15.7.12",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz",
"integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==",
- "devOptional": true
+ "dev": true
},
"node_modules/@types/react": {
"version": "18.3.3",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz",
"integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==",
- "devOptional": true,
+ "dev": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
@@ -3735,15 +3767,6 @@
"@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": {
"version": "7.5.8",
"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": {
"version": "8.5.3",
"resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.5.3.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 53e9c4c47..f964fb9f8 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -20,6 +20,7 @@
"@mantine/modals": "^7.11.0",
"@mantine/notifications": "^7.11.0",
"@tanstack/react-query": "^5.40.1",
+ "@tanstack/react-table": "^8.19.2",
"axios": "^1.6.8",
"braces": "^3.0.3",
"react": "^18.3.1",
@@ -43,7 +44,6 @@
"@types/node": "^20.12.6",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
- "@types/react-table": "^7.7.20",
"@vitejs/plugin-react": "^4.2.1",
"@vitest/coverage-v8": "^1.4.0",
"@vitest/ui": "^1.2.2",
@@ -61,7 +61,6 @@
"prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^3.2.4",
"pretty-quick": "^4.0.0",
- "react-table": "^7.8.0",
"recharts": "^2.12.6",
"sass": "^1.74.1",
"typescript": "^5.4.4",
diff --git a/frontend/src/components/forms/MovieUploadForm.tsx b/frontend/src/components/forms/MovieUploadForm.tsx
index 7e3df4b33..8e318d7ad 100644
--- a/frontend/src/components/forms/MovieUploadForm.tsx
+++ b/frontend/src/components/forms/MovieUploadForm.tsx
@@ -1,5 +1,4 @@
import { FunctionComponent, useEffect, useMemo } from "react";
-import { Column } from "react-table";
import {
Button,
Checkbox,
@@ -17,10 +16,11 @@ import {
faTrash,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { ColumnDef } from "@tanstack/react-table";
import { isString } from "lodash";
import { useMovieSubtitleModification } from "@/apis/hooks";
import { Action, Selector } from "@/components/inputs";
-import { SimpleTable } from "@/components/tables";
+import SimpleTable from "@/components/tables/SimpleTable";
import TextPopover from "@/components/TextPopover";
import { useModals, withModal } from "@/modules/modals";
import { task, TaskGroup } from "@/modules/task";
@@ -143,61 +143,77 @@ const MovieUploadForm: FunctionComponent<Props> = ({
});
});
- const columns = useMemo<Column<SubtitleFile>[]>(
- () => [
- {
- accessor: "validateResult",
- Cell: ({ cell: { value } }) => {
- const icon = useMemo(() => {
- switch (value?.state) {
- case "valid":
- return faCheck;
- case "warning":
- return faInfoCircle;
- case "error":
- return faTimes;
- default:
- return faCircleNotch;
- }
- }, [value?.state]);
+ const ValidateResultCell = ({
+ validateResult,
+ }: {
+ validateResult: SubtitleValidateResult | undefined;
+ }) => {
+ const icon = useMemo(() => {
+ switch (validateResult?.state) {
+ case "valid":
+ return faCheck;
+ case "warning":
+ return faInfoCircle;
+ case "error":
+ return faTimes;
+ default:
+ return faCircleNotch;
+ }
+ }, [validateResult?.state]);
- const color = useMemo<MantineColor | undefined>(() => {
- switch (value?.state) {
- case "valid":
- return "green";
- case "warning":
- return "yellow";
- case "error":
- return "red";
- default:
- return undefined;
- }
- }, [value?.state]);
+ const color = useMemo<MantineColor | undefined>(() => {
+ switch (validateResult?.state) {
+ case "valid":
+ return "green";
+ case "warning":
+ return "yellow";
+ case "error":
+ return "red";
+ default:
+ return undefined;
+ }
+ }, [validateResult?.state]);
- return (
- <TextPopover text={value?.messages}>
- <Text c={color} inline>
- <FontAwesomeIcon icon={icon}></FontAwesomeIcon>
- </Text>
- </TextPopover>
- );
+ return (
+ <TextPopover text={validateResult?.messages}>
+ <Text c={color} inline>
+ <FontAwesomeIcon icon={icon} />
+ </Text>
+ </TextPopover>
+ );
+ };
+
+ const columns = useMemo<ColumnDef<SubtitleFile>[]>(
+ () => [
+ {
+ id: "validateResult",
+ cell: ({
+ row: {
+ original: { validateResult },
+ },
+ }) => {
+ return <ValidateResultCell validateResult={validateResult} />;
},
},
{
- Header: "File",
+ header: "File",
id: "filename",
- accessor: "file",
- Cell: ({ value }) => {
- return <Text className="table-primary">{value.name}</Text>;
+ accessorKey: "file",
+ cell: ({
+ row: {
+ original: { file },
+ },
+ }) => {
+ return <Text className="table-primary">{file.name}</Text>;
},
},
{
- Header: "Forced",
- accessor: "forced",
- Cell: ({ row: { original, index }, value }) => {
+ header: "Forced",
+ accessorKey: "forced",
+ cell: ({ row: { original, index } }) => {
return (
<Checkbox
- checked={value}
+ checked={original.forced}
onChange={({ currentTarget: { checked } }) => {
action.mutate(index, { ...original, forced: checked });
}}
@@ -206,12 +222,12 @@ const MovieUploadForm: FunctionComponent<Props> = ({
},
},
{
- Header: "HI",
- accessor: "hi",
- Cell: ({ row: { original, index }, value }) => {
+ header: "HI",
+ accessorKey: "hi",
+ cell: ({ row: { original, index } }) => {
return (
<Checkbox
- checked={value}
+ checked={original.hi}
onChange={({ currentTarget: { checked } }) => {
action.mutate(index, { ...original, hi: checked });
}}
@@ -220,14 +236,14 @@ const MovieUploadForm: FunctionComponent<Props> = ({
},
},
{
- Header: "Language",
- accessor: "language",
- Cell: ({ row: { original, index }, value }) => {
+ header: "Language",
+ accessorKey: "language",
+ cell: ({ row: { original, index } }) => {
return (
<Selector
{...languageOptions}
className="table-long-break"
- value={value}
+ value={original.language}
onChange={(item) => {
action.mutate(index, { ...original, language: item });
}}
@@ -237,8 +253,7 @@ const MovieUploadForm: FunctionComponent<Props> = ({
},
{
id: "action",
- accessor: "file",
- Cell: ({ row: { index } }) => {
+ cell: ({ row: { index } }) => {
return (
<Action
label="Remove"
diff --git a/frontend/src/components/forms/ProfileEditForm.tsx b/frontend/src/components/forms/ProfileEditForm.tsx
index e994888d2..75e2f9df7 100644
--- a/frontend/src/components/forms/ProfileEditForm.tsx
+++ b/frontend/src/components/forms/ProfileEditForm.tsx
@@ -1,5 +1,4 @@
-import { FunctionComponent, useCallback, useMemo } from "react";
-import { Column } from "react-table";
+import React, { FunctionComponent, useCallback, useMemo } from "react";
import {
Accordion,
Button,
@@ -12,8 +11,10 @@ import {
} from "@mantine/core";
import { useForm } from "@mantine/form";
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 SimpleTable from "@/components/tables/SimpleTable";
import { useModals, withModal } from "@/modules/modals";
import { useArrayAction, useSelectorOptions } from "@/utilities";
import { LOG } from "@/utilities/console";
@@ -145,76 +146,88 @@ const ProfileEditForm: FunctionComponent<Props> = ({
}
}, [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",
- accessor: "id",
+ header: "ID",
+ accessorKey: "id",
},
{
- Header: "Language",
- accessor: "language",
- Cell: ({ value: code, row: { original: item, index } }) => {
- const language = useMemo(
- () =>
- 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: "Language",
+ accessorKey: "language",
+ cell: ({ row: { original: item, index } }) => {
+ return <LanguageCell item={item} index={index} />;
},
},
{
- Header: "Subtitles Type",
- accessor: "forced",
- Cell: ({ row: { original: item, index }, value }) => {
- 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>
- );
+ header: "Subtitles Type",
+ accessorKey: "forced",
+ cell: ({ row: { original: item, index } }) => {
+ return <SubtitleTypeCell item={item} index={index} />;
},
},
{
- Header: "Exclude If Matching Audio",
- accessor: "audio_exclude",
- Cell: ({ row: { original: item, index }, value }) => {
+ header: "Exclude If Matching Audio",
+ accessorKey: "audio_exclude",
+ cell: ({ row: { original: item, index } }) => {
return (
<Checkbox
- checked={value === "True"}
+ checked={item.audio_exclude === "True"}
onChange={({ currentTarget: { checked } }) => {
action.mutate(index, {
...item,
@@ -228,8 +241,7 @@ const ProfileEditForm: FunctionComponent<Props> = ({
},
{
id: "action",
- accessor: "id",
- Cell: ({ row }) => {
+ cell: ({ row }) => {
return (
<Action
label="Remove"
@@ -241,7 +253,7 @@ const ProfileEditForm: FunctionComponent<Props> = ({
},
},
],
- [action, languageOptions],
+ [action, LanguageCell, SubtitleTypeCell],
);
return (
diff --git a/frontend/src/components/forms/SeriesUploadForm.tsx b/frontend/src/components/forms/SeriesUploadForm.tsx
index 99a8e8e30..e4482cab4 100644
--- a/frontend/src/components/forms/SeriesUploadForm.tsx
+++ b/frontend/src/components/forms/SeriesUploadForm.tsx
@@ -1,5 +1,4 @@
import { FunctionComponent, useEffect, useMemo } from "react";
-import { Column } from "react-table";
import {
Button,
Checkbox,
@@ -17,6 +16,7 @@ import {
faTrash,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { ColumnDef } from "@tanstack/react-table";
import { isString } from "lodash";
import {
useEpisodesBySeriesId,
@@ -24,7 +24,7 @@ import {
useSubtitleInfos,
} from "@/apis/hooks";
import { Action, Selector } from "@/components/inputs";
-import { SimpleTable } from "@/components/tables";
+import SimpleTable from "@/components/tables/SimpleTable";
import TextPopover from "@/components/TextPopover";
import { useModals, withModal } from "@/modules/modals";
import { task, TaskGroup } from "@/modules/task";
@@ -169,61 +169,79 @@ const SeriesUploadForm: FunctionComponent<Props> = ({
}
}, [action, episodes.data, infos.data]);
- const columns = useMemo<Column<SubtitleFile>[]>(
- () => [
- {
- accessor: "validateResult",
- Cell: ({ cell: { value } }) => {
- const icon = useMemo(() => {
- switch (value?.state) {
- case "valid":
- return faCheck;
- case "warning":
- return faInfoCircle;
- case "error":
- return faTimes;
- default:
- return faCircleNotch;
- }
- }, [value?.state]);
+ const ValidateResultCell = ({
+ validateResult,
+ }: {
+ validateResult: SubtitleValidateResult | undefined;
+ }) => {
+ const icon = useMemo(() => {
+ switch (validateResult?.state) {
+ case "valid":
+ return faCheck;
+ case "warning":
+ return faInfoCircle;
+ case "error":
+ return faTimes;
+ default:
+ return faCircleNotch;
+ }
+ }, [validateResult?.state]);
- const color = useMemo<MantineColor | undefined>(() => {
- switch (value?.state) {
- case "valid":
- return "green";
- case "warning":
- return "yellow";
- case "error":
- return "red";
- default:
- return undefined;
- }
- }, [value?.state]);
+ const color = useMemo<MantineColor | undefined>(() => {
+ switch (validateResult?.state) {
+ case "valid":
+ return "green";
+ case "warning":
+ return "yellow";
+ case "error":
+ return "red";
+ default:
+ return undefined;
+ }
+ }, [validateResult?.state]);
- return (
- <TextPopover text={value?.messages}>
- <Text color={color} inline>
- <FontAwesomeIcon icon={icon}></FontAwesomeIcon>
- </Text>
- </TextPopover>
- );
+ return (
+ <TextPopover text={validateResult?.messages}>
+ <Text c={color} inline>
+ <FontAwesomeIcon icon={icon}></FontAwesomeIcon>
+ </Text>
+ </TextPopover>
+ );
+ };
+
+ const columns = useMemo<ColumnDef<SubtitleFile>[]>(
+ () => [
+ {
+ id: "validateResult",
+ cell: ({
+ row: {
+ original: { validateResult },
+ },
+ }) => {
+ return <ValidateResultCell validateResult={validateResult} />;
},
},
{
- Header: "File",
+ header: "File",
id: "filename",
- accessor: "file",
- Cell: ({ value: { name } }) => {
+ accessorKey: "file",
+ cell: ({
+ row: {
+ original: {
+ file: { name },
+ },
+ },
+ }) => {
return <Text className="table-primary">{name}</Text>;
},
},
{
- Header: "Forced",
- accessor: "forced",
- Cell: ({ row: { original, index }, value }) => {
+ header: "Forced",
+ accessorKey: "forced",
+ cell: ({ row: { original, index } }) => {
return (
<Checkbox
- checked={value}
+ checked={original.forced}
onChange={({ currentTarget: { checked } }) => {
action.mutate(index, {
...original,
@@ -236,12 +254,12 @@ const SeriesUploadForm: FunctionComponent<Props> = ({
},
},
{
- Header: "HI",
- accessor: "hi",
- Cell: ({ row: { original, index }, value }) => {
+ header: "HI",
+ accessorKey: "hi",
+ cell: ({ row: { original, index } }) => {
return (
<Checkbox
- checked={value}
+ checked={original.hi}
onChange={({ currentTarget: { checked } }) => {
action.mutate(index, {
...original,
@@ -254,7 +272,7 @@ const SeriesUploadForm: FunctionComponent<Props> = ({
},
},
{
- Header: (
+ header: () => (
<Selector
{...languageOptions}
value={null}
@@ -269,13 +287,13 @@ const SeriesUploadForm: FunctionComponent<Props> = ({
}}
></Selector>
),
- accessor: "language",
- Cell: ({ row: { original, index }, value }) => {
+ accessorKey: "language",
+ cell: ({ row: { original, index } }) => {
return (
<Selector
{...languageOptions}
className="table-select"
- value={value}
+ value={original.language}
onChange={(item) => {
action.mutate(index, { ...original, language: item });
}}
@@ -285,17 +303,17 @@ const SeriesUploadForm: FunctionComponent<Props> = ({
},
{
id: "episode",
- Header: "Episode",
- accessor: "episode",
- Cell: ({ value, row }) => {
+ header: "Episode",
+ accessorKey: "episode",
+ cell: ({ row: { original, index } }) => {
return (
<Selector
{...episodeOptions}
searchable
className="table-select"
- value={value}
+ value={original.episode}
onChange={(item) => {
- action.mutate(row.index, { ...row.original, episode: item });
+ action.mutate(index, { ...original, episode: item });
}}
></Selector>
);
@@ -303,8 +321,7 @@ const SeriesUploadForm: FunctionComponent<Props> = ({
},
{
id: "action",
- accessor: "file",
- Cell: ({ row: { index } }) => {
+ cell: ({ row: { index } }) => {
return (
<Action
label="Remove"
diff --git a/frontend/src/components/modals/HistoryModal.tsx b/frontend/src/components/modals/HistoryModal.tsx
index 888f0bafb..88d57ac65 100644
--- a/frontend/src/components/modals/HistoryModal.tsx
+++ b/frontend/src/components/modals/HistoryModal.tsx
@@ -1,21 +1,21 @@
/* eslint-disable camelcase */
import { FunctionComponent, useMemo } from "react";
-import { Column } from "react-table";
import { Badge, Center, Text } from "@mantine/core";
import { faFileExcel, faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { ColumnDef } from "@tanstack/react-table";
import {
useEpisodeAddBlacklist,
useEpisodeHistory,
useMovieAddBlacklist,
useMovieHistory,
} from "@/apis/hooks";
-import { PageTable } from "@/components";
import MutateAction from "@/components/async/MutateAction";
import QueryOverlay from "@/components/async/QueryOverlay";
import { HistoryIcon } from "@/components/bazarr";
import Language from "@/components/bazarr/Language";
import StateIcon from "@/components/StateIcon";
+import PageTable from "@/components/tables/PageTable";
import TextPopover from "@/components/TextPopover";
import { withModal } from "@/modules/modals";
@@ -30,24 +30,34 @@ const MovieHistoryView: FunctionComponent<MovieHistoryViewProps> = ({
const { data } = history;
- const columns = useMemo<Column<History.Movie>[]>(
+ const addMovieToBlacklist = useMovieAddBlacklist();
+
+ const columns = useMemo<ColumnDef<History.Movie>[]>(
() => [
{
- accessor: "action",
- Cell: (row) => (
+ id: "action",
+ cell: ({
+ row: {
+ original: { action },
+ },
+ }) => (
<Center>
- <HistoryIcon action={row.value}></HistoryIcon>
+ <HistoryIcon action={action}></HistoryIcon>
</Center>
),
},
{
- Header: "Language",
- accessor: "language",
- Cell: ({ value }) => {
- if (value) {
+ header: "Language",
+ accessorKey: "language",
+ cell: ({
+ row: {
+ original: { language },
+ },
+ }) => {
+ if (language) {
return (
<Badge>
- <Language.Text value={value} long></Language.Text>
+ <Language.Text value={language} long></Language.Text>
</Badge>
);
} else {
@@ -56,17 +66,20 @@ const MovieHistoryView: FunctionComponent<MovieHistoryViewProps> = ({
},
},
{
- Header: "Provider",
- accessor: "provider",
+ header: "Provider",
+ accessorKey: "provider",
},
{
- Header: "Score",
- accessor: "score",
+ header: "Score",
+ accessorKey: "score",
},
{
- accessor: "matches",
- Cell: (row) => {
- const { matches, dont_matches: dont } = row.row.original;
+ id: "matches",
+ cell: ({
+ row: {
+ original: { matches, dont_matches: dont },
+ },
+ }) => {
if (matches.length || dont.length) {
return (
<StateIcon
@@ -81,31 +94,42 @@ const MovieHistoryView: FunctionComponent<MovieHistoryViewProps> = ({
},
},
{
- Header: "Date",
- accessor: "timestamp",
- Cell: ({ value, row }) => {
+ header: "Date",
+ accessorKey: "timestamp",
+ cell: ({
+ row: {
+ original: { timestamp, parsed_timestamp: parsedTimestamp },
+ },
+ }) => {
return (
- <TextPopover text={row.original.parsed_timestamp}>
- <Text>{value}</Text>
+ <TextPopover text={parsedTimestamp}>
+ <Text>{timestamp}</Text>
</TextPopover>
);
},
},
{
// Actions
- accessor: "blacklisted",
- Cell: ({ row, value }) => {
- const add = useMovieAddBlacklist();
- const { radarrId, provider, subs_id, language, subtitles_path } =
- row.original;
-
+ id: "blacklisted",
+ cell: ({
+ row: {
+ original: {
+ blacklisted,
+ radarrId,
+ provider,
+ subs_id,
+ language,
+ subtitles_path,
+ },
+ },
+ }) => {
if (subs_id && provider && language) {
return (
<MutateAction
label="Add to Blacklist"
- disabled={value}
+ disabled={blacklisted}
icon={faFileExcel}
- mutation={add}
+ mutation={addMovieToBlacklist}
args={() => ({
id: radarrId,
form: {
@@ -123,7 +147,7 @@ const MovieHistoryView: FunctionComponent<MovieHistoryViewProps> = ({
},
},
],
- [],
+ [addMovieToBlacklist],
);
return (
@@ -153,24 +177,34 @@ const EpisodeHistoryView: FunctionComponent<EpisodeHistoryViewProps> = ({
const { data } = history;
- const columns = useMemo<Column<History.Episode>[]>(
+ const addEpisodeToBlacklist = useEpisodeAddBlacklist();
+
+ const columns = useMemo<ColumnDef<History.Episode>[]>(
() => [
{
- accessor: "action",
- Cell: (row) => (
+ id: "action",
+ cell: ({
+ row: {
+ original: { action },
+ },
+ }) => (
<Center>
- <HistoryIcon action={row.value}></HistoryIcon>
+ <HistoryIcon action={action}></HistoryIcon>
</Center>
),
},
{
- Header: "Language",
- accessor: "language",
- Cell: ({ value }) => {
- if (value) {
+ header: "Language",
+ accessorKey: "language",
+ cell: ({
+ row: {
+ original: { language },
+ },
+ }) => {
+ if (language) {
return (
<Badge>
- <Language.Text value={value} long></Language.Text>
+ <Language.Text value={language} long></Language.Text>
</Badge>
);
} else {
@@ -179,16 +213,16 @@ const EpisodeHistoryView: FunctionComponent<EpisodeHistoryViewProps> = ({
},
},
{
- Header: "Provider",
- accessor: "provider",
+ header: "Provider",
+ accessorKey: "provider",
},
{
- Header: "Score",
- accessor: "score",
+ header: "Score",
+ accessorKey: "score",
},
{
- accessor: "matches",
- Cell: (row) => {
+ id: "matches",
+ cell: (row) => {
const { matches, dont_matches: dont } = row.row.original;
if (matches.length || dont.length) {
return (
@@ -204,21 +238,29 @@ const EpisodeHistoryView: FunctionComponent<EpisodeHistoryViewProps> = ({
},
},
{
- Header: "Date",
- accessor: "timestamp",
- Cell: ({ row, value }) => {
+ header: "Date",
+ accessorKey: "timestamp",
+ cell: ({
+ row: {
+ original: { timestamp, parsed_timestamp: parsedTimestamp },
+ },
+ }) => {
return (
- <TextPopover text={row.original.parsed_timestamp}>
- <Text>{value}</Text>
+ <TextPopover text={parsedTimestamp}>
+ <Text>{timestamp}</Text>
</TextPopover>
);
},
},
{
- accessor: "description",
- Cell: ({ value }) => {
+ id: "description",
+ cell: ({
+ row: {
+ original: { description },
+ },
+ }) => {
return (
- <TextPopover text={value}>
+ <TextPopover text={description}>
<FontAwesomeIcon size="sm" icon={faInfoCircle}></FontAwesomeIcon>
</TextPopover>
);
@@ -226,25 +268,27 @@ const EpisodeHistoryView: FunctionComponent<EpisodeHistoryViewProps> = ({
},
{
// Actions
- accessor: "blacklisted",
- Cell: ({ row, value }) => {
- const {
- sonarrEpisodeId,
- sonarrSeriesId,
- provider,
- subs_id,
- language,
- subtitles_path,
- } = row.original;
- const add = useEpisodeAddBlacklist();
-
+ id: "blacklisted",
+ cell: ({
+ row: {
+ original: {
+ blacklisted,
+ sonarrEpisodeId,
+ sonarrSeriesId,
+ provider,
+ subs_id,
+ language,
+ subtitles_path,
+ },
+ },
+ }) => {
if (subs_id && provider && language) {
return (
<MutateAction
label="Add to Blacklist"
- disabled={value}
+ disabled={blacklisted}
icon={faFileExcel}
- mutation={add}
+ mutation={addEpisodeToBlacklist}
args={() => ({
seriesId: sonarrSeriesId,
episodeId: sonarrEpisodeId,
@@ -263,7 +307,7 @@ const EpisodeHistoryView: FunctionComponent<EpisodeHistoryViewProps> = ({
},
},
],
- [],
+ [addEpisodeToBlacklist],
);
return (
diff --git a/frontend/src/components/modals/ManualSearchModal.tsx b/frontend/src/components/modals/ManualSearchModal.tsx
index d29a2deba..81a49f0f3 100644
--- a/frontend/src/components/modals/ManualSearchModal.tsx
+++ b/frontend/src/components/modals/ManualSearchModal.tsx
@@ -1,5 +1,4 @@
-import { useCallback, useMemo, useState } from "react";
-import { Column } from "react-table";
+import React, { useCallback, useMemo, useState } from "react";
import {
Alert,
Anchor,
@@ -18,10 +17,12 @@ import {
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { UseQueryResult } from "@tanstack/react-query";
+import { ColumnDef } from "@tanstack/react-table";
import { isString } from "lodash";
-import { Action, PageTable } from "@/components";
+import { Action } from "@/components";
import Language from "@/components/bazarr/Language";
import StateIcon from "@/components/StateIcon";
+import PageTable from "@/components/tables/PageTable";
import { withModal } from "@/modules/modals";
import { task, TaskGroup } from "@/modules/task";
import { GetItemId } from "@/utilities";
@@ -51,23 +52,63 @@ function ManualSearchView<T extends SupportType>(props: Props<T>) {
void results.refetch();
}, [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",
- accessor: "score",
- Cell: ({ value }) => {
- return <Text className="table-no-wrap">{value}%</Text>;
+ header: "Score",
+ accessorKey: "score",
+ cell: ({
+ row: {
+ original: { score },
+ },
+ }) => {
+ return <Text className="table-no-wrap">{score}%</Text>;
},
},
{
- Header: "Language",
- accessor: "language",
- Cell: ({ row: { original }, value }) => {
+ header: "Language",
+ accessorKey: "language",
+ cell: ({
+ row: {
+ original: { language, hearing_impaired: hi, forced },
+ },
+ }) => {
const lang: Language.Info = {
- code2: value,
- hi: original.hearing_impaired === "True",
- forced: original.forced === "True",
+ code2: language,
+ hi: hi === "True",
+ forced: forced === "True",
name: "",
};
return (
@@ -78,11 +119,15 @@ function ManualSearchView<T extends SupportType>(props: Props<T>) {
},
},
{
- Header: "Provider",
- accessor: "provider",
- Cell: (row) => {
- const value = row.value;
- const { url } = row.row.original;
+ header: "Provider",
+ accessorKey: "provider",
+ cell: ({
+ row: {
+ original: { provider, url },
+ },
+ }) => {
+ const value = provider;
+
if (url) {
return (
<Anchor
@@ -100,49 +145,31 @@ function ManualSearchView<T extends SupportType>(props: Props<T>) {
},
},
{
- Header: "Release",
- accessor: "release_info",
- Cell: ({ value }) => {
- const [open, setOpen] = useState(false);
-
- const items = useMemo(
- () => value.slice(1).map((v, idx) => <Text key={idx}>{v}</Text>),
- [value],
- );
-
- 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: "Release",
+ accessorKey: "release_info",
+ cell: ({
+ row: {
+ original: { release_info: releaseInfo },
+ },
+ }) => {
+ return <ReleaseInfoCell releaseInfo={releaseInfo} />;
},
},
{
- Header: "Uploader",
- accessor: "uploader",
- Cell: ({ value }) => {
- return <Text className="table-no-wrap">{value ?? "-"}</Text>;
+ header: "Uploader",
+ accessorKey: "uploader",
+ cell: ({
+ row: {
+ original: { uploader },
+ },
+ }) => {
+ return <Text className="table-no-wrap">{uploader ?? "-"}</Text>;
},
},
{
- Header: "Match",
- accessor: "matches",
- Cell: (row) => {
+ header: "Match",
+ accessorKey: "matches",
+ cell: (row) => {
const { matches, dont_matches: dont } = row.row.original;
return (
<StateIcon
@@ -154,9 +181,9 @@ function ManualSearchView<T extends SupportType>(props: Props<T>) {
},
},
{
- Header: "Get",
- accessor: "subtitle",
- Cell: ({ row }) => {
+ header: "Get",
+ accessorKey: "subtitle",
+ cell: ({ row }) => {
const result = row.original;
return (
<Action
@@ -180,7 +207,7 @@ function ManualSearchView<T extends SupportType>(props: Props<T>) {
},
},
],
- [download, item],
+ [download, item, ReleaseInfoCell],
);
const bSceneNameAvailable =
diff --git a/frontend/src/components/modals/SubtitleToolsModal.tsx b/frontend/src/components/modals/SubtitleToolsModal.tsx
index 150788b48..dca20d159 100644
--- a/frontend/src/components/modals/SubtitleToolsModal.tsx
+++ b/frontend/src/components/modals/SubtitleToolsModal.tsx
@@ -1,10 +1,17 @@
import { FunctionComponent, useMemo, useState } from "react";
-import { Column, useRowSelect } from "react-table";
-import { Badge, Button, Divider, Group, Stack, Text } from "@mantine/core";
+import {
+ Badge,
+ Button,
+ Checkbox,
+ Divider,
+ Group,
+ Stack,
+ Text,
+} from "@mantine/core";
+import { ColumnDef } from "@tanstack/react-table";
import Language from "@/components/bazarr/Language";
import SubtitleToolsMenu from "@/components/SubtitleToolsMenu";
-import { SimpleTable } from "@/components/tables";
-import { useCustomSelection } from "@/components/tables/plugins";
+import SimpleTable from "@/components/tables/SimpleTable";
import { withModal } from "@/modules/modals";
import { isMovie } from "@/utilities";
@@ -35,24 +42,53 @@ const SubtitleToolView: FunctionComponent<SubtitleToolViewProps> = ({
}) => {
const [selections, setSelections] = useState<TableColumnType[]>([]);
- const columns: Column<TableColumnType>[] = useMemo<Column<TableColumnType>[]>(
+ const columns = useMemo<ColumnDef<TableColumnType>[]>(
() => [
{
- Header: "Language",
- accessor: "raw_language",
- Cell: ({ value }) => (
+ id: "selection",
+ 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: "Language",
+ accessorKey: "raw_language",
+ cell: ({
+ row: {
+ original: { raw_language: rawLanguage },
+ },
+ }) => (
<Badge color="secondary">
- <Language.Text value={value} long></Language.Text>
+ <Language.Text value={rawLanguage} long></Language.Text>
</Badge>
),
},
{
id: "file",
- Header: "File",
- accessor: "path",
- Cell: ({ value }) => {
- const path = value;
-
+ header: "File",
+ accessorKey: "path",
+ cell: ({
+ row: {
+ original: { path },
+ },
+ }) => {
let idx = path.lastIndexOf("/");
if (idx === -1) {
@@ -94,16 +130,15 @@ const SubtitleToolView: FunctionComponent<SubtitleToolViewProps> = ({
[payload],
);
- const plugins = [useRowSelect, useCustomSelection];
-
return (
<Stack>
<SimpleTable
tableStyles={{ emptyText: "No external subtitles found" }}
- plugins={plugins}
+ enableRowSelection={(row) => CanSelectSubtitle(row.original)}
+ onRowSelectionChanged={(rows) =>
+ setSelections(rows.map((r) => r.original))
+ }
columns={columns}
- onSelect={setSelections}
- canSelect={CanSelectSubtitle}
data={data}
></SimpleTable>
<Divider></Divider>
diff --git a/frontend/src/components/tables/BaseTable.tsx b/frontend/src/components/tables/BaseTable.tsx
index 53058032d..b5a867b14 100644
--- a/frontend/src/components/tables/BaseTable.tsx
+++ b/frontend/src/components/tables/BaseTable.tsx
@@ -1,11 +1,17 @@
-import { ReactNode, useMemo } from "react";
-import { HeaderGroup, Row, TableInstance } from "react-table";
+import React, { ReactNode, useMemo } from "react";
import { Box, Skeleton, Table, Text } from "@mantine/core";
+import {
+ flexRender,
+ Header,
+ Row,
+ Table as TableInstance,
+} from "@tanstack/react-table";
import { useIsLoading } from "@/contexts";
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>;
};
@@ -15,60 +21,57 @@ export interface TableStyleProps<T extends object> {
placeholder?: number;
hideHeader?: boolean;
fixHeader?: boolean;
- headersRenderer?: (headers: HeaderGroup<T>[]) => JSX.Element[];
- rowRenderer?: (row: Row<T>) => Nullable<JSX.Element>;
+ headersRenderer?: (headers: Header<T, unknown>[]) => React.JSX.Element[];
+ rowRenderer?: (row: Row<T>) => Nullable<React.JSX.Element>;
}
function DefaultHeaderRenderer<T extends object>(
- headers: HeaderGroup<T>[],
-): JSX.Element[] {
- return headers.map((col) => (
- <Table.Th style={{ whiteSpace: "nowrap" }} {...col.getHeaderProps()}>
- {col.render("Header")}
+ headers: Header<T, unknown>[],
+): React.JSX.Element[] {
+ return headers.map((header) => (
+ <Table.Th style={{ whiteSpace: "nowrap" }} key={header.id}>
+ {flexRender(header.column.columnDef.header, header.getContext())}
</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 (
- <Table.Tr {...row.getRowProps()}>
- {row.cells.map((cell) => (
- <Table.Td {...cell.getCellProps()}>{cell.render("Cell")}</Table.Td>
+ <Table.Tr key={row.id}>
+ {row.getVisibleCells().map((cell) => (
+ <Table.Td key={cell.id}>
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
+ </Table.Td>
))}
</Table.Tr>
);
}
export default function BaseTable<T extends object>(props: BaseTableProps<T>) {
- const {
- headerGroups,
- rows: tableRows,
- page: tablePages,
- prepareRow,
- getTableProps,
- getTableBodyProps,
- tableStyles,
- } = props;
+ const { instance, tableStyles } = props;
const headersRenderer = tableStyles?.headersRenderer ?? DefaultHeaderRenderer;
const rowRenderer = tableStyles?.rowRenderer ?? DefaultRowRenderer;
const colCount = useMemo(() => {
- return headerGroups.reduce(
- (prev, curr) => (curr.headers.length > prev ? curr.headers.length : prev),
- 0,
- );
- }, [headerGroups]);
+ return instance
+ .getHeaderGroups()
+ .reduce(
+ (prev, curr) =>
+ curr.headers.length > prev ? curr.headers.length : prev,
+ 0,
+ );
+ }, [instance]);
- // Switch to usePagination plugin if enabled
- const rows = tablePages ?? tableRows;
-
- const empty = rows.length === 0;
+ const empty = instance.getRowCount() === 0;
const pageSize = usePageSize();
const isLoading = useIsLoading();
let body: ReactNode;
+
if (isLoading) {
body = Array(tableStyles?.placeholder ?? pageSize)
.fill(0)
@@ -88,27 +91,22 @@ export default function BaseTable<T extends object>(props: BaseTableProps<T>) {
</Table.Tr>
);
} else {
- body = rows.map((row) => {
- prepareRow(row);
+ body = instance.getRowModel().rows.map((row) => {
return rowRenderer(row);
});
}
return (
<Box className={styles.container}>
- <Table
- className={styles.table}
- striped={tableStyles?.striped ?? true}
- {...getTableProps()}
- >
+ <Table className={styles.table} striped={tableStyles?.striped ?? true}>
<Table.Thead hidden={tableStyles?.hideHeader}>
- {headerGroups.map((headerGroup) => (
- <Table.Tr {...headerGroup.getHeaderGroupProps()}>
+ {instance.getHeaderGroups().map((headerGroup) => (
+ <Table.Tr key={headerGroup.id}>
{headersRenderer(headerGroup.headers)}
</Table.Tr>
))}
</Table.Thead>
- <Table.Tbody {...getTableBodyProps()}>{body}</Table.Tbody>
+ <Table.Tbody>{body}</Table.Tbody>
</Table>
</Box>
);
diff --git a/frontend/src/components/tables/GroupTable.tsx b/frontend/src/components/tables/GroupTable.tsx
index c05182aa6..b14edf3e6 100644
--- a/frontend/src/components/tables/GroupTable.tsx
+++ b/frontend/src/components/tables/GroupTable.tsx
@@ -1,38 +1,44 @@
-import {
- Cell,
- HeaderGroup,
- Row,
- useExpanded,
- useGroupBy,
- useSortBy,
-} from "react-table";
+import React, { Fragment } from "react";
import { Box, Table, Text } from "@mantine/core";
import { faChevronCircleRight } from "@fortawesome/free-solid-svg-icons";
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>) {
- if (cell.isGrouped) {
+function renderCell<T extends object = object>(
+ cell: Cell<T, unknown>,
+ row: Row<T>,
+) {
+ if (cell.getIsGrouped()) {
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;
} else {
- return cell.render("Cell");
+ return flexRender(cell.column.columnDef.cell, cell.getContext());
}
}
function renderRow<T extends object>(row: Row<T>) {
- if (row.canExpand) {
- const cell = row.cells.find((cell) => cell.isGrouped);
+ if (row.getCanExpand()) {
+ const cell = row.getVisibleCells().find((cell) => cell.getIsGrouped());
+
if (cell) {
- const rotation = row.isExpanded ? 90 : undefined;
+ const rotation = row.getIsExpanded() ? 90 : undefined;
+
return (
- <Table.Tr {...row.getRowProps()}>
- <Table.Td {...cell.getCellProps()} colSpan={row.cells.length}>
- <Text {...row.getToggleRowExpandedProps()} p={2}>
- {cell.render("Cell")}
+ <Table.Tr key={row.id} style={{ cursor: "pointer" }}>
+ <Table.Td key={cell.id} colSpan={row.getVisibleCells().length}>
+ <Text p={2} onClick={() => row.toggleExpanded()}>
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
<Box component="span" mx={12}>
<FontAwesomeIcon
icon={faChevronCircleRight}
@@ -48,13 +54,12 @@ function renderRow<T extends object>(row: Row<T>) {
}
} else {
return (
- <Table.Tr {...row.getRowProps()}>
- {row.cells
- .filter((cell) => !cell.isPlaceholder)
+ <Table.Tr key={row.id}>
+ {row
+ .getVisibleCells()
+ .filter((cell) => !cell.getIsPlaceholder())
.map((cell) => (
- <Table.Td {...cell.getCellProps()}>
- {renderCell(cell, row)}
- </Table.Td>
+ <Table.Td key={cell.id}>{renderCell(cell, row)}</Table.Td>
))}
</Table.Tr>
);
@@ -62,27 +67,34 @@ function renderRow<T extends object>(row: Row<T>) {
}
function renderHeaders<T extends object>(
- headers: HeaderGroup<T>[],
-): JSX.Element[] {
- return headers
- .filter((col) => !col.isGrouped)
- .map((col) => (
- <Table.Th {...col.getHeaderProps()}>{col.render("Header")}</Table.Th>
- ));
+ headers: Header<T, unknown>[],
+): React.JSX.Element[] {
+ return headers.map((header) => {
+ if (header.column.getIsGrouped()) {
+ return <Fragment key={header.id}></Fragment>;
+ }
+
+ return (
+ <Table.Th key={header.id} colSpan={header.colSpan}>
+ {flexRender(header.column.columnDef.header, header.getContext())}
+ </Table.Th>
+ );
+ });
}
type Props<T extends object> = Omit<
SimpleTableProps<T>,
- "plugins" | "headersRenderer" | "rowRenderer"
+ "headersRenderer" | "rowRenderer"
>;
-const plugins = [useGroupBy, useSortBy, useExpanded];
-
function GroupTable<T extends object = object>(props: Props<T>) {
return (
<SimpleTable
{...props}
- plugins={plugins}
+ enableGrouping
+ enableExpanding
+ getGroupedRowModel={getGroupedRowModel()}
+ getExpandedRowModel={getExpandedRowModel()}
tableStyles={{ headersRenderer: renderHeaders, rowRenderer: renderRow }}
></SimpleTable>
);
diff --git a/frontend/src/components/tables/PageTable.tsx b/frontend/src/components/tables/PageTable.tsx
index ee4824db6..476ff2c2b 100644
--- a/frontend/src/components/tables/PageTable.tsx
+++ b/frontend/src/components/tables/PageTable.tsx
@@ -1,55 +1,62 @@
-import { useEffect } from "react";
-import { usePagination, useTable } from "react-table";
+import { MutableRefObject, useEffect } from "react";
+import {
+ getCoreRowModel,
+ getPaginationRowModel,
+ Table,
+ TableOptions,
+ useReactTable,
+} from "@tanstack/react-table";
+import BaseTable, { TableStyleProps } from "@/components/tables/BaseTable";
import { ScrollToTop } from "@/utilities";
import { usePageSize } from "@/utilities/storage";
-import BaseTable from "./BaseTable";
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;
};
-const tablePlugins = [useDefaultSettings, usePagination];
-
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(
- options,
- useDefaultSettings,
- ...tablePlugins,
- ...(plugins ?? []),
- );
+ const pageSize = usePageSize();
- // use page size as specified in UI settings
- instance.state.pageSize = usePageSize();
+ const instance = useReactTable({
+ ...options,
+ getCoreRowModel: getCoreRowModel(),
+ getPaginationRowModel: getPaginationRowModel(),
+ initialState: {
+ pagination: {
+ pageSize: pageSize,
+ },
+ },
+ });
if (instanceRef) {
instanceRef.current = instance;
}
+ const pageIndex = instance.getState().pagination.pageIndex;
+
// Scroll to top when page is changed
useEffect(() => {
if (autoScroll) {
ScrollToTop();
}
- }, [instance.state.pageIndex, autoScroll]);
+ }, [pageIndex, autoScroll]);
+
+ const state = instance.getState();
return (
<>
- <BaseTable
- {...options}
- {...instance}
- plugins={[...tablePlugins, ...(plugins ?? [])]}
- ></BaseTable>
+ <BaseTable {...options} instance={instance}></BaseTable>
<PageControl
- count={instance.pageCount}
- index={instance.state.pageIndex}
- size={instance.state.pageSize}
- total={instance.rows.length}
- goto={instance.gotoPage}
+ count={instance.getPageCount()}
+ index={state.pagination.pageIndex}
+ size={pageSize}
+ total={instance.getRowCount()}
+ goto={instance.setPageIndex}
></PageControl>
</>
);
diff --git a/frontend/src/components/tables/QueryPageTable.tsx b/frontend/src/components/tables/QueryPageTable.tsx
index c144b3b54..797d7a08e 100644
--- a/frontend/src/components/tables/QueryPageTable.tsx
+++ b/frontend/src/components/tables/QueryPageTable.tsx
@@ -1,9 +1,9 @@
import { useEffect } from "react";
import { UsePaginationQueryResult } from "@/apis/queries/hooks";
+import SimpleTable, { SimpleTableProps } from "@/components/tables/SimpleTable";
import { LoadingProvider } from "@/contexts";
import { ScrollToTop } from "@/utilities";
import PageControl from "./PageControl";
-import SimpleTable, { SimpleTableProps } from "./SimpleTable";
type Props<T extends object> = Omit<SimpleTableProps<T>, "data"> & {
query: UsePaginationQueryResult<T>;
diff --git a/frontend/src/components/tables/SimpleTable.tsx b/frontend/src/components/tables/SimpleTable.tsx
index 90f76c7f2..e3e0b7ff3 100644
--- a/frontend/src/components/tables/SimpleTable.tsx
+++ b/frontend/src/components/tables/SimpleTable.tsx
@@ -1,23 +1,65 @@
-import { PluginHook, TableInstance, TableOptions, useTable } from "react-table";
-import BaseTable, { TableStyleProps } from "./BaseTable";
-import { useDefaultSettings } from "./plugins";
+import { MutableRefObject, useEffect, useMemo } from "react";
+import {
+ 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> & {
- plugins?: PluginHook<T>[];
- instanceRef?: React.MutableRefObject<TableInstance<T> | null>;
+export type SimpleTableProps<T extends object> = Omit<
+ TableOptions<T>,
+ "getCoreRowModel"
+> & {
+ instanceRef?: MutableRefObject<Table<T> | null>;
tableStyles?: TableStyleProps<T>;
+ onRowSelectionChanged?: (selectedRows: Row<T>[]) => void;
+ onAllRowsExpandedChanged?: (isAllRowsExpanded: boolean) => void;
};
export default function SimpleTable<T extends object>(
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) {
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>;
}
diff --git a/frontend/src/components/tables/plugins/index.ts b/frontend/src/components/tables/plugins/index.ts
deleted file mode 100644
index 39490a113..000000000
--- a/frontend/src/components/tables/plugins/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { default as useCustomSelection } from "./useCustomSelection";
-export { default as useDefaultSettings } from "./useDefaultSettings";
diff --git a/frontend/src/components/tables/plugins/useCustomSelection.tsx b/frontend/src/components/tables/plugins/useCustomSelection.tsx
deleted file mode 100644
index 572926093..000000000
--- a/frontend/src/components/tables/plugins/useCustomSelection.tsx
+++ /dev/null
@@ -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;
diff --git a/frontend/src/components/tables/plugins/useDefaultSettings.tsx b/frontend/src/components/tables/plugins/useDefaultSettings.tsx
deleted file mode 100644
index ac34334b4..000000000
--- a/frontend/src/components/tables/plugins/useDefaultSettings.tsx
+++ /dev/null
@@ -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;
diff --git a/frontend/src/pages/Blacklist/Movies/table.tsx b/frontend/src/pages/Blacklist/Movies/table.tsx
index 0ac3bf3a8..00730a850 100644
--- a/frontend/src/pages/Blacklist/Movies/table.tsx
+++ b/frontend/src/pages/Blacklist/Movies/table.tsx
@@ -1,56 +1,70 @@
import { FunctionComponent, useMemo } from "react";
import { Link } from "react-router-dom";
-import { Column } from "react-table";
import { Anchor, Text } from "@mantine/core";
import { faTrash } from "@fortawesome/free-solid-svg-icons";
+import { ColumnDef } from "@tanstack/react-table";
import { useMovieDeleteBlacklist } from "@/apis/hooks";
-import { PageTable } from "@/components";
import MutateAction from "@/components/async/MutateAction";
import Language from "@/components/bazarr/Language";
+import PageTable from "@/components/tables/PageTable";
import TextPopover from "@/components/TextPopover";
interface Props {
- blacklist: readonly Blacklist.Movie[];
+ blacklist: Blacklist.Movie[];
}
const Table: FunctionComponent<Props> = ({ blacklist }) => {
- const columns = useMemo<Column<Blacklist.Movie>[]>(
+ const remove = useMovieDeleteBlacklist();
+
+ const columns = useMemo<ColumnDef<Blacklist.Movie>[]>(
() => [
{
- Header: "Name",
- accessor: "title",
- Cell: (row) => {
- const target = `/movies/${row.row.original.radarrId}`;
+ header: "Name",
+ accessorKey: "title",
+ cell: ({
+ row: {
+ original: { radarrId },
+ },
+ }) => {
+ const target = `/movies/${radarrId}`;
return (
<Anchor className="table-primary" component={Link} to={target}>
- {row.value}
+ {radarrId}
</Anchor>
);
},
},
{
- Header: "Language",
- accessor: "language",
- Cell: ({ value }) => {
- if (value) {
- return <Language.Text value={value} long></Language.Text>;
+ header: "Language",
+ accessorKey: "language",
+ cell: ({
+ row: {
+ original: { language },
+ },
+ }) => {
+ if (language) {
+ return <Language.Text value={language} long></Language.Text>;
} else {
return null;
}
},
},
{
- Header: "Provider",
- accessor: "provider",
+ header: "Provider",
+ accessorKey: "provider",
},
{
- Header: "Date",
- accessor: "timestamp",
- Cell: (row) => {
- if (row.value) {
+ header: "Date",
+ accessorKey: "timestamp",
+ cell: ({
+ row: {
+ original: { timestamp, parsed_timestamp: parsedTimestamp },
+ },
+ }) => {
+ if (timestamp) {
return (
- <TextPopover text={row.row.original.parsed_timestamp}>
- <Text>{row.value}</Text>
+ <TextPopover text={parsedTimestamp}>
+ <Text>{timestamp}</Text>
</TextPopover>
);
} else {
@@ -59,10 +73,12 @@ const Table: FunctionComponent<Props> = ({ blacklist }) => {
},
},
{
- accessor: "subs_id",
- Cell: ({ row, value }) => {
- const remove = useMovieDeleteBlacklist();
-
+ id: "subs_id",
+ cell: ({
+ row: {
+ original: { subs_id: subsId, provider },
+ },
+ }) => {
return (
<MutateAction
label="Remove from Blacklist"
@@ -72,9 +88,9 @@ const Table: FunctionComponent<Props> = ({ blacklist }) => {
args={() => ({
all: false,
form: {
- provider: row.original.provider,
+ provider: provider,
// eslint-disable-next-line camelcase
- subs_id: value,
+ subs_id: subsId,
},
})}
></MutateAction>
@@ -82,7 +98,7 @@ const Table: FunctionComponent<Props> = ({ blacklist }) => {
},
},
],
- [],
+ [remove],
);
return (
<PageTable
diff --git a/frontend/src/pages/Blacklist/Series/table.tsx b/frontend/src/pages/Blacklist/Series/table.tsx
index 655b8d67a..3d67e637d 100644
--- a/frontend/src/pages/Blacklist/Series/table.tsx
+++ b/frontend/src/pages/Blacklist/Series/table.tsx
@@ -1,63 +1,77 @@
import { FunctionComponent, useMemo } from "react";
import { Link } from "react-router-dom";
-import { Column } from "react-table";
import { Anchor, Text } from "@mantine/core";
import { faTrash } from "@fortawesome/free-solid-svg-icons";
+import { ColumnDef } from "@tanstack/react-table";
import { useEpisodeDeleteBlacklist } from "@/apis/hooks";
-import { PageTable } from "@/components";
import MutateAction from "@/components/async/MutateAction";
import Language from "@/components/bazarr/Language";
+import PageTable from "@/components/tables/PageTable";
import TextPopover from "@/components/TextPopover";
interface Props {
- blacklist: readonly Blacklist.Episode[];
+ blacklist: Blacklist.Episode[];
}
const Table: FunctionComponent<Props> = ({ blacklist }) => {
- const columns = useMemo<Column<Blacklist.Episode>[]>(
+ const removeFromBlacklist = useEpisodeDeleteBlacklist();
+
+ const columns = useMemo<ColumnDef<Blacklist.Episode>[]>(
() => [
{
- Header: "Series",
- accessor: "seriesTitle",
- Cell: (row) => {
- const target = `/series/${row.row.original.sonarrSeriesId}`;
+ header: "Series",
+ accessorKey: "seriesTitle",
+ cell: ({
+ row: {
+ original: { sonarrSeriesId, seriesTitle },
+ },
+ }) => {
+ const target = `/series/${sonarrSeriesId}`;
return (
<Anchor className="table-primary" component={Link} to={target}>
- {row.value}
+ {seriesTitle}
</Anchor>
);
},
},
{
- Header: "Episode",
- accessor: "episode_number",
+ header: "Episode",
+ accessorKey: "episode_number",
},
{
- accessor: "episodeTitle",
+ id: "episodeTitle",
},
{
- Header: "Language",
- accessor: "language",
- Cell: ({ value }) => {
- if (value) {
- return <Language.Text value={value} long></Language.Text>;
+ header: "Language",
+ accessorKey: "language",
+ cell: ({
+ row: {
+ original: { language },
+ },
+ }) => {
+ if (language) {
+ return <Language.Text value={language} long></Language.Text>;
} else {
return null;
}
},
},
{
- Header: "Provider",
- accessor: "provider",
+ header: "Provider",
+ accessorKey: "provider",
},
{
- Header: "Date",
- accessor: "timestamp",
- Cell: (row) => {
- if (row.value) {
+ header: "Date",
+ accessorKey: "timestamp",
+ cell: ({
+ row: {
+ original: { timestamp, parsed_timestamp: parsedTimestamp },
+ },
+ }) => {
+ if (timestamp) {
return (
- <TextPopover text={row.row.original.parsed_timestamp}>
- <Text>{row.value}</Text>
+ <TextPopover text={parsedTimestamp}>
+ <Text>{timestamp}</Text>
</TextPopover>
);
} else {
@@ -66,22 +80,24 @@ const Table: FunctionComponent<Props> = ({ blacklist }) => {
},
},
{
- accessor: "subs_id",
- Cell: ({ row, value }) => {
- const remove = useEpisodeDeleteBlacklist();
-
+ id: "subs_id",
+ cell: ({
+ row: {
+ original: { subs_id: subsId, provider },
+ },
+ }) => {
return (
<MutateAction
label="Remove from Blacklist"
noReset
icon={faTrash}
- mutation={remove}
+ mutation={removeFromBlacklist}
args={() => ({
all: false,
form: {
- provider: row.original.provider,
+ provider: provider,
// eslint-disable-next-line camelcase
- subs_id: value,
+ subs_id: subsId,
},
})}
></MutateAction>
@@ -89,7 +105,7 @@ const Table: FunctionComponent<Props> = ({ blacklist }) => {
},
},
],
- [],
+ [removeFromBlacklist],
);
return (
<PageTable
diff --git a/frontend/src/pages/Episodes/index.tsx b/frontend/src/pages/Episodes/index.tsx
index 13c112050..8075e77a1 100644
--- a/frontend/src/pages/Episodes/index.tsx
+++ b/frontend/src/pages/Episodes/index.tsx
@@ -21,6 +21,7 @@ import {
faSync,
faWrench,
} from "@fortawesome/free-solid-svg-icons";
+import { Table as TableInstance } from "@tanstack/table-core/build/lib/types";
import {
useEpisodesBySeriesId,
useIsAnyActionRunning,
@@ -41,12 +42,6 @@ import { useLanguageProfileBy } from "@/utilities/languages";
import Table from "./table";
const SeriesEpisodesView: FunctionComponent = () => {
- const [state, setState] = useState({
- expand: false,
- buttonText: "Expand All",
- initial: true,
- });
-
const params = useParams();
const id = Number.parseInt(params.id as string);
@@ -102,18 +97,18 @@ const SeriesEpisodesView: FunctionComponent = () => {
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);
if (isNaN(id) || (isFetched && !series)) {
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 (
<Container px={0} fluid>
<QueryOverlay result={seriesQuery}>
@@ -210,12 +205,14 @@ const SeriesEpisodesView: FunctionComponent = () => {
Edit Series
</Toolbox.Button>
<Toolbox.Button
- icon={state.expand ? faCircleChevronRight : faCircleChevronDown}
+ icon={
+ isAllRowExpanded ? faCircleChevronRight : faCircleChevronDown
+ }
onClick={() => {
- toggleState();
+ tableRef.current?.toggleAllRowsExpanded();
}}
>
- {state.buttonText}
+ {isAllRowExpanded ? "Collapse All" : "Expand All"}
</Toolbox.Button>
</Group>
</Toolbox>
@@ -223,11 +220,11 @@ const SeriesEpisodesView: FunctionComponent = () => {
<ItemOverview item={series ?? null} details={details}></ItemOverview>
<QueryOverlay result={episodesQuery}>
<Table
- expand={state.expand}
- initial={state.initial}
+ ref={tableRef}
episodes={episodes ?? null}
profile={profile}
disabled={hasTask || !series || series.profileId === null}
+ onAllRowsExpandedChanged={setIsAllRowExpanded}
></Table>
</QueryOverlay>
</Stack>
diff --git a/frontend/src/pages/Episodes/table.tsx b/frontend/src/pages/Episodes/table.tsx
index b28e0d7e8..7b8d4494f 100644
--- a/frontend/src/pages/Episodes/table.tsx
+++ b/frontend/src/pages/Episodes/table.tsx
@@ -1,11 +1,4 @@
-import {
- FunctionComponent,
- useCallback,
- useEffect,
- useMemo,
- useRef,
-} from "react";
-import { Column, TableInstance } from "react-table";
+import React, { forwardRef, useCallback, useEffect, useMemo } from "react";
import { Group, Text } from "@mantine/core";
import { faBookmark as farBookmark } from "@fortawesome/free-regular-svg-icons";
import {
@@ -14,6 +7,7 @@ import {
faUser,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { ColumnDef, Table as TableInstance } from "@tanstack/react-table";
import { useDownloadEpisodeSubtitles, useEpisodesProvider } from "@/apis/hooks";
import { useShowOnlyDesired } from "@/apis/hooks/site";
import { Action, GroupTable } from "@/components";
@@ -30,219 +24,227 @@ interface Props {
episodes: Item.Episode[] | null;
disabled?: boolean;
profile?: Language.Profile;
- expand?: boolean;
- initial?: boolean;
+ onAllRowsExpandedChanged: (isAllRowsExpanded: boolean) => void;
}
-const Table: FunctionComponent<Props> = ({
- episodes,
- profile,
- disabled,
- expand,
- initial,
-}) => {
- const onlyDesired = useShowOnlyDesired();
-
- const profileItems = useProfileItemsToLanguages(profile);
- const { mutateAsync } = useDownloadEpisodeSubtitles();
-
- const download = useCallback(
- (item: Item.Episode, result: SearchResultType) => {
- const {
- language,
- hearing_impaired: hi,
- forced,
- provider,
- subtitle,
- original_format: originalFormat,
- } = result;
- const { sonarrSeriesId: seriesId, sonarrEpisodeId: episodeId } = item;
-
- return mutateAsync({
- seriesId,
- episodeId,
- form: {
+const Table = forwardRef<TableInstance<Item.Episode> | null, Props>(
+ ({ episodes, profile, disabled, onAllRowsExpandedChanged }, ref) => {
+ const onlyDesired = useShowOnlyDesired();
+
+ const tableRef =
+ ref as React.MutableRefObject<TableInstance<Item.Episode> | null>;
+
+ const profileItems = useProfileItemsToLanguages(profile);
+
+ const { mutateAsync } = useDownloadEpisodeSubtitles();
+
+ const modals = useModals();
+
+ const download = useCallback(
+ (item: Item.Episode, result: SearchResultType) => {
+ const {
language,
- hi,
+ hearing_impaired: hi,
forced,
provider,
subtitle,
- // eslint-disable-next-line camelcase
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>
+ );
+ },
},
- });
- },
- [mutateAsync],
- );
-
- const columns: Column<Item.Episode>[] = useMemo<Column<Item.Episode>[]>(
- () => [
- {
- accessor: "monitored",
- Cell: (row) => {
- return (
- <FontAwesomeIcon
- title={row.value ? "monitored" : "unmonitored"}
- icon={row.value ? faBookmark : farBookmark}
- ></FontAwesomeIcon>
- );
+ {
+ header: "",
+ accessorKey: "season",
+ cell: ({
+ row: {
+ original: { season },
+ },
+ }) => {
+ return <Text span>Season {season}</Text>;
+ },
},
- },
- {
- accessor: "season",
- Cell: (row) => {
- return <Text span>Season {row.value}</Text>;
+ {
+ header: "Episode",
+ accessorKey: "episode",
},
- },
- {
- Header: "Episode",
- accessor: "episode",
- },
- {
- Header: "Title",
- accessor: "title",
- Cell: ({ value, row }) => {
- return (
- <TextPopover text={row.original.sceneName}>
- <Text className="table-primary">{value}</Text>
- </TextPopover>
- );
+ {
+ header: "Title",
+ accessorKey: "title",
+ cell: ({
+ row: {
+ original: { sceneName, title },
+ },
+ }) => {
+ return (
+ <TextPopover text={sceneName}>
+ <Text className="table-primary">{title}</Text>
+ </TextPopover>
+ );
+ },
},
- },
- {
- Header: "Audio",
- accessor: "audio_language",
- Cell: ({ value }) => <AudioList audios={value}></AudioList>,
- },
- {
- Header: "Subtitles",
- 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: "Audio",
+ accessorKey: "audio_language",
+ cell: ({
+ row: {
+ original: { audio_language: audioLanguage },
+ },
+ }) => <AudioList audios={audioLanguage}></AudioList>,
},
- },
- {
- Header: "Actions",
- accessor: "sonarrEpisodeId",
- Cell: ({ row }) => {
- 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: "Subtitles",
+ accessorKey: "missing_subtitles",
+ cell: ({ row: { original } }) => {
+ return <SubtitlesCell episode={original} />;
+ },
},
- },
- ],
- [onlyDesired, profileItems, disabled, download],
- );
-
- const maxSeason = useMemo(
- () =>
- episodes?.reduce<number>(
- (prev, curr) => Math.max(prev, curr.season),
- 0,
- ) ?? 0,
- [episodes],
- );
-
- const instance = useRef<TableInstance<Item.Episode> | null>(null);
-
- useEffect(() => {
- if (instance.current) {
- if (initial) {
- // start with all rows collapsed
- instance.current.toggleAllRowsExpanded(false);
- // expand the last/current season on initial display
- instance.current.toggleRowExpanded([`season:${maxSeason}`], true);
- } else {
- if (expand !== undefined) {
- instance.current.toggleAllRowsExpanded(expand);
- }
- }
- }
- }, [maxSeason, expand, initial]);
-
- return (
- <GroupTable
- columns={columns}
- data={episodes ?? []}
- instanceRef={instance}
- initialState={{
- sortBy: [
- { id: "season", desc: true },
- { id: "episode", desc: true },
- ],
- groupBy: ["season"],
- }}
- tableStyles={{ emptyText: "No Episode Found For This Series" }}
- ></GroupTable>
- );
-};
+ {
+ header: "Actions",
+ cell: ({ row }) => {
+ 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>
+ );
+ },
+ },
+ ],
+ [disabled, download, modals, SubtitlesCell],
+ );
+
+ const maxSeason = useMemo(
+ () =>
+ episodes?.reduce<number>(
+ (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;
diff --git a/frontend/src/pages/History/Movies/index.tsx b/frontend/src/pages/History/Movies/index.tsx
index 39365e348..92d1aa280 100644
--- a/frontend/src/pages/History/Movies/index.tsx
+++ b/frontend/src/pages/History/Movies/index.tsx
@@ -1,7 +1,6 @@
/* eslint-disable camelcase */
import { FunctionComponent, useMemo } from "react";
import { Link } from "react-router-dom";
-import { Column } from "react-table";
import { Anchor, Badge, Text } from "@mantine/core";
import {
faFileExcel,
@@ -9,6 +8,7 @@ import {
faRecycle,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { ColumnDef } from "@tanstack/react-table";
import { useMovieAddBlacklist, useMovieHistoryPagination } from "@/apis/hooks";
import { MutateAction } from "@/components/async";
import { HistoryIcon } from "@/components/bazarr";
@@ -18,32 +18,40 @@ import TextPopover from "@/components/TextPopover";
import HistoryView from "@/pages/views/HistoryView";
const MoviesHistoryView: FunctionComponent = () => {
- const columns: Column<History.Movie>[] = useMemo<Column<History.Movie>[]>(
+ const addToBlacklist = useMovieAddBlacklist();
+
+ const columns = useMemo<ColumnDef<History.Movie>[]>(
() => [
{
- accessor: "action",
- Cell: (row) => <HistoryIcon action={row.value}></HistoryIcon>,
+ id: "action",
+ cell: ({ row }) => (
+ <HistoryIcon action={row.original.action}></HistoryIcon>
+ ),
},
{
- Header: "Name",
- accessor: "title",
- Cell: ({ row, value }) => {
+ header: "Name",
+ accessorKey: "title",
+ cell: ({ row }) => {
const target = `/movies/${row.original.radarrId}`;
return (
<Anchor className="table-primary" component={Link} to={target}>
- {value}
+ {row.original.title}
</Anchor>
);
},
},
{
- Header: "Language",
- accessor: "language",
- Cell: ({ value }) => {
- if (value) {
+ header: "Language",
+ accessorKey: "language",
+ cell: ({
+ row: {
+ original: { language },
+ },
+ }) => {
+ if (language) {
return (
<Badge>
- <Language.Text value={value} long></Language.Text>
+ <Language.Text value={language} long></Language.Text>
</Badge>
);
} else {
@@ -52,13 +60,13 @@ const MoviesHistoryView: FunctionComponent = () => {
},
},
{
- Header: "Score",
- accessor: "score",
+ header: "Score",
+ accessorKey: "score",
},
{
- Header: "Match",
- accessor: "matches",
- Cell: (row) => {
+ header: "Match",
+ accessorKey: "matches",
+ cell: (row) => {
const { matches, dont_matches: dont } = row.row.original;
if (matches.length || dont.length) {
return (
@@ -74,13 +82,17 @@ const MoviesHistoryView: FunctionComponent = () => {
},
},
{
- Header: "Date",
- accessor: "timestamp",
- Cell: (row) => {
- if (row.value) {
+ header: "Date",
+ accessorKey: "timestamp",
+ cell: ({
+ row: {
+ original: { timestamp, parsed_timestamp },
+ },
+ }) => {
+ if (timestamp) {
return (
- <TextPopover text={row.row.original.parsed_timestamp}>
- <Text>{row.value}</Text>
+ <TextPopover text={parsed_timestamp}>
+ <Text>{timestamp}</Text>
</TextPopover>
);
} else {
@@ -89,21 +101,29 @@ const MoviesHistoryView: FunctionComponent = () => {
},
},
{
- Header: "Info",
- accessor: "description",
- Cell: ({ value }) => {
+ header: "Info",
+ accessorKey: "description",
+ cell: ({
+ row: {
+ original: { description },
+ },
+ }) => {
return (
- <TextPopover text={value}>
+ <TextPopover text={description}>
<FontAwesomeIcon size="sm" icon={faInfoCircle}></FontAwesomeIcon>
</TextPopover>
);
},
},
{
- Header: "Upgrade",
- accessor: "upgradable",
- Cell: (row) => {
- if (row.value) {
+ header: "Upgrade",
+ accessorKey: "upgradable",
+ cell: ({
+ row: {
+ original: { upgradable },
+ },
+ }) => {
+ if (upgradable) {
return (
<TextPopover text="This Subtitle File Is Eligible For An Upgrade.">
<FontAwesomeIcon size="sm" icon={faRecycle}></FontAwesomeIcon>
@@ -115,20 +135,25 @@ const MoviesHistoryView: FunctionComponent = () => {
},
},
{
- Header: "Blacklist",
- accessor: "blacklisted",
- Cell: ({ row, value }) => {
- const add = useMovieAddBlacklist();
- const { radarrId, provider, subs_id, language, subtitles_path } =
- row.original;
+ header: "Blacklist",
+ accessorKey: "blacklisted",
+ cell: ({ row }) => {
+ const {
+ blacklisted,
+ radarrId,
+ provider,
+ subs_id,
+ language,
+ subtitles_path,
+ } = row.original;
if (subs_id && provider && language) {
return (
<MutateAction
label="Add to Blacklist"
- disabled={value}
+ disabled={blacklisted}
icon={faFileExcel}
- mutation={add}
+ mutation={addToBlacklist}
args={() => ({
id: radarrId,
form: {
@@ -146,7 +171,7 @@ const MoviesHistoryView: FunctionComponent = () => {
},
},
],
- [],
+ [addToBlacklist],
);
const query = useMovieHistoryPagination();
diff --git a/frontend/src/pages/History/Series/index.tsx b/frontend/src/pages/History/Series/index.tsx
index 67f18b9d9..a5d75516a 100644
--- a/frontend/src/pages/History/Series/index.tsx
+++ b/frontend/src/pages/History/Series/index.tsx
@@ -1,7 +1,6 @@
/* eslint-disable camelcase */
import { FunctionComponent, useMemo } from "react";
import { Link } from "react-router-dom";
-import { Column } from "react-table";
import { Anchor, Badge, Text } from "@mantine/core";
import {
faFileExcel,
@@ -9,6 +8,7 @@ import {
faRecycle,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { ColumnDef } from "@tanstack/react-table";
import {
useEpisodeAddBlacklist,
useEpisodeHistoryPagination,
@@ -21,44 +21,60 @@ import TextPopover from "@/components/TextPopover";
import HistoryView from "@/pages/views/HistoryView";
const SeriesHistoryView: FunctionComponent = () => {
- const columns: Column<History.Episode>[] = useMemo<Column<History.Episode>[]>(
+ const addToBlacklist = useEpisodeAddBlacklist();
+
+ const columns = useMemo<ColumnDef<History.Episode>[]>(
() => [
{
- accessor: "action",
- Cell: ({ value }) => <HistoryIcon action={value}></HistoryIcon>,
+ id: "action",
+ cell: ({ row: { original } }) => (
+ <HistoryIcon action={original.action}></HistoryIcon>
+ ),
},
{
- Header: "Series",
- accessor: "seriesTitle",
- Cell: (row) => {
- const target = `/series/${row.row.original.sonarrSeriesId}`;
+ header: "Series",
+ accessorKey: "seriesTitle",
+ cell: ({
+ row: {
+ original: { seriesTitle, sonarrSeriesId },
+ },
+ }) => {
+ const target = `/series/${sonarrSeriesId}`;
return (
<Anchor className="table-primary" component={Link} to={target}>
- {row.value}
+ {seriesTitle}
</Anchor>
);
},
},
{
- Header: "Episode",
- accessor: "episode_number",
+ header: "Episode",
+ accessorKey: "episode_number",
},
{
- Header: "Title",
- accessor: "episodeTitle",
- Cell: ({ value }) => {
- return <Text className="table-no-wrap">{value}</Text>;
+ header: "Title",
+ accessorKey: "episodeTitle",
+ cell: ({
+ row: {
+ original: { episodeTitle },
+ },
+ }) => {
+ return <Text className="table-no-wrap">{episodeTitle}</Text>;
},
},
{
- Header: "Language",
- accessor: "language",
- Cell: ({ value }) => {
- if (value) {
+ header: "Language",
+ accessorKey: "language",
+ cell: ({
+ row: {
+ original: { language },
+ },
+ }) => {
+ if (language) {
return (
<Badge color="secondary">
- <Language.Text value={value} long></Language.Text>
+ <Language.Text value={language} long></Language.Text>
</Badge>
);
} else {
@@ -67,13 +83,13 @@ const SeriesHistoryView: FunctionComponent = () => {
},
},
{
- Header: "Score",
- accessor: "score",
+ header: "Score",
+ accessorKey: "score",
},
{
- Header: "Match",
- accessor: "matches",
- Cell: (row) => {
+ header: "Match",
+ accessorKey: "matches",
+ cell: (row) => {
const { matches, dont_matches: dont } = row.row.original;
if (matches.length || dont.length) {
return (
@@ -89,13 +105,17 @@ const SeriesHistoryView: FunctionComponent = () => {
},
},
{
- Header: "Date",
- accessor: "timestamp",
- Cell: (row) => {
- if (row.value) {
+ header: "Date",
+ accessorKey: "timestamp",
+ cell: ({
+ row: {
+ original: { timestamp, parsed_timestamp },
+ },
+ }) => {
+ if (timestamp) {
return (
- <TextPopover text={row.row.original.parsed_timestamp}>
- <Text>{row.value}</Text>
+ <TextPopover text={parsed_timestamp}>
+ <Text>{timestamp}</Text>
</TextPopover>
);
} else {
@@ -104,21 +124,29 @@ const SeriesHistoryView: FunctionComponent = () => {
},
},
{
- Header: "Info",
- accessor: "description",
- Cell: ({ row, value }) => {
+ header: "Info",
+ accessorKey: "description",
+ cell: ({
+ row: {
+ original: { description },
+ },
+ }) => {
return (
- <TextPopover text={value}>
+ <TextPopover text={description}>
<FontAwesomeIcon size="sm" icon={faInfoCircle}></FontAwesomeIcon>
</TextPopover>
);
},
},
{
- Header: "Upgrade",
- accessor: "upgradable",
- Cell: (row) => {
- if (row.value) {
+ header: "Upgrade",
+ accessorKey: "upgradable",
+ cell: ({
+ row: {
+ original: { upgradable },
+ },
+ }) => {
+ if (upgradable) {
return (
<TextPopover text="This Subtitle File Is Eligible For An Upgrade.">
<FontAwesomeIcon size="sm" icon={faRecycle}></FontAwesomeIcon>
@@ -130,9 +158,9 @@ const SeriesHistoryView: FunctionComponent = () => {
},
},
{
- Header: "Blacklist",
- accessor: "blacklisted",
- Cell: ({ row, value }) => {
+ header: "Blacklist",
+ accessorKey: "blacklisted",
+ cell: ({ row }) => {
const {
sonarrEpisodeId,
sonarrSeriesId,
@@ -140,16 +168,15 @@ const SeriesHistoryView: FunctionComponent = () => {
subs_id,
language,
subtitles_path,
+ blacklisted,
} = row.original;
- const add = useEpisodeAddBlacklist();
-
if (subs_id && provider && language) {
return (
<MutateAction
label="Add to Blacklist"
- disabled={value}
+ disabled={blacklisted}
icon={faFileExcel}
- mutation={add}
+ mutation={addToBlacklist}
args={() => ({
seriesId: sonarrSeriesId,
episodeId: sonarrEpisodeId,
@@ -168,7 +195,7 @@ const SeriesHistoryView: FunctionComponent = () => {
},
},
],
- [],
+ [addToBlacklist],
);
const query = useEpisodeHistoryPagination();
diff --git a/frontend/src/pages/Movies/Details/table.tsx b/frontend/src/pages/Movies/Details/table.tsx
index 03593102c..7d0c20a30 100644
--- a/frontend/src/pages/Movies/Details/table.tsx
+++ b/frontend/src/pages/Movies/Details/table.tsx
@@ -1,13 +1,14 @@
-import { FunctionComponent, useMemo } from "react";
-import { Column } from "react-table";
+import React, { FunctionComponent, useMemo } from "react";
import { Badge, Text, TextProps } from "@mantine/core";
import { faEllipsis, faSearch } from "@fortawesome/free-solid-svg-icons";
+import { ColumnDef } from "@tanstack/react-table";
import { isString } from "lodash";
import { useMovieSubtitleModification } from "@/apis/hooks";
import { useShowOnlyDesired } from "@/apis/hooks/site";
-import { Action, SimpleTable } from "@/components";
+import { Action } from "@/components";
import Language from "@/components/bazarr/Language";
import SubtitleToolsMenu from "@/components/SubtitleToolsMenu";
+import SimpleTable from "@/components/tables/SimpleTable";
import { task, TaskGroup } from "@/modules/task";
import { filterSubtitleBy, toPython } from "@/utilities";
import { useProfileItemsToLanguages } from "@/utilities/languages";
@@ -33,35 +34,125 @@ const Table: FunctionComponent<Props> = ({ movie, profile, disabled }) => {
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",
- accessor: "path",
- Cell: ({ value }) => {
+ header: "Subtitle Path",
+ accessorKey: "path",
+ cell: ({
+ row: {
+ original: { path },
+ },
+ }) => {
const props: TextProps = {
className: "table-primary",
};
- if (isSubtitleTrack(value)) {
+ if (isSubtitleTrack(path)) {
return (
<Text className="table-primary">Video File Subtitle Track</Text>
);
- } else if (isSubtitleMissing(value)) {
+ } else if (isSubtitleMissing(path)) {
return (
<Text {...props} c="dimmed">
- {value}
+ {path}
</Text>
);
} else {
- return <Text {...props}>{value}</Text>;
+ return <Text {...props}>{path}</Text>;
}
},
},
{
- Header: "Language",
- accessor: "name",
- Cell: ({ row }) => {
+ header: "Language",
+ accessorKey: "name",
+ cell: ({ row }) => {
if (row.original.path === missingText) {
return (
<Badge color="primary">
@@ -78,99 +169,13 @@ const Table: FunctionComponent<Props> = ({ movie, profile, disabled }) => {
},
},
{
- accessor: "code2",
- Cell: ({ row }) => {
- const {
- 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>
- );
+ id: "code2",
+ cell: ({ row: { original } }) => {
+ return <CodeCell item={original} />;
},
},
],
- [movie, disabled],
+ [CodeCell],
);
const data: Subtitle[] = useMemo(() => {
diff --git a/frontend/src/pages/Movies/Editor.tsx b/frontend/src/pages/Movies/Editor.tsx
index 0c568a5b1..1ec84a52c 100644
--- a/frontend/src/pages/Movies/Editor.tsx
+++ b/frontend/src/pages/Movies/Editor.tsx
@@ -1,6 +1,7 @@
import { FunctionComponent, useMemo } from "react";
-import { Column } from "react-table";
+import { Checkbox } from "@mantine/core";
import { useDocumentTitle } from "@mantine/hooks";
+import { ColumnDef } from "@tanstack/react-table";
import { useMovieModification, useMovies } from "@/apis/hooks";
import { QueryOverlay } from "@/components/async";
import { AudioList } from "@/components/bazarr";
@@ -11,32 +12,63 @@ const MovieMassEditor: FunctionComponent = () => {
const query = useMovies();
const mutation = useMovieModification();
- const columns = useMemo<Column<Item.Movie>[]>(
+ useDocumentTitle("Movies - Bazarr (Mass Editor)");
+
+ const columns = useMemo<ColumnDef<Item.Movie>[]>(
() => [
{
- Header: "Name",
- accessor: "title",
+ id: "selection",
+ 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",
- accessor: "audio_language",
- Cell: ({ value }) => {
- return <AudioList audios={value}></AudioList>;
+ header: "Audio",
+ accessorKey: "audio_language",
+ cell: ({
+ row: {
+ original: { audio_language: audioLanguage },
+ },
+ }) => {
+ return <AudioList audios={audioLanguage}></AudioList>;
},
},
{
- Header: "Languages Profile",
- accessor: "profileId",
- Cell: ({ value }) => {
- return <LanguageProfileName index={value}></LanguageProfileName>;
+ header: "Languages Profile",
+ accessorKey: "profileId",
+ cell: ({
+ row: {
+ original: { profileId },
+ },
+ }) => {
+ return <LanguageProfileName index={profileId}></LanguageProfileName>;
},
},
],
[],
);
- useDocumentTitle("Movies - Bazarr (Mass Editor)");
-
return (
<QueryOverlay result={query}>
<MassEditor
diff --git a/frontend/src/pages/Movies/index.tsx b/frontend/src/pages/Movies/index.tsx
index 1cbd14a36..0429e1fdd 100644
--- a/frontend/src/pages/Movies/index.tsx
+++ b/frontend/src/pages/Movies/index.tsx
@@ -1,11 +1,11 @@
import { FunctionComponent, useMemo } from "react";
import { Link } from "react-router-dom";
-import { Column } from "react-table";
import { Anchor, Badge, Container } from "@mantine/core";
import { useDocumentTitle } from "@mantine/hooks";
import { faBookmark as farBookmark } from "@fortawesome/free-regular-svg-icons";
import { faBookmark, faWrench } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { ColumnDef } from "@tanstack/react-table";
import { useMovieModification, useMoviesPagination } from "@/apis/hooks";
import { Action } from "@/components";
import { AudioList } from "@/components/bazarr";
@@ -17,55 +17,81 @@ import ItemView from "@/pages/views/ItemView";
import { BuildKey } from "@/utilities";
const MovieView: FunctionComponent = () => {
+ const modifyMovie = useMovieModification();
+
+ const modals = useModals();
+
const query = useMoviesPagination();
- const columns: Column<Item.Movie>[] = useMemo<Column<Item.Movie>[]>(
+ const columns = useMemo<ColumnDef<Item.Movie>[]>(
() => [
{
- accessor: "monitored",
- Cell: ({ value }) => (
+ id: "monitored",
+ cell: ({
+ row: {
+ original: { monitored },
+ },
+ }) => (
<FontAwesomeIcon
- title={value ? "monitored" : "unmonitored"}
- icon={value ? faBookmark : farBookmark}
+ title={monitored ? "monitored" : "unmonitored"}
+ icon={monitored ? faBookmark : farBookmark}
></FontAwesomeIcon>
),
},
{
- Header: "Name",
- accessor: "title",
- Cell: ({ row, value }) => {
- const target = `/movies/${row.original.radarrId}`;
+ header: "Name",
+ accessorKey: "title",
+ cell: ({
+ row: {
+ original: { title, radarrId },
+ },
+ }) => {
+ const target = `/movies/${radarrId}`;
return (
<Anchor className="table-primary" component={Link} to={target}>
- {value}
+ {title}
</Anchor>
);
},
},
{
- Header: "Audio",
- accessor: "audio_language",
- Cell: ({ value }) => {
- return <AudioList audios={value}></AudioList>;
+ header: "Audio",
+ accessorKey: "audio_language",
+ cell: ({
+ row: {
+ original: { audio_language: audioLanguage },
+ },
+ }) => {
+ return <AudioList audios={audioLanguage}></AudioList>;
},
},
{
- Header: "Languages Profile",
- accessor: "profileId",
- Cell: ({ value }) => {
+ header: "Languages Profile",
+ accessorKey: "profileId",
+ cell: ({
+ row: {
+ original: { profileId },
+ },
+ }) => {
return (
- <LanguageProfileName index={value} empty=""></LanguageProfileName>
+ <LanguageProfileName
+ index={profileId}
+ empty=""
+ ></LanguageProfileName>
);
},
},
{
- Header: "Missing Subtitles",
- accessor: "missing_subtitles",
- Cell: (row) => {
- const missing = row.value;
+ header: "Missing Subtitles",
+ accessorKey: "missing_subtitles",
+ cell: ({
+ row: {
+ original: { missing_subtitles: missingSubtitles },
+ },
+ }) => {
return (
<>
- {missing.map((v) => (
+ {missingSubtitles.map((v) => (
<Badge
mr="xs"
color="yellow"
@@ -79,10 +105,8 @@ const MovieView: FunctionComponent = () => {
},
},
{
- accessor: "radarrId",
- Cell: ({ row }) => {
- const modals = useModals();
- const mutation = useMovieModification();
+ id: "radarrId",
+ cell: ({ row }) => {
return (
<Action
label="Edit Movie"
@@ -91,7 +115,7 @@ const MovieView: FunctionComponent = () => {
modals.openContextModal(
ItemEditModal,
{
- mutation,
+ mutation: modifyMovie,
item: row.original,
},
{
@@ -105,7 +129,7 @@ const MovieView: FunctionComponent = () => {
},
},
],
- [],
+ [modals, modifyMovie],
);
useDocumentTitle("Movies - Bazarr");
diff --git a/frontend/src/pages/Series/Editor.tsx b/frontend/src/pages/Series/Editor.tsx
index 239481069..45a277d17 100644
--- a/frontend/src/pages/Series/Editor.tsx
+++ b/frontend/src/pages/Series/Editor.tsx
@@ -1,6 +1,7 @@
import { FunctionComponent, useMemo } from "react";
-import { Column } from "react-table";
+import { Checkbox } from "@mantine/core";
import { useDocumentTitle } from "@mantine/hooks";
+import { ColumnDef } from "@tanstack/react-table";
import { useSeries, useSeriesModification } from "@/apis/hooks";
import { QueryOverlay } from "@/components/async";
import { AudioList } from "@/components/bazarr";
@@ -11,24 +12,55 @@ const SeriesMassEditor: FunctionComponent = () => {
const query = useSeries();
const mutation = useSeriesModification();
- const columns = useMemo<Column<Item.Series>[]>(
+ const columns = useMemo<ColumnDef<Item.Series>[]>(
() => [
{
- Header: "Name",
- accessor: "title",
+ id: "selection",
+ 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",
- accessor: "audio_language",
- Cell: ({ value }) => {
- return <AudioList audios={value}></AudioList>;
+ header: "Audio",
+ accessorKey: "audio_language",
+ cell: ({
+ row: {
+ original: { audio_language: audioLanguage },
+ },
+ }) => {
+ return <AudioList audios={audioLanguage}></AudioList>;
},
},
{
- Header: "Languages Profile",
- accessor: "profileId",
- Cell: ({ value }) => {
- return <LanguageProfileName index={value}></LanguageProfileName>;
+ header: "Languages Profile",
+ accessorKey: "profileId",
+ cell: ({
+ row: {
+ original: { profileId },
+ },
+ }) => {
+ return <LanguageProfileName index={profileId}></LanguageProfileName>;
},
},
],
diff --git a/frontend/src/pages/Series/index.tsx b/frontend/src/pages/Series/index.tsx
index 92f63c9ab..229082444 100644
--- a/frontend/src/pages/Series/index.tsx
+++ b/frontend/src/pages/Series/index.tsx
@@ -1,11 +1,11 @@
import { FunctionComponent, useMemo } from "react";
import { Link } from "react-router-dom";
-import { Column } from "react-table";
import { Anchor, Container, Progress } from "@mantine/core";
import { useDocumentTitle } from "@mantine/hooks";
import { faBookmark as farBookmark } from "@fortawesome/free-regular-svg-icons";
import { faBookmark, faWrench } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { ColumnDef } from "@tanstack/react-table";
import { useSeriesModification, useSeriesPagination } from "@/apis/hooks";
import { Action } from "@/components";
import LanguageProfileName from "@/components/bazarr/LanguageProfile";
@@ -18,42 +18,51 @@ const SeriesView: FunctionComponent = () => {
const query = useSeriesPagination();
- const columns: Column<Item.Series>[] = useMemo<Column<Item.Series>[]>(
+ const modals = useModals();
+
+ const columns = useMemo<ColumnDef<Item.Series>[]>(
() => [
{
- accessor: "monitored",
- Cell: ({ value }) => (
+ id: "monitored",
+ cell: ({
+ row: {
+ original: { monitored },
+ },
+ }) => (
<FontAwesomeIcon
- title={value ? "monitored" : "unmonitored"}
- icon={value ? faBookmark : farBookmark}
+ title={monitored ? "monitored" : "unmonitored"}
+ icon={monitored ? faBookmark : farBookmark}
></FontAwesomeIcon>
),
},
{
- Header: "Name",
- accessor: "title",
- Cell: ({ row, value }) => {
- const target = `/series/${row.original.sonarrSeriesId}`;
+ header: "Name",
+ accessorKey: "title",
+ cell: ({ row: { original } }) => {
+ const target = `/series/${original.sonarrSeriesId}`;
return (
<Anchor className="table-primary" component={Link} to={target}>
- {value}
+ {original.title}
</Anchor>
);
},
},
{
- Header: "Languages Profile",
- accessor: "profileId",
- Cell: ({ value }) => {
+ header: "Languages Profile",
+ accessorKey: "profileId",
+ cell: ({ row: { original } }) => {
return (
- <LanguageProfileName index={value} empty=""></LanguageProfileName>
+ <LanguageProfileName
+ index={original.profileId}
+ empty=""
+ ></LanguageProfileName>
);
},
},
{
- Header: "Episodes",
- accessor: "episodeFileCount",
- Cell: (row) => {
+ header: "Episodes",
+ accessorKey: "episodeFileCount",
+ cell: (row) => {
const { episodeFileCount, episodeMissingCount, profileId, title } =
row.row.original;
let progress = 0;
@@ -80,9 +89,8 @@ const SeriesView: FunctionComponent = () => {
},
},
{
- accessor: "sonarrSeriesId",
- Cell: ({ row: { original } }) => {
- const modals = useModals();
+ id: "sonarrSeriesId",
+ cell: ({ row: { original } }) => {
return (
<Action
label="Edit Series"
@@ -105,7 +113,7 @@ const SeriesView: FunctionComponent = () => {
},
},
],
- [mutation],
+ [mutation, modals],
);
useDocumentTitle("Series - Bazarr");
diff --git a/frontend/src/pages/Settings/Languages/equals.tsx b/frontend/src/pages/Settings/Languages/equals.tsx
index dae20a75a..08642f27e 100644
--- a/frontend/src/pages/Settings/Languages/equals.tsx
+++ b/frontend/src/pages/Settings/Languages/equals.tsx
@@ -1,11 +1,12 @@
import { FunctionComponent, useCallback, useMemo } from "react";
-import { Column } from "react-table";
import { Button, Checkbox } from "@mantine/core";
import { faEquals, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { ColumnDef } from "@tanstack/react-table";
import { useLanguages } from "@/apis/hooks";
-import { Action, SimpleTable } from "@/components";
+import { Action } from "@/components";
import LanguageSelector from "@/components/bazarr/LanguageSelector";
+import SimpleTable from "@/components/tables/SimpleTable";
import { languageEqualsKey } from "@/pages/Settings/keys";
import { useFormActions } from "@/pages/Settings/utilities/FormValues";
import { useSettingValue } from "@/pages/Settings/utilities/hooks";
@@ -196,22 +197,22 @@ const EqualsTable: FunctionComponent<EqualsTableProps> = () => {
[equals, setEquals],
);
- const columns = useMemo<Column<LanguageEqualData>[]>(
+ const columns = useMemo<ColumnDef<LanguageEqualData>[]>(
() => [
{
- Header: "Source",
+ header: "Source",
id: "source-lang",
- accessor: "source",
- Cell: ({ value: { content }, row }) => {
+ accessorKey: "source",
+ cell: ({ row: { original, index } }) => {
return (
<LanguageSelector
enabled
- value={content}
+ value={original.source.content}
onChange={(result) => {
if (result !== null) {
- update(row.index, {
- ...row.original,
- source: { ...row.original.source, content: result },
+ update(index, {
+ ...original,
+ source: { ...original.source, content: result },
});
}
}}
@@ -221,12 +222,11 @@ const EqualsTable: FunctionComponent<EqualsTableProps> = () => {
},
{
id: "source-hi",
- accessor: "source",
- Cell: ({ value: { hi }, row }) => {
+ cell: ({ row }) => {
return (
<Checkbox
label="HI"
- checked={hi}
+ checked={row.original.source.hi}
onChange={({ currentTarget: { checked } }) => {
update(row.index, {
...row.original,
@@ -243,12 +243,11 @@ const EqualsTable: FunctionComponent<EqualsTableProps> = () => {
},
{
id: "source-forced",
- accessor: "source",
- Cell: ({ value: { forced }, row }) => {
+ cell: ({ row }) => {
return (
<Checkbox
label="Forced"
- checked={forced}
+ checked={row.original.source.forced}
onChange={({ currentTarget: { checked } }) => {
update(row.index, {
...row.original,
@@ -265,19 +264,18 @@ const EqualsTable: FunctionComponent<EqualsTableProps> = () => {
},
{
id: "equal-icon",
- Cell: () => {
+ cell: () => {
return <FontAwesomeIcon icon={faEquals} />;
},
},
{
- Header: "Target",
+ header: "Target",
id: "target-lang",
- accessor: "target",
- Cell: ({ value: { content }, row }) => {
+ cell: ({ row }) => {
return (
<LanguageSelector
enabled
- value={content}
+ value={row.original.target.content}
onChange={(result) => {
if (result !== null) {
update(row.index, {
@@ -292,12 +290,11 @@ const EqualsTable: FunctionComponent<EqualsTableProps> = () => {
},
{
id: "target-hi",
- accessor: "target",
- Cell: ({ value: { hi }, row }) => {
+ cell: ({ row }) => {
return (
<Checkbox
label="HI"
- checked={hi}
+ checked={row.original.target.hi}
onChange={({ currentTarget: { checked } }) => {
update(row.index, {
...row.original,
@@ -314,12 +311,11 @@ const EqualsTable: FunctionComponent<EqualsTableProps> = () => {
},
{
id: "target-forced",
- accessor: "target",
- Cell: ({ value: { forced }, row }) => {
+ cell: ({ row }) => {
return (
<Checkbox
label="Forced"
- checked={forced}
+ checked={row.original.target.forced}
onChange={({ currentTarget: { checked } }) => {
update(row.index, {
...row.original,
@@ -336,8 +332,7 @@ const EqualsTable: FunctionComponent<EqualsTableProps> = () => {
},
{
id: "action",
- accessor: "target",
- Cell: ({ row }) => {
+ cell: ({ row }) => {
return (
<Action
label="Remove"
diff --git a/frontend/src/pages/Settings/Languages/table.tsx b/frontend/src/pages/Settings/Languages/table.tsx
index c2ed9d968..c32300628 100644
--- a/frontend/src/pages/Settings/Languages/table.tsx
+++ b/frontend/src/pages/Settings/Languages/table.tsx
@@ -1,13 +1,14 @@
import { FunctionComponent, useCallback, useMemo } from "react";
-import { Column } from "react-table";
import { Badge, Button, Group } from "@mantine/core";
import { faTrash, faWrench } from "@fortawesome/free-solid-svg-icons";
+import { ColumnDef } from "@tanstack/react-table";
import { cloneDeep } from "lodash";
-import { Action, SimpleTable } from "@/components";
+import { Action } from "@/components";
import {
anyCutoff,
ProfileEditModal,
} from "@/components/forms/ProfileEditForm";
+import SimpleTable from "@/components/tables/SimpleTable";
import { useModals } from "@/modules/modals";
import { languageProfileKey } from "@/pages/Settings/keys";
import { useFormActions } from "@/pages/Settings/utilities/FormValues";
@@ -40,6 +41,7 @@ const Table: FunctionComponent = () => {
const updateProfile = useCallback(
(profile: Language.Profile) => {
const list = [...profiles];
+
const idx = list.findIndex((v) => v.profileId === profile.profileId);
if (idx !== -1) {
@@ -57,18 +59,20 @@ const Table: FunctionComponent = () => {
submitProfiles(fn(list));
});
- const columns = useMemo<Column<Language.Profile>[]>(
+ const columns = useMemo<ColumnDef<Language.Profile>[]>(
() => [
{
- Header: "Name",
- accessor: "name",
+ header: "Name",
+ accessorKey: "name",
},
{
- Header: "Languages",
- accessor: "items",
- Cell: (row) => {
- const items = row.value;
- const cutoff = row.row.original.cutoff;
+ header: "Languages",
+ accessorKey: "items",
+ cell: ({
+ row: {
+ original: { items, cutoff },
+ },
+ }) => {
return (
<Group gap="xs" wrap="nowrap">
{items.map((v) => {
@@ -82,16 +86,19 @@ const Table: FunctionComponent = () => {
},
},
{
- Header: "Must contain",
- accessor: "mustContain",
- Cell: (row) => {
- const items = row.value;
- if (!items) {
+ header: "Must contain",
+ accessorKey: "mustContain",
+ cell: ({
+ row: {
+ original: { mustContain },
+ },
+ }) => {
+ if (!mustContain) {
return null;
}
return (
<>
- {items.map((v, idx) => {
+ {mustContain.map((v, idx) => {
return (
<Badge key={BuildKey(idx, v)} color="gray">
{v}
@@ -103,16 +110,19 @@ const Table: FunctionComponent = () => {
},
},
{
- Header: "Must not contain",
- accessor: "mustNotContain",
- Cell: (row) => {
- const items = row.value;
- if (!items) {
+ header: "Must not contain",
+ accessorKey: "mustNotContain",
+ cell: ({
+ row: {
+ original: { mustNotContain },
+ },
+ }) => {
+ if (!mustNotContain) {
return null;
}
return (
<>
- {items.map((v, idx) => {
+ {mustNotContain.map((v, idx) => {
return (
<Badge key={BuildKey(idx, v)} color="gray">
{v}
@@ -124,8 +134,8 @@ const Table: FunctionComponent = () => {
},
},
{
- accessor: "profileId",
- Cell: ({ row }) => {
+ id: "profileId",
+ cell: ({ row }) => {
const profile = row.original;
return (
<Group gap="xs" wrap="nowrap">
@@ -160,7 +170,7 @@ const Table: FunctionComponent = () => {
return (
<>
- <SimpleTable columns={columns} data={profiles}></SimpleTable>
+ <SimpleTable columns={columns} data={[...profiles]}></SimpleTable>
<Button
fullWidth
disabled={!canAdd}
diff --git a/frontend/src/pages/Settings/components/pathMapper.tsx b/frontend/src/pages/Settings/components/pathMapper.tsx
index cc525402a..6b2c7baa2 100644
--- a/frontend/src/pages/Settings/components/pathMapper.tsx
+++ b/frontend/src/pages/Settings/components/pathMapper.tsx
@@ -1,10 +1,11 @@
import { FunctionComponent, useCallback, useMemo } from "react";
-import { Column } from "react-table";
import { Button } from "@mantine/core";
import { faArrowCircleRight, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { ColumnDef } from "@tanstack/react-table";
import { capitalize } from "lodash";
-import { Action, FileBrowser, SimpleTable } from "@/components";
+import { Action, FileBrowser } from "@/components";
+import SimpleTable from "@/components/tables/SimpleTable";
import {
moviesEnabledKey,
pathMappingsKey,
@@ -78,16 +79,16 @@ export const PathMappingTable: FunctionComponent<TableProps> = ({ type }) => {
updateRow(fn(data));
});
- const columns = useMemo<Column<PathMappingItem>[]>(
+ const columns = useMemo<ColumnDef<PathMappingItem>[]>(
() => [
{
- Header: capitalize(type),
- accessor: "from",
- Cell: ({ value, row: { original, index } }) => {
+ header: capitalize(type),
+ accessorKey: "from",
+ cell: ({ row: { original, index } }) => {
return (
<FileBrowser
type={type}
- defaultValue={value}
+ defaultValue={original.from}
onChange={(path) => {
action.mutate(index, { ...original, from: path });
}}
@@ -97,17 +98,17 @@ export const PathMappingTable: FunctionComponent<TableProps> = ({ type }) => {
},
{
id: "arrow",
- Cell: () => (
+ cell: () => (
<FontAwesomeIcon icon={faArrowCircleRight}></FontAwesomeIcon>
),
},
{
- Header: "Bazarr",
- accessor: "to",
- Cell: ({ value, row: { original, index } }) => {
+ header: "Bazarr",
+ accessorKey: "to",
+ cell: ({ row: { original, index } }) => {
return (
<FileBrowser
- defaultValue={value}
+ defaultValue={original.to}
type="bazarr"
onChange={(path) => {
action.mutate(index, { ...original, to: path });
@@ -118,8 +119,8 @@ export const PathMappingTable: FunctionComponent<TableProps> = ({ type }) => {
},
{
id: "action",
- accessor: "to",
- Cell: ({ row: { index } }) => {
+ accessorKey: "to",
+ cell: ({ row: { index } }) => {
return (
<Action
label="Remove"
diff --git a/frontend/src/pages/System/Announcements/table.tsx b/frontend/src/pages/System/Announcements/table.tsx
index 7c2ddf127..febb32fa1 100644
--- a/frontend/src/pages/System/Announcements/table.tsx
+++ b/frontend/src/pages/System/Announcements/table.tsx
@@ -1,65 +1,82 @@
import { FunctionComponent, useMemo } from "react";
-import { Column } from "react-table";
import { Anchor, Text } from "@mantine/core";
import { faWindowClose } from "@fortawesome/free-solid-svg-icons";
+import { ColumnDef } from "@tanstack/react-table";
import { useSystemAnnouncementsAddDismiss } from "@/apis/hooks";
-import { SimpleTable } from "@/components";
import { MutateAction } from "@/components/async";
+import SimpleTable from "@/components/tables/SimpleTable";
interface Props {
- announcements: readonly System.Announcements[];
+ announcements: System.Announcements[];
}
const Table: FunctionComponent<Props> = ({ announcements }) => {
- const columns: Column<System.Announcements>[] = useMemo<
- Column<System.Announcements>[]
+ const addDismiss = useSystemAnnouncementsAddDismiss();
+
+ const columns: ColumnDef<System.Announcements>[] = useMemo<
+ ColumnDef<System.Announcements>[]
>(
() => [
{
- Header: "Since",
+ header: "Since",
accessor: "timestamp",
- Cell: ({ value }) => {
- return <Text className="table-primary">{value}</Text>;
+ cell: ({
+ row: {
+ original: { timestamp },
+ },
+ }) => {
+ return <Text className="table-primary">{timestamp}</Text>;
},
},
{
- Header: "Announcement",
+ header: "Announcement",
accessor: "text",
- Cell: ({ value }) => {
- return <Text className="table-primary">{value}</Text>;
+ cell: ({
+ row: {
+ original: { text },
+ },
+ }) => {
+ return <Text className="table-primary">{text}</Text>;
},
},
{
- Header: "More Info",
+ header: "More Info",
accessor: "link",
- Cell: ({ value }) => {
- if (value) {
- return <Label link={value}>Link</Label>;
+ cell: ({
+ row: {
+ original: { link },
+ },
+ }) => {
+ if (link) {
+ return <Label link={link}>Link</Label>;
} else {
return <Text>n/a</Text>;
}
},
},
{
- Header: "Dismiss",
+ header: "Dismiss",
accessor: "hash",
- Cell: ({ row, value }) => {
- const add = useSystemAnnouncementsAddDismiss();
+ cell: ({
+ row: {
+ original: { dismissible, hash },
+ },
+ }) => {
return (
<MutateAction
label="Dismiss announcement"
- disabled={!row.original.dismissible}
+ disabled={!dismissible}
icon={faWindowClose}
- mutation={add}
+ mutation={addDismiss}
args={() => ({
- hash: value,
+ hash: hash,
})}
></MutateAction>
);
},
},
],
- [],
+ [addDismiss],
);
return (
diff --git a/frontend/src/pages/System/Backups/table.tsx b/frontend/src/pages/System/Backups/table.tsx
index efd60f78f..5c9a97f1f 100644
--- a/frontend/src/pages/System/Backups/table.tsx
+++ b/frontend/src/pages/System/Backups/table.tsx
@@ -1,53 +1,74 @@
import { FunctionComponent, useMemo } from "react";
-import { Column } from "react-table";
import { Anchor, Text } from "@mantine/core";
import { faHistory, faTrash } from "@fortawesome/free-solid-svg-icons";
+import { ColumnDef } from "@tanstack/react-table";
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 { Environment } from "@/utilities";
interface Props {
- backups: readonly System.Backups[];
+ backups: System.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",
- accessor: "filename",
- Cell: ({ value }) => {
+ header: "Name",
+ accessorKey: "filename",
+ cell: ({
+ row: {
+ original: { filename },
+ },
+ }) => {
return (
<Anchor
- href={`${Environment.baseUrl}/system/backup/download/${value}`}
+ href={`${Environment.baseUrl}/system/backup/download/${filename}`}
>
- {value}
+ {filename}
</Anchor>
);
},
},
{
- Header: "Size",
- accessor: "size",
- Cell: ({ value }) => {
- return <Text className="table-no-wrap">{value}</Text>;
+ header: "Size",
+ accessorKey: "size",
+ cell: ({
+ row: {
+ original: { size },
+ },
+ }) => {
+ return <Text className="table-no-wrap">{size}</Text>;
},
},
{
- Header: "Time",
- accessor: "date",
- Cell: ({ value }) => {
- return <Text className="table-no-wrap">{value}</Text>;
+ header: "Time",
+ accessorKey: "date",
+ cell: ({
+ row: {
+ original: { date },
+ },
+ }) => {
+ return <Text className="table-no-wrap">{date}</Text>;
},
},
{
id: "restore",
- Header: "Restore",
- accessor: "filename",
- Cell: ({ value }) => {
- const modals = useModals();
- const restore = useRestoreBackups();
+ header: "Restore",
+ accessorKey: "filename",
+ cell: ({
+ row: {
+ original: { filename },
+ },
+ }) => {
return (
<Action
label="Restore"
@@ -56,14 +77,14 @@ const Table: FunctionComponent<Props> = ({ backups }) => {
title: "Restore Backup",
children: (
<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
the restore process.
</Text>
),
labels: { confirm: "Restore", cancel: "Cancel" },
confirmProps: { color: "red" },
- onConfirm: () => restore.mutate(value),
+ onConfirm: () => restore.mutate(filename),
})
}
icon={faHistory}
@@ -72,12 +93,14 @@ const Table: FunctionComponent<Props> = ({ backups }) => {
},
},
{
- id: "delet4",
- Header: "Delete",
- accessor: "filename",
- Cell: ({ value }) => {
- const modals = useModals();
- const remove = useDeleteBackups();
+ id: "delete",
+ header: "Delete",
+ accessorKey: "filename",
+ cell: ({
+ row: {
+ original: { filename },
+ },
+ }) => {
return (
<Action
label="Delete"
@@ -87,12 +110,12 @@ const Table: FunctionComponent<Props> = ({ backups }) => {
title: "Delete Backup",
children: (
<Text size="sm">
- Are you sure you want to delete the backup ({value})?
+ Are you sure you want to delete the backup ({filename})?
</Text>
),
labels: { confirm: "Delete", cancel: "Cancel" },
confirmProps: { color: "red" },
- onConfirm: () => remove.mutate(value),
+ onConfirm: () => remove.mutate(filename),
})
}
icon={faTrash}
@@ -101,7 +124,7 @@ const Table: FunctionComponent<Props> = ({ backups }) => {
},
},
],
- [],
+ [modals, remove, restore],
);
return <PageTable columns={columns} data={backups}></PageTable>;
diff --git a/frontend/src/pages/System/Logs/table.tsx b/frontend/src/pages/System/Logs/table.tsx
index 541a8e285..0b1397c97 100644
--- a/frontend/src/pages/System/Logs/table.tsx
+++ b/frontend/src/pages/System/Logs/table.tsx
@@ -1,5 +1,4 @@
import { FunctionComponent, useMemo } from "react";
-import { Column } from "react-table";
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
import {
faBug,
@@ -10,12 +9,14 @@ import {
faQuestion,
} from "@fortawesome/free-solid-svg-icons";
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 SystemLogModal from "./modal";
interface Props {
- logs: readonly System.Log[];
+ logs: System.Log[];
}
function mapTypeToIcon(type: System.LogType): IconDefinition {
@@ -34,33 +35,40 @@ function mapTypeToIcon(type: System.LogType): IconDefinition {
}
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",
- Cell: (row) => (
- <FontAwesomeIcon icon={mapTypeToIcon(row.value)}></FontAwesomeIcon>
- ),
+ accessorKey: "type",
+ cell: ({
+ row: {
+ original: { type },
+ },
+ }) => <FontAwesomeIcon icon={mapTypeToIcon(type)}></FontAwesomeIcon>,
},
{
Header: "Message",
- accessor: "message",
+ accessorKey: "message",
},
{
Header: "Date",
- accessor: "timestamp",
+ accessorKey: "timestamp",
},
{
- accessor: "exception",
- Cell: ({ value }) => {
- const modals = useModals();
- if (value) {
+ accessorKey: "exception",
+ cell: ({
+ row: {
+ original: { exception },
+ },
+ }) => {
+ if (exception) {
return (
<Action
label="Detail"
icon={faLayerGroup}
onClick={() =>
- modals.openContextModal(SystemLogModal, { stack: value })
+ modals.openContextModal(SystemLogModal, { stack: exception })
}
></Action>
);
@@ -70,7 +78,7 @@ const Table: FunctionComponent<Props> = ({ logs }) => {
},
},
],
- [],
+ [modals],
);
return (
diff --git a/frontend/src/pages/System/Providers/table.tsx b/frontend/src/pages/System/Providers/table.tsx
index 0f9856d99..8e3ff7b89 100644
--- a/frontend/src/pages/System/Providers/table.tsx
+++ b/frontend/src/pages/System/Providers/table.tsx
@@ -1,25 +1,25 @@
import { FunctionComponent, useMemo } from "react";
-import { Column } from "react-table";
-import { SimpleTable } from "@/components";
+import { ColumnDef } from "@tanstack/react-table";
+import SimpleTable from "@/components/tables/SimpleTable";
interface Props {
- providers: readonly System.Provider[];
+ providers: System.Provider[];
}
const Table: FunctionComponent<Props> = (props) => {
- const columns: Column<System.Provider>[] = useMemo<Column<System.Provider>[]>(
+ const columns = useMemo<ColumnDef<System.Provider>[]>(
() => [
{
- Header: "Name",
- accessor: "name",
+ header: "Name",
+ accessorKey: "name",
},
{
- Header: "Status",
- accessor: "status",
+ header: "Status",
+ accessorKey: "status",
},
{
- Header: "Next Retry",
- accessor: "retry",
+ header: "Next Retry",
+ accessorKey: "retry",
},
],
[],
diff --git a/frontend/src/pages/System/Status/table.tsx b/frontend/src/pages/System/Status/table.tsx
index a22f4e435..7dad6757f 100644
--- a/frontend/src/pages/System/Status/table.tsx
+++ b/frontend/src/pages/System/Status/table.tsx
@@ -1,27 +1,35 @@
import { FunctionComponent, useMemo } from "react";
-import { Column } from "react-table";
import { Text } from "@mantine/core";
-import { SimpleTable } from "@/components";
+import { ColumnDef } from "@tanstack/react-table";
+import SimpleTable from "@/components/tables/SimpleTable";
interface Props {
- health: readonly System.Health[];
+ health: System.Health[];
}
const Table: FunctionComponent<Props> = ({ health }) => {
- const columns: Column<System.Health>[] = useMemo<Column<System.Health>[]>(
+ const columns = useMemo<ColumnDef<System.Health>[]>(
() => [
{
- Header: "Object",
- accessor: "object",
- Cell: ({ value }) => {
- return <Text className="table-no-wrap">{value}</Text>;
+ header: "Object",
+ accessorKey: "object",
+ cell: ({
+ row: {
+ original: { object },
+ },
+ }) => {
+ return <Text className="table-no-wrap">{object}</Text>;
},
},
{
- Header: "Issue",
- accessor: "issue",
- Cell: ({ value }) => {
- return <Text className="table-primary">{value}</Text>;
+ header: "Issue",
+ accessorKey: "issue",
+ cell: ({
+ row: {
+ original: { issue },
+ },
+ }) => {
+ return <Text className="table-primary">{issue}</Text>;
},
},
],
diff --git a/frontend/src/pages/System/Tasks/table.tsx b/frontend/src/pages/System/Tasks/table.tsx
index 548cc7b70..ed3248b6f 100644
--- a/frontend/src/pages/System/Tasks/table.tsx
+++ b/frontend/src/pages/System/Tasks/table.tsx
@@ -1,48 +1,59 @@
import { FunctionComponent, useMemo } from "react";
-import { Column, useSortBy } from "react-table";
import { Text } from "@mantine/core";
import { faPlay } from "@fortawesome/free-solid-svg-icons";
+import { ColumnDef, getSortedRowModel } from "@tanstack/react-table";
import { useRunTask } from "@/apis/hooks";
-import { SimpleTable } from "@/components";
import MutateAction from "@/components/async/MutateAction";
+import SimpleTable from "@/components/tables/SimpleTable";
interface Props {
- tasks: readonly System.Task[];
+ tasks: System.Task[];
}
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",
- Cell: ({ value }) => {
- return <Text className="table-primary">{value}</Text>;
+ cell: ({
+ row: {
+ original: { name },
+ },
+ }) => {
+ return <Text className="table-primary">{name}</Text>;
},
},
{
- Header: "Interval",
+ header: "Interval",
accessor: "interval",
- Cell: ({ value }) => {
- return <Text className="table-no-wrap">{value}</Text>;
+ cell: ({
+ row: {
+ original: { interval },
+ },
+ }) => {
+ return <Text className="table-no-wrap">{interval}</Text>;
},
},
{
- Header: "Next Execution",
+ header: "Next Execution",
accessor: "next_run_in",
},
{
- Header: "Run",
+ header: "Run",
accessor: "job_running",
- Cell: ({ row, value }) => {
- const { job_id: jobId } = row.original;
- const runTask = useRunTask();
-
+ cell: ({
+ row: {
+ original: { job_id: jobId, job_running: jobRunning },
+ },
+ }) => {
return (
<MutateAction
label="Run Job"
icon={faPlay}
- iconProps={{ spin: value }}
+ iconProps={{ spin: jobRunning }}
mutation={runTask}
args={() => jobId}
></MutateAction>
@@ -50,15 +61,16 @@ const Table: FunctionComponent<Props> = ({ tasks }) => {
},
},
],
- [],
+ [runTask],
);
return (
<SimpleTable
- initialState={{ sortBy: [{ id: "name", desc: false }] }}
+ initialState={{ sorting: [{ id: "name", desc: false }] }}
columns={columns}
data={tasks}
- plugins={[useSortBy]}
+ enableSorting
+ getSortedRowModel={getSortedRowModel()}
></SimpleTable>
);
};
diff --git a/frontend/src/pages/Wanted/Movies/index.tsx b/frontend/src/pages/Wanted/Movies/index.tsx
index 139bf0c46..7c497f799 100644
--- a/frontend/src/pages/Wanted/Movies/index.tsx
+++ b/frontend/src/pages/Wanted/Movies/index.tsx
@@ -1,9 +1,9 @@
import { FunctionComponent, useMemo } from "react";
import { Link } from "react-router-dom";
-import { Column } from "react-table";
import { Anchor, Badge, Group } from "@mantine/core";
import { faSearch } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { ColumnDef } from "@tanstack/react-table";
import {
useMovieAction,
useMovieSubtitleModification,
@@ -15,32 +15,37 @@ import WantedView from "@/pages/views/WantedView";
import { BuildKey } from "@/utilities";
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",
- Cell: (row) => {
- const target = `/movies/${row.row.original.radarrId}`;
+ cell: ({
+ row: {
+ original: { title, radarrId },
+ },
+ }) => {
+ const target = `/movies/${radarrId}`;
return (
<Anchor component={Link} to={target}>
- {row.value}
+ {title}
</Anchor>
);
},
},
{
- Header: "Missing",
+ header: "Missing",
accessor: "missing_subtitles",
- Cell: ({ row, value }) => {
- const wanted = row.original;
- const { radarrId } = wanted;
-
- const { download } = useMovieSubtitleModification();
-
+ cell: ({
+ row: {
+ original: { radarrId, missing_subtitles: missingSubtitles },
+ },
+ }) => {
return (
<Group gap="sm">
- {value.map((item, idx) => (
+ {missingSubtitles.map((item, idx) => (
<Badge
color={download.isPending ? "gray" : undefined}
leftSection={<FontAwesomeIcon icon={faSearch} />}
@@ -70,7 +75,7 @@ const WantedMoviesView: FunctionComponent = () => {
},
},
],
- [],
+ [download],
);
const { mutateAsync } = useMovieAction();
diff --git a/frontend/src/pages/Wanted/Series/index.tsx b/frontend/src/pages/Wanted/Series/index.tsx
index 082cc7931..0501ecef5 100644
--- a/frontend/src/pages/Wanted/Series/index.tsx
+++ b/frontend/src/pages/Wanted/Series/index.tsx
@@ -1,9 +1,9 @@
import { FunctionComponent, useMemo } from "react";
import { Link } from "react-router-dom";
-import { Column } from "react-table";
import { Anchor, Badge, Group } from "@mantine/core";
import { faSearch } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { ColumnDef } from "@tanstack/react-table";
import {
useEpisodeSubtitleModification,
useEpisodeWantedPagination,
@@ -15,40 +15,51 @@ import WantedView from "@/pages/views/WantedView";
import { BuildKey } from "@/utilities";
const WantedSeriesView: FunctionComponent = () => {
- const columns: Column<Wanted.Episode>[] = useMemo<Column<Wanted.Episode>[]>(
+ const { download } = useEpisodeSubtitleModification();
+
+ const columns = useMemo<ColumnDef<Wanted.Episode>[]>(
() => [
{
- Header: "Name",
- accessor: "seriesTitle",
- Cell: (row) => {
- const target = `/series/${row.row.original.sonarrSeriesId}`;
+ header: "Name",
+ accessorKey: "seriesTitle",
+ cell: ({
+ row: {
+ original: { sonarrSeriesId, seriesTitle },
+ },
+ }) => {
+ const target = `/series/${sonarrSeriesId}`;
return (
<Anchor className="table-primary" component={Link} to={target}>
- {row.value}
+ {seriesTitle}
</Anchor>
);
},
},
{
- Header: "Episode",
- accessor: "episode_number",
+ header: "Episode",
+ accessorKey: "episode_number",
},
{
- accessor: "episodeTitle",
+ accessorKey: "episodeTitle",
},
{
- Header: "Missing",
- accessor: "missing_subtitles",
- Cell: ({ row, value }) => {
- const wanted = row.original;
- const seriesId = wanted.sonarrSeriesId;
- const episodeId = wanted.sonarrEpisodeId;
-
- const { download } = useEpisodeSubtitleModification();
+ header: "Missing",
+ accessorKey: "missing_subtitles",
+ cell: ({
+ row: {
+ original: {
+ sonarrSeriesId,
+ sonarrEpisodeId,
+ missing_subtitles: missingSubtitles,
+ },
+ },
+ }) => {
+ const seriesId = sonarrSeriesId;
+ const episodeId = sonarrEpisodeId;
return (
<Group gap="sm">
- {value.map((item, idx) => (
+ {missingSubtitles.map((item, idx) => (
<Badge
color={download.isPending ? "gray" : undefined}
leftSection={<FontAwesomeIcon icon={faSearch} />}
@@ -79,7 +90,7 @@ const WantedSeriesView: FunctionComponent = () => {
},
},
],
- [],
+ [download],
);
const { mutateAsync } = useSeriesAction();
diff --git a/frontend/src/pages/views/HistoryView.tsx b/frontend/src/pages/views/HistoryView.tsx
index 3553f58c6..f9fe8a27f 100644
--- a/frontend/src/pages/views/HistoryView.tsx
+++ b/frontend/src/pages/views/HistoryView.tsx
@@ -1,13 +1,13 @@
-import { Column } from "react-table";
import { Container } from "@mantine/core";
import { useDocumentTitle } from "@mantine/hooks";
+import { ColumnDef } from "@tanstack/react-table";
import { UsePaginationQueryResult } from "@/apis/queries/hooks";
import { QueryPageTable } from "@/components";
interface Props<T extends History.Base> {
name: string;
query: UsePaginationQueryResult<T>;
- columns: Column<T>[];
+ columns: ColumnDef<T>[];
}
function HistoryView<T extends History.Base = History.Base>({
diff --git a/frontend/src/pages/views/ItemView.tsx b/frontend/src/pages/views/ItemView.tsx
index e00d330ee..c4ff250ea 100644
--- a/frontend/src/pages/views/ItemView.tsx
+++ b/frontend/src/pages/views/ItemView.tsx
@@ -1,12 +1,12 @@
import { useNavigate } from "react-router-dom";
-import { Column } from "react-table";
import { faList } from "@fortawesome/free-solid-svg-icons";
+import { ColumnDef } from "@tanstack/react-table";
import { UsePaginationQueryResult } from "@/apis/queries/hooks";
import { QueryPageTable, Toolbox } from "@/components";
interface Props<T extends Item.Base = Item.Base> {
query: UsePaginationQueryResult<T>;
- columns: Column<T>[];
+ columns: ColumnDef<T>[];
}
function ItemView<T extends Item.Base>({ query, columns }: Props<T>) {
diff --git a/frontend/src/pages/views/MassEditor.tsx b/frontend/src/pages/views/MassEditor.tsx
index ee9ba476a..48068d1c6 100644
--- a/frontend/src/pages/views/MassEditor.tsx
+++ b/frontend/src/pages/views/MassEditor.tsx
@@ -1,22 +1,17 @@
-import { useCallback, useMemo, useState } from "react";
+import { useCallback, useMemo, useRef, useState } from "react";
import { useNavigate } from "react-router-dom";
-import { Column, useRowSelect } from "react-table";
import { Box, Container, useCombobox } from "@mantine/core";
import { faCheck, faUndo } from "@fortawesome/free-solid-svg-icons";
import { UseMutationResult } from "@tanstack/react-query";
+import { ColumnDef, Table } from "@tanstack/react-table";
import { uniqBy } from "lodash";
import { useIsAnyMutationRunning, useLanguageProfiles } from "@/apis/hooks";
-import {
- GroupedSelector,
- GroupedSelectorOptions,
- SimpleTable,
- Toolbox,
-} from "@/components";
-import { useCustomSelection } from "@/components/tables/plugins";
+import { GroupedSelector, GroupedSelectorOptions, Toolbox } from "@/components";
+import SimpleTable from "@/components/tables/SimpleTable";
import { GetItemId, useSelectorOptions } from "@/utilities";
interface MassEditorProps<T extends Item.Base = Item.Base> {
- columns: Column<T>[];
+ columns: ColumnDef<T>[];
data: T[];
mutation: UseMutationResult<void, unknown, FormType.ModifyItem>;
}
@@ -28,6 +23,7 @@ function MassEditor<T extends Item.Base>(props: MassEditorProps<T>) {
const [dirties, setDirties] = useState<T[]>([]);
const hasTask = useIsAnyMutationRunning();
const { data: profiles } = useLanguageProfiles();
+ const tableRef = useRef<Table<T>>(null);
const navigate = useNavigate();
@@ -120,6 +116,8 @@ function MassEditor<T extends Item.Base>(props: MassEditorProps<T>) {
setDirties((dirty) => {
return uniqBy([...newItems, ...dirty], GetItemId);
});
+
+ tableRef.current?.toggleAllRowsSelected(false);
},
[selections],
);
@@ -163,10 +161,13 @@ function MassEditor<T extends Item.Base>(props: MassEditorProps<T>) {
</Box>
</Toolbox>
<SimpleTable
+ instanceRef={tableRef}
columns={columns}
data={data}
- onSelect={setSelections}
- plugins={[useRowSelect, useCustomSelection]}
+ enableRowSelection
+ onRowSelectionChanged={(row) => {
+ setSelections(row.map((r) => r.original));
+ }}
></SimpleTable>
</Container>
);
diff --git a/frontend/src/pages/views/WantedView.tsx b/frontend/src/pages/views/WantedView.tsx
index aa71775ac..e04f583b8 100644
--- a/frontend/src/pages/views/WantedView.tsx
+++ b/frontend/src/pages/views/WantedView.tsx
@@ -1,14 +1,14 @@
-import { Column } from "react-table";
import { Container } from "@mantine/core";
import { useDocumentTitle } from "@mantine/hooks";
import { faSearch } from "@fortawesome/free-solid-svg-icons";
+import { ColumnDef } from "@tanstack/react-table";
import { useIsAnyActionRunning } from "@/apis/hooks";
import { UsePaginationQueryResult } from "@/apis/queries/hooks";
import { QueryPageTable, Toolbox } from "@/components";
interface Props<T extends Wanted.Base> {
name: string;
- columns: Column<T>[];
+ columns: ColumnDef<T>[];
query: UsePaginationQueryResult<T>;
searchAll: () => Promise<void>;
}