aboutsummaryrefslogtreecommitdiffhomepage
path: root/frontend/src/components
diff options
context:
space:
mode:
authorAnderson Shindy Oki <[email protected]>2024-07-05 09:56:13 +0900
committerGitHub <[email protected]>2024-07-05 09:56:13 +0900
commit8beebce2e4db3411e23e7aff59788fcfdbf1753f (patch)
tree5f4c8920c7845fa59ed32a13a7a912650ef5b80c /frontend/src/components
parenta4527a7942fca4c0fe28ec5a2cdad56ee569800c (diff)
downloadbazarr-8beebce2e4db3411e23e7aff59788fcfdbf1753f.tar.gz
bazarr-8beebce2e4db3411e23e7aff59788fcfdbf1753f.zip
Updated tanstack table to v8.x (#2564)
Diffstat (limited to 'frontend/src/components')
-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
14 files changed, 657 insertions, 596 deletions
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;