summaryrefslogtreecommitdiffhomepage
path: root/frontend/src/pages
diff options
context:
space:
mode:
authorJayZed <[email protected]>2024-07-08 17:33:43 -0400
committerJayZed <[email protected]>2024-07-08 17:33:43 -0400
commit4cc6806193127f9d6d3f2dab26969471d9bbf159 (patch)
treebfc90e4b55fa0f48f83e51c4e5947c1f7d7d7a2d /frontend/src/pages
parentd875dc7733c901246881325ee3a84fe5d44b10b9 (diff)
parent5886c20c9c7929bf46836a99c2d9d4eb834638bd (diff)
downloadbazarr-4cc6806193127f9d6d3f2dab26969471d9bbf159.tar.gz
bazarr-4cc6806193127f9d6d3f2dab26969471d9bbf159.zip
Merge branch 'development' of https://github.com/morpheus65535/bazarr into development
Diffstat (limited to 'frontend/src/pages')
-rw-r--r--frontend/src/pages/Authentication.test.tsx2
-rw-r--r--frontend/src/pages/Authentication.tsx8
-rw-r--r--frontend/src/pages/Blacklist/Movies/index.tsx8
-rw-r--r--frontend/src/pages/Blacklist/Movies/table.tsx86
-rw-r--r--frontend/src/pages/Blacklist/Series/index.tsx8
-rw-r--r--frontend/src/pages/Blacklist/Series/table.tsx94
-rw-r--r--frontend/src/pages/Episodes/components.tsx19
-rw-r--r--frontend/src/pages/Episodes/index.tsx85
-rw-r--r--frontend/src/pages/Episodes/table.tsx443
-rw-r--r--frontend/src/pages/History/Movies/index.tsx129
-rw-r--r--frontend/src/pages/History/Series/index.tsx144
-rw-r--r--frontend/src/pages/History/Statistics/HistoryStats.module.scss9
-rw-r--r--frontend/src/pages/History/Statistics/HistoryStats.tsx (renamed from frontend/src/pages/History/Statistics/index.tsx)59
-rw-r--r--frontend/src/pages/History/history.test.tsx2
-rw-r--r--frontend/src/pages/Movies/Details/index.tsx49
-rw-r--r--frontend/src/pages/Movies/Details/table.tsx231
-rw-r--r--frontend/src/pages/Movies/Editor.tsx64
-rw-r--r--frontend/src/pages/Movies/index.tsx103
-rw-r--r--frontend/src/pages/Movies/movies.test.tsx4
-rw-r--r--frontend/src/pages/Series/Editor.tsx60
-rw-r--r--frontend/src/pages/Series/index.tsx86
-rw-r--r--frontend/src/pages/Series/series.test.tsx4
-rw-r--r--frontend/src/pages/Settings/General/index.tsx31
-rw-r--r--frontend/src/pages/Settings/Languages/components.tsx11
-rw-r--r--frontend/src/pages/Settings/Languages/equals.test.ts2
-rw-r--r--frontend/src/pages/Settings/Languages/equals.tsx65
-rw-r--r--frontend/src/pages/Settings/Languages/index.tsx10
-rw-r--r--frontend/src/pages/Settings/Languages/table.tsx82
-rw-r--r--frontend/src/pages/Settings/Notifications/components.tsx31
-rw-r--r--frontend/src/pages/Settings/Notifications/index.tsx13
-rw-r--r--frontend/src/pages/Settings/Providers/components.tsx126
-rw-r--r--frontend/src/pages/Settings/Providers/index.tsx18
-rw-r--r--frontend/src/pages/Settings/Providers/list.ts62
-rw-r--r--frontend/src/pages/Settings/Radarr/index.tsx8
-rw-r--r--frontend/src/pages/Settings/Scheduler/index.tsx4
-rw-r--r--frontend/src/pages/Settings/Sonarr/index.tsx10
-rw-r--r--frontend/src/pages/Settings/Subtitles/index.tsx11
-rw-r--r--frontend/src/pages/Settings/Subtitles/options.ts2
-rw-r--r--frontend/src/pages/Settings/UI/index.tsx4
-rw-r--r--frontend/src/pages/Settings/components/Card.module.scss9
-rw-r--r--frontend/src/pages/Settings/components/Card.tsx35
-rw-r--r--frontend/src/pages/Settings/components/Layout.test.tsx2
-rw-r--r--frontend/src/pages/Settings/components/Layout.tsx22
-rw-r--r--frontend/src/pages/Settings/components/LayoutModal.tsx22
-rw-r--r--frontend/src/pages/Settings/components/Message.tsx4
-rw-r--r--frontend/src/pages/Settings/components/Section.test.tsx8
-rw-r--r--frontend/src/pages/Settings/components/Section.tsx4
-rw-r--r--frontend/src/pages/Settings/components/collapse.tsx6
-rw-r--r--frontend/src/pages/Settings/components/forms.test.tsx8
-rw-r--r--frontend/src/pages/Settings/components/forms.tsx35
-rw-r--r--frontend/src/pages/Settings/components/index.tsx10
-rw-r--r--frontend/src/pages/Settings/components/pathMapper.tsx43
-rw-r--r--frontend/src/pages/Settings/utilities/FormValues.ts4
-rw-r--r--frontend/src/pages/Settings/utilities/hooks.ts8
-rw-r--r--frontend/src/pages/System/Announcements/index.tsx6
-rw-r--r--frontend/src/pages/System/Announcements/table.tsx70
-rw-r--r--frontend/src/pages/System/Backups/index.tsx10
-rw-r--r--frontend/src/pages/System/Backups/table.tsx100
-rw-r--r--frontend/src/pages/System/Logs/index.tsx28
-rw-r--r--frontend/src/pages/System/Logs/modal.tsx4
-rw-r--r--frontend/src/pages/System/Logs/table.tsx44
-rw-r--r--frontend/src/pages/System/Providers/index.tsx10
-rw-r--r--frontend/src/pages/System/Providers/table.tsx20
-rw-r--r--frontend/src/pages/System/Releases/index.tsx14
-rw-r--r--frontend/src/pages/System/Status/index.tsx73
-rw-r--r--frontend/src/pages/System/Status/table.tsx37
-rw-r--r--frontend/src/pages/System/Tasks/index.tsx8
-rw-r--r--frontend/src/pages/System/Tasks/table.tsx61
-rw-r--r--frontend/src/pages/Wanted/Movies/index.tsx51
-rw-r--r--frontend/src/pages/Wanted/Series/index.tsx71
-rw-r--r--frontend/src/pages/errors/CriticalError.tsx6
-rw-r--r--frontend/src/pages/errors/NotFound.tsx4
-rw-r--r--frontend/src/pages/errors/UIError.tsx16
-rw-r--r--frontend/src/pages/views/HistoryView.tsx8
-rw-r--r--frontend/src/pages/views/ItemOverview.tsx118
-rw-r--r--frontend/src/pages/views/ItemView.tsx8
-rw-r--r--frontend/src/pages/views/MassEditor.tsx110
-rw-r--r--frontend/src/pages/views/WantedView.tsx10
78 files changed, 1824 insertions, 1470 deletions
diff --git a/frontend/src/pages/Authentication.test.tsx b/frontend/src/pages/Authentication.test.tsx
index 95bfe3f47..e5dee6e44 100644
--- a/frontend/src/pages/Authentication.test.tsx
+++ b/frontend/src/pages/Authentication.test.tsx
@@ -1,5 +1,5 @@
-import { render, screen } from "@/tests";
import { describe, it } from "vitest";
+import { render, screen } from "@/tests";
import Authentication from "./Authentication";
describe("Authentication", () => {
diff --git a/frontend/src/pages/Authentication.tsx b/frontend/src/pages/Authentication.tsx
index baf21f6cd..7a164c6c4 100644
--- a/frontend/src/pages/Authentication.tsx
+++ b/frontend/src/pages/Authentication.tsx
@@ -1,5 +1,4 @@
-import { useSystem } from "@/apis/hooks";
-import { Environment } from "@/utilities";
+import { FunctionComponent } from "react";
import {
Avatar,
Button,
@@ -11,7 +10,8 @@ import {
TextInput,
} from "@mantine/core";
import { useForm } from "@mantine/form";
-import { FunctionComponent } from "react";
+import { useSystem } from "@/apis/hooks";
+import { Environment } from "@/utilities";
const Authentication: FunctionComponent = () => {
const { login } = useSystem();
@@ -52,7 +52,7 @@ const Authentication: FunctionComponent = () => {
{...form.getInputProps("password")}
></PasswordInput>
<Divider></Divider>
- <Button fullWidth uppercase type="submit">
+ <Button fullWidth tt="uppercase" type="submit">
Login
</Button>
</Stack>
diff --git a/frontend/src/pages/Blacklist/Movies/index.tsx b/frontend/src/pages/Blacklist/Movies/index.tsx
index 67c6a2a7d..9e552fa7d 100644
--- a/frontend/src/pages/Blacklist/Movies/index.tsx
+++ b/frontend/src/pages/Blacklist/Movies/index.tsx
@@ -1,13 +1,13 @@
+import { FunctionComponent } from "react";
+import { Container, Stack } from "@mantine/core";
+import { useDocumentTitle } from "@mantine/hooks";
+import { faTrash } from "@fortawesome/free-solid-svg-icons";
import {
useMovieBlacklist,
useMovieDeleteBlacklist,
} from "@/apis/hooks/movies";
import { Toolbox } from "@/components";
import { QueryOverlay } from "@/components/async";
-import { faTrash } from "@fortawesome/free-solid-svg-icons";
-import { Container, Stack } from "@mantine/core";
-import { useDocumentTitle } from "@mantine/hooks";
-import { FunctionComponent } from "react";
import Table from "./table";
const BlacklistMoviesView: FunctionComponent = () => {
diff --git a/frontend/src/pages/Blacklist/Movies/table.tsx b/frontend/src/pages/Blacklist/Movies/table.tsx
index 9ab06f2ba..00730a850 100644
--- a/frontend/src/pages/Blacklist/Movies/table.tsx
+++ b/frontend/src/pages/Blacklist/Movies/table.tsx
@@ -1,58 +1,70 @@
+import { FunctionComponent, useMemo } from "react";
+import { Link } from "react-router-dom";
+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";
-import { useTableStyles } from "@/styles";
-import { faTrash } from "@fortawesome/free-solid-svg-icons";
-import { Anchor, Text } from "@mantine/core";
-import { FunctionComponent, useMemo } from "react";
-import { Link } from "react-router-dom";
-import { Column } from "react-table";
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}`;
- const { classes } = useTableStyles();
+ header: "Name",
+ accessorKey: "title",
+ cell: ({
+ row: {
+ original: { radarrId },
+ },
+ }) => {
+ const target = `/movies/${radarrId}`;
return (
- <Anchor className={classes.primary} component={Link} to={target}>
- {row.value}
+ <Anchor className="table-primary" component={Link} to={target}>
+ {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 {
@@ -61,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"
@@ -74,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>
@@ -84,7 +98,7 @@ const Table: FunctionComponent<Props> = ({ blacklist }) => {
},
},
],
- [],
+ [remove],
);
return (
<PageTable
diff --git a/frontend/src/pages/Blacklist/Series/index.tsx b/frontend/src/pages/Blacklist/Series/index.tsx
index a4a6d3638..3bdec2b19 100644
--- a/frontend/src/pages/Blacklist/Series/index.tsx
+++ b/frontend/src/pages/Blacklist/Series/index.tsx
@@ -1,10 +1,10 @@
+import { FunctionComponent } from "react";
+import { Container, Stack } from "@mantine/core";
+import { useDocumentTitle } from "@mantine/hooks";
+import { faTrash } from "@fortawesome/free-solid-svg-icons";
import { useEpisodeBlacklist, useEpisodeDeleteBlacklist } from "@/apis/hooks";
import { Toolbox } from "@/components";
import { QueryOverlay } from "@/components/async";
-import { faTrash } from "@fortawesome/free-solid-svg-icons";
-import { Container, Stack } from "@mantine/core";
-import { useDocumentTitle } from "@mantine/hooks";
-import { FunctionComponent } from "react";
import Table from "./table";
const BlacklistSeriesView: FunctionComponent = () => {
diff --git a/frontend/src/pages/Blacklist/Series/table.tsx b/frontend/src/pages/Blacklist/Series/table.tsx
index a67069717..3d67e637d 100644
--- a/frontend/src/pages/Blacklist/Series/table.tsx
+++ b/frontend/src/pages/Blacklist/Series/table.tsx
@@ -1,65 +1,77 @@
+import { FunctionComponent, useMemo } from "react";
+import { Link } from "react-router-dom";
+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";
-import { useTableStyles } from "@/styles";
-import { faTrash } from "@fortawesome/free-solid-svg-icons";
-import { Anchor, Text } from "@mantine/core";
-import { FunctionComponent, useMemo } from "react";
-import { Link } from "react-router-dom";
-import { Column } from "react-table";
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 { classes } = useTableStyles();
- const target = `/series/${row.row.original.sonarrSeriesId}`;
+ header: "Series",
+ accessorKey: "seriesTitle",
+ cell: ({
+ row: {
+ original: { sonarrSeriesId, seriesTitle },
+ },
+ }) => {
+ const target = `/series/${sonarrSeriesId}`;
return (
- <Anchor className={classes.primary} component={Link} to={target}>
- {row.value}
+ <Anchor className="table-primary" component={Link} to={target}>
+ {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 {
@@ -68,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>
@@ -91,7 +105,7 @@ const Table: FunctionComponent<Props> = ({ blacklist }) => {
},
},
],
- [],
+ [removeFromBlacklist],
);
return (
<PageTable
diff --git a/frontend/src/pages/Episodes/components.tsx b/frontend/src/pages/Episodes/components.tsx
index 698785d5e..7b21393fa 100644
--- a/frontend/src/pages/Episodes/components.tsx
+++ b/frontend/src/pages/Episodes/components.tsx
@@ -1,9 +1,10 @@
+import { FunctionComponent, useMemo, useState } from "react";
+import { Badge, MantineColor, Tooltip } from "@mantine/core";
import { useEpisodeSubtitleModification } from "@/apis/hooks";
import Language from "@/components/bazarr/Language";
import SubtitleToolsMenu from "@/components/SubtitleToolsMenu";
import { task, TaskGroup } from "@/modules/task";
-import { Badge, MantineColor, Tooltip } from "@mantine/core";
-import { FunctionComponent, useMemo, useState } from "react";
+import { toPython } from "@/utilities";
interface Props {
seriesId: number;
@@ -24,13 +25,13 @@ export const Subtitle: FunctionComponent<Props> = ({
const disabled = subtitle.path === null;
- const color: MantineColor | undefined = useMemo(() => {
+ const variant: MantineColor | undefined = useMemo(() => {
if (opened && !disabled) {
- return "cyan";
+ return "highlight";
} else if (missing) {
- return "yellow";
+ return "warning";
} else if (disabled) {
- return "gray";
+ return "disabled";
}
}, [disabled, missing, opened]);
@@ -43,14 +44,16 @@ export const Subtitle: FunctionComponent<Props> = ({
type: "episode",
language: subtitle.code2,
path: subtitle.path,
+ forced: toPython(subtitle.forced),
+ hi: toPython(subtitle.hi),
});
}
return list;
- }, [episodeId, subtitle.code2, subtitle.path]);
+ }, [episodeId, subtitle.code2, subtitle.path, subtitle.forced, subtitle.hi]);
const ctx = (
- <Badge color={color}>
+ <Badge variant={variant}>
<Language.Text value={subtitle} long={false}></Language.Text>
</Badge>
);
diff --git a/frontend/src/pages/Episodes/index.tsx b/frontend/src/pages/Episodes/index.tsx
index 28e375744..8075e77a1 100644
--- a/frontend/src/pages/Episodes/index.tsx
+++ b/frontend/src/pages/Episodes/index.tsx
@@ -1,4 +1,27 @@
-import { RouterNames } from "@/Router/RouterNames";
+import {
+ FunctionComponent,
+ useCallback,
+ useMemo,
+ useRef,
+ useState,
+} from "react";
+import { Navigate, useParams } from "react-router-dom";
+import { Container, Group, Stack } from "@mantine/core";
+import { Dropzone } from "@mantine/dropzone";
+import { useDocumentTitle } from "@mantine/hooks";
+import { showNotification } from "@mantine/notifications";
+import {
+ faAdjust,
+ faBriefcase,
+ faCircleChevronDown,
+ faCircleChevronRight,
+ faCloudUploadAlt,
+ faHdd,
+ faSearch,
+ faSync,
+ faWrench,
+} from "@fortawesome/free-solid-svg-icons";
+import { Table as TableInstance } from "@tanstack/table-core/build/lib/types";
import {
useEpisodesBySeriesId,
useIsAnyActionRunning,
@@ -12,41 +35,13 @@ import { ItemEditModal } from "@/components/forms/ItemEditForm";
import { SeriesUploadModal } from "@/components/forms/SeriesUploadForm";
import { SubtitleToolsModal } from "@/components/modals";
import { useModals } from "@/modules/modals";
-import { TaskGroup, notification, task } from "@/modules/task";
+import { notification, task, TaskGroup } from "@/modules/task";
import ItemOverview from "@/pages/views/ItemOverview";
+import { RouterNames } from "@/Router/RouterNames";
import { useLanguageProfileBy } from "@/utilities/languages";
-import {
- faAdjust,
- faBriefcase,
- faCircleChevronDown,
- faCircleChevronRight,
- faCloudUploadAlt,
- faHdd,
- faSearch,
- faSync,
- faWrench,
-} from "@fortawesome/free-solid-svg-icons";
-import { Container, Group, Stack } from "@mantine/core";
-import { Dropzone } from "@mantine/dropzone";
-import { useDocumentTitle } from "@mantine/hooks";
-import { showNotification } from "@mantine/notifications";
-import {
- FunctionComponent,
- useCallback,
- useMemo,
- useRef,
- useState,
-} from "react";
-import { Navigate, useParams } from "react-router-dom";
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}>
@@ -125,7 +120,7 @@ const SeriesEpisodesView: FunctionComponent = () => {
<DropContent></DropContent>
</Dropzone.FullScreen>
<Toolbox>
- <Group spacing="xs">
+ <Group gap="xs">
<Toolbox.Button
icon={faSync}
disabled={!available || hasTask}
@@ -160,7 +155,7 @@ const SeriesEpisodesView: FunctionComponent = () => {
Search
</Toolbox.Button>
</Group>
- <Group spacing="xs">
+ <Group gap="xs">
<Toolbox.Button
disabled={
series === undefined ||
@@ -210,12 +205,14 @@ const SeriesEpisodesView: FunctionComponent = () => {
Edit Series
</Toolbox.Button>
<Toolbox.Button
- icon={state.expand ? faCircleChevronRight : faCircleChevronDown}
+ icon={
+ isAllRowExpanded ? faCircleChevronRight : faCircleChevronDown
+ }
onClick={() => {
- toggleState();
+ tableRef.current?.toggleAllRowsExpanded();
}}
>
- {state.buttonText}
+ {isAllRowExpanded ? "Collapse All" : "Expand All"}
</Toolbox.Button>
</Group>
</Toolbox>
@@ -223,11 +220,11 @@ const SeriesEpisodesView: FunctionComponent = () => {
<ItemOverview item={series ?? null} details={details}></ItemOverview>
<QueryOverlay result={episodesQuery}>
<Table
- expand={state.expand}
- initial={state.initial}
+ ref={tableRef}
episodes={episodes ?? null}
profile={profile}
disabled={hasTask || !series || series.profileId === null}
+ onAllRowsExpandedChanged={setIsAllRowExpanded}
></Table>
</QueryOverlay>
</Stack>
diff --git a/frontend/src/pages/Episodes/table.tsx b/frontend/src/pages/Episodes/table.tsx
index 5a310c359..7b8d4494f 100644
--- a/frontend/src/pages/Episodes/table.tsx
+++ b/frontend/src/pages/Episodes/table.tsx
@@ -1,253 +1,250 @@
+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 {
+ faBookmark,
+ faHistory,
+ 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";
-import TextPopover from "@/components/TextPopover";
import { AudioList } from "@/components/bazarr";
import { EpisodeHistoryModal } from "@/components/modals";
import { EpisodeSearchModal } from "@/components/modals/ManualSearchModal";
+import TextPopover from "@/components/TextPopover";
import { useModals } from "@/modules/modals";
-import { useTableStyles } from "@/styles";
import { BuildKey, filterSubtitleBy } from "@/utilities";
import { useProfileItemsToLanguages } from "@/utilities/languages";
-import { faBookmark as farBookmark } from "@fortawesome/free-regular-svg-icons";
-import {
- faBookmark,
- faHistory,
- faUser,
-} from "@fortawesome/free-solid-svg-icons";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { Group, Text } from "@mantine/core";
-import {
- FunctionComponent,
- useCallback,
- useEffect,
- useMemo,
- useRef,
-} from "react";
-import { Column, TableInstance } from "react-table";
import { Subtitle } from "./components";
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>Season {row.value}</Text>;
+ {
+ header: "Episode",
+ accessorKey: "episode",
},
- },
- {
- Header: "Episode",
- accessor: "episode",
- },
- {
- Header: "Title",
- accessor: "title",
- Cell: ({ value, row }) => {
- const { classes } = useTableStyles();
-
- return (
- <TextPopover text={row.original.sceneName}>
- <Text className={classes.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 spacing="xs" 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 spacing="xs" noWrap>
- <Action
- label="Manual Search"
- disabled={disabled}
- color="dark"
- onClick={() => {
- modals.openContextModal(EpisodeSearchModal, {
- item: row.original,
- download,
- query: useEpisodesProvider,
- });
- }}
- icon={faUser}
- ></Action>
- <Action
- label="History"
- disabled={disabled}
- color="dark"
- onClick={() => {
- modals.openContextModal(
- EpisodeHistoryModal,
- {
- episode: row.original,
- },
- {
- title: `History - ${row.original.title}`,
- },
- );
- }}
- icon={faHistory}
- ></Action>
- </Group>
- );
+ {
+ header: "Subtitles",
+ accessorKey: "missing_subtitles",
+ cell: ({ row: { original } }) => {
+ return <SubtitlesCell episode={original} />;
+ },
},
- },
- ],
- [onlyDesired, profileItems, disabled, download],
- );
-
- const maxSeason = useMemo(
- () =>
- episodes?.reduce<number>(
- (prev, curr) => Math.max(prev, curr.season),
- 0,
- ) ?? 0,
- [episodes],
- );
-
- const instance = useRef<TableInstance<Item.Episode> | null>(null);
-
- useEffect(() => {
- if (instance.current) {
- if (initial) {
- // start with all rows collapsed
- instance.current.toggleAllRowsExpanded(false);
- // expand the last/current season on initial display
- instance.current.toggleRowExpanded([`season:${maxSeason}`], true);
- } else {
- if (expand !== undefined) {
- instance.current.toggleAllRowsExpanded(expand);
- }
- }
- }
- }, [maxSeason, expand, initial]);
-
- return (
- <GroupTable
- columns={columns}
- data={episodes ?? []}
- instanceRef={instance}
- initialState={{
- sortBy: [
- { id: "season", desc: true },
- { id: "episode", desc: true },
- ],
- groupBy: ["season"],
- }}
- tableStyles={{ emptyText: "No Episode Found For This Series" }}
- ></GroupTable>
- );
-};
+ {
+ header: "Actions",
+ cell: ({ row }) => {
+ return (
+ <Group gap="xs" wrap="nowrap">
+ <Action
+ label="Manual Search"
+ disabled={disabled}
+ onClick={() => {
+ modals.openContextModal(EpisodeSearchModal, {
+ item: row.original,
+ download,
+ query: useEpisodesProvider,
+ });
+ }}
+ icon={faUser}
+ ></Action>
+ <Action
+ label="History"
+ disabled={disabled}
+ onClick={() => {
+ modals.openContextModal(
+ EpisodeHistoryModal,
+ {
+ episode: row.original,
+ },
+ {
+ title: `History - ${row.original.title}`,
+ },
+ );
+ }}
+ icon={faHistory}
+ ></Action>
+ </Group>
+ );
+ },
+ },
+ ],
+ [disabled, download, modals, SubtitlesCell],
+ );
+
+ const maxSeason = useMemo(
+ () =>
+ episodes?.reduce<number>(
+ (prev, curr) => Math.max(prev, curr.season),
+ 0,
+ ) ?? 0,
+ [episodes],
+ );
+
+ useEffect(() => {
+ tableRef?.current?.setExpanded(() => ({ [`season:${maxSeason}`]: true }));
+ }, [tableRef, maxSeason]);
+
+ return (
+ <GroupTable
+ columns={columns}
+ data={episodes ?? []}
+ instanceRef={tableRef}
+ onAllRowsExpandedChanged={onAllRowsExpandedChanged}
+ initialState={{
+ sorting: [
+ { id: "season", desc: true },
+ { id: "episode", desc: true },
+ ],
+ grouping: ["season"],
+ }}
+ tableStyles={{ emptyText: "No Episode Found For This Series" }}
+ ></GroupTable>
+ );
+ },
+);
export default Table;
diff --git a/frontend/src/pages/History/Movies/index.tsx b/frontend/src/pages/History/Movies/index.tsx
index ee4e98df0..92d1aa280 100644
--- a/frontend/src/pages/History/Movies/index.tsx
+++ b/frontend/src/pages/History/Movies/index.tsx
@@ -1,4 +1,14 @@
/* eslint-disable camelcase */
+import { FunctionComponent, useMemo } from "react";
+import { Link } from "react-router-dom";
+import { Anchor, Badge, Text } from "@mantine/core";
+import {
+ faFileExcel,
+ faInfoCircle,
+ 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";
@@ -6,46 +16,42 @@ import Language from "@/components/bazarr/Language";
import StateIcon from "@/components/StateIcon";
import TextPopover from "@/components/TextPopover";
import HistoryView from "@/pages/views/HistoryView";
-import { useTableStyles } from "@/styles";
-import {
- faFileExcel,
- faInfoCircle,
- faRecycle,
-} from "@fortawesome/free-solid-svg-icons";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { Anchor, Badge, Text } from "@mantine/core";
-import { FunctionComponent, useMemo } from "react";
-import { Link } from "react-router-dom";
-import { Column } from "react-table";
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 }) => {
- const { classes } = useTableStyles();
+ header: "Name",
+ accessorKey: "title",
+ cell: ({ row }) => {
const target = `/movies/${row.original.radarrId}`;
return (
- <Anchor className={classes.primary} component={Link} to={target}>
- {value}
+ <Anchor className="table-primary" component={Link} to={target}>
+ {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 {
@@ -54,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 (
@@ -76,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 {
@@ -91,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>
@@ -117,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: {
@@ -148,7 +171,7 @@ const MoviesHistoryView: FunctionComponent = () => {
},
},
],
- [],
+ [addToBlacklist],
);
const query = useMovieHistoryPagination();
diff --git a/frontend/src/pages/History/Series/index.tsx b/frontend/src/pages/History/Series/index.tsx
index d6b1469bf..a5d75516a 100644
--- a/frontend/src/pages/History/Series/index.tsx
+++ b/frontend/src/pages/History/Series/index.tsx
@@ -1,4 +1,14 @@
/* eslint-disable camelcase */
+import { FunctionComponent, useMemo } from "react";
+import { Link } from "react-router-dom";
+import { Anchor, Badge, Text } from "@mantine/core";
+import {
+ faFileExcel,
+ faInfoCircle,
+ faRecycle,
+} from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { ColumnDef } from "@tanstack/react-table";
import {
useEpisodeAddBlacklist,
useEpisodeHistoryPagination,
@@ -9,59 +19,62 @@ import Language from "@/components/bazarr/Language";
import StateIcon from "@/components/StateIcon";
import TextPopover from "@/components/TextPopover";
import HistoryView from "@/pages/views/HistoryView";
-import { useTableStyles } from "@/styles";
-import {
- faFileExcel,
- faInfoCircle,
- faRecycle,
-} from "@fortawesome/free-solid-svg-icons";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { Anchor, Badge, Text } from "@mantine/core";
-import { FunctionComponent, useMemo } from "react";
-import { Link } from "react-router-dom";
-import { Column } from "react-table";
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 { classes } = useTableStyles();
- const target = `/series/${row.row.original.sonarrSeriesId}`;
+ header: "Series",
+ accessorKey: "seriesTitle",
+ cell: ({
+ row: {
+ original: { seriesTitle, sonarrSeriesId },
+ },
+ }) => {
+ const target = `/series/${sonarrSeriesId}`;
return (
- <Anchor className={classes.primary} component={Link} to={target}>
- {row.value}
+ <Anchor className="table-primary" component={Link} to={target}>
+ {seriesTitle}
</Anchor>
);
},
},
{
- Header: "Episode",
- accessor: "episode_number",
+ header: "Episode",
+ accessorKey: "episode_number",
},
{
- Header: "Title",
- accessor: "episodeTitle",
- Cell: ({ value }) => {
- const { classes } = useTableStyles();
- return <Text className={classes.noWrap}>{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 {
@@ -70,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 (
@@ -92,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 {
@@ -107,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>
@@ -133,9 +158,9 @@ const SeriesHistoryView: FunctionComponent = () => {
},
},
{
- Header: "Blacklist",
- accessor: "blacklisted",
- Cell: ({ row, value }) => {
+ header: "Blacklist",
+ accessorKey: "blacklisted",
+ cell: ({ row }) => {
const {
sonarrEpisodeId,
sonarrSeriesId,
@@ -143,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,
@@ -171,7 +195,7 @@ const SeriesHistoryView: FunctionComponent = () => {
},
},
],
- [],
+ [addToBlacklist],
);
const query = useEpisodeHistoryPagination();
diff --git a/frontend/src/pages/History/Statistics/HistoryStats.module.scss b/frontend/src/pages/History/Statistics/HistoryStats.module.scss
new file mode 100644
index 000000000..3c7c04e10
--- /dev/null
+++ b/frontend/src/pages/History/Statistics/HistoryStats.module.scss
@@ -0,0 +1,9 @@
+.container {
+ display: flex;
+ flex-direction: column;
+ height: calc(100vh - $header-height);
+}
+
+.chart {
+ height: 90%;
+}
diff --git a/frontend/src/pages/History/Statistics/index.tsx b/frontend/src/pages/History/Statistics/HistoryStats.tsx
index 243225538..0e2d34400 100644
--- a/frontend/src/pages/History/Statistics/index.tsx
+++ b/frontend/src/pages/History/Statistics/HistoryStats.tsx
@@ -1,23 +1,7 @@
-import {
- useHistoryStats,
- useLanguages,
- useSystemProviders,
-} from "@/apis/hooks";
-import { Selector, Toolbox } from "@/components";
-import { QueryOverlay } from "@/components/async";
-import Language from "@/components/bazarr/Language";
-import { Layout } from "@/constants";
-import { useSelectorOptions } from "@/utilities";
-import {
- Box,
- Container,
- SimpleGrid,
- createStyles,
- useMantineTheme,
-} from "@mantine/core";
+import { FunctionComponent, useMemo, useState } from "react";
+import { Box, Container, SimpleGrid, useMantineTheme } from "@mantine/core";
import { useDocumentTitle } from "@mantine/hooks";
import { merge } from "lodash";
-import { FunctionComponent, useMemo, useState } from "react";
import {
Bar,
BarChart,
@@ -28,18 +12,16 @@ import {
XAxis,
YAxis,
} from "recharts";
+import {
+ useHistoryStats,
+ useLanguages,
+ useSystemProviders,
+} from "@/apis/hooks";
+import { Selector, Toolbox } from "@/components";
+import { QueryOverlay } from "@/components/async";
+import { useSelectorOptions } from "@/utilities";
import { actionOptions, timeFrameOptions } from "./options";
-
-const useStyles = createStyles((theme) => ({
- container: {
- display: "flex",
- flexDirection: "column",
- height: `calc(100vh - ${Layout.HEADER_HEIGHT}px)`,
- },
- chart: {
- height: "90%",
- },
-}));
+import styles from "./HistoryStats.module.scss";
const HistoryStats: FunctionComponent = () => {
const { data: providers } = useSystemProviders(true);
@@ -71,8 +53,8 @@ const HistoryStats: FunctionComponent = () => {
date: v.date,
series: v.count,
}));
- const result = merge(movies, series);
- return result;
+
+ return merge(movies, series);
} else {
return [];
}
@@ -80,20 +62,13 @@ const HistoryStats: FunctionComponent = () => {
useDocumentTitle("History Statistics - Bazarr");
- const { classes } = useStyles();
const theme = useMantineTheme();
return (
- <Container fluid px={0} className={classes.container}>
+ <Container fluid px={0} className={styles.container}>
<QueryOverlay result={stats}>
<Toolbox>
- <SimpleGrid
- cols={4}
- breakpoints={[
- { maxWidth: "sm", cols: 4 },
- { maxWidth: "xs", cols: 2 },
- ]}
- >
+ <SimpleGrid cols={{ base: 4, xs: 2 }}>
<Selector
placeholder="Time..."
options={timeFrameOptions}
@@ -123,9 +98,9 @@ const HistoryStats: FunctionComponent = () => {
></Selector>
</SimpleGrid>
</Toolbox>
- <Box className={classes.chart} m="xs">
+ <Box className={styles.chart} m="xs">
<ResponsiveContainer>
- <BarChart className={classes.chart} data={convertedData}>
+ <BarChart className={styles.chart} data={convertedData}>
<CartesianGrid strokeDasharray="4 2"></CartesianGrid>
<XAxis dataKey="date"></XAxis>
<YAxis allowDecimals={false}></YAxis>
diff --git a/frontend/src/pages/History/history.test.tsx b/frontend/src/pages/History/history.test.tsx
index 1de1e6c5d..277a268fb 100644
--- a/frontend/src/pages/History/history.test.tsx
+++ b/frontend/src/pages/History/history.test.tsx
@@ -1,7 +1,7 @@
import { renderTest, RenderTestCase } from "@/tests/render";
+import HistoryStats from "./Statistics/HistoryStats";
import MoviesHistoryView from "./Movies";
import SeriesHistoryView from "./Series";
-import HistoryStats from "./Statistics";
const cases: RenderTestCase[] = [
{
diff --git a/frontend/src/pages/Movies/Details/index.tsx b/frontend/src/pages/Movies/Details/index.tsx
index a6b4b0aa8..709f03905 100644
--- a/frontend/src/pages/Movies/Details/index.tsx
+++ b/frontend/src/pages/Movies/Details/index.tsx
@@ -1,4 +1,21 @@
-import { RouterNames } from "@/Router/RouterNames";
+import { FunctionComponent, useCallback, useRef } from "react";
+import { Navigate, useParams } from "react-router-dom";
+import { Container, Group, Menu, Stack } from "@mantine/core";
+import { Dropzone } from "@mantine/dropzone";
+import { useDocumentTitle } from "@mantine/hooks";
+import { showNotification } from "@mantine/notifications";
+import {
+ faCloudUploadAlt,
+ faEllipsis,
+ faHistory,
+ faSearch,
+ faSync,
+ faToolbox,
+ faUser,
+ faWrench,
+} from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { isNumber } from "lodash";
import {
useDownloadMovieSubtitles,
useIsMovieActionRunning,
@@ -16,27 +33,10 @@ import { MovieUploadModal } from "@/components/forms/MovieUploadForm";
import { MovieHistoryModal, SubtitleToolsModal } from "@/components/modals";
import { MovieSearchModal } from "@/components/modals/ManualSearchModal";
import { useModals } from "@/modules/modals";
-import { TaskGroup, notification, task } from "@/modules/task";
+import { notification, task, TaskGroup } from "@/modules/task";
import ItemOverview from "@/pages/views/ItemOverview";
+import { RouterNames } from "@/Router/RouterNames";
import { useLanguageProfileBy } from "@/utilities/languages";
-import {
- faCloudUploadAlt,
- faEllipsis,
- faHistory,
- faSearch,
- faSync,
- faToolbox,
- faUser,
- faWrench,
-} from "@fortawesome/free-solid-svg-icons";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { Container, Group, Menu, Stack } from "@mantine/core";
-import { Dropzone } from "@mantine/dropzone";
-import { useDocumentTitle } from "@mantine/hooks";
-import { showNotification } from "@mantine/notifications";
-import { isNumber } from "lodash";
-import { FunctionComponent, useCallback, useRef } from "react";
-import { Navigate, useParams } from "react-router-dom";
import Table from "./table";
const MovieDetailView: FunctionComponent = () => {
@@ -123,7 +123,7 @@ const MovieDetailView: FunctionComponent = () => {
<DropContent></DropContent>
</Dropzone.FullScreen>
<Toolbox>
- <Group spacing="xs">
+ <Group gap="xs">
<Toolbox.Button
icon={faSync}
disabled={hasTask}
@@ -168,7 +168,7 @@ const MovieDetailView: FunctionComponent = () => {
Manual
</Toolbox.Button>
</Group>
- <Group spacing="xs">
+ <Group gap="xs">
<Toolbox.Button
disabled={!allowEdit || movie.profileId === null || hasTask}
icon={faCloudUploadAlt}
@@ -198,14 +198,13 @@ const MovieDetailView: FunctionComponent = () => {
<Menu.Target>
<Action
label="More Actions"
- color="dark"
icon={faEllipsis}
disabled={hasTask}
/>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item
- icon={<FontAwesomeIcon icon={faToolbox} />}
+ leftSection={<FontAwesomeIcon icon={faToolbox} />}
onClick={() => {
if (movie) {
modals.openContextModal(SubtitleToolsModal, {
@@ -217,7 +216,7 @@ const MovieDetailView: FunctionComponent = () => {
Mass Edit
</Menu.Item>
<Menu.Item
- icon={<FontAwesomeIcon icon={faHistory} />}
+ leftSection={<FontAwesomeIcon icon={faHistory} />}
onClick={() => {
if (movie) {
modals.openContextModal(MovieHistoryModal, { movie });
diff --git a/frontend/src/pages/Movies/Details/table.tsx b/frontend/src/pages/Movies/Details/table.tsx
index 0a327b745..7d0c20a30 100644
--- a/frontend/src/pages/Movies/Details/table.tsx
+++ b/frontend/src/pages/Movies/Details/table.tsx
@@ -1,17 +1,17 @@
+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 { useTableStyles } from "@/styles";
-import { filterSubtitleBy } from "@/utilities";
+import { filterSubtitleBy, toPython } from "@/utilities";
import { useProfileItemsToLanguages } from "@/utilities/languages";
-import { faEllipsis, faSearch } from "@fortawesome/free-solid-svg-icons";
-import { Badge, Text, TextProps } from "@mantine/core";
-import { isString } from "lodash";
-import { FunctionComponent, useMemo } from "react";
-import { Column } from "react-table";
const missingText = "Missing Subtitles";
@@ -34,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 }) => {
- const { classes } = useTableStyles();
-
+ header: "Subtitle Path",
+ accessorKey: "path",
+ cell: ({
+ row: {
+ original: { path },
+ },
+ }) => {
const props: TextProps = {
- className: classes.primary,
+ className: "table-primary",
};
- if (isSubtitleTrack(value)) {
- return <Text {...props}>Video File Subtitle Track</Text>;
- } else if (isSubtitleMissing(value)) {
+ if (isSubtitleTrack(path)) {
+ return (
+ <Text className="table-primary">Video File Subtitle Track</Text>
+ );
+ } else if (isSubtitleMissing(path)) {
return (
- <Text {...props} color="dimmed">
- {value}
+ <Text {...props} c="dimmed">
+ {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">
@@ -79,98 +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,
- });
- }
-
- return list;
- }, [code2, path]);
-
- 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)}
- color="dark"
- icon={faEllipsis}
- ></Action>
- </SubtitleToolsMenu>
- );
+ id: "code2",
+ cell: ({ row: { original } }) => {
+ return <CodeCell item={original} />;
},
},
],
- [movie, disabled],
+ [CodeCell],
);
const data: Subtitle[] = useMemo(() => {
diff --git a/frontend/src/pages/Movies/Editor.tsx b/frontend/src/pages/Movies/Editor.tsx
index a196f9deb..1ec84a52c 100644
--- a/frontend/src/pages/Movies/Editor.tsx
+++ b/frontend/src/pages/Movies/Editor.tsx
@@ -1,42 +1,74 @@
+import { FunctionComponent, useMemo } from "react";
+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";
import LanguageProfileName from "@/components/bazarr/LanguageProfile";
import MassEditor from "@/pages/views/MassEditor";
-import { useDocumentTitle } from "@mantine/hooks";
-import { FunctionComponent, useMemo } from "react";
-import { Column } from "react-table";
const MovieMassEditor: FunctionComponent = () => {
const query = useMovies();
const mutation = useMovieModification();
- const columns = useMemo<Column<Item.Movie>[]>(
+ useDocumentTitle("Movies - Bazarr (Mass Editor)");
+
+ const columns = useMemo<ColumnDef<Item.Movie>[]>(
() => [
{
- Header: "Name",
- accessor: "title",
+ id: "selection",
+ header: ({ table }) => {
+ return (
+ <Checkbox
+ id="table-header-selection"
+ indeterminate={table.getIsSomeRowsSelected()}
+ checked={table.getIsAllRowsSelected()}
+ onChange={table.getToggleAllRowsSelectedHandler()}
+ ></Checkbox>
+ );
+ },
+ cell: ({ row: { index, getIsSelected, getToggleSelectedHandler } }) => {
+ return (
+ <Checkbox
+ id={`table-cell-${index}`}
+ checked={getIsSelected()}
+ onChange={getToggleSelectedHandler()}
+ onClick={getToggleSelectedHandler()}
+ ></Checkbox>
+ );
+ },
+ },
+ {
+ header: "Name",
+ accessorKey: "title",
},
{
- Header: "Audio",
- accessor: "audio_language",
- Cell: ({ value }) => {
- return <AudioList audios={value}></AudioList>;
+ header: "Audio",
+ accessorKey: "audio_language",
+ cell: ({
+ row: {
+ original: { audio_language: audioLanguage },
+ },
+ }) => {
+ return <AudioList audios={audioLanguage}></AudioList>;
},
},
{
- Header: "Languages Profile",
- accessor: "profileId",
- Cell: ({ value }) => {
- return <LanguageProfileName index={value}></LanguageProfileName>;
+ header: "Languages Profile",
+ accessorKey: "profileId",
+ cell: ({
+ row: {
+ original: { profileId },
+ },
+ }) => {
+ return <LanguageProfileName index={profileId}></LanguageProfileName>;
},
},
],
[],
);
- useDocumentTitle("Movies - Bazarr (Mass Editor)");
-
return (
<QueryOverlay result={query}>
<MassEditor
diff --git a/frontend/src/pages/Movies/index.tsx b/frontend/src/pages/Movies/index.tsx
index dd9f531e1..0429e1fdd 100644
--- a/frontend/src/pages/Movies/index.tsx
+++ b/frontend/src/pages/Movies/index.tsx
@@ -1,3 +1,11 @@
+import { FunctionComponent, useMemo } from "react";
+import { Link } from "react-router-dom";
+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";
@@ -6,68 +14,84 @@ import LanguageProfileName from "@/components/bazarr/LanguageProfile";
import { ItemEditModal } from "@/components/forms/ItemEditForm";
import { useModals } from "@/modules/modals";
import ItemView from "@/pages/views/ItemView";
-import { useTableStyles } from "@/styles";
import { BuildKey } from "@/utilities";
-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 { Anchor, Badge, Container } from "@mantine/core";
-import { useDocumentTitle } from "@mantine/hooks";
-import { FunctionComponent, useMemo } from "react";
-import { Link } from "react-router-dom";
-import { Column } from "react-table";
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 { classes } = useTableStyles();
- const target = `/movies/${row.original.radarrId}`;
+ header: "Name",
+ accessorKey: "title",
+ cell: ({
+ row: {
+ original: { title, radarrId },
+ },
+ }) => {
+ const target = `/movies/${radarrId}`;
return (
- <Anchor className={classes.primary} component={Link} to={target}>
- {value}
+ <Anchor className="table-primary" component={Link} to={target}>
+ {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"
@@ -81,20 +105,17 @@ const MovieView: FunctionComponent = () => {
},
},
{
- accessor: "radarrId",
- Cell: ({ row }) => {
- const modals = useModals();
- const mutation = useMovieModification();
+ id: "radarrId",
+ cell: ({ row }) => {
return (
<Action
label="Edit Movie"
tooltip={{ position: "left" }}
- variant="light"
onClick={() =>
modals.openContextModal(
ItemEditModal,
{
- mutation,
+ mutation: modifyMovie,
item: row.original,
},
{
@@ -108,7 +129,7 @@ const MovieView: FunctionComponent = () => {
},
},
],
- [],
+ [modals, modifyMovie],
);
useDocumentTitle("Movies - Bazarr");
diff --git a/frontend/src/pages/Movies/movies.test.tsx b/frontend/src/pages/Movies/movies.test.tsx
index fe5691a15..c4ac8133a 100644
--- a/frontend/src/pages/Movies/movies.test.tsx
+++ b/frontend/src/pages/Movies/movies.test.tsx
@@ -1,7 +1,7 @@
-import { render } from "@/tests";
import { describe } from "vitest";
-import MovieView from ".";
+import { render } from "@/tests";
import MovieMassEditor from "./Editor";
+import MovieView from ".";
describe("Movies page", () => {
it("should render", () => {
diff --git a/frontend/src/pages/Series/Editor.tsx b/frontend/src/pages/Series/Editor.tsx
index 4db9a4c1d..45a277d17 100644
--- a/frontend/src/pages/Series/Editor.tsx
+++ b/frontend/src/pages/Series/Editor.tsx
@@ -1,34 +1,66 @@
+import { FunctionComponent, useMemo } from "react";
+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";
import LanguageProfileName from "@/components/bazarr/LanguageProfile";
import MassEditor from "@/pages/views/MassEditor";
-import { useDocumentTitle } from "@mantine/hooks";
-import { FunctionComponent, useMemo } from "react";
-import { Column } from "react-table";
const SeriesMassEditor: FunctionComponent = () => {
const query = useSeries();
const mutation = useSeriesModification();
- const columns = useMemo<Column<Item.Series>[]>(
+ const columns = useMemo<ColumnDef<Item.Series>[]>(
() => [
{
- Header: "Name",
- accessor: "title",
+ id: "selection",
+ header: ({ table }) => {
+ return (
+ <Checkbox
+ id="table-header-selection"
+ indeterminate={table.getIsSomeRowsSelected()}
+ checked={table.getIsAllRowsSelected()}
+ onChange={table.getToggleAllRowsSelectedHandler()}
+ ></Checkbox>
+ );
+ },
+ cell: ({ row: { index, getIsSelected, getToggleSelectedHandler } }) => {
+ return (
+ <Checkbox
+ id={`table-cell-${index}`}
+ checked={getIsSelected()}
+ onChange={getToggleSelectedHandler()}
+ onClick={getToggleSelectedHandler()}
+ ></Checkbox>
+ );
+ },
+ },
+ {
+ header: "Name",
+ accessorKey: "title",
},
{
- Header: "Audio",
- accessor: "audio_language",
- Cell: ({ value }) => {
- return <AudioList audios={value}></AudioList>;
+ header: "Audio",
+ accessorKey: "audio_language",
+ cell: ({
+ row: {
+ original: { audio_language: audioLanguage },
+ },
+ }) => {
+ return <AudioList audios={audioLanguage}></AudioList>;
},
},
{
- Header: "Languages Profile",
- accessor: "profileId",
- Cell: ({ value }) => {
- return <LanguageProfileName index={value}></LanguageProfileName>;
+ header: "Languages Profile",
+ accessorKey: "profileId",
+ cell: ({
+ row: {
+ original: { profileId },
+ },
+ }) => {
+ return <LanguageProfileName index={profileId}></LanguageProfileName>;
},
},
],
diff --git a/frontend/src/pages/Series/index.tsx b/frontend/src/pages/Series/index.tsx
index 66921347c..229082444 100644
--- a/frontend/src/pages/Series/index.tsx
+++ b/frontend/src/pages/Series/index.tsx
@@ -1,61 +1,68 @@
+import { FunctionComponent, useMemo } from "react";
+import { Link } from "react-router-dom";
+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";
import { ItemEditModal } from "@/components/forms/ItemEditForm";
import { useModals } from "@/modules/modals";
import ItemView from "@/pages/views/ItemView";
-import { useTableStyles } from "@/styles";
-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 { Anchor, Container, Progress } from "@mantine/core";
-import { useDocumentTitle } from "@mantine/hooks";
-import { FunctionComponent, useMemo } from "react";
-import { Link } from "react-router-dom";
-import { Column } from "react-table";
const SeriesView: FunctionComponent = () => {
const mutation = useSeriesModification();
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 { classes } = useTableStyles();
- const target = `/series/${row.original.sonarrSeriesId}`;
+ header: "Name",
+ accessorKey: "title",
+ cell: ({ row: { original } }) => {
+ const target = `/series/${original.sonarrSeriesId}`;
return (
- <Anchor className={classes.primary} component={Link} to={target}>
- {value}
+ <Anchor className="table-primary" component={Link} to={target}>
+ {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;
@@ -70,25 +77,24 @@ const SeriesView: FunctionComponent = () => {
}
return (
- <Progress
- key={title}
- size="xl"
- color={episodeMissingCount === 0 ? "brand" : "yellow"}
- value={progress}
- label={label}
- ></Progress>
+ <Progress.Root key={title} size="xl">
+ <Progress.Section
+ value={progress}
+ color={episodeMissingCount === 0 ? "brand" : "yellow"}
+ >
+ <Progress.Label>{label}</Progress.Label>
+ </Progress.Section>
+ </Progress.Root>
);
},
},
{
- accessor: "sonarrSeriesId",
- Cell: ({ row: { original } }) => {
- const modals = useModals();
+ id: "sonarrSeriesId",
+ cell: ({ row: { original } }) => {
return (
<Action
label="Edit Series"
tooltip={{ position: "left" }}
- variant="light"
onClick={() =>
modals.openContextModal(
ItemEditModal,
@@ -107,7 +113,7 @@ const SeriesView: FunctionComponent = () => {
},
},
],
- [mutation],
+ [mutation, modals],
);
useDocumentTitle("Series - Bazarr");
diff --git a/frontend/src/pages/Series/series.test.tsx b/frontend/src/pages/Series/series.test.tsx
index 6813c6e19..b8fd9fad5 100644
--- a/frontend/src/pages/Series/series.test.tsx
+++ b/frontend/src/pages/Series/series.test.tsx
@@ -1,7 +1,7 @@
-import { render } from "@/tests";
import { describe } from "vitest";
-import SeriesView from ".";
+import { render } from "@/tests";
import SeriesMassEditor from "./Editor";
+import SeriesView from ".";
describe("Series page", () => {
it("should render", () => {
diff --git a/frontend/src/pages/Settings/General/index.tsx b/frontend/src/pages/Settings/General/index.tsx
index 8cc5ea8c3..312d09d1f 100644
--- a/frontend/src/pages/Settings/General/index.tsx
+++ b/frontend/src/pages/Settings/General/index.tsx
@@ -1,12 +1,11 @@
-import { Environment, toggleState } from "@/utilities";
+import { FunctionComponent, useState } from "react";
+import { Box, Group as MantineGroup, Text as MantineText } from "@mantine/core";
+import { useClipboard } from "@mantine/hooks";
import {
faCheck,
faClipboard,
faSync,
} from "@fortawesome/free-solid-svg-icons";
-import { Group as MantineGroup, Text as MantineText } from "@mantine/core";
-import { useClipboard } from "@mantine/hooks";
-import { FunctionComponent, useState } from "react";
import {
Action,
Check,
@@ -20,7 +19,8 @@ import {
Section,
Selector,
Text,
-} from "../components";
+} from "@/pages/Settings/components";
+import { Environment, toggleState } from "@/utilities";
import { branchOptions, proxyOptions, securityOptions } from "./options";
const characters = "abcdef0123456789";
@@ -54,7 +54,7 @@ const SettingsGeneralView: FunctionComponent = () => {
></Number>
<Text
label="Base URL"
- icon="/"
+ leftSection="/"
settingKey="settings-general-base_url"
settingOptions={{
onLoaded: (s) => s.general.base_url?.slice(1) ?? "",
@@ -87,15 +87,14 @@ const SettingsGeneralView: FunctionComponent = () => {
rightSectionWidth={95}
rightSectionProps={{ style: { justifyContent: "flex-end" } }}
rightSection={
- <MantineGroup spacing="xs" mx="xs" position="right">
+ <MantineGroup gap="xs" mx="xs" justify="right">
{
// Clipboard API is only available in secure contexts See: https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API#interfaces
window.isSecureContext && (
<Action
label="Copy API Key"
- variant="light"
settingKey={settingApiKey}
- color={copied ? "green" : undefined}
+ c={copied ? "green" : undefined}
icon={copied ? faCheck : faClipboard}
onClick={(update, value) => {
if (value) {
@@ -108,9 +107,8 @@ const SettingsGeneralView: FunctionComponent = () => {
}
<Action
label="Regenerate"
- variant="light"
settingKey={settingApiKey}
- color="red"
+ c="red"
icon={faSync}
onClick={(update) => {
update(generateApiKey());
@@ -204,13 +202,12 @@ const SettingsGeneralView: FunctionComponent = () => {
<Number
label="Retention"
settingKey="settings-backup-retention"
- styles={{
- rightSection: { width: "4rem", justifyContent: "flex-end" },
- }}
rightSection={
- <MantineText size="xs" px="sm" color="dimmed">
- Days
- </MantineText>
+ <Box w="4rem" style={{ justifyContent: "flex-end" }}>
+ <MantineText size="xs" px="sm" c="dimmed">
+ Days
+ </MantineText>
+ </Box>
}
></Number>
</Section>
diff --git a/frontend/src/pages/Settings/Languages/components.tsx b/frontend/src/pages/Settings/Languages/components.tsx
index de3e89c3e..9c3cf8e94 100644
--- a/frontend/src/pages/Settings/Languages/components.tsx
+++ b/frontend/src/pages/Settings/Languages/components.tsx
@@ -1,16 +1,15 @@
+import { FunctionComponent, useMemo } from "react";
+import { Input } from "@mantine/core";
import {
MultiSelector,
MultiSelectorProps,
SelectorOption,
} from "@/components";
-import { Language } from "@/components/bazarr";
+import { Selector, SelectorProps } from "@/pages/Settings/components";
+import { useFormActions } from "@/pages/Settings/utilities/FormValues";
+import { BaseInput } from "@/pages/Settings/utilities/hooks";
import { useSelectorOptions } from "@/utilities";
-import { Input } from "@mantine/core";
-import { FunctionComponent, useMemo } from "react";
import { useLatestEnabledLanguages, useLatestProfiles } from ".";
-import { Selector, SelectorProps } from "../components";
-import { useFormActions } from "../utilities/FormValues";
-import { BaseInput } from "../utilities/hooks";
type LanguageSelectorProps = Omit<
MultiSelectorProps<Language.Info>,
diff --git a/frontend/src/pages/Settings/Languages/equals.test.ts b/frontend/src/pages/Settings/Languages/equals.test.ts
index ead613946..5a74db797 100644
--- a/frontend/src/pages/Settings/Languages/equals.test.ts
+++ b/frontend/src/pages/Settings/Languages/equals.test.ts
@@ -1,10 +1,10 @@
+import { describe, expect, it } from "vitest";
import {
decodeEqualData,
encodeEqualData,
LanguageEqualData,
LanguageEqualImmediateData,
} from "@/pages/Settings/Languages/equals";
-import { describe, expect, it } from "vitest";
describe("Equals Parser", () => {
it("should parse from string correctly", () => {
diff --git a/frontend/src/pages/Settings/Languages/equals.tsx b/frontend/src/pages/Settings/Languages/equals.tsx
index a4fe95eee..08642f27e 100644
--- a/frontend/src/pages/Settings/Languages/equals.tsx
+++ b/frontend/src/pages/Settings/Languages/equals.tsx
@@ -1,15 +1,16 @@
+import { FunctionComponent, useCallback, useMemo } from "react";
+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";
import { LOG } from "@/utilities/console";
-import { faEquals, faTrash } from "@fortawesome/free-solid-svg-icons";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { Button, Checkbox } from "@mantine/core";
-import { FunctionComponent, useCallback, useMemo } from "react";
-import { Column } from "react-table";
interface GenericEqualTarget<T> {
content: T;
@@ -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,13 +332,12 @@ const EqualsTable: FunctionComponent<EqualsTableProps> = () => {
},
{
id: "action",
- accessor: "target",
- Cell: ({ row }) => {
+ cell: ({ row }) => {
return (
<Action
label="Remove"
icon={faTrash}
- color="red"
+ c="red"
onClick={() => remove(row.index)}
></Action>
);
@@ -355,7 +350,7 @@ const EqualsTable: FunctionComponent<EqualsTableProps> = () => {
return (
<>
<SimpleTable data={equals} columns={columns}></SimpleTable>
- <Button fullWidth disabled={!canAdd} color="light" onClick={add}>
+ <Button fullWidth disabled={!canAdd} onClick={add}>
{canAdd ? "Add Equal" : "No Enabled Languages"}
</Button>
</>
diff --git a/frontend/src/pages/Settings/Languages/index.tsx b/frontend/src/pages/Settings/Languages/index.tsx
index 61733c992..9fe562920 100644
--- a/frontend/src/pages/Settings/Languages/index.tsx
+++ b/frontend/src/pages/Settings/Languages/index.tsx
@@ -1,6 +1,5 @@
-import { useLanguageProfiles, useLanguages } from "@/apis/hooks";
-import { useEnabledLanguages } from "@/utilities/languages";
import { FunctionComponent } from "react";
+import { useLanguageProfiles, useLanguages } from "@/apis/hooks";
import {
Check,
CollapseBox,
@@ -8,14 +7,15 @@ import {
Message,
Section,
Selector,
-} from "../components";
+} from "@/pages/Settings/components";
import {
defaultUndAudioLang,
defaultUndEmbeddedSubtitlesLang,
enabledLanguageKey,
languageProfileKey,
-} from "../keys";
-import { useSettingValue } from "../utilities/hooks";
+} from "@/pages/Settings/keys";
+import { useSettingValue } from "@/pages/Settings/utilities/hooks";
+import { useEnabledLanguages } from "@/utilities/languages";
import { LanguageSelector, ProfileSelector } from "./components";
import EqualsTable from "./equals";
import Table from "./table";
diff --git a/frontend/src/pages/Settings/Languages/table.tsx b/frontend/src/pages/Settings/Languages/table.tsx
index a1ee217e8..c32300628 100644
--- a/frontend/src/pages/Settings/Languages/table.tsx
+++ b/frontend/src/pages/Settings/Languages/table.tsx
@@ -1,18 +1,19 @@
-import { Action, SimpleTable } from "@/components";
+import { FunctionComponent, useCallback, useMemo } from "react";
+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 } from "@/components";
import {
- ProfileEditModal,
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";
import { BuildKey, useArrayAction } from "@/utilities";
-import { faTrash, faWrench } from "@fortawesome/free-solid-svg-icons";
-import { Badge, Button, Group } from "@mantine/core";
-import { cloneDeep } from "lodash";
-import { FunctionComponent, useCallback, useMemo } from "react";
-import { Column } from "react-table";
import { useLatestEnabledLanguages, useLatestProfiles } from ".";
-import { languageProfileKey } from "../keys";
-import { useFormActions } from "../utilities/FormValues";
const Table: FunctionComponent = () => {
const profiles = useLatestProfiles();
@@ -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,20 +59,22 @@ 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 spacing="xs" noWrap>
+ <Group gap="xs" wrap="nowrap">
{items.map((v) => {
const isCutoff = v.id === cutoff || cutoff === anyCutoff;
return (
@@ -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,14 +134,15 @@ const Table: FunctionComponent = () => {
},
},
{
- accessor: "profileId",
- Cell: ({ row }) => {
+ id: "profileId",
+ cell: ({ row }) => {
const profile = row.original;
return (
- <Group spacing="xs" noWrap>
+ <Group gap="xs" wrap="nowrap">
<Action
label="Edit Profile"
icon={faWrench}
+ c="gray"
onClick={() => {
modals.openContextModal(ProfileEditModal, {
languages,
@@ -143,7 +154,7 @@ const Table: FunctionComponent = () => {
<Action
label="Remove"
icon={faTrash}
- color="red"
+ c="red"
onClick={() => action.remove(row.index)}
></Action>
</Group>
@@ -159,11 +170,10 @@ const Table: FunctionComponent = () => {
return (
<>
- <SimpleTable columns={columns} data={profiles}></SimpleTable>
+ <SimpleTable columns={columns} data={[...profiles]}></SimpleTable>
<Button
fullWidth
disabled={!canAdd}
- color="light"
onClick={() => {
const profile = {
profileId: nextProfileId,
diff --git a/frontend/src/pages/Settings/Notifications/components.tsx b/frontend/src/pages/Settings/Notifications/components.tsx
index 1a2b20f65..8fa17abb2 100644
--- a/frontend/src/pages/Settings/Notifications/components.tsx
+++ b/frontend/src/pages/Settings/Notifications/components.tsx
@@ -1,9 +1,4 @@
-import api from "@/apis/raw";
-import { Selector } from "@/components";
-import MutateButton from "@/components/async/MutateButton";
-import { useModals, withModal } from "@/modules/modals";
-import { BuildKey, useSelectorOptions } from "@/utilities";
-import FormUtils from "@/utilities/form";
+import { FunctionComponent, useCallback, useMemo } from "react";
import {
Button,
Divider,
@@ -13,12 +8,20 @@ import {
Textarea,
} from "@mantine/core";
import { useForm } from "@mantine/form";
+import { useMutation } from "@tanstack/react-query";
import { isObject } from "lodash";
-import { FunctionComponent, useCallback, useMemo } from "react";
-import { useMutation } from "react-query";
-import { Card } from "../components";
-import { notificationsKey } from "../keys";
-import { useSettingValue, useUpdateArray } from "../utilities/hooks";
+import api from "@/apis/raw";
+import { Selector } from "@/components";
+import MutateButton from "@/components/async/MutateButton";
+import { useModals, withModal } from "@/modules/modals";
+import { Card } from "@/pages/Settings/components";
+import { notificationsKey } from "@/pages/Settings/keys";
+import {
+ useSettingValue,
+ useUpdateArray,
+} from "@/pages/Settings/utilities/hooks";
+import { BuildKey, useSelectorOptions } from "@/utilities";
+import FormUtils from "@/utilities/form";
const notificationHook = (notifications: Settings.NotificationInfo[]) => {
return notifications.map((info) => JSON.stringify(info));
@@ -60,7 +63,9 @@ const NotificationForm: FunctionComponent<Props> = ({
},
});
- const test = useMutation((url: string) => api.system.testNotification(url));
+ const test = useMutation({
+ mutationFn: (url: string) => api.system.testNotification(url),
+ });
return (
<form
@@ -90,7 +95,7 @@ const NotificationForm: FunctionComponent<Props> = ({
></Textarea>
</div>
<Divider></Divider>
- <Group position="right">
+ <Group justify="right">
<MutateButton mutation={test} args={() => form.values.url}>
Test
</MutateButton>
diff --git a/frontend/src/pages/Settings/Notifications/index.tsx b/frontend/src/pages/Settings/Notifications/index.tsx
index f764bbd61..54ce0d0b8 100644
--- a/frontend/src/pages/Settings/Notifications/index.tsx
+++ b/frontend/src/pages/Settings/Notifications/index.tsx
@@ -1,12 +1,19 @@
-import { Anchor, Blockquote, Text } from "@mantine/core";
+// eslint-disable-next-line simple-import-sort/imports
import { FunctionComponent } from "react";
-import { Check, Layout, Message, Section } from "../components";
+import { Anchor, Blockquote, Text } from "@mantine/core";
+import { Check, Layout, Message, Section } from "@/pages/Settings/components";
import { NotificationView } from "./components";
+import { faQuoteLeftAlt } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
const SettingsNotificationsView: FunctionComponent = () => {
return (
<Layout name="Notifications">
- <Blockquote>
+ <Blockquote
+ bg="transparent"
+ mt="xl"
+ icon={<FontAwesomeIcon icon={faQuoteLeftAlt}></FontAwesomeIcon>}
+ >
<Text>
Thanks to caronc for his work on{" "}
<Anchor
diff --git a/frontend/src/pages/Settings/Providers/components.tsx b/frontend/src/pages/Settings/Providers/components.tsx
index 87abe7571..72e2c3b1f 100644
--- a/frontend/src/pages/Settings/Providers/components.tsx
+++ b/frontend/src/pages/Settings/Providers/components.tsx
@@ -1,52 +1,70 @@
-import { Selector } from "@/components";
-import { useModals, withModal } from "@/modules/modals";
-import { BuildKey, useSelectorOptions } from "@/utilities";
-import { ASSERT } from "@/utilities/console";
import {
+ FunctionComponent,
+ useCallback,
+ useMemo,
+ useRef,
+ useState,
+} from "react";
+import {
+ AutocompleteProps,
Button,
Divider,
Group,
- Text as MantineText,
SimpleGrid,
Stack,
+ Text as MantineText,
} from "@mantine/core";
import { useForm } from "@mantine/form";
import { capitalize } from "lodash";
-import {
- FunctionComponent,
- forwardRef,
- useCallback,
- useMemo,
- useRef,
- useState,
-} from "react";
+import { Selector } from "@/components";
+import { useModals, withModal } from "@/modules/modals";
import {
Card,
Check,
Chips,
- Selector as GlobalSelector,
Message,
Password,
ProviderTestButton,
+ Selector as GlobalSelector,
Text,
-} from "../components";
+} from "@/pages/Settings/components";
import {
FormContext,
FormValues,
runHooks,
useFormActions,
useStagedValues,
-} from "../utilities/FormValues";
-import { SettingsProvider, useSettings } from "../utilities/SettingsProvider";
-import { useSettingValue } from "../utilities/hooks";
-import { ProviderInfo, ProviderList } from "./list";
+} from "@/pages/Settings/utilities/FormValues";
+import { useSettingValue } from "@/pages/Settings/utilities/hooks";
+import {
+ SettingsProvider,
+ useSettings,
+} from "@/pages/Settings/utilities/SettingsProvider";
+import { BuildKey, useSelectorOptions } from "@/utilities";
+import { ASSERT } from "@/utilities/console";
+import { ProviderInfo } from "./list";
-const ProviderKey = "settings-general-enabled_providers";
+type SettingsKey =
+ | "settings-general-enabled_providers"
+ | "settings-general-enabled_integrations";
-export const ProviderView: FunctionComponent = () => {
+interface ProviderViewProps {
+ availableOptions: Readonly<ProviderInfo[]>;
+ settingsKey: SettingsKey;
+}
+
+interface ProviderSelect {
+ value: string;
+ payload: ProviderInfo;
+}
+
+export const ProviderView: FunctionComponent<ProviderViewProps> = ({
+ availableOptions,
+ settingsKey,
+}) => {
const settings = useSettings();
const staged = useStagedValues();
- const providers = useSettingValue<string[]>(ProviderKey);
+ const providers = useSettingValue<string[]>(settingsKey);
const { update } = useFormActions();
@@ -61,17 +79,27 @@ export const ProviderView: FunctionComponent = () => {
staged,
settings,
onChange: update,
+ availableOptions: availableOptions,
+ settingsKey: settingsKey,
});
}
},
- [modals, providers, settings, staged, update],
+ [
+ modals,
+ providers,
+ settings,
+ staged,
+ update,
+ availableOptions,
+ settingsKey,
+ ],
);
const cards = useMemo(() => {
if (providers) {
return providers
.flatMap((v) => {
- const item = ProviderList.find((inn) => inn.key === v);
+ const item = availableOptions.find((inn) => inn.key === v);
if (item) {
return item;
} else {
@@ -89,7 +117,7 @@ export const ProviderView: FunctionComponent = () => {
} else {
return [];
}
- }, [providers, select]);
+ }, [providers, select, availableOptions]);
return (
<SimpleGrid cols={3}>
@@ -106,19 +134,20 @@ interface ProviderToolProps {
staged: LooseObject;
settings: Settings;
onChange: (v: LooseObject) => void;
+ availableOptions: Readonly<ProviderInfo[]>;
+ settingsKey: Readonly<SettingsKey>;
}
-const SelectItem = forwardRef<
- HTMLDivElement,
- { payload: ProviderInfo; label: string }
->(({ payload: { description }, label, ...other }, ref) => {
+const SelectItem: AutocompleteProps["renderOption"] = ({ option }) => {
+ const provider = option as ProviderSelect;
+
return (
- <Stack spacing={1} ref={ref} {...other}>
- <MantineText size="md">{label}</MantineText>
- <MantineText size="xs">{description}</MantineText>
+ <Stack gap={1}>
+ <MantineText size="md">{provider.value}</MantineText>
+ <MantineText size="xs">{provider.payload.description}</MantineText>
</Stack>
);
-});
+};
const ProviderTool: FunctionComponent<ProviderToolProps> = ({
payload,
@@ -126,6 +155,8 @@ const ProviderTool: FunctionComponent<ProviderToolProps> = ({
staged,
settings,
onChange,
+ availableOptions,
+ settingsKey,
}) => {
const modals = useModals();
@@ -147,11 +178,11 @@ const ProviderTool: FunctionComponent<ProviderToolProps> = ({
if (idx !== -1) {
const newProviders = [...enabledProviders];
newProviders.splice(idx, 1);
- onChangeRef.current({ [ProviderKey]: newProviders });
+ onChangeRef.current({ [settingsKey]: newProviders });
modals.closeAll();
}
}
- }, [payload, enabledProviders, modals]);
+ }, [payload, enabledProviders, modals, settingsKey]);
const submit = useCallback(
(values: FormValues) => {
@@ -161,8 +192,7 @@ const ProviderTool: FunctionComponent<ProviderToolProps> = ({
// Add this provider if not exist
if (enabledProviders.find((v) => v === info.key) === undefined) {
- const newProviders = [...enabledProviders, info.key];
- changes[ProviderKey] = newProviders;
+ changes[settingsKey] = [...enabledProviders, info.key];
}
// Apply submit hooks
@@ -172,7 +202,7 @@ const ProviderTool: FunctionComponent<ProviderToolProps> = ({
modals.closeAll();
}
},
- [info, enabledProviders, modals],
+ [info, enabledProviders, modals, settingsKey],
);
const canSave = info !== null;
@@ -188,18 +218,18 @@ const ProviderTool: FunctionComponent<ProviderToolProps> = ({
}
}, []);
- const availableOptions = useMemo(
+ const options = useMemo(
() =>
- ProviderList.filter(
+ availableOptions.filter(
(v) =>
enabledProviders?.find((p) => p === v.key && p !== info?.key) ===
undefined,
),
- [info?.key, enabledProviders],
+ [info?.key, enabledProviders, availableOptions],
);
- const options = useSelectorOptions(
- availableOptions,
+ const selectorOptions = useSelectorOptions(
+ options,
(v) => v.name ?? capitalize(v.key),
);
@@ -275,21 +305,21 @@ const ProviderTool: FunctionComponent<ProviderToolProps> = ({
}
});
- return <Stack spacing="xs">{elements}</Stack>;
+ return <Stack gap="xs">{elements}</Stack>;
}, [info]);
return (
<SettingsProvider value={settings}>
<FormContext.Provider value={form}>
<Stack>
- <Stack spacing="xs">
+ <Stack gap="xs">
<Selector
data-autofocus
searchable
placeholder="Click to Select a Provider"
- itemComponent={SelectItem}
+ renderOption={SelectItem}
disabled={payload !== null}
- {...options}
+ {...selectorOptions}
value={info}
onChange={onSelect}
></Selector>
@@ -300,7 +330,7 @@ const ProviderTool: FunctionComponent<ProviderToolProps> = ({
</div>
</Stack>
<Divider></Divider>
- <Group position="right">
+ <Group justify="right">
<Button hidden={!payload} color="red" onClick={deletePayload}>
Delete
</Button>
diff --git a/frontend/src/pages/Settings/Providers/index.tsx b/frontend/src/pages/Settings/Providers/index.tsx
index bd8b648ff..a179ecda3 100644
--- a/frontend/src/pages/Settings/Providers/index.tsx
+++ b/frontend/src/pages/Settings/Providers/index.tsx
@@ -1,6 +1,5 @@
-import { antiCaptchaOption } from "@/pages/Settings/Providers/options";
-import { Anchor } from "@mantine/core";
import { FunctionComponent } from "react";
+import { Anchor } from "@mantine/core";
import {
CollapseBox,
Layout,
@@ -9,14 +8,19 @@ import {
Section,
Selector,
Text,
-} from "../components";
+} from "@/pages/Settings/components";
+import { antiCaptchaOption } from "@/pages/Settings/Providers/options";
import { ProviderView } from "./components";
+import { IntegrationList, ProviderList } from "./list";
const SettingsProvidersView: FunctionComponent = () => {
return (
<Layout name="Providers">
<Section header="Providers">
- <ProviderView></ProviderView>
+ <ProviderView
+ availableOptions={ProviderList}
+ settingsKey="settings-general-enabled_providers"
+ ></ProviderView>
</Section>
<Section header="Anti-Captcha Options">
<Selector
@@ -58,6 +62,12 @@ const SettingsProvidersView: FunctionComponent = () => {
<Message>Link to subscribe</Message>
</CollapseBox>
</Section>
+ <Section header="Integrations">
+ <ProviderView
+ availableOptions={IntegrationList}
+ settingsKey="settings-general-enabled_integrations"
+ ></ProviderView>
+ </Section>
</Layout>
);
};
diff --git a/frontend/src/pages/Settings/Providers/list.ts b/frontend/src/pages/Settings/Providers/list.ts
index 967526187..b2f9a33c7 100644
--- a/frontend/src/pages/Settings/Providers/list.ts
+++ b/frontend/src/pages/Settings/Providers/list.ts
@@ -1,5 +1,5 @@
-import { SelectorOption } from "@/components";
import { ReactText } from "react";
+import { SelectorOption } from "@/components";
type Input<T, N> = {
type: N;
@@ -65,6 +65,21 @@ export const ProviderList: Readonly<ProviderInfo[]> = [
],
},
{
+ key: "animetosho",
+ name: "Anime Tosho",
+ description:
+ "Anime Tosho is a free, completely automated service which mirrors most torrents posted on TokyoTosho's anime category, Nyaa.si's English translated anime category and AniDex's anime category.",
+ inputs: [
+ {
+ type: "text",
+ key: "search_threshold",
+ defaultValue: 6,
+ name: "Search Threshold. Increase if you often cannot find subtitles for your Anime. Note that increasing the value will decrease the performance of the search for each Episode.",
+ },
+ ],
+ message: "Requires AniDB Integration.",
+ },
+ {
key: "argenteam_dump",
name: "Argenteam Dump",
description: "Subtitles dump of the now extinct Argenteam",
@@ -357,9 +372,17 @@ export const ProviderList: Readonly<ProviderInfo[]> = [
},
{ key: "subdivx", description: "LATAM Spanish / Spanish Subtitles Provider" },
{
+ key: "subdl",
+ inputs: [
+ {
+ type: "text",
+ key: "api_key",
+ },
+ ],
+ },
+ {
key: "subf2m",
name: "subf2m.co",
- description: "Subscene Alternative Provider",
inputs: [
{
type: "switch",
@@ -391,20 +414,6 @@ export const ProviderList: Readonly<ProviderInfo[]> = [
description:
"Greek Subtitles Provider.\nRequires anti-captcha provider to solve captchas for each download.",
},
- {
- key: "subscene",
- inputs: [
- {
- type: "text",
- key: "username",
- },
- {
- type: "password",
- key: "password",
- },
- ],
- description: "Broken, may not work for some. Use subf2m instead.",
- },
{ key: "subscenter", description: "Hebrew Subtitles Provider" },
{
key: "subsunacs",
@@ -538,3 +547,24 @@ export const ProviderList: Readonly<ProviderInfo[]> = [
description: "Chinese Subtitles Provider. Anti-captcha required.",
},
];
+
+export const IntegrationList: Readonly<ProviderInfo[]> = [
+ {
+ key: "anidb",
+ name: "AniDB",
+ description:
+ "AniDB is non-profit database of anime information that is freely open to the public.",
+ inputs: [
+ {
+ type: "text",
+ key: "api_client",
+ name: "API Client",
+ },
+ {
+ type: "text",
+ key: "api_client_ver",
+ name: "API Client Version",
+ },
+ ],
+ },
+];
diff --git a/frontend/src/pages/Settings/Radarr/index.tsx b/frontend/src/pages/Settings/Radarr/index.tsx
index 8cd038ab8..b2e858178 100644
--- a/frontend/src/pages/Settings/Radarr/index.tsx
+++ b/frontend/src/pages/Settings/Radarr/index.tsx
@@ -1,5 +1,5 @@
-import { Code } from "@mantine/core";
import { FunctionComponent } from "react";
+import { Code } from "@mantine/core";
import {
Check,
Chips,
@@ -13,8 +13,8 @@ import {
Slider,
Text,
URLTestButton,
-} from "../components";
-import { moviesEnabledKey } from "../keys";
+} from "@/pages/Settings/components";
+import { moviesEnabledKey } from "@/pages/Settings/keys";
import { timeoutOptions } from "./options";
const SettingsRadarrView: FunctionComponent = () => {
@@ -30,7 +30,7 @@ const SettingsRadarrView: FunctionComponent = () => {
<Number label="Port" settingKey="settings-radarr-port"></Number>
<Text
label="Base URL"
- icon="/"
+ leftSection="/"
settingKey="settings-radarr-base_url"
settingOptions={{
onLoaded: (s) => s.radarr.base_url?.slice(1) ?? "",
diff --git a/frontend/src/pages/Settings/Scheduler/index.tsx b/frontend/src/pages/Settings/Scheduler/index.tsx
index a6cd2ca74..df88725b2 100644
--- a/frontend/src/pages/Settings/Scheduler/index.tsx
+++ b/frontend/src/pages/Settings/Scheduler/index.tsx
@@ -1,5 +1,5 @@
-import { SelectorOption } from "@/components";
import { FunctionComponent, useMemo } from "react";
+import { SelectorOption } from "@/components";
import {
Check,
CollapseBox,
@@ -7,7 +7,7 @@ import {
Message,
Section,
Selector,
-} from "../components";
+} from "@/pages/Settings/components";
import {
backupOptions,
dayOptions,
diff --git a/frontend/src/pages/Settings/Sonarr/index.tsx b/frontend/src/pages/Settings/Sonarr/index.tsx
index 1d2125568..ed66ef679 100644
--- a/frontend/src/pages/Settings/Sonarr/index.tsx
+++ b/frontend/src/pages/Settings/Sonarr/index.tsx
@@ -1,5 +1,5 @@
-import { Code } from "@mantine/core";
import { FunctionComponent } from "react";
+import { Code } from "@mantine/core";
import {
Check,
Chips,
@@ -14,9 +14,9 @@ import {
Slider,
Text,
URLTestButton,
-} from "../components";
-import { seriesEnabledKey } from "../keys";
-import { seriesTypeOptions } from "../options";
+} from "@/pages/Settings/components";
+import { seriesEnabledKey } from "@/pages/Settings/keys";
+import { seriesTypeOptions } from "@/pages/Settings/options";
import { timeoutOptions } from "./options";
const SettingsSonarrView: FunctionComponent = () => {
@@ -32,7 +32,7 @@ const SettingsSonarrView: FunctionComponent = () => {
<Number label="Port" settingKey="settings-sonarr-port"></Number>
<Text
label="Base URL"
- icon="/"
+ leftSection="/"
settingKey="settings-sonarr-base_url"
settingOptions={{
onLoaded: (s) => s.sonarr.base_url?.slice(1) ?? "",
diff --git a/frontend/src/pages/Settings/Subtitles/index.tsx b/frontend/src/pages/Settings/Subtitles/index.tsx
index f6e0cae37..a2250e5a9 100644
--- a/frontend/src/pages/Settings/Subtitles/index.tsx
+++ b/frontend/src/pages/Settings/Subtitles/index.tsx
@@ -1,5 +1,5 @@
-import { Code, Space, Table } from "@mantine/core";
import { FunctionComponent } from "react";
+import { Code, Space, Table } from "@mantine/core";
import {
Check,
CollapseBox,
@@ -10,11 +10,11 @@ import {
Selector,
Slider,
Text,
-} from "../components";
+} from "@/pages/Settings/components";
import {
SubzeroColorModification,
SubzeroModification,
-} from "../utilities/modifications";
+} from "@/pages/Settings/utilities/modifications";
import {
adaptiveSearchingDelayOption,
adaptiveSearchingDeltaOption,
@@ -409,8 +409,7 @@ const SettingsSubtitlesView: FunctionComponent = () => {
settingKey="settings-subsync-use_subsync"
></Check>
<Message>
- Enable automatic subtitles synchronization after downloading a
- subtitle.
+ Enable automatic synchronization after downloading subtitles.
</Message>
<CollapseBox indent settingKey="settings-subsync-use_subsync">
<MultiSelector
@@ -502,7 +501,7 @@ const SettingsSubtitlesView: FunctionComponent = () => {
label="Command"
settingKey="settings-general-postprocessing_cmd"
></Text>
- <Table highlightOnHover fontSize="sm">
+ <Table highlightOnHover fs="sm">
<tbody>{commandOptionElements}</tbody>
</Table>
</CollapseBox>
diff --git a/frontend/src/pages/Settings/Subtitles/options.ts b/frontend/src/pages/Settings/Subtitles/options.ts
index 75fc4b027..b14d88f44 100644
--- a/frontend/src/pages/Settings/Subtitles/options.ts
+++ b/frontend/src/pages/Settings/Subtitles/options.ts
@@ -1,5 +1,5 @@
import { SelectorOption } from "@/components";
-import { ProviderList } from "../Providers/list";
+import { ProviderList } from "@/pages/Settings/Providers/list";
export const hiExtensionOptions: SelectorOption<string>[] = [
{
diff --git a/frontend/src/pages/Settings/UI/index.tsx b/frontend/src/pages/Settings/UI/index.tsx
index c7b6ada1b..a4410f0ba 100644
--- a/frontend/src/pages/Settings/UI/index.tsx
+++ b/frontend/src/pages/Settings/UI/index.tsx
@@ -1,6 +1,6 @@
-import { uiPageSizeKey } from "@/utilities/storage";
import { FunctionComponent } from "react";
-import { Layout, Section, Selector } from "../components";
+import { Layout, Section, Selector } from "@/pages/Settings/components";
+import { uiPageSizeKey } from "@/utilities/storage";
import { colorSchemeOptions, pageSizeOptions } from "./options";
const SettingsUIView: FunctionComponent = () => {
diff --git a/frontend/src/pages/Settings/components/Card.module.scss b/frontend/src/pages/Settings/components/Card.module.scss
new file mode 100644
index 000000000..746e55e65
--- /dev/null
+++ b/frontend/src/pages/Settings/components/Card.module.scss
@@ -0,0 +1,9 @@
+.card {
+ border-radius: var(--mantine-radius-sm);
+ border: 1px solid var(--mantine-color-gray-7);
+
+ &:hover {
+ box-shadow: var(--mantine-shadow-md);
+ border: 1px solid $color-brand-5;
+ }
+}
diff --git a/frontend/src/pages/Settings/components/Card.tsx b/frontend/src/pages/Settings/components/Card.tsx
index 4f3bd4fbf..69df15636 100644
--- a/frontend/src/pages/Settings/components/Card.tsx
+++ b/frontend/src/pages/Settings/components/Card.tsx
@@ -1,30 +1,8 @@
+import { FunctionComponent } from "react";
+import { Center, Stack, Text, UnstyledButton } from "@mantine/core";
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import {
- Center,
- createStyles,
- Stack,
- Text,
- UnstyledButton,
-} from "@mantine/core";
-import { FunctionComponent } from "react";
-
-const useCardStyles = createStyles((theme) => {
- return {
- card: {
- borderRadius: theme.radius.sm,
- border: `1px solid ${theme.colors.gray[7]}`,
-
- "&:hover": {
- boxShadow: theme.shadows.md,
- border: `1px solid ${theme.colors.brand[5]}`,
- },
- },
- stack: {
- height: "100%",
- },
- };
-});
+import styles from "./Card.module.scss";
interface CardProps {
header?: string;
@@ -39,16 +17,15 @@ export const Card: FunctionComponent<CardProps> = ({
plus,
onClick,
}) => {
- const { classes } = useCardStyles();
return (
- <UnstyledButton p="lg" onClick={onClick} className={classes.card}>
+ <UnstyledButton p="lg" onClick={onClick} className={styles.card}>
{plus ? (
<Center>
<FontAwesomeIcon size="2x" icon={faPlus}></FontAwesomeIcon>
</Center>
) : (
- <Stack className={classes.stack} spacing={0} align="flex-start">
- <Text weight="bold">{header}</Text>
+ <Stack h="100%" gap={0} align="flex-start">
+ <Text fw="bold">{header}</Text>
<Text hidden={description === undefined}>{description}</Text>
</Stack>
)}
diff --git a/frontend/src/pages/Settings/components/Layout.test.tsx b/frontend/src/pages/Settings/components/Layout.test.tsx
index 512d0310c..a890bc277 100644
--- a/frontend/src/pages/Settings/components/Layout.test.tsx
+++ b/frontend/src/pages/Settings/components/Layout.test.tsx
@@ -1,6 +1,6 @@
-import { render, screen } from "@/tests";
import { Text } from "@mantine/core";
import { describe, it } from "vitest";
+import { render, screen } from "@/tests";
import Layout from "./Layout";
describe("Settings layout", () => {
diff --git a/frontend/src/pages/Settings/components/Layout.tsx b/frontend/src/pages/Settings/components/Layout.tsx
index b20c8092b..da72818fa 100644
--- a/frontend/src/pages/Settings/components/Layout.tsx
+++ b/frontend/src/pages/Settings/components/Layout.tsx
@@ -1,16 +1,20 @@
+import { FunctionComponent, ReactNode, useCallback, useMemo } from "react";
+import { Badge, Container, Group, LoadingOverlay } from "@mantine/core";
+import { useForm } from "@mantine/form";
+import { useDocumentTitle } from "@mantine/hooks";
+import { faSave } from "@fortawesome/free-solid-svg-icons";
import { useSettingsMutation, useSystemSettings } from "@/apis/hooks";
import { Toolbox } from "@/components";
import { LoadingProvider } from "@/contexts";
+import {
+ FormContext,
+ FormValues,
+ runHooks,
+} from "@/pages/Settings/utilities/FormValues";
+import { SettingsProvider } from "@/pages/Settings/utilities/SettingsProvider";
import { useOnValueChange } from "@/utilities";
import { LOG } from "@/utilities/console";
import { usePrompt } from "@/utilities/routers";
-import { faSave } from "@fortawesome/free-solid-svg-icons";
-import { Badge, Container, Group, LoadingOverlay } from "@mantine/core";
-import { useForm } from "@mantine/form";
-import { useDocumentTitle } from "@mantine/hooks";
-import { FunctionComponent, ReactNode, useCallback, useMemo } from "react";
-import { FormContext, FormValues, runHooks } from "../utilities/FormValues";
-import { SettingsProvider } from "../utilities/SettingsProvider";
interface Props {
name: string;
@@ -21,7 +25,7 @@ const Layout: FunctionComponent<Props> = (props) => {
const { children, name } = props;
const { data: settings, isLoading, isRefetching } = useSystemSettings();
- const { mutate, isLoading: isMutating } = useSettingsMutation();
+ const { mutate, isPending: isMutating } = useSettingsMutation();
const form = useForm<FormValues>({
initialValues: {
@@ -73,7 +77,7 @@ const Layout: FunctionComponent<Props> = (props) => {
icon={faSave}
loading={isMutating}
disabled={totalStagedCount === 0}
- rightIcon={
+ rightSection={
<Badge size="xs" radius="sm" hidden={totalStagedCount === 0}>
{totalStagedCount}
</Badge>
diff --git a/frontend/src/pages/Settings/components/LayoutModal.tsx b/frontend/src/pages/Settings/components/LayoutModal.tsx
index cb4d5a1b5..9702ad96e 100644
--- a/frontend/src/pages/Settings/components/LayoutModal.tsx
+++ b/frontend/src/pages/Settings/components/LayoutModal.tsx
@@ -1,7 +1,4 @@
-import { useSettingsMutation, useSystemSettings } from "@/apis/hooks";
-import { LoadingProvider } from "@/contexts";
-import { useOnValueChange } from "@/utilities";
-import { LOG } from "@/utilities/console";
+import { FunctionComponent, ReactNode, useCallback, useMemo } from "react";
import {
Button,
Container,
@@ -11,9 +8,16 @@ import {
Space,
} from "@mantine/core";
import { useForm } from "@mantine/form";
-import { FunctionComponent, ReactNode, useCallback, useMemo } from "react";
-import { FormContext, FormValues, runHooks } from "../utilities/FormValues";
-import { SettingsProvider } from "../utilities/SettingsProvider";
+import { useSettingsMutation, useSystemSettings } from "@/apis/hooks";
+import { LoadingProvider } from "@/contexts";
+import {
+ FormContext,
+ FormValues,
+ runHooks,
+} from "@/pages/Settings/utilities/FormValues";
+import { SettingsProvider } from "@/pages/Settings/utilities/SettingsProvider";
+import { useOnValueChange } from "@/utilities";
+import { LOG } from "@/utilities/console";
interface Props {
children: ReactNode;
@@ -24,7 +28,7 @@ const LayoutModal: FunctionComponent<Props> = (props) => {
const { children, callbackModal } = props;
const { data: settings, isLoading, isRefetching } = useSystemSettings();
- const { mutate, isLoading: isMutating } = useSettingsMutation();
+ const { mutate, isPending: isMutating } = useSettingsMutation();
const form = useForm<FormValues>({
initialValues: {
@@ -74,7 +78,7 @@ const LayoutModal: FunctionComponent<Props> = (props) => {
<Space h="md" />
<Divider></Divider>
<Space h="md" />
- <Group position="right">
+ <Group justify="right">
<Button
type="submit"
disabled={totalStagedCount === 0}
diff --git a/frontend/src/pages/Settings/components/Message.tsx b/frontend/src/pages/Settings/components/Message.tsx
index 301df7bab..67f519485 100644
--- a/frontend/src/pages/Settings/components/Message.tsx
+++ b/frontend/src/pages/Settings/components/Message.tsx
@@ -1,5 +1,5 @@
-import { Text } from "@mantine/core";
import { FunctionComponent, PropsWithChildren } from "react";
+import { Text } from "@mantine/core";
interface MessageProps {
type?: "warning" | "info";
@@ -12,7 +12,7 @@ export const Message: FunctionComponent<Props> = ({
children,
}) => {
return (
- <Text size="sm" color={type === "info" ? "dimmed" : "yellow"} my={0}>
+ <Text size="sm" c={type === "info" ? "dimmed" : "yellow"} my={0}>
{children}
</Text>
);
diff --git a/frontend/src/pages/Settings/components/Section.test.tsx b/frontend/src/pages/Settings/components/Section.test.tsx
index e7f270e0d..535bd8be2 100644
--- a/frontend/src/pages/Settings/components/Section.test.tsx
+++ b/frontend/src/pages/Settings/components/Section.test.tsx
@@ -1,12 +1,12 @@
-import { rawRender, screen } from "@/tests";
import { Text } from "@mantine/core";
import { describe, it } from "vitest";
+import { render, screen } from "@/tests";
import { Section } from "./Section";
describe("Settings section", () => {
const header = "Section Header";
it("should show header", () => {
- rawRender(<Section header="Section Header"></Section>);
+ render(<Section header="Section Header"></Section>);
expect(screen.getByText(header)).toBeDefined();
expect(screen.getByRole("separator")).toBeDefined();
@@ -14,7 +14,7 @@ describe("Settings section", () => {
it("should show children", () => {
const text = "Section Child";
- rawRender(
+ render(
<Section header="Section Header">
<Text>{text}</Text>
</Section>,
@@ -26,7 +26,7 @@ describe("Settings section", () => {
it("should work with hidden", () => {
const text = "Section Child";
- rawRender(
+ render(
<Section header="Section Header" hidden>
<Text>{text}</Text>
</Section>,
diff --git a/frontend/src/pages/Settings/components/Section.tsx b/frontend/src/pages/Settings/components/Section.tsx
index 36f56ff8d..1e6a2e0b8 100644
--- a/frontend/src/pages/Settings/components/Section.tsx
+++ b/frontend/src/pages/Settings/components/Section.tsx
@@ -1,5 +1,5 @@
-import { Divider, Stack, Title } from "@mantine/core";
import { FunctionComponent, PropsWithChildren } from "react";
+import { Divider, Stack, Title } from "@mantine/core";
interface SectionProps {
header: string;
@@ -14,7 +14,7 @@ export const Section: FunctionComponent<Props> = ({
children,
}) => {
return (
- <Stack hidden={hidden} spacing="xs" my="lg">
+ <Stack hidden={hidden} gap="xs" my="lg">
<Title order={4}>{header}</Title>
<Divider></Divider>
{children}
diff --git a/frontend/src/pages/Settings/components/collapse.tsx b/frontend/src/pages/Settings/components/collapse.tsx
index 1dcffbd97..d502ecc69 100644
--- a/frontend/src/pages/Settings/components/collapse.tsx
+++ b/frontend/src/pages/Settings/components/collapse.tsx
@@ -1,6 +1,6 @@
-import { Collapse, Stack } from "@mantine/core";
import { FunctionComponent, PropsWithChildren, useMemo, useRef } from "react";
-import { useSettingValue } from "../utilities/hooks";
+import { Collapse, Stack } from "@mantine/core";
+import { useSettingValue } from "@/pages/Settings/utilities/hooks";
interface ContentProps {
settingKey: string;
@@ -31,7 +31,7 @@ const CollapseBox: FunctionComponent<Props> = ({
return (
<Collapse in={open} pl={indent ? "md" : undefined}>
- <Stack spacing="xs">{children}</Stack>
+ <Stack gap="xs">{children}</Stack>
</Collapse>
);
};
diff --git a/frontend/src/pages/Settings/components/forms.test.tsx b/frontend/src/pages/Settings/components/forms.test.tsx
index 19c66ade0..a88d2bec7 100644
--- a/frontend/src/pages/Settings/components/forms.test.tsx
+++ b/frontend/src/pages/Settings/components/forms.test.tsx
@@ -1,8 +1,8 @@
-import { rawRender, RenderOptions, screen } from "@/tests";
-import { useForm } from "@mantine/form";
import { FunctionComponent, PropsWithChildren, ReactElement } from "react";
+import { useForm } from "@mantine/form";
import { describe, it } from "vitest";
-import { FormContext, FormValues } from "../utilities/FormValues";
+import { FormContext, FormValues } from "@/pages/Settings/utilities/FormValues";
+import { render, RenderOptions, screen } from "@/tests";
import { Number, Text } from "./forms";
const FormSupport: FunctionComponent<PropsWithChildren> = ({ children }) => {
@@ -18,7 +18,7 @@ const FormSupport: FunctionComponent<PropsWithChildren> = ({ children }) => {
const formRender = (
ui: ReactElement,
options?: Omit<RenderOptions, "wrapper">,
-) => rawRender(ui, { wrapper: FormSupport, ...options });
+) => render(<FormSupport>{ui}</FormSupport>);
describe("Settings form", () => {
describe("number component", () => {
diff --git a/frontend/src/pages/Settings/components/forms.tsx b/frontend/src/pages/Settings/components/forms.tsx
index 3e1d3f12f..95134db92 100644
--- a/frontend/src/pages/Settings/components/forms.tsx
+++ b/frontend/src/pages/Settings/components/forms.tsx
@@ -1,7 +1,20 @@
+import { FunctionComponent, ReactNode, ReactText } from "react";
+import {
+ Input,
+ NumberInput,
+ NumberInputProps,
+ PasswordInput,
+ PasswordInputProps,
+ Slider as MantineSlider,
+ SliderProps as MantineSliderProps,
+ Switch,
+ TextInput,
+ TextInputProps,
+} from "@mantine/core";
import {
+ Action as GlobalAction,
FileBrowser,
FileBrowserProps,
- Action as GlobalAction,
MultiSelector as GlobalMultiSelector,
MultiSelectorProps as GlobalMultiSelectorProps,
Selector as GlobalSelector,
@@ -9,21 +22,8 @@ import {
} from "@/components";
import { ActionProps as GlobalActionProps } from "@/components/inputs/Action";
import ChipInput, { ChipInputProps } from "@/components/inputs/ChipInput";
+import { BaseInput, useBaseInput } from "@/pages/Settings/utilities/hooks";
import { useSliderMarks } from "@/utilities";
-import {
- Input,
- Slider as MantineSlider,
- SliderProps as MantineSliderProps,
- NumberInput,
- NumberInputProps,
- PasswordInput,
- PasswordInputProps,
- Switch,
- TextInput,
- TextInputProps,
-} from "@mantine/core";
-import { FunctionComponent, ReactNode, ReactText } from "react";
-import { BaseInput, useBaseInput } from "../utilities/hooks";
export type NumberProps = BaseInput<number> & NumberInputProps;
@@ -38,6 +38,11 @@ export const Number: FunctionComponent<NumberProps> = (props) => {
if (val === "") {
val = 0;
}
+
+ if (typeof val === "string") {
+ return update(+val);
+ }
+
update(val);
}}
></NumberInput>
diff --git a/frontend/src/pages/Settings/components/index.tsx b/frontend/src/pages/Settings/components/index.tsx
index 99d1658bc..5e7882bbc 100644
--- a/frontend/src/pages/Settings/components/index.tsx
+++ b/frontend/src/pages/Settings/components/index.tsx
@@ -1,7 +1,7 @@
-import api from "@/apis/raw";
-import { Button } from "@mantine/core";
import { FunctionComponent, useCallback, useEffect, useState } from "react";
-import { useSettingValue } from "../utilities/hooks";
+import { Button } from "@mantine/core";
+import api from "@/apis/raw";
+import { useSettingValue } from "@/pages/Settings/utilities/hooks";
export const URLTestButton: FunctionComponent<{
category: "sonarr" | "radarr";
@@ -56,7 +56,7 @@ export const URLTestButton: FunctionComponent<{
}, [address, port, url, apikey, ssl]);
return (
- <Button onClick={click} color={color} title={title}>
+ <Button autoContrast onClick={click} variant={color} title={title}>
{title}
</Button>
);
@@ -107,7 +107,7 @@ export const ProviderTestButton: FunctionComponent<{
}, [testUrl]);
return (
- <Button onClick={click} color={color} title={title}>
+ <Button onClick={click} variant={color} title={title}>
{title}
</Button>
);
diff --git a/frontend/src/pages/Settings/components/pathMapper.tsx b/frontend/src/pages/Settings/components/pathMapper.tsx
index 8bb3514b7..6b2c7baa2 100644
--- a/frontend/src/pages/Settings/components/pathMapper.tsx
+++ b/frontend/src/pages/Settings/components/pathMapper.tsx
@@ -1,19 +1,20 @@
-import { Action, FileBrowser, SimpleTable } from "@/components";
-import { useArrayAction } from "@/utilities";
+import { FunctionComponent, useCallback, useMemo } from "react";
+import { Button } from "@mantine/core";
import { faArrowCircleRight, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { Button } from "@mantine/core";
+import { ColumnDef } from "@tanstack/react-table";
import { capitalize } from "lodash";
-import { FunctionComponent, useCallback, useMemo } from "react";
-import { Column } from "react-table";
+import { Action, FileBrowser } from "@/components";
+import SimpleTable from "@/components/tables/SimpleTable";
import {
moviesEnabledKey,
pathMappingsKey,
pathMappingsMovieKey,
seriesEnabledKey,
-} from "../keys";
-import { useFormActions } from "../utilities/FormValues";
-import { useSettingValue } from "../utilities/hooks";
+} from "@/pages/Settings/keys";
+import { useFormActions } from "@/pages/Settings/utilities/FormValues";
+import { useSettingValue } from "@/pages/Settings/utilities/hooks";
+import { useArrayAction } from "@/utilities";
import { Message } from "./Message";
type SupportType = "sonarr" | "radarr";
@@ -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"
@@ -141,7 +142,7 @@ export const PathMappingTable: FunctionComponent<TableProps> = ({ type }) => {
columns={columns}
data={data}
></SimpleTable>
- <Button fullWidth color="light" onClick={addRow}>
+ <Button fullWidth onClick={addRow}>
Add
</Button>
</>
diff --git a/frontend/src/pages/Settings/utilities/FormValues.ts b/frontend/src/pages/Settings/utilities/FormValues.ts
index d5d1774f5..32e6af226 100644
--- a/frontend/src/pages/Settings/utilities/FormValues.ts
+++ b/frontend/src/pages/Settings/utilities/FormValues.ts
@@ -1,6 +1,6 @@
-import { LOG } from "@/utilities/console";
-import type { UseFormReturnType } from "@mantine/form";
import { createContext, useCallback, useContext, useRef } from "react";
+import type { UseFormReturnType } from "@mantine/form";
+import { LOG } from "@/utilities/console";
export const FormContext = createContext<UseFormReturnType<FormValues> | null>(
null,
diff --git a/frontend/src/pages/Settings/utilities/hooks.ts b/frontend/src/pages/Settings/utilities/hooks.ts
index da874314e..00c8b9bef 100644
--- a/frontend/src/pages/Settings/utilities/hooks.ts
+++ b/frontend/src/pages/Settings/utilities/hooks.ts
@@ -1,12 +1,12 @@
-import { LOG } from "@/utilities/console";
-import { get, isNull, isUndefined, uniqBy } from "lodash";
import { useCallback, useMemo, useRef } from "react";
+import { get, isNull, isUndefined, uniqBy } from "lodash";
import {
HookType,
useFormActions,
useStagedValues,
-} from "../utilities/FormValues";
-import { useSettings } from "../utilities/SettingsProvider";
+} from "@/pages/Settings/utilities/FormValues";
+import { useSettings } from "@/pages/Settings/utilities/SettingsProvider";
+import { LOG } from "@/utilities/console";
export interface BaseInput<T> {
disabled?: boolean;
diff --git a/frontend/src/pages/System/Announcements/index.tsx b/frontend/src/pages/System/Announcements/index.tsx
index 4e204431e..de9cdea3b 100644
--- a/frontend/src/pages/System/Announcements/index.tsx
+++ b/frontend/src/pages/System/Announcements/index.tsx
@@ -1,8 +1,8 @@
-import { useSystemAnnouncements } from "@/apis/hooks";
-import { QueryOverlay } from "@/components/async";
+import { FunctionComponent } from "react";
import { Container } from "@mantine/core";
import { useDocumentTitle } from "@mantine/hooks";
-import { FunctionComponent } from "react";
+import { useSystemAnnouncements } from "@/apis/hooks";
+import { QueryOverlay } from "@/components/async";
import Table from "./table";
const SystemAnnouncementsView: FunctionComponent = () => {
diff --git a/frontend/src/pages/System/Announcements/table.tsx b/frontend/src/pages/System/Announcements/table.tsx
index 74a160190..febb32fa1 100644
--- a/frontend/src/pages/System/Announcements/table.tsx
+++ b/frontend/src/pages/System/Announcements/table.tsx
@@ -1,68 +1,82 @@
+import { FunctionComponent, useMemo } from "react";
+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 { useTableStyles } from "@/styles";
-import { faWindowClose } from "@fortawesome/free-solid-svg-icons";
-import { Anchor, Text } from "@mantine/core";
-import { FunctionComponent, useMemo } from "react";
-import { Column } from "react-table";
+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 }) => {
- const { classes } = useTableStyles();
- return <Text className={classes.primary}>{value}</Text>;
+ cell: ({
+ row: {
+ original: { timestamp },
+ },
+ }) => {
+ return <Text className="table-primary">{timestamp}</Text>;
},
},
{
- Header: "Announcement",
+ header: "Announcement",
accessor: "text",
- Cell: ({ value }) => {
- const { classes } = useTableStyles();
- return <Text className={classes.primary}>{value}</Text>;
+ cell: ({
+ row: {
+ original: { text },
+ },
+ }) => {
+ return <Text className="table-primary">{text}</Text>;
},
},
{
- Header: "More Info",
+ header: "More Info",
accessor: "link",
- Cell: ({ value }) => {
- if (value) {
- return <Label link={value}>Link</Label>;
+ cell: ({
+ row: {
+ original: { link },
+ },
+ }) => {
+ if (link) {
+ return <Label link={link}>Link</Label>;
} else {
return <Text>n/a</Text>;
}
},
},
{
- Header: "Dismiss",
+ header: "Dismiss",
accessor: "hash",
- Cell: ({ row, value }) => {
- const add = useSystemAnnouncementsAddDismiss();
+ cell: ({
+ row: {
+ original: { dismissible, hash },
+ },
+ }) => {
return (
<MutateAction
label="Dismiss announcement"
- disabled={!row.original.dismissible}
+ disabled={!dismissible}
icon={faWindowClose}
- mutation={add}
+ mutation={addDismiss}
args={() => ({
- hash: value,
+ hash: hash,
})}
></MutateAction>
);
},
},
],
- [],
+ [addDismiss],
);
return (
diff --git a/frontend/src/pages/System/Backups/index.tsx b/frontend/src/pages/System/Backups/index.tsx
index 0a19f2a9a..1057623d1 100644
--- a/frontend/src/pages/System/Backups/index.tsx
+++ b/frontend/src/pages/System/Backups/index.tsx
@@ -1,16 +1,16 @@
+import { FunctionComponent } from "react";
+import { Container } from "@mantine/core";
+import { useDocumentTitle } from "@mantine/hooks";
+import { faFileArchive } from "@fortawesome/free-solid-svg-icons";
import { useCreateBackups, useSystemBackups } from "@/apis/hooks";
import { Toolbox } from "@/components";
import { QueryOverlay } from "@/components/async";
-import { faFileArchive } from "@fortawesome/free-solid-svg-icons";
-import { Container } from "@mantine/core";
-import { useDocumentTitle } from "@mantine/hooks";
-import { FunctionComponent } from "react";
import Table from "./table";
const SystemBackupsView: FunctionComponent = () => {
const backups = useSystemBackups();
- const { mutate: backup, isLoading: isResetting } = useCreateBackups();
+ const { mutate: backup, isPending: isResetting } = useCreateBackups();
useDocumentTitle("Backups - Bazarr (System)");
diff --git a/frontend/src/pages/System/Backups/table.tsx b/frontend/src/pages/System/Backups/table.tsx
index 4f9eeae44..5c9a97f1f 100644
--- a/frontend/src/pages/System/Backups/table.tsx
+++ b/frontend/src/pages/System/Backups/table.tsx
@@ -1,56 +1,74 @@
+import { FunctionComponent, useMemo } from "react";
+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 { useTableStyles } from "@/styles";
import { Environment } from "@/utilities";
-import { faHistory, faTrash } from "@fortawesome/free-solid-svg-icons";
-import { Anchor, Text } from "@mantine/core";
-import { FunctionComponent, useMemo } from "react";
-import { Column } from "react-table";
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 }) => {
- const { classes } = useTableStyles();
- return <Text className={classes.noWrap}>{value}</Text>;
+ header: "Size",
+ accessorKey: "size",
+ cell: ({
+ row: {
+ original: { size },
+ },
+ }) => {
+ return <Text className="table-no-wrap">{size}</Text>;
},
},
{
- Header: "Time",
- accessor: "date",
- Cell: ({ value }) => {
- const { classes } = useTableStyles();
- return <Text className={classes.noWrap}>{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"
@@ -59,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}
@@ -75,27 +93,29 @@ 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"
- color="red"
+ c="red"
onClick={() =>
modals.openConfirmModal({
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}
@@ -104,7 +124,7 @@ const Table: FunctionComponent<Props> = ({ backups }) => {
},
},
],
- [],
+ [modals, remove, restore],
);
return <PageTable columns={columns} data={backups}></PageTable>;
diff --git a/frontend/src/pages/System/Logs/index.tsx b/frontend/src/pages/System/Logs/index.tsx
index d77e102d8..cb984a192 100644
--- a/frontend/src/pages/System/Logs/index.tsx
+++ b/frontend/src/pages/System/Logs/index.tsx
@@ -1,25 +1,25 @@
-import { useDeleteLogs, useSystemLogs, useSystemSettings } from "@/apis/hooks";
-import { Toolbox } from "@/components";
-import { QueryOverlay } from "@/components/async";
-import { Check, LayoutModal, Message, Text } from "@/pages/Settings/components";
-import { Environment } from "@/utilities";
+import { FunctionComponent, useCallback } from "react";
+import { Badge, Container, Group, Stack } from "@mantine/core";
+import { useDocumentTitle } from "@mantine/hooks";
+import { useModals } from "@mantine/modals";
import {
faDownload,
faFilter,
faSync,
faTrash,
} from "@fortawesome/free-solid-svg-icons";
-import { Badge, Container, Group, Stack } from "@mantine/core";
-import { useDocumentTitle } from "@mantine/hooks";
-import { useModals } from "@mantine/modals";
-import { FunctionComponent, useCallback } from "react";
+import { useDeleteLogs, useSystemLogs, useSystemSettings } from "@/apis/hooks";
+import { Toolbox } from "@/components";
+import { QueryOverlay } from "@/components/async";
+import { Check, LayoutModal, Message, Text } from "@/pages/Settings/components";
+import { Environment } from "@/utilities";
import Table from "./table";
const SystemLogsView: FunctionComponent = () => {
const logs = useSystemLogs();
const { isFetching, data, refetch } = logs;
- const { mutate, isLoading } = useDeleteLogs();
+ const { mutate, isPending } = useDeleteLogs();
const download = useCallback(() => {
window.open(`${Environment.baseUrl}/bazarr.log`);
@@ -86,7 +86,7 @@ const SystemLogsView: FunctionComponent = () => {
<Container fluid px={0}>
<QueryOverlay result={logs}>
<Toolbox>
- <Group spacing="xs">
+ <Group gap="xs">
<Toolbox.Button
loading={isFetching}
icon={faSync}
@@ -98,17 +98,17 @@ const SystemLogsView: FunctionComponent = () => {
Download
</Toolbox.Button>
<Toolbox.Button
- loading={isLoading}
+ loading={isPending}
icon={faTrash}
onClick={() => mutate()}
>
Empty
</Toolbox.Button>
<Toolbox.Button
- loading={isLoading}
+ loading={isPending}
icon={faFilter}
onClick={openFilterModal}
- rightIcon={
+ rightSection={
suffix() !== "" ? (
<Badge size="xs" radius="sm">
{suffix()}
diff --git a/frontend/src/pages/System/Logs/modal.tsx b/frontend/src/pages/System/Logs/modal.tsx
index 297909757..efd687ac0 100644
--- a/frontend/src/pages/System/Logs/modal.tsx
+++ b/frontend/src/pages/System/Logs/modal.tsx
@@ -1,6 +1,6 @@
-import { withModal } from "@/modules/modals";
-import { Code, Text } from "@mantine/core";
import { FunctionComponent, useMemo } from "react";
+import { Code, Text } from "@mantine/core";
+import { withModal } from "@/modules/modals";
interface Props {
stack: string;
diff --git a/frontend/src/pages/System/Logs/table.tsx b/frontend/src/pages/System/Logs/table.tsx
index 5a36f0f2b..0b1397c97 100644
--- a/frontend/src/pages/System/Logs/table.tsx
+++ b/frontend/src/pages/System/Logs/table.tsx
@@ -1,5 +1,4 @@
-import { Action, PageTable } from "@/components";
-import { useModals } from "@/modules/modals";
+import { FunctionComponent, useMemo } from "react";
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 { FunctionComponent, useMemo } from "react";
-import { Column } from "react-table";
+import { ColumnDef } from "@tanstack/react-table";
+import { Action } from "@/components";
+import PageTable from "@/components/tables/PageTable";
+import { useModals } from "@/modules/modals";
import SystemLogModal from "./modal";
interface Props {
- logs: readonly System.Log[];
+ logs: System.Log[];
}
function mapTypeToIcon(type: System.LogType): IconDefinition {
@@ -34,33 +35,40 @@ function mapTypeToIcon(type: System.LogType): IconDefinition {
}
const Table: FunctionComponent<Props> = ({ logs }) => {
- const columns: Column<System.Log>[] = useMemo<Column<System.Log>[]>(
+ const modals = useModals();
+
+ const columns = useMemo<ColumnDef<System.Log>[]>(
() => [
{
- accessor: "type",
- Cell: (row) => (
- <FontAwesomeIcon icon={mapTypeToIcon(row.value)}></FontAwesomeIcon>
- ),
+ accessorKey: "type",
+ cell: ({
+ row: {
+ original: { type },
+ },
+ }) => <FontAwesomeIcon icon={mapTypeToIcon(type)}></FontAwesomeIcon>,
},
{
Header: "Message",
- accessor: "message",
+ accessorKey: "message",
},
{
Header: "Date",
- accessor: "timestamp",
+ accessorKey: "timestamp",
},
{
- accessor: "exception",
- Cell: ({ value }) => {
- const modals = useModals();
- if (value) {
+ accessorKey: "exception",
+ cell: ({
+ row: {
+ original: { exception },
+ },
+ }) => {
+ if (exception) {
return (
<Action
label="Detail"
icon={faLayerGroup}
onClick={() =>
- modals.openContextModal(SystemLogModal, { stack: value })
+ modals.openContextModal(SystemLogModal, { stack: exception })
}
></Action>
);
@@ -70,7 +78,7 @@ const Table: FunctionComponent<Props> = ({ logs }) => {
},
},
],
- [],
+ [modals],
);
return (
diff --git a/frontend/src/pages/System/Providers/index.tsx b/frontend/src/pages/System/Providers/index.tsx
index cd7086221..8b73d53b0 100644
--- a/frontend/src/pages/System/Providers/index.tsx
+++ b/frontend/src/pages/System/Providers/index.tsx
@@ -1,10 +1,10 @@
+import { FunctionComponent } from "react";
+import { Container, Group } from "@mantine/core";
+import { useDocumentTitle } from "@mantine/hooks";
+import { faSync, faTrash } from "@fortawesome/free-solid-svg-icons";
import { useResetProvider, useSystemProviders } from "@/apis/hooks";
import { Toolbox } from "@/components";
import { QueryOverlay } from "@/components/async";
-import { faSync, faTrash } from "@fortawesome/free-solid-svg-icons";
-import { Container, Group } from "@mantine/core";
-import { useDocumentTitle } from "@mantine/hooks";
-import { FunctionComponent } from "react";
import Table from "./table";
const SystemProvidersView: FunctionComponent = () => {
@@ -12,7 +12,7 @@ const SystemProvidersView: FunctionComponent = () => {
const { isFetching, data, refetch } = providers;
- const { mutate: reset, isLoading: isResetting } = useResetProvider();
+ const { mutate: reset, isPending: isResetting } = useResetProvider();
useDocumentTitle("Providers - Bazarr (System)");
diff --git a/frontend/src/pages/System/Providers/table.tsx b/frontend/src/pages/System/Providers/table.tsx
index 961da65fb..8e3ff7b89 100644
--- a/frontend/src/pages/System/Providers/table.tsx
+++ b/frontend/src/pages/System/Providers/table.tsx
@@ -1,25 +1,25 @@
-import { SimpleTable } from "@/components";
import { FunctionComponent, useMemo } from "react";
-import { Column } from "react-table";
+import { ColumnDef } from "@tanstack/react-table";
+import SimpleTable from "@/components/tables/SimpleTable";
interface Props {
- providers: readonly System.Provider[];
+ providers: System.Provider[];
}
const Table: FunctionComponent<Props> = (props) => {
- const columns: Column<System.Provider>[] = useMemo<Column<System.Provider>[]>(
+ const columns = useMemo<ColumnDef<System.Provider>[]>(
() => [
{
- Header: "Name",
- accessor: "name",
+ header: "Name",
+ accessorKey: "name",
},
{
- Header: "Status",
- accessor: "status",
+ header: "Status",
+ accessorKey: "status",
},
{
- Header: "Next Retry",
- accessor: "retry",
+ header: "Next Retry",
+ accessorKey: "retry",
},
],
[],
diff --git a/frontend/src/pages/System/Releases/index.tsx b/frontend/src/pages/System/Releases/index.tsx
index f205da086..908e5ba5c 100644
--- a/frontend/src/pages/System/Releases/index.tsx
+++ b/frontend/src/pages/System/Releases/index.tsx
@@ -1,6 +1,4 @@
-import { useSystemReleases } from "@/apis/hooks";
-import { QueryOverlay } from "@/components/async";
-import { BuildKey } from "@/utilities";
+import { FunctionComponent, useMemo } from "react";
import {
Badge,
Card,
@@ -12,7 +10,9 @@ import {
Text,
} from "@mantine/core";
import { useDocumentTitle } from "@mantine/hooks";
-import { FunctionComponent, useMemo } from "react";
+import { useSystemReleases } from "@/apis/hooks";
+import { QueryOverlay } from "@/components/async";
+import { BuildKey } from "@/utilities";
const SystemReleasesView: FunctionComponent = () => {
const releases = useSystemReleases();
@@ -21,9 +21,9 @@ const SystemReleasesView: FunctionComponent = () => {
useDocumentTitle("Releases - Bazarr (System)");
return (
- <Container size={600} py={12}>
+ <Container size="md" py={12}>
<QueryOverlay result={releases}>
- <Stack spacing="lg">
+ <Stack gap="lg">
{data?.map((v, idx) => (
<ReleaseCard key={BuildKey(idx, v.date)} {...v}></ReleaseCard>
))}
@@ -47,7 +47,7 @@ const ReleaseCard: FunctionComponent<ReleaseInfo> = ({
return (
<Card shadow="md" p="lg">
<Group>
- <Text weight="bold">{name}</Text>
+ <Text fw="bold">{name}</Text>
<Badge color="blue">{date}</Badge>
<Badge color={prerelease ? "yellow" : "green"}>
{prerelease ? "Development" : "Master"}
diff --git a/frontend/src/pages/System/Status/index.tsx b/frontend/src/pages/System/Status/index.tsx
index 80757c4c7..bcd0e175d 100644
--- a/frontend/src/pages/System/Status/index.tsx
+++ b/frontend/src/pages/System/Status/index.tsx
@@ -1,15 +1,10 @@
-import { useSystemHealth, useSystemStatus } from "@/apis/hooks";
-import { QueryOverlay } from "@/components/async";
-import { GithubRepoRoot } from "@/constants";
-import { Environment, useInterval } from "@/utilities";
-import { IconDefinition } from "@fortawesome/fontawesome-common-types";
import {
- faDiscord,
- faGithub,
- faWikipediaW,
-} from "@fortawesome/free-brands-svg-icons";
-import { faCode, faPaperPlane } from "@fortawesome/free-solid-svg-icons";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+ FunctionComponent,
+ PropsWithChildren,
+ ReactNode,
+ useCallback,
+ useState,
+} from "react";
import {
Anchor,
Container,
@@ -20,14 +15,25 @@ import {
Text,
} from "@mantine/core";
import { useDocumentTitle } from "@mantine/hooks";
-import moment from "moment";
+import { IconDefinition } from "@fortawesome/fontawesome-common-types";
import {
- FunctionComponent,
- PropsWithChildren,
- ReactNode,
- useCallback,
- useState,
-} from "react";
+ faDiscord,
+ faGithub,
+ faWikipediaW,
+} from "@fortawesome/free-brands-svg-icons";
+import { faCode, faPaperPlane } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { useSystemHealth, useSystemStatus } from "@/apis/hooks";
+import { QueryOverlay } from "@/components/async";
+import { GithubRepoRoot } from "@/constants";
+import { Environment, useInterval } from "@/utilities";
+import {
+ divisorDay,
+ divisorHour,
+ divisorMinute,
+ divisorSecond,
+ formatTime,
+} from "@/utilities/time";
import Table from "./table";
interface InfoProps {
@@ -40,7 +46,7 @@ function Row(props: InfoProps): JSX.Element {
return (
<Grid columns={10}>
<Grid.Col span={2}>
- <Text size="sm" align="right" weight="bold">
+ <Text size="sm" ta="right" fw="bold">
{title}
</Text>
</Grid.Col>
@@ -79,9 +85,12 @@ const InfoContainer: FunctionComponent<
return (
<Stack>
<Divider
- labelProps={{ size: "medium", weight: "bold" }}
labelPosition="left"
- label={title}
+ label={
+ <Text size="md" fw="bold">
+ {title}
+ </Text>
+ }
></Divider>
{children}
<Space />
@@ -98,15 +107,19 @@ const SystemStatusView: FunctionComponent = () => {
const update = useCallback(() => {
const startTime = status?.start_time;
if (startTime) {
- const duration = moment.duration(
- moment().utc().unix() - startTime,
- "seconds",
- ),
- days = duration.days(),
- hours = duration.hours().toString().padStart(2, "0"),
- minutes = duration.minutes().toString().padStart(2, "0"),
- seconds = duration.seconds().toString().padStart(2, "0");
- setUptime(days + "d " + hours + ":" + minutes + ":" + seconds);
+ // Current time in seconds
+ const currentTime = Math.floor(Date.now() / 1000);
+
+ const uptimeInSeconds = currentTime - startTime;
+
+ const uptime: string = formatTime(uptimeInSeconds, [
+ { unit: "d", divisor: divisorDay },
+ { unit: "h", divisor: divisorHour },
+ { unit: "m", divisor: divisorMinute },
+ { unit: "s", divisor: divisorSecond },
+ ]);
+
+ setUptime(uptime);
}
}, [status?.start_time]);
diff --git a/frontend/src/pages/System/Status/table.tsx b/frontend/src/pages/System/Status/table.tsx
index 3b8a87e8a..7dad6757f 100644
--- a/frontend/src/pages/System/Status/table.tsx
+++ b/frontend/src/pages/System/Status/table.tsx
@@ -1,30 +1,35 @@
-import { SimpleTable } from "@/components";
-import { useTableStyles } from "@/styles";
-import { Text } from "@mantine/core";
import { FunctionComponent, useMemo } from "react";
-import { Column } from "react-table";
+import { Text } from "@mantine/core";
+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 }) => {
- const { classes } = useTableStyles();
- return <Text className={classes.noWrap}>{value}</Text>;
+ header: "Object",
+ accessorKey: "object",
+ cell: ({
+ row: {
+ original: { object },
+ },
+ }) => {
+ return <Text className="table-no-wrap">{object}</Text>;
},
},
{
- Header: "Issue",
- accessor: "issue",
- Cell: ({ value }) => {
- const { classes } = useTableStyles();
- return <Text className={classes.primary}>{value}</Text>;
+ header: "Issue",
+ accessorKey: "issue",
+ cell: ({
+ row: {
+ original: { issue },
+ },
+ }) => {
+ return <Text className="table-primary">{issue}</Text>;
},
},
],
diff --git a/frontend/src/pages/System/Tasks/index.tsx b/frontend/src/pages/System/Tasks/index.tsx
index 17e429152..b384ea460 100644
--- a/frontend/src/pages/System/Tasks/index.tsx
+++ b/frontend/src/pages/System/Tasks/index.tsx
@@ -1,10 +1,10 @@
+import { FunctionComponent } from "react";
+import { Container } from "@mantine/core";
+import { useDocumentTitle } from "@mantine/hooks";
+import { faSync } from "@fortawesome/free-solid-svg-icons";
import { useSystemTasks } from "@/apis/hooks";
import { Toolbox } from "@/components";
import { QueryOverlay } from "@/components/async";
-import { faSync } from "@fortawesome/free-solid-svg-icons";
-import { Container } from "@mantine/core";
-import { useDocumentTitle } from "@mantine/hooks";
-import { FunctionComponent } from "react";
import Table from "./table";
const SystemTasksView: FunctionComponent = () => {
diff --git a/frontend/src/pages/System/Tasks/table.tsx b/frontend/src/pages/System/Tasks/table.tsx
index ea45af49d..ed3248b6f 100644
--- a/frontend/src/pages/System/Tasks/table.tsx
+++ b/frontend/src/pages/System/Tasks/table.tsx
@@ -1,51 +1,59 @@
+import { FunctionComponent, useMemo } from "react";
+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 { useTableStyles } from "@/styles";
-import { faPlay } from "@fortawesome/free-solid-svg-icons";
-import { Text } from "@mantine/core";
-import { FunctionComponent, useMemo } from "react";
-import { Column, useSortBy } from "react-table";
+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 }) => {
- const { classes } = useTableStyles();
- return <Text className={classes.primary}>{value}</Text>;
+ cell: ({
+ row: {
+ original: { name },
+ },
+ }) => {
+ return <Text className="table-primary">{name}</Text>;
},
},
{
- Header: "Interval",
+ header: "Interval",
accessor: "interval",
- Cell: ({ value }) => {
- const { classes } = useTableStyles();
- return <Text className={classes.noWrap}>{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>
@@ -53,15 +61,16 @@ const Table: FunctionComponent<Props> = ({ tasks }) => {
},
},
],
- [],
+ [runTask],
);
return (
<SimpleTable
- initialState={{ sortBy: [{ id: "name", desc: false }] }}
+ initialState={{ sorting: [{ id: "name", desc: false }] }}
columns={columns}
data={tasks}
- plugins={[useSortBy]}
+ enableSorting
+ getSortedRowModel={getSortedRowModel()}
></SimpleTable>
);
};
diff --git a/frontend/src/pages/Wanted/Movies/index.tsx b/frontend/src/pages/Wanted/Movies/index.tsx
index 57fb6d6ed..7c497f799 100644
--- a/frontend/src/pages/Wanted/Movies/index.tsx
+++ b/frontend/src/pages/Wanted/Movies/index.tsx
@@ -1,48 +1,53 @@
+import { FunctionComponent, useMemo } from "react";
+import { Link } from "react-router-dom";
+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,
useMovieWantedPagination,
} from "@/apis/hooks";
import Language from "@/components/bazarr/Language";
-import { TaskGroup, task } from "@/modules/task";
+import { task, TaskGroup } from "@/modules/task";
import WantedView from "@/pages/views/WantedView";
import { BuildKey } from "@/utilities";
-import { faSearch } from "@fortawesome/free-solid-svg-icons";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { Anchor, Badge, Group } from "@mantine/core";
-import { FunctionComponent, useMemo } from "react";
-import { Link } from "react-router-dom";
-import { Column } from "react-table";
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 spacing="sm">
- {value.map((item, idx) => (
+ <Group gap="sm">
+ {missingSubtitles.map((item, idx) => (
<Badge
- color={download.isLoading ? "gray" : undefined}
+ color={download.isPending ? "gray" : undefined}
leftSection={<FontAwesomeIcon icon={faSearch} />}
key={BuildKey(idx, item.code2)}
style={{ cursor: "pointer" }}
@@ -70,7 +75,7 @@ const WantedMoviesView: FunctionComponent = () => {
},
},
],
- [],
+ [download],
);
const { mutateAsync } = useMovieAction();
diff --git a/frontend/src/pages/Wanted/Series/index.tsx b/frontend/src/pages/Wanted/Series/index.tsx
index 96507bccd..0501ecef5 100644
--- a/frontend/src/pages/Wanted/Series/index.tsx
+++ b/frontend/src/pages/Wanted/Series/index.tsx
@@ -1,58 +1,67 @@
+import { FunctionComponent, useMemo } from "react";
+import { Link } from "react-router-dom";
+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,
useSeriesAction,
} from "@/apis/hooks";
import Language from "@/components/bazarr/Language";
-import { TaskGroup, task } from "@/modules/task";
+import { task, TaskGroup } from "@/modules/task";
import WantedView from "@/pages/views/WantedView";
-import { useTableStyles } from "@/styles";
import { BuildKey } from "@/utilities";
-import { faSearch } from "@fortawesome/free-solid-svg-icons";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { Anchor, Badge, Group } from "@mantine/core";
-import { FunctionComponent, useMemo } from "react";
-import { Link } from "react-router-dom";
-import { Column } from "react-table";
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}`;
- const { classes } = useTableStyles();
+ header: "Name",
+ accessorKey: "seriesTitle",
+ cell: ({
+ row: {
+ original: { sonarrSeriesId, seriesTitle },
+ },
+ }) => {
+ const target = `/series/${sonarrSeriesId}`;
return (
- <Anchor className={classes.primary} component={Link} to={target}>
- {row.value}
+ <Anchor className="table-primary" component={Link} to={target}>
+ {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 spacing="sm">
- {value.map((item, idx) => (
+ <Group gap="sm">
+ {missingSubtitles.map((item, idx) => (
<Badge
- color={download.isLoading ? "gray" : undefined}
+ color={download.isPending ? "gray" : undefined}
leftSection={<FontAwesomeIcon icon={faSearch} />}
key={BuildKey(idx, item.code2)}
style={{ cursor: "pointer" }}
@@ -81,7 +90,7 @@ const WantedSeriesView: FunctionComponent = () => {
},
},
],
- [],
+ [download],
);
const { mutateAsync } = useSeriesAction();
diff --git a/frontend/src/pages/errors/CriticalError.tsx b/frontend/src/pages/errors/CriticalError.tsx
index 2c8d0202b..22070c2a7 100644
--- a/frontend/src/pages/errors/CriticalError.tsx
+++ b/frontend/src/pages/errors/CriticalError.tsx
@@ -1,8 +1,8 @@
-import { Reload } from "@/utilities";
+import { FunctionComponent } from "react";
+import { Alert, Container, Text } from "@mantine/core";
import { faExclamationTriangle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { Alert, Container, Text } from "@mantine/core";
-import { FunctionComponent } from "react";
+import { Reload } from "@/utilities";
interface Props {
message: string;
diff --git a/frontend/src/pages/errors/NotFound.tsx b/frontend/src/pages/errors/NotFound.tsx
index d81c31d7f..da4ba8229 100644
--- a/frontend/src/pages/errors/NotFound.tsx
+++ b/frontend/src/pages/errors/NotFound.tsx
@@ -1,7 +1,7 @@
+import { FunctionComponent } from "react";
+import { Box, Center, Container, Text, Title } from "@mantine/core";
import { faEyeSlash as fasEyeSlash } from "@fortawesome/free-regular-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { Box, Center, Container, Text, Title } from "@mantine/core";
-import { FunctionComponent } from "react";
const NotFound: FunctionComponent = () => {
return (
diff --git a/frontend/src/pages/errors/UIError.tsx b/frontend/src/pages/errors/UIError.tsx
index 4f26d0d0c..030f6ba11 100644
--- a/frontend/src/pages/errors/UIError.tsx
+++ b/frontend/src/pages/errors/UIError.tsx
@@ -1,7 +1,4 @@
-import { GithubRepoRoot } from "@/constants";
-import { Reload } from "@/utilities";
-import { faDizzy } from "@fortawesome/free-regular-svg-icons";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { FunctionComponent, useMemo } from "react";
import {
Anchor,
Box,
@@ -13,7 +10,10 @@ import {
Text,
Title,
} from "@mantine/core";
-import { FunctionComponent, useMemo } from "react";
+import { faDizzy } from "@fortawesome/free-regular-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { GithubRepoRoot } from "@/constants";
+import { Reload } from "@/utilities";
const Placeholder = "********";
@@ -45,13 +45,11 @@ const UIError: FunctionComponent<Props> = ({ error }) => {
<Center my="xl">
<Code>{stack}</Code>
</Center>
- <Group position="center">
+ <Group justify="center">
<Anchor href={`${GithubRepoRoot}/issues/new/choose`} target="_blank">
<Button color="yellow">Report Issue</Button>
</Anchor>
- <Button onClick={Reload} color="light">
- Reload Page
- </Button>
+ <Button onClick={Reload}>Reload Page</Button>
</Group>
</Container>
);
diff --git a/frontend/src/pages/views/HistoryView.tsx b/frontend/src/pages/views/HistoryView.tsx
index 2ecc74afb..f9fe8a27f 100644
--- a/frontend/src/pages/views/HistoryView.tsx
+++ b/frontend/src/pages/views/HistoryView.tsx
@@ -1,13 +1,13 @@
-import { UsePaginationQueryResult } from "@/apis/queries/hooks";
-import { QueryPageTable } from "@/components";
import { Container } from "@mantine/core";
import { useDocumentTitle } from "@mantine/hooks";
-import { Column } from "react-table";
+import { ColumnDef } from "@tanstack/react-table";
+import { UsePaginationQueryResult } from "@/apis/queries/hooks";
+import { QueryPageTable } from "@/components";
interface Props<T extends History.Base> {
name: string;
query: UsePaginationQueryResult<T>;
- columns: Column<T>[];
+ columns: ColumnDef<T>[];
}
function HistoryView<T extends History.Base = History.Base>({
diff --git a/frontend/src/pages/views/ItemOverview.tsx b/frontend/src/pages/views/ItemOverview.tsx
index d95944db3..15b43aab1 100644
--- a/frontend/src/pages/views/ItemOverview.tsx
+++ b/frontend/src/pages/views/ItemOverview.tsx
@@ -1,23 +1,4 @@
-import { Language } from "@/components/bazarr";
-import { BuildKey } from "@/utilities";
-import {
- useLanguageProfileBy,
- useProfileItemsToLanguages,
-} from "@/utilities/languages";
-import {
- faFolder,
- faBookmark as farBookmark,
-} from "@fortawesome/free-regular-svg-icons";
-import {
- IconDefinition,
- faBookmark,
- faClone,
- faLanguage,
- faMusic,
- faStream,
- faTags,
-} from "@fortawesome/free-solid-svg-icons";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import React, { FunctionComponent, useMemo } from "react";
import {
BackgroundImage,
Badge,
@@ -28,45 +9,53 @@ import {
HoverCard,
Image,
List,
- MediaQuery,
Stack,
Text,
Title,
- createStyles,
+ Tooltip,
} from "@mantine/core";
-import { FunctionComponent, useMemo } from "react";
+import {
+ faBookmark as farBookmark,
+ faFolder,
+} from "@fortawesome/free-regular-svg-icons";
+import {
+ faBookmark,
+ faClone,
+ faLanguage,
+ faMusic,
+ faStream,
+ faTags,
+ IconDefinition,
+} from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { Language } from "@/components/bazarr";
+import { BuildKey } from "@/utilities";
+import {
+ useLanguageProfileBy,
+ useProfileItemsToLanguages,
+} from "@/utilities/languages";
interface Props {
item: Item.Base | null;
details?: { icon: IconDefinition; text: string }[];
}
-const useStyles = createStyles((theme) => {
- return {
- poster: {
- maxWidth: "250px",
- },
- col: {
- maxWidth: "100%",
- },
- group: {
- maxWidth: "100%",
- },
- };
-});
-
const ItemOverview: FunctionComponent<Props> = (props) => {
const { item, details } = props;
- const { classes } = useStyles();
-
const detailBadges = useMemo(() => {
- const badges: (JSX.Element | null)[] = [];
+ const badges: (React.JSX.Element | null)[] = [];
if (item) {
badges.push(
<ItemBadge key="file-path" icon={faFolder} title="File Path">
- {item.path}
+ <Tooltip
+ label={item.path}
+ multiline
+ style={{ overflowWrap: "anywhere" }}
+ >
+ <span>{item.path}</span>
+ </Tooltip>
</ItemBadge>,
);
@@ -108,7 +97,7 @@ const ItemOverview: FunctionComponent<Props> = (props) => {
const profileItems = useProfileItemsToLanguages(profile);
const languageBadges = useMemo(() => {
- const badges: (JSX.Element | null)[] = [];
+ const badges: (React.JSX.Element | null)[] = [];
if (profile) {
badges.push(
@@ -147,24 +136,24 @@ const ItemOverview: FunctionComponent<Props> = (props) => {
m={0}
style={{
backgroundColor: "rgba(0,0,0,0.7)",
- flexWrap: "nowrap",
+ }}
+ styles={{
+ inner: { flexWrap: "nowrap" },
}}
>
- <MediaQuery smallerThan="sm" styles={{ display: "none" }}>
- <Grid.Col span={3}>
- <Image
- src={item?.poster}
- mx="auto"
- className={classes.poster}
- withPlaceholder
- ></Image>
- </Grid.Col>
- </MediaQuery>
- <Grid.Col span={8} className={classes.col}>
- <Stack align="flex-start" spacing="xs" mx={6}>
- <Group align="flex-start" noWrap className={classes.group}>
+ <Grid.Col span={3} visibleFrom="sm">
+ <Image
+ src={item?.poster}
+ mx="auto"
+ maw="250px"
+ fallbackSrc="https://placehold.co/250x250?text=Placeholder"
+ ></Image>
+ </Grid.Col>
+ <Grid.Col span={8} maw="100%" style={{ overflow: "hidden" }}>
+ <Stack align="flex-start" gap="xs" mx={6}>
+ <Group align="flex-start" wrap="nowrap" maw="100%">
<Title my={0}>
- <Text inherit color="white">
+ <Text inherit c="white">
<Box component="span" mr={12}>
<FontAwesomeIcon
title={item?.monitored ? "monitored" : "unmonitored"}
@@ -176,10 +165,7 @@ const ItemOverview: FunctionComponent<Props> = (props) => {
</Title>
<HoverCard position="bottom" withArrow>
<HoverCard.Target>
- <Text
- hidden={item?.alternativeTitles.length === 0}
- color="white"
- >
+ <Text hidden={item?.alternativeTitles.length === 0} c="white">
<FontAwesomeIcon icon={faClone} />
</Text>
</HoverCard.Target>
@@ -192,16 +178,16 @@ const ItemOverview: FunctionComponent<Props> = (props) => {
</HoverCard.Dropdown>
</HoverCard>
</Group>
- <Group spacing="xs" className={classes.group}>
+ <Group gap="xs" maw="100%">
{detailBadges}
</Group>
- <Group spacing="xs" className={classes.group}>
+ <Group gap="xs" maw="100%">
{audioBadges}
</Group>
- <Group spacing="xs" className={classes.group}>
+ <Group gap="xs" maw="100%">
{languageBadges}
</Group>
- <Text size="sm" color="white">
+ <Text size="sm" c="white">
{item?.overview}
</Text>
</Stack>
@@ -223,8 +209,8 @@ const ItemBadge: FunctionComponent<ItemBadgeProps> = ({
}) => (
<Badge
leftSection={<FontAwesomeIcon icon={icon}></FontAwesomeIcon>}
+ variant="light"
radius="sm"
- color="dark"
size="sm"
style={{ textTransform: "none" }}
aria-label={title}
diff --git a/frontend/src/pages/views/ItemView.tsx b/frontend/src/pages/views/ItemView.tsx
index 8fdaf83c8..c4ff250ea 100644
--- a/frontend/src/pages/views/ItemView.tsx
+++ b/frontend/src/pages/views/ItemView.tsx
@@ -1,12 +1,12 @@
+import { useNavigate } from "react-router-dom";
+import { faList } from "@fortawesome/free-solid-svg-icons";
+import { ColumnDef } from "@tanstack/react-table";
import { UsePaginationQueryResult } from "@/apis/queries/hooks";
import { QueryPageTable, Toolbox } from "@/components";
-import { faList } from "@fortawesome/free-solid-svg-icons";
-import { useNavigate } from "react-router-dom";
-import { Column } from "react-table";
interface Props<T extends Item.Base = Item.Base> {
query: UsePaginationQueryResult<T>;
- columns: Column<T>[];
+ columns: ColumnDef<T>[];
}
function ItemView<T extends Item.Base>({ query, columns }: Props<T>) {
diff --git a/frontend/src/pages/views/MassEditor.tsx b/frontend/src/pages/views/MassEditor.tsx
index b15a55e83..48068d1c6 100644
--- a/frontend/src/pages/views/MassEditor.tsx
+++ b/frontend/src/pages/views/MassEditor.tsx
@@ -1,18 +1,17 @@
-import { useIsAnyMutationRunning, useLanguageProfiles } from "@/apis/hooks";
-import { SimpleTable, Toolbox } from "@/components";
-import { Selector, SelectorOption } from "@/components/inputs";
-import { useCustomSelection } from "@/components/tables/plugins";
-import { GetItemId, useSelectorOptions } from "@/utilities";
+import { useCallback, useMemo, useRef, useState } from "react";
+import { useNavigate } from "react-router-dom";
+import { Box, Container, useCombobox } from "@mantine/core";
import { faCheck, faUndo } from "@fortawesome/free-solid-svg-icons";
-import { Box, Container } from "@mantine/core";
+import { UseMutationResult } from "@tanstack/react-query";
+import { ColumnDef, Table } from "@tanstack/react-table";
import { uniqBy } from "lodash";
-import { useCallback, useMemo, useState } from "react";
-import { UseMutationResult } from "react-query";
-import { useNavigate } from "react-router-dom";
-import { Column, useRowSelect } from "react-table";
+import { useIsAnyMutationRunning, useLanguageProfiles } from "@/apis/hooks";
+import { GroupedSelector, GroupedSelectorOptions, Toolbox } from "@/components";
+import SimpleTable from "@/components/tables/SimpleTable";
+import { GetItemId, useSelectorOptions } from "@/utilities";
interface MassEditorProps<T extends Item.Base = Item.Base> {
- columns: Column<T>[];
+ columns: ColumnDef<T>[];
data: T[];
mutation: UseMutationResult<void, unknown, FormType.ModifyItem>;
}
@@ -24,6 +23,7 @@ function MassEditor<T extends Item.Base>(props: MassEditorProps<T>) {
const [dirties, setDirties] = useState<T[]>([]);
const hasTask = useIsAnyMutationRunning();
const { data: profiles } = useLanguageProfiles();
+ const tableRef = useRef<Table<T>>(null);
const navigate = useNavigate();
@@ -37,14 +37,25 @@ function MassEditor<T extends Item.Base>(props: MassEditorProps<T>) {
const profileOptions = useSelectorOptions(profiles ?? [], (v) => v.name);
const profileOptionsWithAction = useMemo<
- SelectorOption<Language.Profile | null>[]
- >(
- () => [
- { label: "Clear", value: null, group: "Action" },
- ...profileOptions.options,
- ],
- [profileOptions.options],
- );
+ GroupedSelectorOptions<string>[]
+ >(() => {
+ return [
+ {
+ group: "Actions",
+ items: [{ label: "Clear", value: "", profileId: null }],
+ },
+ {
+ group: "Profiles",
+ items: profileOptions.options.map((a) => {
+ return {
+ value: a.value.profileId.toString(),
+ label: a.label,
+ profileId: a.value.profileId,
+ };
+ }),
+ },
+ ];
+ }, [profileOptions.options]);
const getKey = useCallback((value: Language.Profile | null) => {
if (value) {
@@ -56,11 +67,20 @@ function MassEditor<T extends Item.Base>(props: MassEditorProps<T>) {
const { mutateAsync } = mutation;
+ /**
+ * Submit the form that contains the series id and the respective profile id set in chunks to prevent payloads too
+ * large when we have a high amount of series or movies being applied the profile. The chunks are executed in order
+ * since there are no much benefit on executing in parallel, also parallelism could result in high load on the server
+ * side if not throttled properly.
+ */
const save = useCallback(() => {
+ const chunkSize = 1000;
+
const form: FormType.ModifyItem = {
id: [],
profileid: [],
};
+
dirties.forEach((v) => {
const id = GetItemId(v);
if (id) {
@@ -68,32 +88,63 @@ function MassEditor<T extends Item.Base>(props: MassEditorProps<T>) {
form.profileid.push(v.profileId);
}
});
- return mutateAsync(form);
+
+ const mutateInChunks = async (
+ ids: number[],
+ profileIds: (number | null)[],
+ ) => {
+ if (ids.length === 0) return;
+
+ const chunkIds = ids.slice(0, chunkSize);
+ const chunkProfileIds = profileIds.slice(0, chunkSize);
+
+ await mutateAsync({
+ id: chunkIds,
+ profileid: chunkProfileIds,
+ });
+
+ await mutateInChunks(ids.slice(chunkSize), profileIds.slice(chunkSize));
+ };
+
+ return mutateInChunks(form.id, form.profileid);
}, [dirties, mutateAsync]);
const setProfiles = useCallback(
- (profile: Language.Profile | null) => {
- const id = profile?.profileId ?? null;
+ (id: number | null) => {
const newItems = selections.map((v) => ({ ...v, profileId: id }));
setDirties((dirty) => {
return uniqBy([...newItems, ...dirty], GetItemId);
});
+
+ tableRef.current?.toggleAllRowsSelected(false);
},
[selections],
);
+
+ const combobox = useCombobox();
+
return (
<Container fluid px={0}>
<Toolbox>
<Box>
- <Selector
- allowDeselect
+ <GroupedSelector
+ onClick={() => combobox.openDropdown()}
+ onDropdownClose={() => {
+ combobox.resetSelectedOption();
+ }}
placeholder="Change Profile"
+ withCheckIcon={false}
options={profileOptionsWithAction}
getkey={getKey}
disabled={selections.length === 0}
- onChange={setProfiles}
- ></Selector>
+ comboboxProps={{
+ store: combobox,
+ onOptionSubmit: (value) => {
+ setProfiles(value ? +value : null);
+ },
+ }}
+ ></GroupedSelector>
</Box>
<Box>
<Toolbox.Button icon={faUndo} onClick={onEnded}>
@@ -110,10 +161,13 @@ function MassEditor<T extends Item.Base>(props: MassEditorProps<T>) {
</Box>
</Toolbox>
<SimpleTable
+ instanceRef={tableRef}
columns={columns}
data={data}
- onSelect={setSelections}
- plugins={[useRowSelect, useCustomSelection]}
+ enableRowSelection
+ onRowSelectionChanged={(row) => {
+ setSelections(row.map((r) => r.original));
+ }}
></SimpleTable>
</Container>
);
diff --git a/frontend/src/pages/views/WantedView.tsx b/frontend/src/pages/views/WantedView.tsx
index 5605bf337..e04f583b8 100644
--- a/frontend/src/pages/views/WantedView.tsx
+++ b/frontend/src/pages/views/WantedView.tsx
@@ -1,14 +1,14 @@
+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";
-import { faSearch } from "@fortawesome/free-solid-svg-icons";
-import { Container } from "@mantine/core";
-import { useDocumentTitle } from "@mantine/hooks";
-import { Column } from "react-table";
interface Props<T extends Wanted.Base> {
name: string;
- columns: Column<T>[];
+ columns: ColumnDef<T>[];
query: UsePaginationQueryResult<T>;
searchAll: () => Promise<void>;
}