aboutsummaryrefslogtreecommitdiffhomepage
path: root/frontend/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src/components')
-rw-r--r--frontend/src/components/ErrorBoundary.tsx2
-rw-r--r--frontend/src/components/Search.tsx74
-rw-r--r--frontend/src/components/StateIcon.tsx20
-rw-r--r--frontend/src/components/SubtitleToolsMenu.tsx28
-rw-r--r--frontend/src/components/TextPopover.tsx9
-rw-r--r--frontend/src/components/async/Lazy.tsx2
-rw-r--r--frontend/src/components/async/MutateAction.tsx7
-rw-r--r--frontend/src/components/async/MutateButton.tsx5
-rw-r--r--frontend/src/components/async/QueryOverlay.tsx8
-rw-r--r--frontend/src/components/bazarr/AudioList.tsx9
-rw-r--r--frontend/src/components/bazarr/HistoryIcon.tsx4
-rw-r--r--frontend/src/components/bazarr/Language.test.tsx18
-rw-r--r--frontend/src/components/bazarr/Language.tsx6
-rw-r--r--frontend/src/components/bazarr/LanguageProfile.tsx2
-rw-r--r--frontend/src/components/bazarr/LanguageSelector.tsx2
-rw-r--r--frontend/src/components/forms/ColorToolForm.tsx6
-rw-r--r--frontend/src/components/forms/FrameRateForm.tsx14
-rw-r--r--frontend/src/components/forms/ItemEditForm.tsx14
-rw-r--r--frontend/src/components/forms/MovieUploadForm.tsx187
-rw-r--r--frontend/src/components/forms/ProfileEditForm.module.scss13
-rw-r--r--frontend/src/components/forms/ProfileEditForm.tsx199
-rw-r--r--frontend/src/components/forms/SeriesUploadForm.tsx203
-rw-r--r--frontend/src/components/forms/SyncSubtitleForm.tsx73
-rw-r--r--frontend/src/components/forms/TimeOffsetForm.tsx13
-rw-r--r--frontend/src/components/forms/TranslationForm.tsx10
-rw-r--r--frontend/src/components/index.tsx2
-rw-r--r--frontend/src/components/inputs/Action.test.tsx8
-rw-r--r--frontend/src/components/inputs/Action.tsx12
-rw-r--r--frontend/src/components/inputs/ChipInput.test.tsx10
-rw-r--r--frontend/src/components/inputs/ChipInput.tsx50
-rw-r--r--frontend/src/components/inputs/DropContent.module.scss4
-rw-r--r--frontend/src/components/inputs/DropContent.tsx24
-rw-r--r--frontend/src/components/inputs/FileBrowser.tsx31
-rw-r--r--frontend/src/components/inputs/Selector.test.tsx35
-rw-r--r--frontend/src/components/inputs/Selector.tsx45
-rw-r--r--frontend/src/components/modals/HistoryModal.tsx203
-rw-r--r--frontend/src/components/modals/ManualSearchModal.tsx188
-rw-r--r--frontend/src/components/modals/SubtitleToolsModal.tsx75
-rw-r--r--frontend/src/components/tables/BaseTable.module.scss9
-rw-r--r--frontend/src/components/tables/BaseTable.tsx131
-rw-r--r--frontend/src/components/tables/GroupTable.tsx86
-rw-r--r--frontend/src/components/tables/PageControl.tsx7
-rw-r--r--frontend/src/components/tables/PageTable.tsx63
-rw-r--r--frontend/src/components/tables/QueryPageTable.tsx4
-rw-r--r--frontend/src/components/tables/SimpleTable.tsx60
-rw-r--r--frontend/src/components/tables/plugins/index.ts2
-rw-r--r--frontend/src/components/tables/plugins/useCustomSelection.tsx113
-rw-r--r--frontend/src/components/tables/plugins/useDefaultSettings.tsx33
-rw-r--r--frontend/src/components/toolbox/Button.tsx8
-rw-r--r--frontend/src/components/toolbox/Toolbox.module.scss9
-rw-r--r--frontend/src/components/toolbox/Toolbox.tsx (renamed from frontend/src/components/toolbox/index.tsx)15
51 files changed, 1121 insertions, 1034 deletions
diff --git a/frontend/src/components/ErrorBoundary.tsx b/frontend/src/components/ErrorBoundary.tsx
index a29200e47..4e39dd9dc 100644
--- a/frontend/src/components/ErrorBoundary.tsx
+++ b/frontend/src/components/ErrorBoundary.tsx
@@ -1,5 +1,5 @@
-import UIError from "@/pages/errors/UIError";
import { Component, PropsWithChildren } from "react";
+import UIError from "@/pages/errors/UIError";
interface State {
error: Error | null;
diff --git a/frontend/src/components/Search.tsx b/frontend/src/components/Search.tsx
index bc4a9f8d3..b506afee3 100644
--- a/frontend/src/components/Search.tsx
+++ b/frontend/src/components/Search.tsx
@@ -1,15 +1,10 @@
-import { useServerSearch } from "@/apis/hooks";
-import { useDebouncedValue } from "@/utilities";
+import { FunctionComponent, useMemo, useState } from "react";
+import { useNavigate } from "react-router-dom";
+import { Autocomplete, ComboboxItem, OptionsFilter, Text } from "@mantine/core";
import { faSearch } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import {
- Anchor,
- Autocomplete,
- createStyles,
- SelectItemProps,
-} from "@mantine/core";
-import { forwardRef, FunctionComponent, useMemo, useState } from "react";
-import { Link } from "react-router-dom";
+import { useServerSearch } from "@/apis/hooks";
+import { useDebouncedValue } from "@/utilities";
type SearchResultItem = {
value: string;
@@ -18,7 +13,7 @@ type SearchResultItem = {
function useSearch(query: string) {
const debouncedQuery = useDebouncedValue(query, 500);
- const { data } = useServerSearch(debouncedQuery, debouncedQuery.length > 0);
+ const { data } = useServerSearch(debouncedQuery, debouncedQuery.length >= 0);
return useMemo<SearchResultItem[]>(
() =>
@@ -31,7 +26,6 @@ function useSearch(query: string) {
} else {
throw new Error("Unknown search result");
}
-
return {
value: `${v.title} (${v.year})`,
link,
@@ -41,59 +35,43 @@ function useSearch(query: string) {
);
}
-const useStyles = createStyles((theme) => {
- return {
- result: {
- color:
- theme.colorScheme === "light"
- ? theme.colors.dark[8]
- : theme.colors.gray[1],
- },
- };
-});
-
-type ResultCompProps = SelectItemProps & SearchResultItem;
-
-const ResultComponent = forwardRef<HTMLDivElement, ResultCompProps>(
- ({ link, value }, ref) => {
- const styles = useStyles();
+const optionsFilter: OptionsFilter = ({ options, search }) => {
+ const lowercaseSearch = search.toLowerCase();
+ const trimmedSearch = search.trim();
+ return (options as ComboboxItem[]).filter((option) => {
return (
- <Anchor
- component={Link}
- to={link}
- underline={false}
- className={styles.classes.result}
- p="sm"
- >
- {value}
- </Anchor>
+ option.value.toLowerCase().includes(lowercaseSearch) ||
+ option.value
+ .normalize("NFD")
+ .replace(/[\u0300-\u036f]/g, "")
+ .toLowerCase()
+ .includes(trimmedSearch)
);
- },
-);
+ });
+};
const Search: FunctionComponent = () => {
+ const navigate = useNavigate();
const [query, setQuery] = useState("");
const results = useSearch(query);
return (
<Autocomplete
- icon={<FontAwesomeIcon icon={faSearch} />}
- itemComponent={ResultComponent}
+ leftSection={<FontAwesomeIcon icon={faSearch} />}
+ renderOption={(input) => <Text p="xs">{input.option.value}</Text>}
placeholder="Search"
size="sm"
data={results}
value={query}
+ scrollAreaProps={{ type: "auto" }}
+ maxDropdownHeight={400}
onChange={setQuery}
onBlur={() => setQuery("")}
- filter={(value, item) =>
- item.value.toLowerCase().includes(value.toLowerCase().trim()) ||
- item.value
- .normalize("NFD")
- .replace(/[\u0300-\u036f]/g, "")
- .toLowerCase()
- .includes(value.trim())
+ filter={optionsFilter}
+ onOptionSubmit={(option) =>
+ navigate(results.find((a) => a.value === option)?.link || "/")
}
></Autocomplete>
);
diff --git a/frontend/src/components/StateIcon.tsx b/frontend/src/components/StateIcon.tsx
index f9683f63a..31e0b5243 100644
--- a/frontend/src/components/StateIcon.tsx
+++ b/frontend/src/components/StateIcon.tsx
@@ -1,4 +1,6 @@
-import { BuildKey } from "@/utilities";
+import { FunctionComponent } from "react";
+import { Group, List, Popover, Stack, Text } from "@mantine/core";
+import { useHover } from "@mantine/hooks";
import {
faCheck,
faCheckCircle,
@@ -7,9 +9,7 @@ import {
faTimes,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { Group, List, Popover, Stack, Text } from "@mantine/core";
-import { useHover } from "@mantine/hooks";
-import { FunctionComponent } from "react";
+import { BuildKey } from "@/utilities";
interface StateIconProps {
matches: string[];
@@ -31,7 +31,7 @@ const StateIcon: FunctionComponent<StateIconProps> = ({
return <FontAwesomeIcon icon={faListCheck} />;
} else {
return (
- <Text color={hasIssues ? "yellow" : "green"}>
+ <Text c={hasIssues ? "yellow" : "green"} span>
<FontAwesomeIcon
icon={hasIssues ? faExclamationCircle : faCheckCircle}
/>
@@ -48,9 +48,9 @@ const StateIcon: FunctionComponent<StateIconProps> = ({
</Text>
</Popover.Target>
<Popover.Dropdown>
- <Group position="left" spacing="xl" noWrap grow>
- <Stack align="flex-start" justify="flex-start" spacing="xs" mb="auto">
- <Text color="green">
+ <Group justify="left" gap="xl" wrap="nowrap" grow>
+ <Stack align="flex-start" justify="flex-start" gap="xs" mb="auto">
+ <Text c="green">
<FontAwesomeIcon icon={faCheck}></FontAwesomeIcon>
</Text>
<List>
@@ -59,8 +59,8 @@ const StateIcon: FunctionComponent<StateIconProps> = ({
))}
</List>
</Stack>
- <Stack align="flex-start" justify="flex-start" spacing="xs" mb="auto">
- <Text color="yellow">
+ <Stack align="flex-start" justify="flex-start" gap="xs" mb="auto">
+ <Text c="yellow">
<FontAwesomeIcon icon={faTimes}></FontAwesomeIcon>
</Text>
<List>
diff --git a/frontend/src/components/SubtitleToolsMenu.tsx b/frontend/src/components/SubtitleToolsMenu.tsx
index e36a1e9e1..545c87478 100644
--- a/frontend/src/components/SubtitleToolsMenu.tsx
+++ b/frontend/src/components/SubtitleToolsMenu.tsx
@@ -1,11 +1,5 @@
-import { useSubtitleAction } from "@/apis/hooks";
-import { ColorToolModal } from "@/components/forms/ColorToolForm";
-import { FrameRateModal } from "@/components/forms/FrameRateForm";
-import { TimeOffsetModal } from "@/components/forms/TimeOffsetForm";
-import { TranslationModal } from "@/components/forms/TranslationForm";
-import { useModals } from "@/modules/modals";
-import { ModalComponent } from "@/modules/modals/WithModal";
-import { task } from "@/modules/task";
+import { FunctionComponent, ReactElement, useCallback, useMemo } from "react";
+import { Divider, List, Menu, MenuProps, ScrollArea } from "@mantine/core";
import {
faClock,
faCode,
@@ -23,8 +17,14 @@ import {
IconDefinition,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { Divider, List, Menu, MenuProps, ScrollArea } from "@mantine/core";
-import { FunctionComponent, ReactElement, useCallback, useMemo } from "react";
+import { useSubtitleAction } from "@/apis/hooks";
+import { ColorToolModal } from "@/components/forms/ColorToolForm";
+import { FrameRateModal } from "@/components/forms/FrameRateForm";
+import { TimeOffsetModal } from "@/components/forms/TimeOffsetForm";
+import { TranslationModal } from "@/components/forms/TranslationForm";
+import { useModals } from "@/modules/modals";
+import { ModalComponent } from "@/modules/modals/WithModal";
+import { task } from "@/modules/task";
import { SyncSubtitleModal } from "./forms/SyncSubtitleForm";
export interface ToolOptions {
@@ -127,6 +127,8 @@ const SubtitleToolsMenu: FunctionComponent<Props> = ({
type: s.type,
language: s.language,
path: s.path,
+ hi: s.hi,
+ forced: s.forced,
};
task.create(s.path, name, mutateAsync, { action, form });
});
@@ -148,7 +150,7 @@ const SubtitleToolsMenu: FunctionComponent<Props> = ({
<Menu.Item
key={tool.key}
disabled={disabledTools}
- icon={<FontAwesomeIcon icon={tool.icon}></FontAwesomeIcon>}
+ leftSection={<FontAwesomeIcon icon={tool.icon}></FontAwesomeIcon>}
onClick={() => {
if (tool.modal) {
modals.openContextModal(tool.modal, { selections });
@@ -164,7 +166,7 @@ const SubtitleToolsMenu: FunctionComponent<Props> = ({
<Menu.Label>Actions</Menu.Label>
<Menu.Item
disabled={selections.length !== 0 || onAction === undefined}
- icon={<FontAwesomeIcon icon={faSearch}></FontAwesomeIcon>}
+ leftSection={<FontAwesomeIcon icon={faSearch}></FontAwesomeIcon>}
onClick={() => {
onAction?.("search");
}}
@@ -174,7 +176,7 @@ const SubtitleToolsMenu: FunctionComponent<Props> = ({
<Menu.Item
disabled={selections.length === 0 || onAction === undefined}
color="red"
- icon={<FontAwesomeIcon icon={faTrash}></FontAwesomeIcon>}
+ leftSection={<FontAwesomeIcon icon={faTrash}></FontAwesomeIcon>}
onClick={() => {
modals.openConfirmModal({
title: "The following subtitles will be deleted",
diff --git a/frontend/src/components/TextPopover.tsx b/frontend/src/components/TextPopover.tsx
index 8fda5913e..03dd58700 100644
--- a/frontend/src/components/TextPopover.tsx
+++ b/frontend/src/components/TextPopover.tsx
@@ -1,7 +1,7 @@
+import { FunctionComponent, ReactElement } from "react";
import { Tooltip, TooltipProps } from "@mantine/core";
import { useHover } from "@mantine/hooks";
import { isNull, isUndefined } from "lodash";
-import { FunctionComponent, ReactElement } from "react";
interface TextPopoverProps {
children: ReactElement;
@@ -21,7 +21,12 @@ const TextPopover: FunctionComponent<TextPopoverProps> = ({
}
return (
- <Tooltip opened={hovered} label={text} {...tooltip}>
+ <Tooltip
+ opened={hovered}
+ label={text}
+ {...tooltip}
+ style={{ textWrap: "wrap" }}
+ >
<div ref={ref}>{children}</div>
</Tooltip>
);
diff --git a/frontend/src/components/async/Lazy.tsx b/frontend/src/components/async/Lazy.tsx
index 2a0496223..317c0feb3 100644
--- a/frontend/src/components/async/Lazy.tsx
+++ b/frontend/src/components/async/Lazy.tsx
@@ -1,5 +1,5 @@
-import { LoadingOverlay } from "@mantine/core";
import { FunctionComponent, PropsWithChildren, Suspense } from "react";
+import { LoadingOverlay } from "@mantine/core";
const Lazy: FunctionComponent<PropsWithChildren> = ({ children }) => {
return <Suspense fallback={<LoadingOverlay visible />}>{children}</Suspense>;
diff --git a/frontend/src/components/async/MutateAction.tsx b/frontend/src/components/async/MutateAction.tsx
index 920fe4ff3..92c102ea9 100644
--- a/frontend/src/components/async/MutateAction.tsx
+++ b/frontend/src/components/async/MutateAction.tsx
@@ -1,7 +1,7 @@
import { useCallback, useState } from "react";
-import { UseMutationResult } from "react-query";
-import { Action } from "../inputs";
-import { ActionProps } from "../inputs/Action";
+import { UseMutationResult } from "@tanstack/react-query";
+import { Action } from "@/components/inputs";
+import { ActionProps } from "@/components/inputs/Action";
type MutateActionProps<DATA, VAR> = Omit<
ActionProps,
@@ -16,7 +16,6 @@ type MutateActionProps<DATA, VAR> = Omit<
function MutateAction<DATA, VAR>({
mutation,
- noReset,
onSuccess,
onError,
args,
diff --git a/frontend/src/components/async/MutateButton.tsx b/frontend/src/components/async/MutateButton.tsx
index 8d0f68541..908c2dfda 100644
--- a/frontend/src/components/async/MutateButton.tsx
+++ b/frontend/src/components/async/MutateButton.tsx
@@ -1,6 +1,6 @@
-import { Button, ButtonProps } from "@mantine/core";
import { useCallback, useState } from "react";
-import { UseMutationResult } from "react-query";
+import { Button, ButtonProps } from "@mantine/core";
+import { UseMutationResult } from "@tanstack/react-query";
type MutateButtonProps<DATA, VAR> = Omit<
ButtonProps,
@@ -15,7 +15,6 @@ type MutateButtonProps<DATA, VAR> = Omit<
function MutateButton<DATA, VAR>({
mutation,
- noReset,
onSuccess,
onError,
args,
diff --git a/frontend/src/components/async/QueryOverlay.tsx b/frontend/src/components/async/QueryOverlay.tsx
index 24b95ab18..1672989ff 100644
--- a/frontend/src/components/async/QueryOverlay.tsx
+++ b/frontend/src/components/async/QueryOverlay.tsx
@@ -1,7 +1,7 @@
-import { LoadingProvider } from "@/contexts";
-import { LoadingOverlay } from "@mantine/core";
import { FunctionComponent, ReactNode } from "react";
-import { UseQueryResult } from "react-query";
+import { LoadingOverlay } from "@mantine/core";
+import { UseQueryResult } from "@tanstack/react-query";
+import { LoadingProvider } from "@/contexts";
interface QueryOverlayProps {
result: UseQueryResult<unknown, unknown>;
@@ -12,7 +12,7 @@ interface QueryOverlayProps {
const QueryOverlay: FunctionComponent<QueryOverlayProps> = ({
children,
global = false,
- result: { isLoading, isError, error },
+ result: { isLoading },
}) => {
return (
<LoadingProvider value={isLoading}>
diff --git a/frontend/src/components/bazarr/AudioList.tsx b/frontend/src/components/bazarr/AudioList.tsx
index ac9cce743..f0dc07c0d 100644
--- a/frontend/src/components/bazarr/AudioList.tsx
+++ b/frontend/src/components/bazarr/AudioList.tsx
@@ -1,6 +1,7 @@
-import { BuildKey } from "@/utilities";
-import { Badge, BadgeProps, Group, GroupProps } from "@mantine/core";
import { FunctionComponent } from "react";
+import { Badge, BadgeProps, Group, GroupProps } from "@mantine/core";
+import { BuildKey } from "@/utilities";
+import { normalizeAudioLanguage } from "@/utilities/languages";
export type AudioListProps = GroupProps & {
audios: Language.Info[];
@@ -13,10 +14,10 @@ const AudioList: FunctionComponent<AudioListProps> = ({
...group
}) => {
return (
- <Group spacing="xs" {...group}>
+ <Group gap="xs" {...group}>
{audios.map((audio, idx) => (
<Badge color="blue" key={BuildKey(idx, audio.code2)} {...badgeProps}>
- {audio.name}
+ {normalizeAudioLanguage(audio.name)}
</Badge>
))}
</Group>
diff --git a/frontend/src/components/bazarr/HistoryIcon.tsx b/frontend/src/components/bazarr/HistoryIcon.tsx
index e6c0f2411..add0cd1fd 100644
--- a/frontend/src/components/bazarr/HistoryIcon.tsx
+++ b/frontend/src/components/bazarr/HistoryIcon.tsx
@@ -1,3 +1,5 @@
+import { FunctionComponent } from "react";
+import { Tooltip } from "@mantine/core";
import {
faClock,
faClosedCaptioning,
@@ -9,8 +11,6 @@ import {
faUser,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { Tooltip } from "@mantine/core";
-import { FunctionComponent } from "react";
enum HistoryAction {
Delete = 0,
diff --git a/frontend/src/components/bazarr/Language.test.tsx b/frontend/src/components/bazarr/Language.test.tsx
index 9e0e0fab8..2cad5d4c8 100644
--- a/frontend/src/components/bazarr/Language.test.tsx
+++ b/frontend/src/components/bazarr/Language.test.tsx
@@ -1,5 +1,5 @@
-import { rawRender, screen } from "@/tests";
import { describe, it } from "vitest";
+import { render, screen } from "@/tests";
import { Language } from ".";
describe("Language text", () => {
@@ -9,13 +9,13 @@ describe("Language text", () => {
};
it("should show short text", () => {
- rawRender(<Language.Text value={testLanguage}></Language.Text>);
+ render(<Language.Text value={testLanguage}></Language.Text>);
expect(screen.getByText(testLanguage.code2)).toBeDefined();
});
it("should show long text", () => {
- rawRender(<Language.Text value={testLanguage} long></Language.Text>);
+ render(<Language.Text value={testLanguage} long></Language.Text>);
expect(screen.getByText(testLanguage.name)).toBeDefined();
});
@@ -23,7 +23,7 @@ describe("Language text", () => {
const testLanguageWithHi: Language.Info = { ...testLanguage, hi: true };
it("should show short text with HI", () => {
- rawRender(<Language.Text value={testLanguageWithHi}></Language.Text>);
+ render(<Language.Text value={testLanguageWithHi}></Language.Text>);
const expectedText = `${testLanguageWithHi.code2}:HI`;
@@ -31,7 +31,7 @@ describe("Language text", () => {
});
it("should show long text with HI", () => {
- rawRender(<Language.Text value={testLanguageWithHi} long></Language.Text>);
+ render(<Language.Text value={testLanguageWithHi} long></Language.Text>);
const expectedText = `${testLanguageWithHi.name} HI`;
@@ -44,7 +44,7 @@ describe("Language text", () => {
};
it("should show short text with Forced", () => {
- rawRender(<Language.Text value={testLanguageWithForced}></Language.Text>);
+ render(<Language.Text value={testLanguageWithForced}></Language.Text>);
const expectedText = `${testLanguageWithHi.code2}:Forced`;
@@ -52,9 +52,7 @@ describe("Language text", () => {
});
it("should show long text with Forced", () => {
- rawRender(
- <Language.Text value={testLanguageWithForced} long></Language.Text>,
- );
+ render(<Language.Text value={testLanguageWithForced} long></Language.Text>);
const expectedText = `${testLanguageWithHi.name} Forced`;
@@ -75,7 +73,7 @@ describe("Language list", () => {
];
it("should show all languages", () => {
- rawRender(<Language.List value={elements}></Language.List>);
+ render(<Language.List value={elements}></Language.List>);
elements.forEach((value) => {
expect(screen.getByText(value.name)).toBeDefined();
diff --git a/frontend/src/components/bazarr/Language.tsx b/frontend/src/components/bazarr/Language.tsx
index e5627c82e..6315d9102 100644
--- a/frontend/src/components/bazarr/Language.tsx
+++ b/frontend/src/components/bazarr/Language.tsx
@@ -1,6 +1,6 @@
-import { BuildKey } from "@/utilities";
-import { Badge, Group, Text, TextProps } from "@mantine/core";
import { FunctionComponent, useMemo } from "react";
+import { Badge, Group, Text, TextProps } from "@mantine/core";
+import { BuildKey } from "@/utilities";
type LanguageTextProps = TextProps & {
value: Language.Info;
@@ -49,7 +49,7 @@ type LanguageListProps = {
const LanguageList: FunctionComponent<LanguageListProps> = ({ value }) => {
return (
- <Group spacing="xs">
+ <Group gap="xs">
{value.map((v) => (
<Badge key={BuildKey(v.code2, v.code2, v.hi)}>{v.name}</Badge>
))}
diff --git a/frontend/src/components/bazarr/LanguageProfile.tsx b/frontend/src/components/bazarr/LanguageProfile.tsx
index 75b7b73ca..a234268c3 100644
--- a/frontend/src/components/bazarr/LanguageProfile.tsx
+++ b/frontend/src/components/bazarr/LanguageProfile.tsx
@@ -1,5 +1,5 @@
-import { useLanguageProfiles } from "@/apis/hooks";
import { FunctionComponent, useMemo } from "react";
+import { useLanguageProfiles } from "@/apis/hooks";
interface Props {
index: number | null;
diff --git a/frontend/src/components/bazarr/LanguageSelector.tsx b/frontend/src/components/bazarr/LanguageSelector.tsx
index c2219ca7c..8954403bd 100644
--- a/frontend/src/components/bazarr/LanguageSelector.tsx
+++ b/frontend/src/components/bazarr/LanguageSelector.tsx
@@ -1,7 +1,7 @@
+import { FunctionComponent, useMemo } from "react";
import { useLanguages } from "@/apis/hooks";
import { Selector, SelectorProps } from "@/components/inputs";
import { useSelectorOptions } from "@/utilities";
-import { FunctionComponent, useMemo } from "react";
interface LanguageSelectorProps
extends Omit<SelectorProps<Language.Server>, "options" | "getkey"> {
diff --git a/frontend/src/components/forms/ColorToolForm.tsx b/frontend/src/components/forms/ColorToolForm.tsx
index a37819bee..9deac9bf4 100644
--- a/frontend/src/components/forms/ColorToolForm.tsx
+++ b/frontend/src/components/forms/ColorToolForm.tsx
@@ -1,11 +1,11 @@
+import { FunctionComponent } from "react";
+import { Button, Divider, Stack } from "@mantine/core";
+import { useForm } from "@mantine/form";
import { useSubtitleAction } from "@/apis/hooks";
import { Selector, SelectorOption } from "@/components";
import { useModals, withModal } from "@/modules/modals";
import { task } from "@/modules/task";
import FormUtils from "@/utilities/form";
-import { Button, Divider, Stack } from "@mantine/core";
-import { useForm } from "@mantine/form";
-import { FunctionComponent } from "react";
const TaskName = "Changing Color";
diff --git a/frontend/src/components/forms/FrameRateForm.tsx b/frontend/src/components/forms/FrameRateForm.tsx
index 7e7eca24c..7c57daf28 100644
--- a/frontend/src/components/forms/FrameRateForm.tsx
+++ b/frontend/src/components/forms/FrameRateForm.tsx
@@ -1,10 +1,10 @@
+import { FunctionComponent } from "react";
+import { Button, Divider, Group, NumberInput, Stack } from "@mantine/core";
+import { useForm } from "@mantine/form";
import { useSubtitleAction } from "@/apis/hooks";
import { useModals, withModal } from "@/modules/modals";
import { task } from "@/modules/task";
import FormUtils from "@/utilities/form";
-import { Button, Divider, Group, NumberInput, Stack } from "@mantine/core";
-import { useForm } from "@mantine/form";
-import { FunctionComponent } from "react";
const TaskName = "Changing Frame Rate";
@@ -55,15 +55,17 @@ const FrameRateForm: FunctionComponent<Props> = ({ selections, onSubmit }) => {
})}
>
<Stack>
- <Group spacing="xs" grow>
+ <Group gap="xs" grow>
<NumberInput
placeholder="From"
- precision={2}
+ decimalScale={2}
+ fixedDecimalScale
{...form.getInputProps("from")}
></NumberInput>
<NumberInput
placeholder="To"
- precision={2}
+ decimalScale={2}
+ fixedDecimalScale
{...form.getInputProps("to")}
></NumberInput>
</Group>
diff --git a/frontend/src/components/forms/ItemEditForm.tsx b/frontend/src/components/forms/ItemEditForm.tsx
index 9f3856d54..392338500 100644
--- a/frontend/src/components/forms/ItemEditForm.tsx
+++ b/frontend/src/components/forms/ItemEditForm.tsx
@@ -1,11 +1,11 @@
+import { FunctionComponent, useMemo } from "react";
+import { Button, Divider, Group, LoadingOverlay, Stack } from "@mantine/core";
+import { useForm } from "@mantine/form";
+import { UseMutationResult } from "@tanstack/react-query";
import { useLanguageProfiles } from "@/apis/hooks";
import { MultiSelector, Selector } from "@/components/inputs";
import { useModals, withModal } from "@/modules/modals";
import { GetItemId, useSelectorOptions } from "@/utilities";
-import { Button, Divider, Group, LoadingOverlay, Stack } from "@mantine/core";
-import { useForm } from "@mantine/form";
-import { FunctionComponent, useMemo } from "react";
-import { UseMutationResult } from "react-query";
interface Props {
mutation: UseMutationResult<void, unknown, FormType.ModifyItem, unknown>;
@@ -21,7 +21,7 @@ const ItemEditForm: FunctionComponent<Props> = ({
onCancel,
}) => {
const { data, isFetching } = useLanguageProfiles();
- const { isLoading, mutate } = mutation;
+ const { isPending, mutate } = mutation;
const modals = useModals();
const profileOptions = useSelectorOptions(
@@ -47,7 +47,7 @@ const ItemEditForm: FunctionComponent<Props> = ({
(v) => v.code2,
);
- const isOverlayVisible = isLoading || isFetching || item === null;
+ const isOverlayVisible = isPending || isFetching || item === null;
return (
<form
@@ -80,7 +80,7 @@ const ItemEditForm: FunctionComponent<Props> = ({
label="Languages Profile"
></Selector>
<Divider></Divider>
- <Group position="right">
+ <Group justify="right">
<Button
disabled={isOverlayVisible}
onClick={() => {
diff --git a/frontend/src/components/forms/MovieUploadForm.tsx b/frontend/src/components/forms/MovieUploadForm.tsx
index b51614770..8e318d7ad 100644
--- a/frontend/src/components/forms/MovieUploadForm.tsx
+++ b/frontend/src/components/forms/MovieUploadForm.tsx
@@ -1,37 +1,35 @@
-import { useMovieSubtitleModification } from "@/apis/hooks";
-import { useModals, withModal } from "@/modules/modals";
-import { TaskGroup, task } from "@/modules/task";
-import { useTableStyles } from "@/styles";
-import { useArrayAction, useSelectorOptions } from "@/utilities";
-import FormUtils from "@/utilities/form";
-import {
- useLanguageProfileBy,
- useProfileItemsToLanguages,
-} from "@/utilities/languages";
-import {
- faCheck,
- faCircleNotch,
- faInfoCircle,
- faTimes,
- faTrash,
-} from "@fortawesome/free-solid-svg-icons";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { FunctionComponent, useEffect, useMemo } from "react";
import {
Button,
Checkbox,
- createStyles,
Divider,
MantineColor,
Stack,
Text,
} from "@mantine/core";
import { useForm } from "@mantine/form";
+import {
+ faCheck,
+ faCircleNotch,
+ faInfoCircle,
+ faTimes,
+ faTrash,
+} from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { ColumnDef } from "@tanstack/react-table";
import { isString } from "lodash";
-import { FunctionComponent, useEffect, useMemo } from "react";
-import { Column } from "react-table";
-import TextPopover from "../TextPopover";
-import { Action, Selector } from "../inputs";
-import { SimpleTable } from "../tables";
+import { useMovieSubtitleModification } from "@/apis/hooks";
+import { Action, Selector } from "@/components/inputs";
+import SimpleTable from "@/components/tables/SimpleTable";
+import TextPopover from "@/components/TextPopover";
+import { useModals, withModal } from "@/modules/modals";
+import { task, TaskGroup } from "@/modules/task";
+import { useArrayAction, useSelectorOptions } from "@/utilities";
+import FormUtils from "@/utilities/form";
+import {
+ useLanguageProfileBy,
+ useProfileItemsToLanguages,
+} from "@/utilities/languages";
type SubtitleFile = {
file: File;
@@ -79,21 +77,12 @@ interface Props {
onComplete?: () => void;
}
-const useStyles = createStyles((theme) => {
- return {
- wrapper: {
- overflowWrap: "anywhere",
- },
- };
-});
-
const MovieUploadForm: FunctionComponent<Props> = ({
files,
movie,
onComplete,
}) => {
const modals = useModals();
- const { classes } = useStyles();
const profile = useLanguageProfileBy(movie.profileId);
@@ -154,63 +143,77 @@ const MovieUploadForm: FunctionComponent<Props> = ({
});
});
- const columns = useMemo<Column<SubtitleFile>[]>(
- () => [
- {
- accessor: "validateResult",
- Cell: ({ cell: { value } }) => {
- const icon = useMemo(() => {
- switch (value?.state) {
- case "valid":
- return faCheck;
- case "warning":
- return faInfoCircle;
- case "error":
- return faTimes;
- default:
- return faCircleNotch;
- }
- }, [value?.state]);
+ const ValidateResultCell = ({
+ validateResult,
+ }: {
+ validateResult: SubtitleValidateResult | undefined;
+ }) => {
+ const icon = useMemo(() => {
+ switch (validateResult?.state) {
+ case "valid":
+ return faCheck;
+ case "warning":
+ return faInfoCircle;
+ case "error":
+ return faTimes;
+ default:
+ return faCircleNotch;
+ }
+ }, [validateResult?.state]);
- const color = useMemo<MantineColor | undefined>(() => {
- switch (value?.state) {
- case "valid":
- return "green";
- case "warning":
- return "yellow";
- case "error":
- return "red";
- default:
- return undefined;
- }
- }, [value?.state]);
+ const color = useMemo<MantineColor | undefined>(() => {
+ switch (validateResult?.state) {
+ case "valid":
+ return "green";
+ case "warning":
+ return "yellow";
+ case "error":
+ return "red";
+ default:
+ return undefined;
+ }
+ }, [validateResult?.state]);
- return (
- <TextPopover text={value?.messages}>
- <Text color={color} inline>
- <FontAwesomeIcon icon={icon}></FontAwesomeIcon>
- </Text>
- </TextPopover>
- );
+ return (
+ <TextPopover text={validateResult?.messages}>
+ <Text c={color} inline>
+ <FontAwesomeIcon icon={icon} />
+ </Text>
+ </TextPopover>
+ );
+ };
+
+ const columns = useMemo<ColumnDef<SubtitleFile>[]>(
+ () => [
+ {
+ id: "validateResult",
+ cell: ({
+ row: {
+ original: { validateResult },
+ },
+ }) => {
+ return <ValidateResultCell validateResult={validateResult} />;
},
},
{
- Header: "File",
+ header: "File",
id: "filename",
- accessor: "file",
- Cell: ({ value }) => {
- const { classes } = useTableStyles();
-
- return <Text className={classes.primary}>{value.name}</Text>;
+ accessorKey: "file",
+ cell: ({
+ row: {
+ original: { file },
+ },
+ }) => {
+ return <Text className="table-primary">{file.name}</Text>;
},
},
{
- Header: "Forced",
- accessor: "forced",
- Cell: ({ row: { original, index }, value }) => {
+ header: "Forced",
+ accessorKey: "forced",
+ cell: ({ row: { original, index } }) => {
return (
<Checkbox
- checked={value}
+ checked={original.forced}
onChange={({ currentTarget: { checked } }) => {
action.mutate(index, { ...original, forced: checked });
}}
@@ -219,12 +222,12 @@ const MovieUploadForm: FunctionComponent<Props> = ({
},
},
{
- Header: "HI",
- accessor: "hi",
- Cell: ({ row: { original, index }, value }) => {
+ header: "HI",
+ accessorKey: "hi",
+ cell: ({ row: { original, index } }) => {
return (
<Checkbox
- checked={value}
+ checked={original.hi}
onChange={({ currentTarget: { checked } }) => {
action.mutate(index, { ...original, hi: checked });
}}
@@ -233,15 +236,14 @@ const MovieUploadForm: FunctionComponent<Props> = ({
},
},
{
- Header: "Language",
- accessor: "language",
- Cell: ({ row: { original, index }, value }) => {
- const { classes } = useTableStyles();
+ header: "Language",
+ accessorKey: "language",
+ cell: ({ row: { original, index } }) => {
return (
<Selector
{...languageOptions}
- className={classes.select}
- value={value}
+ className="table-long-break"
+ value={original.language}
onChange={(item) => {
action.mutate(index, { ...original, language: item });
}}
@@ -251,13 +253,12 @@ const MovieUploadForm: FunctionComponent<Props> = ({
},
{
id: "action",
- accessor: "file",
- Cell: ({ row: { index } }) => {
+ cell: ({ row: { index } }) => {
return (
<Action
label="Remove"
icon={faTrash}
- color="red"
+ c="red"
onClick={() => action.remove(index)}
></Action>
);
@@ -289,7 +290,7 @@ const MovieUploadForm: FunctionComponent<Props> = ({
modals.closeSelf();
})}
>
- <Stack className={classes.wrapper}>
+ <Stack className="table-long-break">
<SimpleTable columns={columns} data={form.values.files}></SimpleTable>
<Divider></Divider>
<Button type="submit">Upload</Button>
diff --git a/frontend/src/components/forms/ProfileEditForm.module.scss b/frontend/src/components/forms/ProfileEditForm.module.scss
new file mode 100644
index 000000000..3d4a8e177
--- /dev/null
+++ b/frontend/src/components/forms/ProfileEditForm.module.scss
@@ -0,0 +1,13 @@
+.content {
+ @include smaller-than($mantine-breakpoint-md) {
+ padding: 0;
+ }
+}
+
+.evenly {
+ flex-wrap: wrap;
+
+ & > div {
+ flex: 1;
+ }
+}
diff --git a/frontend/src/components/forms/ProfileEditForm.tsx b/frontend/src/components/forms/ProfileEditForm.tsx
index eecacd73e..267951fcb 100644
--- a/frontend/src/components/forms/ProfileEditForm.tsx
+++ b/frontend/src/components/forms/ProfileEditForm.tsx
@@ -1,14 +1,9 @@
-import { Action, Selector, SelectorOption, SimpleTable } from "@/components";
-import { useModals, withModal } from "@/modules/modals";
-import { useTableStyles } from "@/styles";
-import { useArrayAction, useSelectorOptions } from "@/utilities";
-import { LOG } from "@/utilities/console";
-import FormUtils from "@/utilities/form";
-import { faTrash } from "@fortawesome/free-solid-svg-icons";
+import React, { FunctionComponent, useCallback, useMemo } from "react";
import {
Accordion,
Button,
Checkbox,
+ Flex,
Select,
Stack,
Switch,
@@ -16,9 +11,16 @@ import {
TextInput,
} from "@mantine/core";
import { useForm } from "@mantine/form";
-import { FunctionComponent, useCallback, useMemo } from "react";
-import { Column } from "react-table";
-import ChipInput from "../inputs/ChipInput";
+import { faTrash } from "@fortawesome/free-solid-svg-icons";
+import { ColumnDef } from "@tanstack/react-table";
+import { Action, Selector, SelectorOption } from "@/components";
+import ChipInput from "@/components/inputs/ChipInput";
+import SimpleTable from "@/components/tables/SimpleTable";
+import { useModals, withModal } from "@/modules/modals";
+import { useArrayAction, useSelectorOptions } from "@/utilities";
+import { LOG } from "@/utilities/console";
+import FormUtils from "@/utilities/form";
+import styles from "./ProfileEditForm.module.scss";
export const anyCutoff = 65535;
@@ -71,9 +73,16 @@ const ProfileEditForm: FunctionComponent<Props> = ({
(value) => value.length > 0,
"Must have a name",
),
+ tag: FormUtils.validation((value) => {
+ if (!value) {
+ return true;
+ }
+
+ return /^[a-z_0-9-]+$/.test(value);
+ }, "Only lowercase alphanumeric characters, underscores (_) and hyphens (-) are allowed"),
items: FormUtils.validation(
(value) => value.length > 0,
- "Must contain at lease 1 language",
+ "Must contain at least 1 language",
),
},
});
@@ -145,78 +154,88 @@ const ProfileEditForm: FunctionComponent<Props> = ({
}
}, [form, languages]);
- const columns = useMemo<Column<Language.ProfileItem>[]>(
+ const LanguageCell = React.memo(
+ ({ item, index }: { item: Language.ProfileItem; index: number }) => {
+ const code = useMemo(
+ () =>
+ languageOptions.options.find((l) => l.value.code2 === item.language)
+ ?.value ?? null,
+ [item.language],
+ );
+
+ return (
+ <Selector
+ {...languageOptions}
+ className="table-select"
+ value={code}
+ onChange={(value) => {
+ if (value) {
+ item.language = value.code2;
+ action.mutate(index, { ...item, language: value.code2 });
+ }
+ }}
+ ></Selector>
+ );
+ },
+ );
+
+ const SubtitleTypeCell = React.memo(
+ ({ item, index }: { item: Language.ProfileItem; index: number }) => {
+ const selectValue = useMemo(() => {
+ if (item.forced === "True") {
+ return "forced";
+ } else if (item.hi === "True") {
+ return "hi";
+ } else {
+ return "normal";
+ }
+ }, [item.forced, item.hi]);
+
+ return (
+ <Select
+ value={selectValue}
+ data={subtitlesTypeOptions}
+ onChange={(value) => {
+ if (value) {
+ action.mutate(index, {
+ ...item,
+ hi: value === "hi" ? "True" : "False",
+ forced: value === "forced" ? "True" : "False",
+ });
+ }
+ }}
+ ></Select>
+ );
+ },
+ );
+
+ const columns = useMemo<ColumnDef<Language.ProfileItem>[]>(
() => [
{
- Header: "ID",
- accessor: "id",
+ header: "ID",
+ accessorKey: "id",
},
{
- Header: "Language",
- accessor: "language",
- Cell: ({ value: code, row: { original: item, index } }) => {
- const language = useMemo(
- () =>
- languageOptions.options.find((l) => l.value.code2 === code)
- ?.value ?? null,
- [code],
- );
-
- const { classes } = useTableStyles();
-
- return (
- <Selector
- {...languageOptions}
- className={classes.select}
- value={language}
- onChange={(value) => {
- if (value) {
- item.language = value.code2;
- action.mutate(index, { ...item, language: value.code2 });
- }
- }}
- ></Selector>
- );
+ header: "Language",
+ accessorKey: "language",
+ cell: ({ row: { original: item, index } }) => {
+ return <LanguageCell item={item} index={index} />;
},
},
{
- Header: "Subtitles Type",
- accessor: "forced",
- Cell: ({ row: { original: item, index }, value }) => {
- const selectValue = useMemo(() => {
- if (item.forced === "True") {
- return "forced";
- } else if (item.hi === "True") {
- return "hi";
- } else {
- return "normal";
- }
- }, [item.forced, item.hi]);
-
- return (
- <Select
- value={selectValue}
- data={subtitlesTypeOptions}
- onChange={(value) => {
- if (value) {
- action.mutate(index, {
- ...item,
- hi: value === "hi" ? "True" : "False",
- forced: value === "forced" ? "True" : "False",
- });
- }
- }}
- ></Select>
- );
+ header: "Subtitles Type",
+ accessorKey: "forced",
+ cell: ({ row: { original: item, index } }) => {
+ return <SubtitleTypeCell item={item} index={index} />;
},
},
{
- Header: "Exclude If Matching Audio",
- accessor: "audio_exclude",
- Cell: ({ row: { original: item, index }, value }) => {
+ header: "Exclude If Matching Audio",
+ accessorKey: "audio_exclude",
+ cell: ({ row: { original: item, index } }) => {
return (
<Checkbox
- checked={value === "True"}
+ checked={item.audio_exclude === "True"}
onChange={({ currentTarget: { checked } }) => {
action.mutate(index, {
...item,
@@ -230,20 +249,19 @@ const ProfileEditForm: FunctionComponent<Props> = ({
},
{
id: "action",
- accessor: "id",
- Cell: ({ row }) => {
+ cell: ({ row }) => {
return (
<Action
label="Remove"
icon={faTrash}
- color="red"
+ c="red"
onClick={() => action.remove(row.index)}
></Action>
);
},
},
],
- [action, languageOptions],
+ [action, LanguageCell, SubtitleTypeCell],
);
return (
@@ -255,29 +273,40 @@ const ProfileEditForm: FunctionComponent<Props> = ({
})}
>
<Stack>
- <TextInput label="Name" {...form.getInputProps("name")}></TextInput>
+ <Flex
+ direction={{ base: "column", sm: "row" }}
+ gap="sm"
+ className={styles.evenly}
+ >
+ <TextInput label="Name" {...form.getInputProps("name")}></TextInput>
+ <TextInput
+ label="Tag"
+ {...form.getInputProps("tag")}
+ onBlur={() =>
+ form.setFieldValue(
+ "tag",
+ (prev) =>
+ prev?.toLowerCase().trim().replace(/\s+/g, "_") ?? undefined,
+ )
+ }
+ ></TextInput>
+ </Flex>
<Accordion
multiple
chevronPosition="right"
defaultValue={["Languages"]}
- styles={(theme) => ({
- content: {
- [theme.fn.smallerThan("md")]: {
- padding: 0,
- },
- },
- })}
+ className={styles.content}
>
<Accordion.Item value="Languages">
<Stack>
- {form.errors.items}
<SimpleTable
columns={columns}
data={form.values.items}
></SimpleTable>
- <Button fullWidth color="light" onClick={addItem}>
+ <Button fullWidth onClick={addItem}>
Add Language
</Button>
+ <Text c="var(--mantine-color-error)">{form.errors.items}</Text>
<Selector
clearable
label="Cutoff"
diff --git a/frontend/src/components/forms/SeriesUploadForm.tsx b/frontend/src/components/forms/SeriesUploadForm.tsx
index 5ce9c821a..e4482cab4 100644
--- a/frontend/src/components/forms/SeriesUploadForm.tsx
+++ b/frontend/src/components/forms/SeriesUploadForm.tsx
@@ -1,41 +1,39 @@
+import { FunctionComponent, useEffect, useMemo } from "react";
+import {
+ Button,
+ Checkbox,
+ Divider,
+ MantineColor,
+ Stack,
+ Text,
+} from "@mantine/core";
+import { useForm } from "@mantine/form";
+import {
+ faCheck,
+ faCircleNotch,
+ faInfoCircle,
+ faTimes,
+ faTrash,
+} from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { ColumnDef } from "@tanstack/react-table";
+import { isString } from "lodash";
import {
useEpisodesBySeriesId,
useEpisodeSubtitleModification,
useSubtitleInfos,
} from "@/apis/hooks";
+import { Action, Selector } from "@/components/inputs";
+import SimpleTable from "@/components/tables/SimpleTable";
+import TextPopover from "@/components/TextPopover";
import { useModals, withModal } from "@/modules/modals";
import { task, TaskGroup } from "@/modules/task";
-import { useTableStyles } from "@/styles";
import { useArrayAction, useSelectorOptions } from "@/utilities";
import FormUtils from "@/utilities/form";
import {
useLanguageProfileBy,
useProfileItemsToLanguages,
} from "@/utilities/languages";
-import {
- faCheck,
- faCircleNotch,
- faInfoCircle,
- faTimes,
- faTrash,
-} from "@fortawesome/free-solid-svg-icons";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import {
- Button,
- Checkbox,
- createStyles,
- Divider,
- MantineColor,
- Stack,
- Text,
-} from "@mantine/core";
-import { useForm } from "@mantine/form";
-import { isString } from "lodash";
-import { FunctionComponent, useEffect, useMemo } from "react";
-import { Column } from "react-table";
-import { Action, Selector } from "../inputs";
-import { SimpleTable } from "../tables";
-import TextPopover from "../TextPopover";
type SubtitleFile = {
file: File;
@@ -86,21 +84,12 @@ interface Props {
onComplete?: VoidFunction;
}
-const useStyles = createStyles((theme) => {
- return {
- wrapper: {
- overflowWrap: "anywhere",
- },
- };
-});
-
const SeriesUploadForm: FunctionComponent<Props> = ({
series,
files,
onComplete,
}) => {
const modals = useModals();
- const { classes } = useStyles();
const episodes = useEpisodesBySeriesId(series.sonarrSeriesId);
const episodeOptions = useSelectorOptions(
episodes.data ?? [],
@@ -180,62 +169,79 @@ const SeriesUploadForm: FunctionComponent<Props> = ({
}
}, [action, episodes.data, infos.data]);
- const columns = useMemo<Column<SubtitleFile>[]>(
- () => [
- {
- accessor: "validateResult",
- Cell: ({ cell: { value } }) => {
- const icon = useMemo(() => {
- switch (value?.state) {
- case "valid":
- return faCheck;
- case "warning":
- return faInfoCircle;
- case "error":
- return faTimes;
- default:
- return faCircleNotch;
- }
- }, [value?.state]);
+ const ValidateResultCell = ({
+ validateResult,
+ }: {
+ validateResult: SubtitleValidateResult | undefined;
+ }) => {
+ const icon = useMemo(() => {
+ switch (validateResult?.state) {
+ case "valid":
+ return faCheck;
+ case "warning":
+ return faInfoCircle;
+ case "error":
+ return faTimes;
+ default:
+ return faCircleNotch;
+ }
+ }, [validateResult?.state]);
- const color = useMemo<MantineColor | undefined>(() => {
- switch (value?.state) {
- case "valid":
- return "green";
- case "warning":
- return "yellow";
- case "error":
- return "red";
- default:
- return undefined;
- }
- }, [value?.state]);
+ const color = useMemo<MantineColor | undefined>(() => {
+ switch (validateResult?.state) {
+ case "valid":
+ return "green";
+ case "warning":
+ return "yellow";
+ case "error":
+ return "red";
+ default:
+ return undefined;
+ }
+ }, [validateResult?.state]);
- return (
- <TextPopover text={value?.messages}>
- <Text color={color} inline>
- <FontAwesomeIcon icon={icon}></FontAwesomeIcon>
- </Text>
- </TextPopover>
- );
+ return (
+ <TextPopover text={validateResult?.messages}>
+ <Text c={color} inline>
+ <FontAwesomeIcon icon={icon}></FontAwesomeIcon>
+ </Text>
+ </TextPopover>
+ );
+ };
+
+ const columns = useMemo<ColumnDef<SubtitleFile>[]>(
+ () => [
+ {
+ id: "validateResult",
+ cell: ({
+ row: {
+ original: { validateResult },
+ },
+ }) => {
+ return <ValidateResultCell validateResult={validateResult} />;
},
},
{
- Header: "File",
+ header: "File",
id: "filename",
- accessor: "file",
- Cell: ({ value: { name } }) => {
- const { classes } = useTableStyles();
- return <Text className={classes.primary}>{name}</Text>;
+ accessorKey: "file",
+ cell: ({
+ row: {
+ original: {
+ file: { name },
+ },
+ },
+ }) => {
+ return <Text className="table-primary">{name}</Text>;
},
},
{
- Header: "Forced",
- accessor: "forced",
- Cell: ({ row: { original, index }, value }) => {
+ header: "Forced",
+ accessorKey: "forced",
+ cell: ({ row: { original, index } }) => {
return (
<Checkbox
- checked={value}
+ checked={original.forced}
onChange={({ currentTarget: { checked } }) => {
action.mutate(index, {
...original,
@@ -248,12 +254,12 @@ const SeriesUploadForm: FunctionComponent<Props> = ({
},
},
{
- Header: "HI",
- accessor: "hi",
- Cell: ({ row: { original, index }, value }) => {
+ header: "HI",
+ accessorKey: "hi",
+ cell: ({ row: { original, index } }) => {
return (
<Checkbox
- checked={value}
+ checked={original.hi}
onChange={({ currentTarget: { checked } }) => {
action.mutate(index, {
...original,
@@ -266,7 +272,7 @@ const SeriesUploadForm: FunctionComponent<Props> = ({
},
},
{
- Header: (
+ header: () => (
<Selector
{...languageOptions}
value={null}
@@ -281,14 +287,13 @@ const SeriesUploadForm: FunctionComponent<Props> = ({
}}
></Selector>
),
- accessor: "language",
- Cell: ({ row: { original, index }, value }) => {
- const { classes } = useTableStyles();
+ accessorKey: "language",
+ cell: ({ row: { original, index } }) => {
return (
<Selector
{...languageOptions}
- className={classes.select}
- value={value}
+ className="table-select"
+ value={original.language}
onChange={(item) => {
action.mutate(index, { ...original, language: item });
}}
@@ -298,18 +303,17 @@ const SeriesUploadForm: FunctionComponent<Props> = ({
},
{
id: "episode",
- Header: "Episode",
- accessor: "episode",
- Cell: ({ value, row }) => {
- const { classes } = useTableStyles();
+ header: "Episode",
+ accessorKey: "episode",
+ cell: ({ row: { original, index } }) => {
return (
<Selector
{...episodeOptions}
searchable
- className={classes.select}
- value={value}
+ className="table-select"
+ value={original.episode}
onChange={(item) => {
- action.mutate(row.index, { ...row.original, episode: item });
+ action.mutate(index, { ...original, episode: item });
}}
></Selector>
);
@@ -317,13 +321,12 @@ const SeriesUploadForm: FunctionComponent<Props> = ({
},
{
id: "action",
- accessor: "file",
- Cell: ({ row: { index } }) => {
+ cell: ({ row: { index } }) => {
return (
<Action
label="Remove"
icon={faTrash}
- color="red"
+ c="red"
onClick={() => action.remove(index)}
></Action>
);
@@ -368,7 +371,7 @@ const SeriesUploadForm: FunctionComponent<Props> = ({
modals.closeSelf();
})}
>
- <Stack className={classes.wrapper}>
+ <Stack className="table-long-break">
<SimpleTable columns={columns} data={form.values.files}></SimpleTable>
<Divider></Divider>
<Button type="submit">Upload</Button>
diff --git a/frontend/src/components/forms/SyncSubtitleForm.tsx b/frontend/src/components/forms/SyncSubtitleForm.tsx
index b5136fc85..63953fb2d 100644
--- a/frontend/src/components/forms/SyncSubtitleForm.tsx
+++ b/frontend/src/components/forms/SyncSubtitleForm.tsx
@@ -1,20 +1,23 @@
/* eslint-disable camelcase */
-
+import { FunctionComponent } from "react";
+import { Alert, Button, Checkbox, Divider, Stack, Text } from "@mantine/core";
+import { useForm } from "@mantine/form";
+import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
useRefTracksByEpisodeId,
useRefTracksByMovieId,
useSubtitleAction,
} from "@/apis/hooks";
+import {
+ GroupedSelector,
+ GroupedSelectorOptions,
+ Selector,
+} from "@/components/inputs";
import { useModals, withModal } from "@/modules/modals";
import { task } from "@/modules/task";
import { syncMaxOffsetSecondsOptions } from "@/pages/Settings/Subtitles/options";
-import { toPython } from "@/utilities";
-import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { Alert, Button, Checkbox, Divider, Stack, Text } from "@mantine/core";
-import { useForm } from "@mantine/form";
-import { FunctionComponent } from "react";
-import { Selector, SelectorOption } from "../inputs";
+import { fromPython, toPython } from "@/utilities";
const TaskName = "Syncing Subtitle";
@@ -37,15 +40,21 @@ function useReferencedSubtitles(
const mediaData = mediaType === "episode" ? episodeData : movieData;
- const subtitles: { group: string; value: string; label: string }[] = [];
+ const subtitles: GroupedSelectorOptions<string>[] = [];
if (!mediaData.data) {
return [];
} else {
if (mediaData.data.audio_tracks.length > 0) {
+ const embeddedAudioGroup: GroupedSelectorOptions<string> = {
+ group: "Embedded audio tracks",
+ items: [],
+ };
+
+ subtitles.push(embeddedAudioGroup);
+
mediaData.data.audio_tracks.forEach((item) => {
- subtitles.push({
- group: "Embedded audio tracks",
+ embeddedAudioGroup.items.push({
value: item.stream,
label: `${item.name || item.language} (${item.stream})`,
});
@@ -53,9 +62,15 @@ function useReferencedSubtitles(
}
if (mediaData.data.embedded_subtitles_tracks.length > 0) {
+ const embeddedSubtitlesTrackGroup: GroupedSelectorOptions<string> = {
+ group: "Embedded subtitles tracks",
+ items: [],
+ };
+
+ subtitles.push(embeddedSubtitlesTrackGroup);
+
mediaData.data.embedded_subtitles_tracks.forEach((item) => {
- subtitles.push({
- group: "Embedded subtitles tracks",
+ embeddedSubtitlesTrackGroup.items.push({
value: item.stream,
label: `${item.name || item.language} (${item.stream})`,
});
@@ -63,10 +78,16 @@ function useReferencedSubtitles(
}
if (mediaData.data.external_subtitles_tracks.length > 0) {
+ const externalSubtitlesFilesGroup: GroupedSelectorOptions<string> = {
+ group: "External Subtitles files",
+ items: [],
+ };
+
+ subtitles.push(externalSubtitlesFilesGroup);
+
mediaData.data.external_subtitles_tracks.forEach((item) => {
if (item) {
- subtitles.push({
- group: "External Subtitles files",
+ externalSubtitlesFilesGroup.items.push({
value: item.path,
label: item.name,
});
@@ -88,6 +109,8 @@ interface FormValues {
maxOffsetSeconds?: string;
noFixFramerate: boolean;
gss: boolean;
+ hi?: boolean;
+ forced?: boolean;
}
const SyncSubtitleForm: FunctionComponent<Props> = ({
@@ -101,20 +124,20 @@ const SyncSubtitleForm: FunctionComponent<Props> = ({
const { mutateAsync } = useSubtitleAction();
const modals = useModals();
- const mediaType = selections[0].type;
- const mediaId = selections[0].id;
- const subtitlesPath = selections[0].path;
+ const subtitle = selections[0];
- const subtitles: SelectorOption<string>[] = useReferencedSubtitles(
- mediaType,
- mediaId,
- subtitlesPath,
- );
+ const mediaType = subtitle.type;
+ const mediaId = subtitle.id;
+ const subtitlesPath = subtitle.path;
+
+ const subtitles = useReferencedSubtitles(mediaType, mediaId, subtitlesPath);
const form = useForm<FormValues>({
initialValues: {
noFixFramerate: false,
gss: false,
+ hi: fromPython(subtitle.hi),
+ forced: fromPython(subtitle.forced),
},
});
@@ -145,14 +168,14 @@ const SyncSubtitleForm: FunctionComponent<Props> = ({
>
<Text size="sm">{selections.length} subtitles selected</Text>
</Alert>
- <Selector
+ <GroupedSelector
clearable
disabled={subtitles.length === 0 || selections.length !== 1}
label="Reference"
placeholder="Default: choose automatically within video file"
options={subtitles}
{...form.getInputProps("reference")}
- ></Selector>
+ ></GroupedSelector>
<Selector
clearable
label="Max Offset Seconds"
diff --git a/frontend/src/components/forms/TimeOffsetForm.tsx b/frontend/src/components/forms/TimeOffsetForm.tsx
index 2792d64d8..1a7739dd9 100644
--- a/frontend/src/components/forms/TimeOffsetForm.tsx
+++ b/frontend/src/components/forms/TimeOffsetForm.tsx
@@ -1,12 +1,12 @@
+import { FunctionComponent } from "react";
+import { Button, Divider, Group, NumberInput, Stack } from "@mantine/core";
+import { useForm } from "@mantine/form";
+import { faMinus, faPlus } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useSubtitleAction } from "@/apis/hooks";
import { useModals, withModal } from "@/modules/modals";
import { task } from "@/modules/task";
import FormUtils from "@/utilities/form";
-import { faMinus, faPlus } from "@fortawesome/free-solid-svg-icons";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { Button, Divider, Group, NumberInput, Stack } from "@mantine/core";
-import { useForm } from "@mantine/form";
-import { FunctionComponent } from "react";
const TaskName = "Changing Time";
@@ -70,10 +70,11 @@ const TimeOffsetForm: FunctionComponent<Props> = ({ selections, onSubmit }) => {
})}
>
<Stack>
- <Group align="end" spacing="xs" noWrap>
+ <Group align="end" gap="xs" wrap="nowrap">
<Button
color="gray"
variant="filled"
+ style={{ overflow: "visible" }}
onClick={() =>
form.setValues((f) => ({ ...f, positive: !f.positive }))
}
diff --git a/frontend/src/components/forms/TranslationForm.tsx b/frontend/src/components/forms/TranslationForm.tsx
index 976b2f72f..20aa08478 100644
--- a/frontend/src/components/forms/TranslationForm.tsx
+++ b/frontend/src/components/forms/TranslationForm.tsx
@@ -1,14 +1,14 @@
+import { FunctionComponent, useMemo } from "react";
+import { Alert, Button, Divider, Stack } from "@mantine/core";
+import { useForm } from "@mantine/form";
+import { isObject } from "lodash";
import { useSubtitleAction } from "@/apis/hooks";
+import { Selector } from "@/components/inputs";
import { useModals, withModal } from "@/modules/modals";
import { task } from "@/modules/task";
import { useSelectorOptions } from "@/utilities";
import FormUtils from "@/utilities/form";
import { useEnabledLanguages } from "@/utilities/languages";
-import { Alert, Button, Divider, Stack } from "@mantine/core";
-import { useForm } from "@mantine/form";
-import { isObject } from "lodash";
-import { FunctionComponent, useMemo } from "react";
-import { Selector } from "../inputs";
const TaskName = "Translating Subtitles";
diff --git a/frontend/src/components/index.tsx b/frontend/src/components/index.tsx
index c3d7b4763..5ea97cf04 100644
--- a/frontend/src/components/index.tsx
+++ b/frontend/src/components/index.tsx
@@ -1,4 +1,4 @@
export { default as Search } from "./Search";
export * from "./inputs";
export * from "./tables";
-export { default as Toolbox } from "./toolbox";
+export { default as Toolbox } from "./toolbox/Toolbox";
diff --git a/frontend/src/components/inputs/Action.test.tsx b/frontend/src/components/inputs/Action.test.tsx
index 189aca076..dc8972630 100644
--- a/frontend/src/components/inputs/Action.test.tsx
+++ b/frontend/src/components/inputs/Action.test.tsx
@@ -1,7 +1,7 @@
-import { rawRender, screen } from "@/tests";
import { faStickyNote } from "@fortawesome/free-regular-svg-icons";
import userEvent from "@testing-library/user-event";
import { describe, it, vitest } from "vitest";
+import { render, screen } from "@/tests";
import Action from "./Action";
const testLabel = "Test Label";
@@ -9,7 +9,7 @@ const testIcon = faStickyNote;
describe("Action button", () => {
it("should be a button", () => {
- rawRender(<Action icon={testIcon} label={testLabel}></Action>);
+ render(<Action icon={testIcon} label={testLabel}></Action>);
const element = screen.getByRole("button", { name: testLabel });
expect(element.getAttribute("type")).toEqual("button");
@@ -17,7 +17,7 @@ describe("Action button", () => {
});
it("should show icon", () => {
- rawRender(<Action icon={testIcon} label={testLabel}></Action>);
+ render(<Action icon={testIcon} label={testLabel}></Action>);
// TODO: use getBy...
const element = screen.getByRole("img", { hidden: true });
@@ -27,7 +27,7 @@ describe("Action button", () => {
it("should call on-click event when clicked", async () => {
const onClickFn = vitest.fn();
- rawRender(
+ render(
<Action icon={testIcon} label={testLabel} onClick={onClickFn}></Action>,
);
diff --git a/frontend/src/components/inputs/Action.tsx b/frontend/src/components/inputs/Action.tsx
index 236baf112..95477f090 100644
--- a/frontend/src/components/inputs/Action.tsx
+++ b/frontend/src/components/inputs/Action.tsx
@@ -1,15 +1,15 @@
-import { IconDefinition } from "@fortawesome/fontawesome-common-types";
-import {
- FontAwesomeIcon,
- FontAwesomeIconProps,
-} from "@fortawesome/react-fontawesome";
+import { forwardRef } from "react";
import {
ActionIcon,
ActionIconProps,
Tooltip,
TooltipProps,
} from "@mantine/core";
-import { forwardRef } from "react";
+import { IconDefinition } from "@fortawesome/fontawesome-common-types";
+import {
+ FontAwesomeIcon,
+ FontAwesomeIconProps,
+} from "@fortawesome/react-fontawesome";
export type ActionProps = MantineComp<ActionIconProps, "button"> & {
icon: IconDefinition;
diff --git a/frontend/src/components/inputs/ChipInput.test.tsx b/frontend/src/components/inputs/ChipInput.test.tsx
index cb52ee30c..4035966fc 100644
--- a/frontend/src/components/inputs/ChipInput.test.tsx
+++ b/frontend/src/components/inputs/ChipInput.test.tsx
@@ -1,6 +1,6 @@
-import { rawRender, screen } from "@/tests";
import userEvent from "@testing-library/user-event";
import { describe, it, vitest } from "vitest";
+import { render, screen } from "@/tests";
import ChipInput from "./ChipInput";
describe("ChipInput", () => {
@@ -8,7 +8,7 @@ describe("ChipInput", () => {
// TODO: Support default value
it.skip("should works with default value", () => {
- rawRender(<ChipInput defaultValue={existedValues}></ChipInput>);
+ render(<ChipInput defaultValue={existedValues}></ChipInput>);
existedValues.forEach((value) => {
expect(screen.getByText(value)).toBeDefined();
@@ -16,7 +16,7 @@ describe("ChipInput", () => {
});
it("should works with value", () => {
- rawRender(<ChipInput value={existedValues}></ChipInput>);
+ render(<ChipInput value={existedValues}></ChipInput>);
existedValues.forEach((value) => {
expect(screen.getByText(value)).toBeDefined();
@@ -29,9 +29,7 @@ describe("ChipInput", () => {
expect(values).toContain(typedValue);
});
- rawRender(
- <ChipInput value={existedValues} onChange={mockedFn}></ChipInput>,
- );
+ render(<ChipInput value={existedValues} onChange={mockedFn}></ChipInput>);
const element = screen.getByRole("searchbox");
diff --git a/frontend/src/components/inputs/ChipInput.tsx b/frontend/src/components/inputs/ChipInput.tsx
index 4308f7189..1fa57084c 100644
--- a/frontend/src/components/inputs/ChipInput.tsx
+++ b/frontend/src/components/inputs/ChipInput.tsx
@@ -1,35 +1,29 @@
-import { useSelectorOptions } from "@/utilities";
import { FunctionComponent } from "react";
-import { MultiSelector, MultiSelectorProps } from "./Selector";
+import { TagsInput } from "@mantine/core";
-export type ChipInputProps = Omit<
- MultiSelectorProps<string>,
- | "searchable"
- | "creatable"
- | "getCreateLabel"
- | "onCreate"
- | "options"
- | "getkey"
->;
-
-const ChipInput: FunctionComponent<ChipInputProps> = ({ ...props }) => {
- const { value, onChange } = props;
-
- const options = useSelectorOptions(value ?? [], (v) => v);
+export interface ChipInputProps {
+ defaultValue?: string[] | undefined;
+ value?: readonly string[] | null;
+ label?: string;
+ onChange?: (value: string[]) => void;
+}
+const ChipInput: FunctionComponent<ChipInputProps> = ({
+ defaultValue,
+ value,
+ label,
+ onChange,
+}: ChipInputProps) => {
+ // TODO: Replace with our own custom implementation instead of just using the
+ // built-in TagsInput. https://mantine.dev/combobox/?e=MultiSelectCreatable
return (
- <MultiSelector
- {...props}
- {...options}
- creatable
- searchable
- getCreateLabel={(query) => `Add "${query}"`}
- onCreate={(query) => {
- onChange?.([...(value ?? []), query]);
- return query;
- }}
- buildOption={(value) => value}
- ></MultiSelector>
+ <TagsInput
+ defaultValue={defaultValue}
+ label={label}
+ value={value ? value?.map((v) => v) : []}
+ onChange={onChange}
+ clearable
+ ></TagsInput>
);
};
diff --git a/frontend/src/components/inputs/DropContent.module.scss b/frontend/src/components/inputs/DropContent.module.scss
new file mode 100644
index 000000000..c6c0f848a
--- /dev/null
+++ b/frontend/src/components/inputs/DropContent.module.scss
@@ -0,0 +1,4 @@
+.container {
+ pointer-events: none;
+ min-height: 220px;
+}
diff --git a/frontend/src/components/inputs/DropContent.tsx b/frontend/src/components/inputs/DropContent.tsx
index 38556220d..c4bcf2877 100644
--- a/frontend/src/components/inputs/DropContent.tsx
+++ b/frontend/src/components/inputs/DropContent.tsx
@@ -1,27 +1,17 @@
+import { FunctionComponent } from "react";
+import { Group, Stack, Text } from "@mantine/core";
+import { Dropzone } from "@mantine/dropzone";
import {
faArrowUp,
faFileCirclePlus,
faXmark,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { Group, Stack, Text, createStyles } from "@mantine/core";
-import { Dropzone } from "@mantine/dropzone";
-import { FunctionComponent } from "react";
-
-const useStyle = createStyles((theme) => {
- return {
- container: {
- pointerEvents: "none",
- minHeight: 220,
- },
- };
-});
+import styles from "./DropContent.module.scss";
export const DropContent: FunctionComponent = () => {
- const { classes } = useStyle();
-
return (
- <Group position="center" spacing="xl" className={classes.container}>
+ <Group justify="center" gap="xl" className={styles.container}>
<Dropzone.Idle>
<FontAwesomeIcon icon={faFileCirclePlus} size="2x" />
</Dropzone.Idle>
@@ -31,9 +21,9 @@ export const DropContent: FunctionComponent = () => {
<Dropzone.Reject>
<FontAwesomeIcon icon={faXmark} size="2x" />
</Dropzone.Reject>
- <Stack spacing={0}>
+ <Stack gap={0}>
<Text size="lg">Upload Subtitles</Text>
- <Text color="dimmed" size="sm">
+ <Text c="dimmed" size="sm">
Attach as many files as you like, you will need to select file
metadata before uploading
</Text>
diff --git a/frontend/src/components/inputs/FileBrowser.tsx b/frontend/src/components/inputs/FileBrowser.tsx
index ce57a4938..bba66a66e 100644
--- a/frontend/src/components/inputs/FileBrowser.tsx
+++ b/frontend/src/components/inputs/FileBrowser.tsx
@@ -1,8 +1,13 @@
-import { useFileSystem } from "@/apis/hooks";
+import { FunctionComponent, useEffect, useMemo, useRef, useState } from "react";
+import {
+ Autocomplete,
+ AutocompleteProps,
+ ComboboxItem,
+ OptionsFilter,
+} from "@mantine/core";
import { faFolder } from "@fortawesome/free-regular-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { Autocomplete, AutocompleteProps } from "@mantine/core";
-import { FunctionComponent, useEffect, useMemo, useRef, useState } from "react";
+import { useFileSystem } from "@/apis/hooks";
// TODO: use fortawesome icons
const backKey = "⏎ Back";
@@ -75,24 +80,28 @@ export const FileBrowser: FunctionComponent<FileBrowserProps> = ({
const ref = useRef<HTMLInputElement>(null);
+ const optionsFilter: OptionsFilter = ({ options, search }) => {
+ return (options as ComboboxItem[]).filter((option) => {
+ if (search === backKey) {
+ return true;
+ }
+
+ return option.value.includes(search);
+ });
+ };
+
return (
<Autocomplete
{...props}
ref={ref}
- icon={<FontAwesomeIcon icon={faFolder}></FontAwesomeIcon>}
+ leftSection={<FontAwesomeIcon icon={faFolder}></FontAwesomeIcon>}
placeholder="Click to start"
data={data}
value={value}
// Temporary solution of infinite dropdown items, fix later
limit={NaN}
maxDropdownHeight={240}
- filter={(value, item) => {
- if (item.value === backKey) {
- return true;
- } else {
- return item.value.includes(value);
- }
- }}
+ filter={optionsFilter}
onChange={(val) => {
if (val !== backKey) {
setValue(val);
diff --git a/frontend/src/components/inputs/Selector.test.tsx b/frontend/src/components/inputs/Selector.test.tsx
index a7b6cfb85..a28772a2d 100644
--- a/frontend/src/components/inputs/Selector.test.tsx
+++ b/frontend/src/components/inputs/Selector.test.tsx
@@ -1,6 +1,6 @@
-import { rawRender, screen } from "@/tests";
import userEvent from "@testing-library/user-event";
import { describe, it, vitest } from "vitest";
+import { render, screen } from "@/tests";
import { Selector, SelectorOption } from "./Selector";
const selectorName = "Test Selections";
@@ -18,20 +18,17 @@ const testOptions: SelectorOption<string>[] = [
describe("Selector", () => {
describe("options", () => {
it("should work with the SelectorOption", () => {
- rawRender(
- <Selector name={selectorName} options={testOptions}></Selector>,
- );
+ render(<Selector name={selectorName} options={testOptions}></Selector>);
- // TODO: selectorName
- expect(screen.getByRole("searchbox")).toBeDefined();
+ testOptions.forEach((o) => {
+ expect(screen.getByText(o.label)).toBeDefined();
+ });
});
it("should display when clicked", async () => {
- rawRender(
- <Selector name={selectorName} options={testOptions}></Selector>,
- );
+ render(<Selector name={selectorName} options={testOptions}></Selector>);
- const element = screen.getByRole("searchbox");
+ const element = screen.getByTestId("input-selector");
await userEvent.click(element);
@@ -44,7 +41,7 @@ describe("Selector", () => {
it("shouldn't show default value", async () => {
const option = testOptions[0];
- rawRender(
+ render(
<Selector
name={selectorName}
options={testOptions}
@@ -57,7 +54,7 @@ describe("Selector", () => {
it("shouldn't show value", async () => {
const option = testOptions[0];
- rawRender(
+ render(
<Selector
name={selectorName}
options={testOptions}
@@ -75,7 +72,7 @@ describe("Selector", () => {
const mockedFn = vitest.fn((value: string | null) => {
expect(value).toEqual(clickedOption.value);
});
- rawRender(
+ render(
<Selector
name={selectorName}
options={testOptions}
@@ -83,13 +80,13 @@ describe("Selector", () => {
></Selector>,
);
- const element = screen.getByRole("searchbox");
+ const element = screen.getByTestId("input-selector");
await userEvent.click(element);
await userEvent.click(screen.getByText(clickedOption.label));
- expect(mockedFn).toBeCalled();
+ expect(mockedFn).toHaveBeenCalled();
});
});
@@ -115,7 +112,7 @@ describe("Selector", () => {
const mockedFn = vitest.fn((value: { name: string } | null) => {
expect(value).toEqual(clickedOption.value);
});
- rawRender(
+ render(
<Selector
name={selectorName}
options={objectOptions}
@@ -124,20 +121,20 @@ describe("Selector", () => {
></Selector>,
);
- const element = screen.getByRole("searchbox");
+ const element = screen.getByTestId("input-selector");
await userEvent.click(element);
await userEvent.click(screen.getByText(clickedOption.label));
- expect(mockedFn).toBeCalled();
+ expect(mockedFn).toHaveBeenCalled();
});
});
describe("placeholder", () => {
it("should show when no selection", () => {
const placeholder = "Empty Selection";
- rawRender(
+ render(
<Selector
name={selectorName}
options={testOptions}
diff --git a/frontend/src/components/inputs/Selector.tsx b/frontend/src/components/inputs/Selector.tsx
index 0af276fc4..092fd24e7 100644
--- a/frontend/src/components/inputs/Selector.tsx
+++ b/frontend/src/components/inputs/Selector.tsx
@@ -1,23 +1,24 @@
-import { LOG } from "@/utilities/console";
+import { useCallback, useMemo, useRef } from "react";
import {
+ ComboboxItem,
+ ComboboxItemGroup,
MultiSelect,
MultiSelectProps,
Select,
- SelectItem,
SelectProps,
} from "@mantine/core";
import { isNull, isUndefined } from "lodash";
-import { useCallback, useMemo, useRef } from "react";
+import { LOG } from "@/utilities/console";
export type SelectorOption<T> = Override<
{
value: T;
label: string;
},
- SelectItem
+ ComboboxItem
>;
-type SelectItemWithPayload<T> = SelectItem & {
+type SelectItemWithPayload<T> = ComboboxItem & {
payload: T;
};
@@ -34,6 +35,33 @@ function DefaultKeyBuilder<T>(value: T) {
}
}
+export interface GroupedSelectorOptions<T> {
+ group: string;
+ items: SelectorOption<T>[];
+}
+
+export type GroupedSelectorProps<T> = Override<
+ {
+ options: ComboboxItemGroup[];
+ getkey?: (value: T) => string;
+ },
+ Omit<SelectProps, "data">
+>;
+
+export function GroupedSelector<T>({
+ options,
+ ...select
+}: GroupedSelectorProps<T>) {
+ return (
+ <Select
+ data-testid="input-selector"
+ comboboxProps={{ withinPortal: true }}
+ data={options}
+ {...select}
+ ></Select>
+ );
+}
+
export type SelectorProps<T> = Override<
{
value?: T | null;
@@ -84,7 +112,7 @@ export function Selector<T>({
}, [defaultValue, keyRef]);
const wrappedOnChange = useCallback(
- (value: string) => {
+ (value: string | null) => {
const payload = data.find((v) => v.value === value)?.payload ?? null;
onChange?.(payload);
},
@@ -93,7 +121,8 @@ export function Selector<T>({
return (
<Select
- withinPortal={true}
+ data-testid="input-selector"
+ comboboxProps={{ withinPortal: true }}
data={data}
defaultValue={wrappedDefaultValue}
value={wrappedValue}
@@ -144,6 +173,7 @@ export function MultiSelector<T>({
() => value && value.map(labelRef.current),
[value],
);
+
const wrappedDefaultValue = useMemo(
() => defaultValue && defaultValue.map(labelRef.current),
[defaultValue],
@@ -168,6 +198,7 @@ export function MultiSelector<T>({
return (
<MultiSelect
{...select}
+ hidePickedOptions
value={wrappedValue}
defaultValue={wrappedDefaultValue}
onChange={wrappedOnChange}
diff --git a/frontend/src/components/modals/HistoryModal.tsx b/frontend/src/components/modals/HistoryModal.tsx
index cc4197c44..88d57ac65 100644
--- a/frontend/src/components/modals/HistoryModal.tsx
+++ b/frontend/src/components/modals/HistoryModal.tsx
@@ -1,23 +1,23 @@
/* eslint-disable camelcase */
+import { FunctionComponent, useMemo } from "react";
+import { Badge, Center, Text } from "@mantine/core";
+import { faFileExcel, faInfoCircle } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { ColumnDef } from "@tanstack/react-table";
import {
useEpisodeAddBlacklist,
useEpisodeHistory,
useMovieAddBlacklist,
useMovieHistory,
} from "@/apis/hooks";
+import MutateAction from "@/components/async/MutateAction";
+import QueryOverlay from "@/components/async/QueryOverlay";
+import { HistoryIcon } from "@/components/bazarr";
+import Language from "@/components/bazarr/Language";
import StateIcon from "@/components/StateIcon";
+import PageTable from "@/components/tables/PageTable";
+import TextPopover from "@/components/TextPopover";
import { withModal } from "@/modules/modals";
-import { faFileExcel, faInfoCircle } from "@fortawesome/free-solid-svg-icons";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { Badge, Center, Text } from "@mantine/core";
-import { FunctionComponent, useMemo } from "react";
-import { Column } from "react-table";
-import { PageTable } from "..";
-import TextPopover from "../TextPopover";
-import MutateAction from "../async/MutateAction";
-import QueryOverlay from "../async/QueryOverlay";
-import { HistoryIcon } from "../bazarr";
-import Language from "../bazarr/Language";
interface MovieHistoryViewProps {
movie: Item.Movie;
@@ -30,24 +30,34 @@ const MovieHistoryView: FunctionComponent<MovieHistoryViewProps> = ({
const { data } = history;
- const columns = useMemo<Column<History.Movie>[]>(
+ const addMovieToBlacklist = useMovieAddBlacklist();
+
+ const columns = useMemo<ColumnDef<History.Movie>[]>(
() => [
{
- accessor: "action",
- Cell: (row) => (
+ id: "action",
+ cell: ({
+ row: {
+ original: { action },
+ },
+ }) => (
<Center>
- <HistoryIcon action={row.value}></HistoryIcon>
+ <HistoryIcon action={action}></HistoryIcon>
</Center>
),
},
{
- Header: "Language",
- accessor: "language",
- Cell: ({ value }) => {
- if (value) {
+ header: "Language",
+ accessorKey: "language",
+ cell: ({
+ row: {
+ original: { language },
+ },
+ }) => {
+ if (language) {
return (
<Badge>
- <Language.Text value={value} long></Language.Text>
+ <Language.Text value={language} long></Language.Text>
</Badge>
);
} else {
@@ -56,17 +66,20 @@ const MovieHistoryView: FunctionComponent<MovieHistoryViewProps> = ({
},
},
{
- Header: "Provider",
- accessor: "provider",
+ header: "Provider",
+ accessorKey: "provider",
},
{
- Header: "Score",
- accessor: "score",
+ header: "Score",
+ accessorKey: "score",
},
{
- accessor: "matches",
- Cell: (row) => {
- const { matches, dont_matches: dont } = row.row.original;
+ id: "matches",
+ cell: ({
+ row: {
+ original: { matches, dont_matches: dont },
+ },
+ }) => {
if (matches.length || dont.length) {
return (
<StateIcon
@@ -81,31 +94,42 @@ const MovieHistoryView: FunctionComponent<MovieHistoryViewProps> = ({
},
},
{
- Header: "Date",
- accessor: "timestamp",
- Cell: ({ value, row }) => {
+ header: "Date",
+ accessorKey: "timestamp",
+ cell: ({
+ row: {
+ original: { timestamp, parsed_timestamp: parsedTimestamp },
+ },
+ }) => {
return (
- <TextPopover text={row.original.parsed_timestamp}>
- <Text>{value}</Text>
+ <TextPopover text={parsedTimestamp}>
+ <Text>{timestamp}</Text>
</TextPopover>
);
},
},
{
// Actions
- accessor: "blacklisted",
- Cell: ({ row, value }) => {
- const add = useMovieAddBlacklist();
- const { radarrId, provider, subs_id, language, subtitles_path } =
- row.original;
-
+ id: "blacklisted",
+ cell: ({
+ row: {
+ original: {
+ blacklisted,
+ radarrId,
+ provider,
+ subs_id,
+ language,
+ subtitles_path,
+ },
+ },
+ }) => {
if (subs_id && provider && language) {
return (
<MutateAction
label="Add to Blacklist"
- disabled={value}
+ disabled={blacklisted}
icon={faFileExcel}
- mutation={add}
+ mutation={addMovieToBlacklist}
args={() => ({
id: radarrId,
form: {
@@ -123,7 +147,7 @@ const MovieHistoryView: FunctionComponent<MovieHistoryViewProps> = ({
},
},
],
- [],
+ [addMovieToBlacklist],
);
return (
@@ -153,24 +177,34 @@ const EpisodeHistoryView: FunctionComponent<EpisodeHistoryViewProps> = ({
const { data } = history;
- const columns = useMemo<Column<History.Episode>[]>(
+ const addEpisodeToBlacklist = useEpisodeAddBlacklist();
+
+ const columns = useMemo<ColumnDef<History.Episode>[]>(
() => [
{
- accessor: "action",
- Cell: (row) => (
+ id: "action",
+ cell: ({
+ row: {
+ original: { action },
+ },
+ }) => (
<Center>
- <HistoryIcon action={row.value}></HistoryIcon>
+ <HistoryIcon action={action}></HistoryIcon>
</Center>
),
},
{
- Header: "Language",
- accessor: "language",
- Cell: ({ value }) => {
- if (value) {
+ header: "Language",
+ accessorKey: "language",
+ cell: ({
+ row: {
+ original: { language },
+ },
+ }) => {
+ if (language) {
return (
<Badge>
- <Language.Text value={value} long></Language.Text>
+ <Language.Text value={language} long></Language.Text>
</Badge>
);
} else {
@@ -179,16 +213,16 @@ const EpisodeHistoryView: FunctionComponent<EpisodeHistoryViewProps> = ({
},
},
{
- Header: "Provider",
- accessor: "provider",
+ header: "Provider",
+ accessorKey: "provider",
},
{
- Header: "Score",
- accessor: "score",
+ header: "Score",
+ accessorKey: "score",
},
{
- accessor: "matches",
- Cell: (row) => {
+ id: "matches",
+ cell: (row) => {
const { matches, dont_matches: dont } = row.row.original;
if (matches.length || dont.length) {
return (
@@ -204,21 +238,29 @@ const EpisodeHistoryView: FunctionComponent<EpisodeHistoryViewProps> = ({
},
},
{
- Header: "Date",
- accessor: "timestamp",
- Cell: ({ row, value }) => {
+ header: "Date",
+ accessorKey: "timestamp",
+ cell: ({
+ row: {
+ original: { timestamp, parsed_timestamp: parsedTimestamp },
+ },
+ }) => {
return (
- <TextPopover text={row.original.parsed_timestamp}>
- <Text>{value}</Text>
+ <TextPopover text={parsedTimestamp}>
+ <Text>{timestamp}</Text>
</TextPopover>
);
},
},
{
- accessor: "description",
- Cell: ({ value }) => {
+ id: "description",
+ cell: ({
+ row: {
+ original: { description },
+ },
+ }) => {
return (
- <TextPopover text={value}>
+ <TextPopover text={description}>
<FontAwesomeIcon size="sm" icon={faInfoCircle}></FontAwesomeIcon>
</TextPopover>
);
@@ -226,25 +268,27 @@ const EpisodeHistoryView: FunctionComponent<EpisodeHistoryViewProps> = ({
},
{
// Actions
- accessor: "blacklisted",
- Cell: ({ row, value }) => {
- const {
- sonarrEpisodeId,
- sonarrSeriesId,
- provider,
- subs_id,
- language,
- subtitles_path,
- } = row.original;
- const add = useEpisodeAddBlacklist();
-
+ id: "blacklisted",
+ cell: ({
+ row: {
+ original: {
+ blacklisted,
+ sonarrEpisodeId,
+ sonarrSeriesId,
+ provider,
+ subs_id,
+ language,
+ subtitles_path,
+ },
+ },
+ }) => {
if (subs_id && provider && language) {
return (
<MutateAction
label="Add to Blacklist"
- disabled={value}
+ disabled={blacklisted}
icon={faFileExcel}
- mutation={add}
+ mutation={addEpisodeToBlacklist}
args={() => ({
seriesId: sonarrSeriesId,
episodeId: sonarrEpisodeId,
@@ -263,12 +307,13 @@ const EpisodeHistoryView: FunctionComponent<EpisodeHistoryViewProps> = ({
},
},
],
- [],
+ [addEpisodeToBlacklist],
);
return (
<QueryOverlay result={history}>
<PageTable
+ autoScroll={false}
tableStyles={{ emptyText: "No history found", placeholder: 5 }}
columns={columns}
data={data ?? []}
diff --git a/frontend/src/components/modals/ManualSearchModal.tsx b/frontend/src/components/modals/ManualSearchModal.tsx
index 24799130d..81a49f0f3 100644
--- a/frontend/src/components/modals/ManualSearchModal.tsx
+++ b/frontend/src/components/modals/ManualSearchModal.tsx
@@ -1,13 +1,4 @@
-import { withModal } from "@/modules/modals";
-import { task, TaskGroup } from "@/modules/task";
-import { useTableStyles } from "@/styles";
-import { GetItemId } from "@/utilities";
-import {
- faCaretDown,
- faDownload,
- faInfoCircle,
-} from "@fortawesome/free-solid-svg-icons";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import React, { useCallback, useMemo, useState } from "react";
import {
Alert,
Anchor,
@@ -19,21 +10,28 @@ import {
Stack,
Text,
} from "@mantine/core";
+import {
+ faCaretDown,
+ faDownload,
+ faInfoCircle,
+} from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { UseQueryResult } from "@tanstack/react-query";
+import { ColumnDef } from "@tanstack/react-table";
import { isString } from "lodash";
-import { useCallback, useMemo, useState } from "react";
-import { UseQueryResult } from "react-query";
-import { Column } from "react-table";
-import { Action, PageTable } from "..";
-import Language from "../bazarr/Language";
-import StateIcon from "../StateIcon";
+import { Action } from "@/components";
+import Language from "@/components/bazarr/Language";
+import StateIcon from "@/components/StateIcon";
+import PageTable from "@/components/tables/PageTable";
+import { withModal } from "@/modules/modals";
+import { task, TaskGroup } from "@/modules/task";
+import { GetItemId } from "@/utilities";
type SupportType = Item.Movie | Item.Episode;
interface Props<T extends SupportType> {
download: (item: T, result: SearchResultType) => Promise<void>;
- query: (
- id?: number,
- ) => UseQueryResult<SearchResultType[] | undefined, unknown>;
+ query: (id?: number) => UseQueryResult<SearchResultType[] | undefined>;
item: T;
}
@@ -50,27 +48,67 @@ function ManualSearchView<T extends SupportType>(props: Props<T>) {
const search = useCallback(() => {
setSearchStarted(true);
- results.refetch();
+
+ void results.refetch();
}, [results]);
- const columns = useMemo<Column<SearchResultType>[]>(
+ const ReleaseInfoCell = React.memo(
+ ({ releaseInfo }: { releaseInfo: string[] }) => {
+ const [open, setOpen] = useState(false);
+
+ const items = useMemo(
+ () => releaseInfo.slice(1).map((v, idx) => <Text key={idx}>{v}</Text>),
+ [releaseInfo],
+ );
+
+ if (releaseInfo.length === 0) {
+ return <Text c="dimmed">Cannot get release info</Text>;
+ }
+
+ return (
+ <Stack gap={0} onClick={() => setOpen((o) => !o)}>
+ <Text className="table-primary" span>
+ {releaseInfo[0]}
+ {releaseInfo.length > 1 && (
+ <FontAwesomeIcon
+ icon={faCaretDown}
+ rotation={open ? 180 : undefined}
+ ></FontAwesomeIcon>
+ )}
+ </Text>
+ <Collapse in={open}>
+ <>{items}</>
+ </Collapse>
+ </Stack>
+ );
+ },
+ );
+
+ const columns = useMemo<ColumnDef<SearchResultType>[]>(
() => [
{
- Header: "Score",
- accessor: "score",
- Cell: ({ value }) => {
- const { classes } = useTableStyles();
- return <Text className={classes.noWrap}>{value}%</Text>;
+ header: "Score",
+ accessorKey: "score",
+ cell: ({
+ row: {
+ original: { score },
+ },
+ }) => {
+ return <Text className="table-no-wrap">{score}%</Text>;
},
},
{
- Header: "Language",
- accessor: "language",
- Cell: ({ row: { original }, value }) => {
+ header: "Language",
+ accessorKey: "language",
+ cell: ({
+ row: {
+ original: { language, hearing_impaired: hi, forced },
+ },
+ }) => {
const lang: Language.Info = {
- code2: value,
- hi: original.hearing_impaired === "True",
- forced: original.forced === "True",
+ code2: language,
+ hi: hi === "True",
+ forced: forced === "True",
name: "",
};
return (
@@ -81,16 +119,19 @@ function ManualSearchView<T extends SupportType>(props: Props<T>) {
},
},
{
- Header: "Provider",
- accessor: "provider",
- Cell: (row) => {
- const { classes } = useTableStyles();
- const value = row.value;
- const { url } = row.row.original;
+ header: "Provider",
+ accessorKey: "provider",
+ cell: ({
+ row: {
+ original: { provider, url },
+ },
+ }) => {
+ const value = provider;
+
if (url) {
return (
<Anchor
- className={classes.noWrap}
+ className="table-no-wrap"
href={url}
target="_blank"
rel="noopener noreferrer"
@@ -104,51 +145,31 @@ function ManualSearchView<T extends SupportType>(props: Props<T>) {
},
},
{
- Header: "Release",
- accessor: "release_info",
- Cell: ({ value }) => {
- const { classes } = useTableStyles();
- const [open, setOpen] = useState(false);
-
- const items = useMemo(
- () => value.slice(1).map((v, idx) => <Text key={idx}>{v}</Text>),
- [value],
- );
-
- if (value.length === 0) {
- return <Text color="dimmed">Cannot get release info</Text>;
- }
-
- return (
- <Stack spacing={0} onClick={() => setOpen((o) => !o)}>
- <Text className={classes.primary}>
- {value[0]}
- {value.length > 1 && (
- <FontAwesomeIcon
- icon={faCaretDown}
- rotation={open ? 180 : undefined}
- ></FontAwesomeIcon>
- )}
- </Text>
- <Collapse in={open}>
- <>{items}</>
- </Collapse>
- </Stack>
- );
+ header: "Release",
+ accessorKey: "release_info",
+ cell: ({
+ row: {
+ original: { release_info: releaseInfo },
+ },
+ }) => {
+ return <ReleaseInfoCell releaseInfo={releaseInfo} />;
},
},
{
- Header: "Uploader",
- accessor: "uploader",
- Cell: ({ value }) => {
- const { classes } = useTableStyles();
- return <Text className={classes.noWrap}>{value ?? "-"}</Text>;
+ header: "Uploader",
+ accessorKey: "uploader",
+ cell: ({
+ row: {
+ original: { uploader },
+ },
+ }) => {
+ return <Text className="table-no-wrap">{uploader ?? "-"}</Text>;
},
},
{
- Header: "Match",
- accessor: "matches",
- Cell: (row) => {
+ header: "Match",
+ accessorKey: "matches",
+ cell: (row) => {
const { matches, dont_matches: dont } = row.row.original;
return (
<StateIcon
@@ -160,16 +181,15 @@ function ManualSearchView<T extends SupportType>(props: Props<T>) {
},
},
{
- Header: "Get",
- accessor: "subtitle",
- Cell: ({ row }) => {
+ header: "Get",
+ accessorKey: "subtitle",
+ cell: ({ row }) => {
const result = row.original;
return (
<Action
label="Download"
icon={faDownload}
- color="brand"
- variant="light"
+ c="brand"
disabled={item === null}
onClick={() => {
if (!item) return;
@@ -187,7 +207,7 @@ function ManualSearchView<T extends SupportType>(props: Props<T>) {
},
},
],
- [download, item],
+ [download, item, ReleaseInfoCell],
);
const bSceneNameAvailable =
diff --git a/frontend/src/components/modals/SubtitleToolsModal.tsx b/frontend/src/components/modals/SubtitleToolsModal.tsx
index 2ba99ec73..dca20d159 100644
--- a/frontend/src/components/modals/SubtitleToolsModal.tsx
+++ b/frontend/src/components/modals/SubtitleToolsModal.tsx
@@ -1,12 +1,19 @@
+import { FunctionComponent, useMemo, useState } from "react";
+import {
+ Badge,
+ Button,
+ Checkbox,
+ Divider,
+ Group,
+ Stack,
+ Text,
+} from "@mantine/core";
+import { ColumnDef } from "@tanstack/react-table";
import Language from "@/components/bazarr/Language";
import SubtitleToolsMenu from "@/components/SubtitleToolsMenu";
-import { SimpleTable } from "@/components/tables";
-import { useCustomSelection } from "@/components/tables/plugins";
+import SimpleTable from "@/components/tables/SimpleTable";
import { withModal } from "@/modules/modals";
import { isMovie } from "@/utilities";
-import { Badge, Button, Divider, Group, Stack, Text } from "@mantine/core";
-import { FunctionComponent, useMemo, useState } from "react";
-import { Column, useRowSelect } from "react-table";
type SupportType = Item.Episode | Item.Movie;
@@ -35,24 +42,53 @@ const SubtitleToolView: FunctionComponent<SubtitleToolViewProps> = ({
}) => {
const [selections, setSelections] = useState<TableColumnType[]>([]);
- const columns: Column<TableColumnType>[] = useMemo<Column<TableColumnType>[]>(
+ const columns = useMemo<ColumnDef<TableColumnType>[]>(
() => [
{
- Header: "Language",
- accessor: "raw_language",
- Cell: ({ value }) => (
+ id: "selection",
+ header: ({ table }) => {
+ return (
+ <Checkbox
+ id="table-header-selection"
+ indeterminate={table.getIsSomeRowsSelected()}
+ checked={table.getIsAllRowsSelected()}
+ onChange={table.getToggleAllRowsSelectedHandler()}
+ ></Checkbox>
+ );
+ },
+ cell: ({ row: { index, getIsSelected, getToggleSelectedHandler } }) => {
+ return (
+ <Checkbox
+ id={`table-cell-${index}`}
+ checked={getIsSelected()}
+ onChange={getToggleSelectedHandler()}
+ onClick={getToggleSelectedHandler()}
+ ></Checkbox>
+ );
+ },
+ },
+ {
+ header: "Language",
+ accessorKey: "raw_language",
+ cell: ({
+ row: {
+ original: { raw_language: rawLanguage },
+ },
+ }) => (
<Badge color="secondary">
- <Language.Text value={value} long></Language.Text>
+ <Language.Text value={rawLanguage} long></Language.Text>
</Badge>
),
},
{
id: "file",
- Header: "File",
- accessor: "path",
- Cell: ({ value }) => {
- const path = value;
-
+ header: "File",
+ accessorKey: "path",
+ cell: ({
+ row: {
+ original: { path },
+ },
+ }) => {
let idx = path.lastIndexOf("/");
if (idx === -1) {
@@ -94,16 +130,15 @@ const SubtitleToolView: FunctionComponent<SubtitleToolViewProps> = ({
[payload],
);
- const plugins = [useRowSelect, useCustomSelection];
-
return (
<Stack>
<SimpleTable
tableStyles={{ emptyText: "No external subtitles found" }}
- plugins={plugins}
+ enableRowSelection={(row) => CanSelectSubtitle(row.original)}
+ onRowSelectionChanged={(rows) =>
+ setSelections(rows.map((r) => r.original))
+ }
columns={columns}
- onSelect={setSelections}
- canSelect={CanSelectSubtitle}
data={data}
></SimpleTable>
<Divider></Divider>
diff --git a/frontend/src/components/tables/BaseTable.module.scss b/frontend/src/components/tables/BaseTable.module.scss
new file mode 100644
index 000000000..e1e1eff0b
--- /dev/null
+++ b/frontend/src/components/tables/BaseTable.module.scss
@@ -0,0 +1,9 @@
+.container {
+ display: block;
+ max-width: 100%;
+ overflow-x: auto;
+}
+
+.table {
+ border-collapse: collapse;
+}
diff --git a/frontend/src/components/tables/BaseTable.tsx b/frontend/src/components/tables/BaseTable.tsx
index 6ec49e61a..b5a867b14 100644
--- a/frontend/src/components/tables/BaseTable.tsx
+++ b/frontend/src/components/tables/BaseTable.tsx
@@ -1,10 +1,17 @@
+import React, { ReactNode, useMemo } from "react";
+import { Box, Skeleton, Table, Text } from "@mantine/core";
+import {
+ flexRender,
+ Header,
+ Row,
+ Table as TableInstance,
+} from "@tanstack/react-table";
import { useIsLoading } from "@/contexts";
import { usePageSize } from "@/utilities/storage";
-import { Box, createStyles, Skeleton, Table, Text } from "@mantine/core";
-import { ReactNode, useMemo } from "react";
-import { HeaderGroup, Row, TableInstance } from "react-table";
+import styles from "@/components/tables/BaseTable.module.scss";
-export type BaseTableProps<T extends object> = TableInstance<T> & {
+export type BaseTableProps<T extends object> = {
+ instance: TableInstance<T>;
tableStyles?: TableStyleProps<T>;
};
@@ -14,116 +21,92 @@ export interface TableStyleProps<T extends object> {
placeholder?: number;
hideHeader?: boolean;
fixHeader?: boolean;
- headersRenderer?: (headers: HeaderGroup<T>[]) => JSX.Element[];
- rowRenderer?: (row: Row<T>) => Nullable<JSX.Element>;
+ headersRenderer?: (headers: Header<T, unknown>[]) => React.JSX.Element[];
+ rowRenderer?: (row: Row<T>) => Nullable<React.JSX.Element>;
}
-const useStyles = createStyles((theme) => {
- return {
- container: {
- display: "block",
- maxWidth: "100%",
- overflowX: "auto",
- },
- table: {
- borderCollapse: "collapse",
- },
- header: {},
- };
-});
-
function DefaultHeaderRenderer<T extends object>(
- headers: HeaderGroup<T>[],
-): JSX.Element[] {
- return headers.map((col) => (
- <th style={{ whiteSpace: "nowrap" }} {...col.getHeaderProps()}>
- {col.render("Header")}
- </th>
+ headers: Header<T, unknown>[],
+): React.JSX.Element[] {
+ return headers.map((header) => (
+ <Table.Th style={{ whiteSpace: "nowrap" }} key={header.id}>
+ {flexRender(header.column.columnDef.header, header.getContext())}
+ </Table.Th>
));
}
-function DefaultRowRenderer<T extends object>(row: Row<T>): JSX.Element | null {
+function DefaultRowRenderer<T extends object>(
+ row: Row<T>,
+): React.JSX.Element | null {
return (
- <tr {...row.getRowProps()}>
- {row.cells.map((cell) => (
- <td {...cell.getCellProps()}>{cell.render("Cell")}</td>
+ <Table.Tr key={row.id}>
+ {row.getVisibleCells().map((cell) => (
+ <Table.Td key={cell.id}>
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
+ </Table.Td>
))}
- </tr>
+ </Table.Tr>
);
}
export default function BaseTable<T extends object>(props: BaseTableProps<T>) {
- const {
- headerGroups,
- rows: tableRows,
- page: tablePages,
- prepareRow,
- getTableProps,
- getTableBodyProps,
- tableStyles,
- } = props;
+ const { instance, tableStyles } = props;
const headersRenderer = tableStyles?.headersRenderer ?? DefaultHeaderRenderer;
const rowRenderer = tableStyles?.rowRenderer ?? DefaultRowRenderer;
- const { classes } = useStyles();
-
const colCount = useMemo(() => {
- return headerGroups.reduce(
- (prev, curr) => (curr.headers.length > prev ? curr.headers.length : prev),
- 0,
- );
- }, [headerGroups]);
+ return instance
+ .getHeaderGroups()
+ .reduce(
+ (prev, curr) =>
+ curr.headers.length > prev ? curr.headers.length : prev,
+ 0,
+ );
+ }, [instance]);
- // Switch to usePagination plugin if enabled
- const rows = tablePages ?? tableRows;
-
- const empty = rows.length === 0;
+ const empty = instance.getRowCount() === 0;
const pageSize = usePageSize();
const isLoading = useIsLoading();
let body: ReactNode;
+
if (isLoading) {
body = Array(tableStyles?.placeholder ?? pageSize)
.fill(0)
.map((_, i) => (
- <tr key={i}>
- <td colSpan={colCount}>
+ <Table.Tr key={i}>
+ <Table.Td colSpan={colCount}>
<Skeleton height={24}></Skeleton>
- </td>
- </tr>
+ </Table.Td>
+ </Table.Tr>
));
} else if (empty && tableStyles?.emptyText) {
body = (
- <tr>
- <td colSpan={colCount}>
- <Text align="center">{tableStyles.emptyText}</Text>
- </td>
- </tr>
+ <Table.Tr>
+ <Table.Td colSpan={colCount}>
+ <Text ta="center">{tableStyles.emptyText}</Text>
+ </Table.Td>
+ </Table.Tr>
);
} else {
- body = rows.map((row) => {
- prepareRow(row);
+ body = instance.getRowModel().rows.map((row) => {
return rowRenderer(row);
});
}
return (
- <Box className={classes.container}>
- <Table
- className={classes.table}
- striped={tableStyles?.striped ?? true}
- {...getTableProps()}
- >
- <thead className={classes.header} hidden={tableStyles?.hideHeader}>
- {headerGroups.map((headerGroup) => (
- <tr {...headerGroup.getHeaderGroupProps()}>
+ <Box className={styles.container}>
+ <Table className={styles.table} striped={tableStyles?.striped ?? true}>
+ <Table.Thead hidden={tableStyles?.hideHeader}>
+ {instance.getHeaderGroups().map((headerGroup) => (
+ <Table.Tr key={headerGroup.id}>
{headersRenderer(headerGroup.headers)}
- </tr>
+ </Table.Tr>
))}
- </thead>
- <tbody {...getTableBodyProps()}>{body}</tbody>
+ </Table.Thead>
+ <Table.Tbody>{body}</Table.Tbody>
</Table>
</Box>
);
diff --git a/frontend/src/components/tables/GroupTable.tsx b/frontend/src/components/tables/GroupTable.tsx
index 3a8be3d1b..b14edf3e6 100644
--- a/frontend/src/components/tables/GroupTable.tsx
+++ b/frontend/src/components/tables/GroupTable.tsx
@@ -1,38 +1,44 @@
+import React, { Fragment } from "react";
+import { Box, Table, Text } from "@mantine/core";
import { faChevronCircleRight } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { Box, Text } from "@mantine/core";
import {
Cell,
- HeaderGroup,
+ flexRender,
+ getExpandedRowModel,
+ getGroupedRowModel,
+ Header,
Row,
- useExpanded,
- useGroupBy,
- useSortBy,
-} from "react-table";
-import SimpleTable, { SimpleTableProps } from "./SimpleTable";
+} from "@tanstack/react-table";
+import SimpleTable, { SimpleTableProps } from "@/components/tables/SimpleTable";
-function renderCell<T extends object = object>(cell: Cell<T>, row: Row<T>) {
- if (cell.isGrouped) {
+function renderCell<T extends object = object>(
+ cell: Cell<T, unknown>,
+ row: Row<T>,
+) {
+ if (cell.getIsGrouped()) {
return (
- <div {...row.getToggleRowExpandedProps()}>{cell.render("Cell")}</div>
+ <div>{flexRender(cell.column.columnDef.cell, cell.getContext())}</div>
);
- } else if (row.canExpand || cell.isAggregated) {
+ } else if (row.getCanExpand() || cell.getIsAggregated()) {
return null;
} else {
- return cell.render("Cell");
+ return flexRender(cell.column.columnDef.cell, cell.getContext());
}
}
function renderRow<T extends object>(row: Row<T>) {
- if (row.canExpand) {
- const cell = row.cells.find((cell) => cell.isGrouped);
+ if (row.getCanExpand()) {
+ const cell = row.getVisibleCells().find((cell) => cell.getIsGrouped());
+
if (cell) {
- const rotation = row.isExpanded ? 90 : undefined;
+ const rotation = row.getIsExpanded() ? 90 : undefined;
+
return (
- <tr {...row.getRowProps()}>
- <td {...cell.getCellProps()} colSpan={row.cells.length}>
- <Text {...row.getToggleRowExpandedProps()} p={2}>
- {cell.render("Cell")}
+ <Table.Tr key={row.id} style={{ cursor: "pointer" }}>
+ <Table.Td key={cell.id} colSpan={row.getVisibleCells().length}>
+ <Text p={2} onClick={() => row.toggleExpanded()}>
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
<Box component="span" mx={12}>
<FontAwesomeIcon
icon={faChevronCircleRight}
@@ -40,45 +46,55 @@ function renderRow<T extends object>(row: Row<T>) {
></FontAwesomeIcon>
</Box>
</Text>
- </td>
- </tr>
+ </Table.Td>
+ </Table.Tr>
);
} else {
return null;
}
} else {
return (
- <tr {...row.getRowProps()}>
- {row.cells
- .filter((cell) => !cell.isPlaceholder)
+ <Table.Tr key={row.id}>
+ {row
+ .getVisibleCells()
+ .filter((cell) => !cell.getIsPlaceholder())
.map((cell) => (
- <td {...cell.getCellProps()}>{renderCell(cell, row)}</td>
+ <Table.Td key={cell.id}>{renderCell(cell, row)}</Table.Td>
))}
- </tr>
+ </Table.Tr>
);
}
}
function renderHeaders<T extends object>(
- headers: HeaderGroup<T>[],
-): JSX.Element[] {
- return headers
- .filter((col) => !col.isGrouped)
- .map((col) => <th {...col.getHeaderProps()}>{col.render("Header")}</th>);
+ headers: Header<T, unknown>[],
+): React.JSX.Element[] {
+ return headers.map((header) => {
+ if (header.column.getIsGrouped()) {
+ return <Fragment key={header.id}></Fragment>;
+ }
+
+ return (
+ <Table.Th key={header.id} colSpan={header.colSpan}>
+ {flexRender(header.column.columnDef.header, header.getContext())}
+ </Table.Th>
+ );
+ });
}
type Props<T extends object> = Omit<
SimpleTableProps<T>,
- "plugins" | "headersRenderer" | "rowRenderer"
+ "headersRenderer" | "rowRenderer"
>;
-const plugins = [useGroupBy, useSortBy, useExpanded];
-
function GroupTable<T extends object = object>(props: Props<T>) {
return (
<SimpleTable
{...props}
- plugins={plugins}
+ enableGrouping
+ enableExpanding
+ getGroupedRowModel={getGroupedRowModel()}
+ getExpandedRowModel={getExpandedRowModel()}
tableStyles={{ headersRenderer: renderHeaders, rowRenderer: renderRow }}
></SimpleTable>
);
diff --git a/frontend/src/components/tables/PageControl.tsx b/frontend/src/components/tables/PageControl.tsx
index 0767593de..bcdf290e3 100644
--- a/frontend/src/components/tables/PageControl.tsx
+++ b/frontend/src/components/tables/PageControl.tsx
@@ -1,6 +1,7 @@
-import { useIsLoading } from "@/contexts";
-import { Group, Pagination, Text } from "@mantine/core";
import { FunctionComponent, useEffect } from "react";
+import { Group, Pagination, Text } from "@mantine/core";
+import { useIsLoading } from "@/contexts";
+
interface Props {
count: number;
index: number;
@@ -28,7 +29,7 @@ const PageControl: FunctionComponent<Props> = ({
}, [total, goto]);
return (
- <Group p={16} position="apart">
+ <Group p={16} justify="space-between">
<Text size="sm">
Show {start} to {end} of {total} entries
</Text>
diff --git a/frontend/src/components/tables/PageTable.tsx b/frontend/src/components/tables/PageTable.tsx
index 4f64fe7b8..476ff2c2b 100644
--- a/frontend/src/components/tables/PageTable.tsx
+++ b/frontend/src/components/tables/PageTable.tsx
@@ -1,55 +1,62 @@
+import { MutableRefObject, useEffect } from "react";
+import {
+ getCoreRowModel,
+ getPaginationRowModel,
+ Table,
+ TableOptions,
+ useReactTable,
+} from "@tanstack/react-table";
+import BaseTable, { TableStyleProps } from "@/components/tables/BaseTable";
import { ScrollToTop } from "@/utilities";
import { usePageSize } from "@/utilities/storage";
-import { useEffect } from "react";
-import { usePagination, useTable } from "react-table";
-import BaseTable from "./BaseTable";
import PageControl from "./PageControl";
-import { SimpleTableProps } from "./SimpleTable";
-import { useDefaultSettings } from "./plugins";
-type Props<T extends object> = SimpleTableProps<T> & {
+type Props<T extends object> = Omit<TableOptions<T>, "getCoreRowModel"> & {
+ instanceRef?: MutableRefObject<Table<T> | null>;
+ tableStyles?: TableStyleProps<T>;
autoScroll?: boolean;
};
-const tablePlugins = [useDefaultSettings, usePagination];
-
export default function PageTable<T extends object>(props: Props<T>) {
- const { autoScroll = true, plugins, instanceRef, ...options } = props;
+ const { instanceRef, autoScroll, ...options } = props;
- const instance = useTable(
- options,
- useDefaultSettings,
- ...tablePlugins,
- ...(plugins ?? []),
- );
+ const pageSize = usePageSize();
- // use page size as specified in UI settings
- instance.state.pageSize = usePageSize();
+ const instance = useReactTable({
+ ...options,
+ getCoreRowModel: getCoreRowModel(),
+ getPaginationRowModel: getPaginationRowModel(),
+ initialState: {
+ pagination: {
+ pageSize: pageSize,
+ },
+ },
+ });
if (instanceRef) {
instanceRef.current = instance;
}
+ const pageIndex = instance.getState().pagination.pageIndex;
+
// Scroll to top when page is changed
useEffect(() => {
if (autoScroll) {
ScrollToTop();
}
- }, [instance.state.pageIndex, autoScroll]);
+ }, [pageIndex, autoScroll]);
+
+ const state = instance.getState();
return (
<>
- <BaseTable
- {...options}
- {...instance}
- plugins={[...tablePlugins, ...(plugins ?? [])]}
- ></BaseTable>
+ <BaseTable {...options} instance={instance}></BaseTable>
<PageControl
- count={instance.pageCount}
- index={instance.state.pageIndex}
- size={instance.state.pageSize}
- total={instance.rows.length}
- goto={instance.gotoPage}
+ count={instance.getPageCount()}
+ index={state.pagination.pageIndex}
+ size={pageSize}
+ total={instance.getRowCount()}
+ goto={instance.setPageIndex}
></PageControl>
</>
);
diff --git a/frontend/src/components/tables/QueryPageTable.tsx b/frontend/src/components/tables/QueryPageTable.tsx
index 81eccee13..797d7a08e 100644
--- a/frontend/src/components/tables/QueryPageTable.tsx
+++ b/frontend/src/components/tables/QueryPageTable.tsx
@@ -1,9 +1,9 @@
+import { useEffect } from "react";
import { UsePaginationQueryResult } from "@/apis/queries/hooks";
+import SimpleTable, { SimpleTableProps } from "@/components/tables/SimpleTable";
import { LoadingProvider } from "@/contexts";
import { ScrollToTop } from "@/utilities";
-import { useEffect } from "react";
import PageControl from "./PageControl";
-import SimpleTable, { SimpleTableProps } from "./SimpleTable";
type Props<T extends object> = Omit<SimpleTableProps<T>, "data"> & {
query: UsePaginationQueryResult<T>;
diff --git a/frontend/src/components/tables/SimpleTable.tsx b/frontend/src/components/tables/SimpleTable.tsx
index 90f76c7f2..e3e0b7ff3 100644
--- a/frontend/src/components/tables/SimpleTable.tsx
+++ b/frontend/src/components/tables/SimpleTable.tsx
@@ -1,23 +1,65 @@
-import { PluginHook, TableInstance, TableOptions, useTable } from "react-table";
-import BaseTable, { TableStyleProps } from "./BaseTable";
-import { useDefaultSettings } from "./plugins";
+import { MutableRefObject, useEffect, useMemo } from "react";
+import {
+ getCoreRowModel,
+ Row,
+ Table,
+ TableOptions,
+ useReactTable,
+} from "@tanstack/react-table";
+import BaseTable, { TableStyleProps } from "@/components/tables/BaseTable";
+import { usePageSize } from "@/utilities/storage";
-export type SimpleTableProps<T extends object> = TableOptions<T> & {
- plugins?: PluginHook<T>[];
- instanceRef?: React.MutableRefObject<TableInstance<T> | null>;
+export type SimpleTableProps<T extends object> = Omit<
+ TableOptions<T>,
+ "getCoreRowModel"
+> & {
+ instanceRef?: MutableRefObject<Table<T> | null>;
tableStyles?: TableStyleProps<T>;
+ onRowSelectionChanged?: (selectedRows: Row<T>[]) => void;
+ onAllRowsExpandedChanged?: (isAllRowsExpanded: boolean) => void;
};
export default function SimpleTable<T extends object>(
props: SimpleTableProps<T>,
) {
- const { plugins, instanceRef, tableStyles, ...options } = props;
+ const {
+ instanceRef,
+ tableStyles,
+ onRowSelectionChanged,
+ onAllRowsExpandedChanged,
+ ...options
+ } = props;
- const instance = useTable(options, useDefaultSettings, ...(plugins ?? []));
+ const pageSize = usePageSize();
+
+ const instance = useReactTable({
+ ...options,
+ getCoreRowModel: getCoreRowModel(),
+ autoResetPageIndex: false,
+ autoResetExpanded: false,
+ pageCount: pageSize,
+ });
if (instanceRef) {
instanceRef.current = instance;
}
- return <BaseTable tableStyles={tableStyles} {...instance}></BaseTable>;
+ const selectedRows = instance.getSelectedRowModel().rows;
+
+ const memoizedRows = useMemo(() => selectedRows, [selectedRows]);
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ const memoizedRowSelectionChanged = useMemo(() => onRowSelectionChanged, []);
+
+ const isAllRowsExpanded = instance.getIsAllRowsExpanded();
+
+ useEffect(() => {
+ memoizedRowSelectionChanged?.(memoizedRows);
+ }, [memoizedRowSelectionChanged, memoizedRows]);
+
+ useEffect(() => {
+ onAllRowsExpandedChanged?.(isAllRowsExpanded);
+ }, [onAllRowsExpandedChanged, isAllRowsExpanded]);
+
+ return <BaseTable tableStyles={tableStyles} instance={instance}></BaseTable>;
}
diff --git a/frontend/src/components/tables/plugins/index.ts b/frontend/src/components/tables/plugins/index.ts
deleted file mode 100644
index 39490a113..000000000
--- a/frontend/src/components/tables/plugins/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { default as useCustomSelection } from "./useCustomSelection";
-export { default as useDefaultSettings } from "./useDefaultSettings";
diff --git a/frontend/src/components/tables/plugins/useCustomSelection.tsx b/frontend/src/components/tables/plugins/useCustomSelection.tsx
deleted file mode 100644
index d6ea82de4..000000000
--- a/frontend/src/components/tables/plugins/useCustomSelection.tsx
+++ /dev/null
@@ -1,113 +0,0 @@
-import { Checkbox as MantineCheckbox } from "@mantine/core";
-import { forwardRef, useEffect, useRef } from "react";
-import {
- CellProps,
- Column,
- ColumnInstance,
- HeaderProps,
- Hooks,
- MetaBase,
- TableInstance,
- TableToggleCommonProps,
- ensurePluginOrder,
-} from "react-table";
-
-const pluginName = "useCustomSelection";
-
-const checkboxId = "---selection---";
-
-interface CheckboxProps {
- idIn: string;
- disabled?: boolean;
-}
-
-const Checkbox = forwardRef<
- HTMLInputElement,
- TableToggleCommonProps & CheckboxProps
->(({ indeterminate, checked, disabled, idIn, ...rest }, ref) => {
- const defaultRef = useRef<HTMLInputElement>(null);
- const resolvedRef = ref || defaultRef;
-
- useEffect(() => {
- if (typeof resolvedRef === "object" && resolvedRef.current) {
- resolvedRef.current.indeterminate = indeterminate ?? false;
-
- if (disabled) {
- resolvedRef.current.checked = false;
- } else {
- resolvedRef.current.checked = checked ?? false;
- }
- }
- }, [resolvedRef, indeterminate, checked, disabled]);
-
- return (
- <MantineCheckbox
- key={idIn}
- disabled={disabled}
- ref={resolvedRef}
- {...rest}
- ></MantineCheckbox>
- );
-});
-
-function useCustomSelection<T extends object>(hooks: Hooks<T>) {
- hooks.visibleColumns.push(visibleColumns);
- hooks.useInstance.push(useInstance);
-}
-
-useCustomSelection.pluginName = pluginName;
-
-function useInstance<T extends object>(instance: TableInstance<T>) {
- const {
- plugins,
- rows,
- onSelect,
- canSelect,
- state: { selectedRowIds },
- } = instance;
-
- ensurePluginOrder(plugins, ["useRowSelect"], pluginName);
-
- useEffect(() => {
- // Performance
- let items = Object.keys(selectedRowIds).flatMap(
- (v) => rows.find((n) => n.id === v)?.original ?? [],
- );
-
- if (canSelect) {
- items = items.filter((v) => canSelect(v));
- }
-
- onSelect && onSelect(items);
- }, [selectedRowIds, onSelect, rows, canSelect]);
-}
-
-function visibleColumns<T extends object>(
- columns: ColumnInstance<T>[],
- meta: MetaBase<T>,
-): Column<T>[] {
- const { instance } = meta;
- const checkbox: Column<T> = {
- id: checkboxId,
- Header: ({ getToggleAllRowsSelectedProps }: HeaderProps<T>) => (
- <Checkbox
- idIn="table-header-selection"
- {...getToggleAllRowsSelectedProps()}
- ></Checkbox>
- ),
- Cell: ({ row }: CellProps<T>) => {
- const canSelect = instance.canSelect;
- const disabled = (canSelect && !canSelect(row.original)) ?? false;
- return (
- <Checkbox
- idIn={`table-cell-${row.index}`}
- disabled={disabled}
- {...row.getToggleRowSelectedProps()}
- ></Checkbox>
- );
- },
- };
- return [checkbox, ...columns];
-}
-
-export default useCustomSelection;
diff --git a/frontend/src/components/tables/plugins/useDefaultSettings.tsx b/frontend/src/components/tables/plugins/useDefaultSettings.tsx
deleted file mode 100644
index c833c9f79..000000000
--- a/frontend/src/components/tables/plugins/useDefaultSettings.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import { usePageSize } from "@/utilities/storage";
-import { Hooks, TableOptions } from "react-table";
-
-const pluginName = "useLocalSettings";
-
-function useDefaultSettings<T extends object>(hooks: Hooks<T>) {
- hooks.useOptions.push(useOptions);
-}
-useDefaultSettings.pluginName = pluginName;
-
-function useOptions<T extends object>(options: TableOptions<T>) {
- const pageSize = usePageSize();
-
- if (options.autoResetPage === undefined) {
- options.autoResetPage = false;
- }
-
- if (options.autoResetExpanded === undefined) {
- options.autoResetExpanded = false;
- }
-
- if (options.initialState === undefined) {
- options.initialState = {};
- }
-
- if (options.initialState.pageSize === undefined) {
- options.initialState.pageSize = pageSize;
- }
-
- return options;
-}
-
-export default useDefaultSettings;
diff --git a/frontend/src/components/toolbox/Button.tsx b/frontend/src/components/toolbox/Button.tsx
index 735ef3ca1..0a1d311e1 100644
--- a/frontend/src/components/toolbox/Button.tsx
+++ b/frontend/src/components/toolbox/Button.tsx
@@ -1,6 +1,3 @@
-import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { Button, ButtonProps, Text } from "@mantine/core";
import {
ComponentProps,
FunctionComponent,
@@ -8,6 +5,9 @@ import {
useCallback,
useState,
} from "react";
+import { Button, ButtonProps, Text } from "@mantine/core";
+import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
type ToolboxButtonProps = Omit<ButtonProps, "color" | "variant" | "leftIcon"> &
Omit<ComponentProps<"button">, "ref"> & {
@@ -24,7 +24,7 @@ const ToolboxButton: FunctionComponent<ToolboxButtonProps> = ({
<Button
color="dark"
variant="subtle"
- leftIcon={<FontAwesomeIcon icon={icon}></FontAwesomeIcon>}
+ leftSection={<FontAwesomeIcon icon={icon}></FontAwesomeIcon>}
{...props}
>
<Text size="xs">{children}</Text>
diff --git a/frontend/src/components/toolbox/Toolbox.module.scss b/frontend/src/components/toolbox/Toolbox.module.scss
new file mode 100644
index 000000000..10529fd27
--- /dev/null
+++ b/frontend/src/components/toolbox/Toolbox.module.scss
@@ -0,0 +1,9 @@
+.group {
+ @include light {
+ background-color: var(--mantine-color-gray-3);
+ }
+
+ @include dark {
+ background-color: var(--mantine-color-dark-5);
+ }
+}
diff --git a/frontend/src/components/toolbox/index.tsx b/frontend/src/components/toolbox/Toolbox.tsx
index 6995e111d..f67ac60b1 100644
--- a/frontend/src/components/toolbox/index.tsx
+++ b/frontend/src/components/toolbox/Toolbox.tsx
@@ -1,15 +1,7 @@
-import { createStyles, Group } from "@mantine/core";
import { FunctionComponent, PropsWithChildren } from "react";
+import { Group } from "@mantine/core";
import ToolboxButton, { ToolboxMutateButton } from "./Button";
-
-const useStyles = createStyles((theme) => ({
- group: {
- backgroundColor:
- theme.colorScheme === "light"
- ? theme.colors.gray[3]
- : theme.colors.dark[5],
- },
-}));
+import styles from "./Toolbox.module.scss";
declare type ToolboxComp = FunctionComponent<PropsWithChildren> & {
Button: typeof ToolboxButton;
@@ -17,9 +9,8 @@ declare type ToolboxComp = FunctionComponent<PropsWithChildren> & {
};
const Toolbox: ToolboxComp = ({ children }) => {
- const { classes } = useStyles();
return (
- <Group p={12} position="apart" className={classes.group}>
+ <Group p={12} justify="space-between" className={styles.group}>
{children}
</Group>
);