Updated tanstack table to v8.x (#2564)

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

@ -16,6 +16,7 @@
"@mantine/modals": "^7.11.0",
"@mantine/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",

@ -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",

@ -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"

@ -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 (

@ -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"

@ -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 (

@ -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 =

@ -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>

@ -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>
);

@ -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>
);

@ -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>
</>
);

@ -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>;

@ -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>;
}

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

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

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

@ -1,56 +1,70 @@
import { FunctionComponent, useMemo } from "react";
import { 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

@ -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

@ -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>

@ -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;

@ -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();

@ -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();

@ -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(() => {

@ -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

@ -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");

@ -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>;
},
},
],

@ -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");

@ -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"

@ -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}

@ -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"

@ -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 (

@ -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>;

@ -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 (

@ -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",
},
],
[],

@ -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>;
},
},
],

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

@ -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();

@ -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();

@ -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>({

@ -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>) {

@ -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>
);

@ -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>;
}

Loading…
Cancel
Save