chore(web): relocations and cleanups (#957)

* delete manifest (vite-plugin-pwa generates it)

* fix upper case letter on screen components

* fix imports of screens components missing upper case

* remove default export from Base.tsx

* move RegexPlayground to settings import

* replace some relative path imports

* remove React and ununsed imports

* small alignments on vite.config.ts

* move Dashboard and Releases to screens

* move filters/index.tsx to filters/index.ts

* remove default export from APIKeyAddForm

* remove default export from FilterAddForm

* organize imports and exports for the router

* add .vscode workspace to gitignore

* some touchs on .gitignore file

* fix some eslint rules
This commit is contained in:
Fabricio Silva 2023-07-21 16:33:51 +01:00 committed by GitHub
parent 72bb2ddadb
commit c7ec93722b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 187 additions and 230 deletions

View file

@ -10,16 +10,12 @@
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#141415" />
<meta
name="description"
content="autobrr"
/>
<meta name="description" content="autobrr" />
<link rel="apple-touch-icon" href="/logo192.png" />
<link rel="apple-touch-icon" href="/apple-touch-icon-iphone-60x60.png">
<link rel="apple-touch-icon" sizes="60x60" href="/apple-touch-icon-ipad-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="/apple-touch-icon-iphone-retina-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="/apple-touch-icon-ipad-retina-152x152.png">
<!-- <link crossorigin="use-credentials" rel="manifest" href="/manifest.json" />-->
<link rel="apple-touch-icon" href="/apple-touch-icon-iphone-60x60.png" />
<link rel="apple-touch-icon" sizes="60x60" href="/apple-touch-icon-ipad-76x76.png" />
<link rel="apple-touch-icon" sizes="114x114" href="/apple-touch-icon-iphone-retina-120x120.png" />
<link rel="apple-touch-icon" sizes="144x144" href="/apple-touch-icon-ipad-retina-152x152.png" />
<title>autobrr</title>
<base href="{{.BaseUrl}}">
<script>

View file

@ -1,25 +0,0 @@
{
"short_name": "autobrr",
"name": "autobrr",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": "/",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View file

@ -3,9 +3,9 @@
* SPDX-License-Identifier: GPL-2.0-or-later
*/
import { baseUrl, sseBaseUrl } from "../utils";
import { AuthContext } from "../utils/Context";
import { GithubRelease } from "../types/Update";
import { baseUrl, sseBaseUrl } from "@utils";
import { AuthContext } from "@utils/Context";
import { GithubRelease } from "@app/types/Update";
interface ConfigType {
body?: BodyInit | Record<string, unknown> | unknown;

View file

@ -9,10 +9,10 @@ import { ArrowPathIcon, CheckIcon } from "@heroicons/react/24/solid";
import { ClockIcon, ExclamationCircleIcon, NoSymbolIcon } from "@heroicons/react/24/outline";
import { classNames, simplifyDate } from "@utils";
import { Tooltip } from "../tooltips/Tooltip";
import { Tooltip } from "@components/tooltips/Tooltip";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { APIClient } from "@api/APIClient";
import { filterKeys } from "@screens/filters/list";
import { filterKeys } from "@screens/filters/List";
import { toast } from "react-hot-toast";
import Toast from "@components/notifications/Toast";
import { RingResizeSpinner } from "@components/Icons";

View file

@ -3,7 +3,7 @@
* SPDX-License-Identifier: GPL-2.0-or-later
*/
import React, { Fragment } from "react";
import { Fragment } from "react";
import { Field, FieldProps } from "formik";
import { Listbox, Transition } from "@headlessui/react";
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/react/24/solid";

View file

@ -8,7 +8,7 @@ import { Field } from "formik";
import Select, { components, ControlProps, InputProps, MenuProps, OptionProps } from "react-select";
import { OptionBasicTyped } from "@domain/constants";
import CreatableSelect from "react-select/creatable";
import { CustomTooltip } from "../tooltips/CustomTooltip";
import { CustomTooltip } from "@components/tooltips/CustomTooltip";
interface SelectFieldProps<T> {
name: string;

View file

@ -9,7 +9,7 @@ import { Field } from "formik";
import { Switch as HeadlessSwitch } from "@headlessui/react";
import { classNames } from "@utils";
import { CustomTooltip } from "../tooltips/CustomTooltip";
import { CustomTooltip } from "@components/tooltips/CustomTooltip";
type SwitchProps<V = unknown> = {
label?: string
@ -137,4 +137,4 @@ const SwitchGroup = ({
</HeadlessSwitch.Group>
);
export { SwitchGroup };
export { SwitchGroup };

View file

@ -3,7 +3,7 @@
* SPDX-License-Identifier: GPL-2.0-or-later
*/
import React, { FC, forwardRef, ReactNode } from "react";
import { FC, forwardRef, ReactNode } from "react";
import { DeepMap, FieldError, Path, RegisterOptions, UseFormRegister } from "react-hook-form";
import { classNames, get } from "@utils";
import { useToggle } from "@hooks/hooks";
@ -194,4 +194,3 @@ export const PasswordInput = <TFormValues extends Record<string, unknown>>({
</div>
);
};

View file

@ -3,13 +3,13 @@
* SPDX-License-Identifier: GPL-2.0-or-later
*/
import React, { FC, Fragment } from "react";
import { FC, Fragment, MutableRefObject } from "react";
import { Dialog, Transition } from "@headlessui/react";
import { ExclamationTriangleIcon } from "@heroicons/react/24/solid";
interface DeleteModalProps {
isOpen: boolean;
buttonRef: React.MutableRefObject<HTMLElement | null> | undefined;
buttonRef: MutableRefObject<HTMLElement | null> | undefined;
toggle: () => void;
deleteAction: () => void;
title: string;
@ -94,4 +94,4 @@ export const DeleteModal: FC<DeleteModalProps> = ({ isOpen, buttonRef, toggle, d
</div>
</Dialog>
</Transition.Root>
);
);

View file

@ -3,15 +3,15 @@
* SPDX-License-Identifier: GPL-2.0-or-later
*/
import React, { Fragment, useRef } from "react";
import { Fragment, useRef, ReactNode, ReactElement } from "react";
import { XMarkIcon } from "@heroicons/react/24/solid";
import { Dialog, Transition } from "@headlessui/react";
import { Form, Formik } from "formik";
import type { FormikValues } from "formik";
import DEBUG from "../debug";
import DEBUG from "@components/debug";
import { useToggle } from "@hooks/hooks";
import { DeleteModal } from "../modals";
import { DeleteModal } from "@components/modals";
import { classNames } from "@utils";
interface SlideOverProps<DataType> {
@ -21,14 +21,14 @@ interface SlideOverProps<DataType> {
onSubmit: (values?: DataType) => void;
isOpen: boolean;
toggle: () => void;
children?: (values: DataType) => React.ReactNode;
children?: (values: DataType) => ReactNode;
deleteAction?: () => void;
type: "CREATE" | "UPDATE";
testFn?: (data: unknown) => void;
isTesting?: boolean;
isTestSuccessful?: boolean;
isTestError?: boolean;
extraButtons?: (values: DataType) => React.ReactNode;
extraButtons?: (values: DataType) => ReactNode;
}
function SlideOver<DataType>({
@ -46,7 +46,7 @@ function SlideOver<DataType>({
isTestSuccessful,
isTestError,
extraButtons
}: SlideOverProps<DataType>): React.ReactElement {
}: SlideOverProps<DataType>): ReactElement {
const cancelModalButtonRef = useRef<HTMLInputElement | null>(null);
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
@ -215,4 +215,4 @@ function SlideOver<DataType>({
);
}
export { SlideOver };
export { SlideOver };

View file

@ -4,31 +4,18 @@
*/
import { BrowserRouter, Route, Routes } from "react-router-dom";
import { Login } from "@screens/auth/login";
import { Onboarding } from "@screens/auth/onboarding";
import Base from "@screens/Base";
import { Dashboard } from "@screens/dashboard";
import { FilterDetails, Filters } from "@screens/filters";
import { Logs } from "@screens/Logs";
import { Releases } from "@screens/releases";
import Settings from "@screens/Settings";
import {
APISettings,
ApplicationSettings,
DownloadClientSettings,
FeedSettings,
IndexerSettings,
IrcSettings,
LogSettings,
NotificationSettings,
ReleaseSettings
} from "@screens/settings/index";
import { RegexPlayground } from "@screens/settings/RegexPlayground";
import { NotFound } from "@components/alerts/NotFound";
import { baseUrl } from "@utils";
import { NotFound } from "@components/alerts/NotFound";
import { Base } from "@screens/Base";
import { Dashboard } from "@screens/Dashboard";
import { Logs } from "@screens/Logs";
import { Filters, FilterDetails } from "@screens/filters";
import { Releases } from "@screens/Releases";
import { Settings } from "@screens/Settings";
import * as SettingsSubPage from "@screens/settings/index";
import { Login, Onboarding } from "@screens/auth";
export const LocalRouter = ({ isLoggedIn }: { isLoggedIn: boolean }) => (
<BrowserRouter basename={baseUrl()}>
{isLoggedIn ? (
@ -43,16 +30,16 @@ export const LocalRouter = ({ isLoggedIn }: { isLoggedIn: boolean }) => (
<Route path=":filterId/*" element={<FilterDetails />} />
</Route>
<Route path="settings" element={<Settings />}>
<Route index element={<ApplicationSettings />} />
<Route path="logs" element={<LogSettings />} />
<Route path="api-keys" element={<APISettings />} />
<Route path="indexers" element={<IndexerSettings />} />
<Route path="feeds" element={<FeedSettings />} />
<Route path="irc" element={<IrcSettings />} />
<Route path="clients" element={<DownloadClientSettings />} />
<Route path="notifications" element={<NotificationSettings />} />
<Route path="releases" element={<ReleaseSettings />} />
<Route path="regex-playground" element={<RegexPlayground />} />
<Route index element={<SettingsSubPage.Application />} />
<Route path="logs" element={<SettingsSubPage.Logs />} />
<Route path="api-keys" element={<SettingsSubPage.Api />} />
<Route path="indexers" element={<SettingsSubPage.Indexer />} />
<Route path="feeds" element={<SettingsSubPage.Feed />} />
<Route path="irc" element={<SettingsSubPage.Irc />} />
<Route path="clients" element={<SettingsSubPage.DownloadClient />} />
<Route path="notifications" element={<SettingsSubPage.Notification />} />
<Route path="releases" element={<SettingsSubPage.Release />} />
<Route path="regex-playground" element={<SettingsSubPage.RegexPlayground />} />
</Route>
</Route>
</Routes>
@ -63,4 +50,4 @@ export const LocalRouter = ({ isLoggedIn }: { isLoggedIn: boolean }) => (
</Routes>
)}
</BrowserRouter>
);
);

View file

@ -15,14 +15,14 @@ import { useNavigate } from "react-router-dom";
import { APIClient } from "@api/APIClient";
import DEBUG from "@components/debug";
import Toast from "@components/notifications/Toast";
import { filterKeys } from "@screens/filters/list";
import { filterKeys } from "@screens/filters/List";
interface filterAddFormProps {
isOpen: boolean;
toggle: () => void;
}
function FilterAddForm({ isOpen, toggle }: filterAddFormProps) {
export function FilterAddForm({ isOpen, toggle }: filterAddFormProps) {
const queryClient = useQueryClient();
const navigate = useNavigate();
const mutation = useMutation({
@ -169,5 +169,3 @@ function FilterAddForm({ isOpen, toggle }: filterAddFormProps) {
</Transition.Root>
);
}
export default FilterAddForm;

View file

@ -3,7 +3,7 @@
* SPDX-License-Identifier: GPL-2.0-or-later
*/
export { default as FilterAddForm } from "./filters/FilterAddForm";
export { FilterAddForm } from "./filters/FilterAddForm";
export { DownloadClientAddForm, DownloadClientUpdateForm } from "./settings/DownloadClientForms";
export { IndexerAddForm, IndexerUpdateForm } from "./settings/IndexerForms";

View file

@ -21,20 +21,20 @@ interface apiKeyAddFormProps {
toggle: () => void;
}
function APIKeyAddForm({ isOpen, toggle }: apiKeyAddFormProps) {
export function APIKeyAddForm({ isOpen, toggle }: apiKeyAddFormProps) {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (apikey: APIKey) => APIClient.apikeys.create(apikey),
onSuccess: (_, key) => {
queryClient.invalidateQueries({ queryKey: apiKeys.lists() });
toast.custom((t) => <Toast type="success" body={`API key ${key.name} was added`} t={t}/>);
toggle();
}
});
const handleSubmit = (data: unknown) => mutation.mutate(data as APIKey);
const validate = (values: FormikValues) => {
const errors = {} as FormikErrors<FormikValues>;
@ -156,5 +156,3 @@ function APIKeyAddForm({ isOpen, toggle }: apiKeyAddFormProps) {
</Transition.Root>
);
}
export default APIKeyAddForm;

View file

@ -3,7 +3,7 @@
* SPDX-License-Identifier: GPL-2.0-or-later
*/
import React, { Fragment, useRef, useState } from "react";
import { Fragment, useRef, useState, ReactElement } from "react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { Dialog, Transition } from "@headlessui/react";
import { XMarkIcon } from "@heroicons/react/24/solid";
@ -24,7 +24,7 @@ import {
SwitchGroupWide,
TextFieldWide
} from "@components/inputs";
import DownloadClient, { clientKeys } from "@screens/settings/DownloadClient";
import { clientKeys } from "@screens/settings/DownloadClient";
import { SelectFieldWide } from "@components/inputs/input_wide";
interface InitialValuesSettings {
@ -181,7 +181,7 @@ function FormFieldsPorla() {
required={true}
/>
<SwitchGroupWide name="tls" label="TLS" />
<PasswordFieldWide name="settings.apikey" label="Auth token" required={true}/>
@ -322,7 +322,7 @@ function FormFieldsSabnzbd() {
}
export interface componentMapType {
[key: string]: React.ReactElement;
[key: string]: ReactElement;
}
export const componentMap: componentMapType = {

View file

@ -3,7 +3,7 @@
* SPDX-License-Identifier: GPL-2.0-or-later
*/
import React, { Fragment, useState } from "react";
import { Fragment, useState } from "react";
import { toast } from "react-hot-toast";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import Select, { components, ControlProps, InputProps, MenuProps, OptionProps } from "react-select";
@ -25,8 +25,8 @@ import { feedKeys } from "@screens/settings/Feed";
import { indexerKeys } from "@screens/settings/Indexer";
const Input = (props: InputProps) => (
<components.Input
{...props}
<components.Input
{...props}
inputClassName="outline-none border-none shadow-none focus:ring-transparent"
className="text-gray-400 dark:text-gray-100"
children={props.children}
@ -34,15 +34,15 @@ const Input = (props: InputProps) => (
);
const Control = (props: ControlProps) => (
<components.Control
{...props}
<components.Control
{...props}
className="p-1 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:text-gray-100 sm:text-sm"
children={props.children}
/>
);
const Menu = (props: MenuProps) => (
<components.Menu
<components.Menu
{...props}
className="dark:bg-gray-800 border border-gray-300 dark:border-gray-700 dark:text-gray-400 rounded-md shadow-sm cursor-pointer"
children={props.children}
@ -50,7 +50,7 @@ const Menu = (props: MenuProps) => (
);
const Option = (props: OptionProps) => (
<components.Option
<components.Option
{...props}
className="dark:text-gray-400 dark:bg-gray-800 dark:hover:bg-gray-900 dark:focus:bg-gray-900 cursor-pointer"
children={props.children}
@ -521,7 +521,7 @@ export function IndexerAddForm({ isOpen, toggle }: AddProps) {
options={data && data.sort((a, b) => a.name.localeCompare(b.name)).map(v => ({
label: v.name,
value: v.identifier
}))}
}))}
/>
)}
</Field>
@ -634,7 +634,7 @@ function TestApiButton({ values, show }: TestApiButtonProps) {
id: values.id,
api_key: values.settings.api_key
};
if (values.settings.api_user) {
req.api_user = values.settings.api_user;
}
@ -833,4 +833,4 @@ export function IndexerUpdateForm({ isOpen, toggle, indexer }: UpdateProps) {
)}
</SlideOver>
);
}
}

View file

@ -3,7 +3,7 @@
* SPDX-License-Identifier: GPL-2.0-or-later
*/
import React, { Fragment } from "react";
import { Fragment } from "react";
import { Link, NavLink, Outlet } from "react-router-dom";
import { Disclosure, Menu, Transition } from "@headlessui/react";
import { ArrowTopRightOnSquareIcon, UserIcon } from "@heroicons/react/24/solid";
@ -30,7 +30,7 @@ const nav: Array<NavItem> = [
{ name: "Logs", path: "/logs" }
];
export default function Base() {
export const Base = () => {
const authContext = AuthContext.useValue();
const { data } = useQuery({
@ -250,4 +250,4 @@ export default function Base() {
<Outlet />
</div>
);
}
};

View file

@ -3,8 +3,8 @@
* SPDX-License-Identifier: GPL-2.0-or-later
*/
import { Stats } from "./Stats";
import { ActivityTable } from "./ActivityTable";
import { Stats } from "./dashboard/Stats";
import { ActivityTable } from "./dashboard/ActivityTable";
export const Dashboard = () => (
<main className="py-10">
@ -13,4 +13,4 @@ export const Dashboard = () => (
<ActivityTable />
</div>
</main>
);
);

View file

@ -4,7 +4,6 @@
*/
import { Fragment, useEffect, useRef, useState } from "react";
import { ExclamationTriangleIcon } from "@heroicons/react/24/solid";
import format from "date-fns/format";
import { DebounceInput } from "react-debounce-input";
import {
@ -49,7 +48,7 @@ export const Logs = () => {
const [logs, setLogs] = useState<LogEvent[]>([]);
const [searchFilter, setSearchFilter] = useState("");
const [regexPattern, setRegexPattern] = useState<RegExp | null>(null);
const [_regexPattern, setRegexPattern] = useState<RegExp | null>(null);
const [filteredLogs, setFilteredLogs] = useState<LogEvent[]>([]);
const [isInvalidRegex, setIsInvalidRegex] = useState(false);
@ -170,7 +169,7 @@ export const Logs = () => {
};
export const LogFiles = () => {
const { isLoading, data } = useQuery({
const { data } = useQuery({
queryKey: ["log-files"],
queryFn: () => APIClient.logs.files(),
retry: false,
@ -323,7 +322,7 @@ const LogsDropdown = () => {
>
<div className="p-3">
<Menu.Item>
{({ active }) => (
{() => (
<Checkbox
label="Scroll to bottom on new message"
value={settings.scrollOnNewLog}
@ -332,7 +331,7 @@ const LogsDropdown = () => {
)}
</Menu.Item>
<Menu.Item>
{({ active }) => (
{() => (
<Checkbox
label="Indent log lines"
description="Indent each log line according to their respective starting position."
@ -342,7 +341,7 @@ const LogsDropdown = () => {
)}
</Menu.Item>
<Menu.Item>
{({ active }) => (
{() => (
<Checkbox
label="Hide wrapped text"
description="Hides text that is meant to be wrapped."

View file

@ -3,7 +3,7 @@
* SPDX-License-Identifier: GPL-2.0-or-later
*/
import { ReleaseTable } from "./ReleaseTable";
import { ReleaseTable } from "./releases/ReleaseTable";
export const Releases = () => (
<main>
@ -16,4 +16,4 @@ export const Releases = () => (
<ReleaseTable />
</div>
</main>
);
);

View file

@ -83,7 +83,7 @@ function SidebarNav({ subNavigation }: SidebarNavProps) {
);
}
export default function Settings() {
export function Settings() {
return (
<main>
<header className="py-10">
@ -103,4 +103,3 @@ export default function Settings() {
</main>
);
}

View file

@ -23,7 +23,7 @@ export const Onboarding = () => {
if (!values.username)
obj.username = "Required";
if (!values.password1)
obj.password1 = "Required";
@ -32,7 +32,7 @@ export const Onboarding = () => {
if (values.password1 !== values.password2)
obj.password2 = "Passwords don't match!";
return obj;
};
@ -83,4 +83,3 @@ export const Onboarding = () => {
</div>
);
};

View file

@ -3,5 +3,5 @@
* SPDX-License-Identifier: GPL-2.0-or-later
*/
export { default as Filters } from "./list";
export { default as FilterDetails } from "./details";
export { Login } from "./Login";
export { Onboarding } from "./Onboarding";

View file

@ -3,7 +3,7 @@
* SPDX-License-Identifier: GPL-2.0-or-later
*/
import React, { Fragment, useEffect, useRef, useState } from "react";
import { Fragment, useEffect, useRef, useState } from "react";
import { useMutation, useQuery } from "@tanstack/react-query";
import { Field, FieldArray, FieldProps, FormikValues, useFormikContext } from "formik";
import { Dialog, Switch as SwitchBasic, Transition } from "@headlessui/react";
@ -24,7 +24,7 @@ import { EmptyListState } from "@components/emptystates";
import { useToggle } from "@hooks/hooks";
import { classNames } from "@utils";
import { DeleteModal } from "@components/modals";
import { CollapsableSection } from "./details";
import { CollapsableSection } from "./Details";
import { TextArea } from "@components/inputs/input";
import Toast from "@components/notifications/Toast";
@ -121,7 +121,7 @@ interface TypeFormProps {
const TypeForm = ({ action, idx, clients }: TypeFormProps) => {
const { setFieldValue } = useFormikContext();
const resetClientField = (action: Action, idx: number, prevActionType: string): void => {
const fieldName = `actions.${idx}.client_id`;
@ -728,4 +728,4 @@ function FilterActionsItem({ action, clients, idx, initialEdit, remove }: Filter
)}
</li>
);
}
}

View file

@ -3,7 +3,7 @@
* SPDX-License-Identifier: GPL-2.0-or-later
*/
import React, { useEffect, useRef } from "react";
import { useEffect, useRef, ReactNode } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { NavLink, Route, Routes, useLocation, useNavigate, useParams } from "react-router-dom";
import { toast } from "react-hot-toast";
@ -11,7 +11,6 @@ import { Form, Formik, FormikValues, useFormikContext } from "formik";
import { z } from "zod";
import { toFormikValidationSchema } from "zod-formik-adapter";
import { ChevronDownIcon, ChevronRightIcon } from "@heroicons/react/24/solid";
import TextareaAutosize from "react-textarea-autosize";
import {
CODECS_OPTIONS,
@ -47,10 +46,9 @@ import DEBUG from "@components/debug";
import Toast from "@components/notifications/Toast";
import { DeleteModal } from "@components/modals";
import { TitleSubtitle } from "@components/headings";
import { RegexTextAreaField, TextArea } from "@components/inputs/input";
import { TextAreaAutoResize } from "../../components/inputs/input";
import { FilterActions } from "./action";
import { filterKeys } from "./list";
import { RegexTextAreaField, TextArea, TextAreaAutoResize } from "@components/inputs/input";
import { FilterActions } from "./Action";
import { filterKeys } from "./List";
interface tabType {
name: string;
@ -214,7 +212,7 @@ const schema = z.object({
actions: z.array(actionSchema)
});
export default function FilterDetails() {
export function FilterDetails() {
const queryClient = useQueryClient();
const navigate = useNavigate();
const { filterId } = useParams<{ filterId: string }>();
@ -223,6 +221,7 @@ export default function FilterDetails() {
navigate("/filters");
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const id = parseInt(filterId!);
const { isLoading, data: filter } = useQuery({
@ -695,7 +694,7 @@ function WarningAlert({ text, alert, colors }: WarningAlertProps) {
interface CollapsableSectionProps {
title: string;
subtitle: string;
children: React.ReactNode;
children: ReactNode;
defaultOpen?: boolean;
}
@ -795,4 +794,3 @@ export function External() {
</div>
);
}

View file

@ -77,26 +77,26 @@ const FilterListReducer = (state: FilterListState, action: Actions): FilterListS
}
};
export default function Filters() {
export function Filters() {
const queryClient = useQueryClient();
const [createFilterIsOpen, setCreateFilterIsOpen] = useState(false);
const toggleCreateFilter = () => {
setCreateFilterIsOpen(!createFilterIsOpen);
};
};
const [showImportModal, setShowImportModal] = useState(false);
const [importJson, setImportJson] = useState("");
// This function handles the import of a filter from a JSON string
const handleImportJson = async () => {
try {
const importedData = JSON.parse(importJson);
// Extract the filter data and name from the imported object
const importedFilter = importedData.data;
const filterName = importedData.name;
// Check if the required properties are present and add them with default values if they are missing
const requiredProperties = ["resolutions", "sources", "codecs", "containers"];
requiredProperties.forEach((property) => {
@ -104,10 +104,10 @@ export default function Filters() {
importedFilter[property] = [];
}
});
// Fetch existing filters from the API
const existingFilters = await APIClient.filters.getAll();
// Create a unique filter title by appending an incremental number if title is taken by another filter
let nameCounter = 0;
let uniqueFilterName = filterName;
@ -115,18 +115,18 @@ export default function Filters() {
nameCounter++;
uniqueFilterName = `${filterName}-${nameCounter}`;
}
// Create a new filter using the API
const newFilter: Filter = {
...importedFilter,
name: uniqueFilterName
};
await APIClient.filters.create(newFilter);
// Update the filter list
queryClient.invalidateQueries({ queryKey: filterKeys.lists() });
toast.custom((t) => <Toast type="success" body="Filter imported successfully." t={t} />);
setShowImportModal(false);
} catch (error) {
@ -135,7 +135,7 @@ export default function Filters() {
toast.custom((t) => <Toast type="error" body="Failed to import JSON data. Please check your input." t={t} />);
}
};
return (
<main>
<FilterAddForm isOpen={createFilterIsOpen} toggle={toggleCreateFilter} />
@ -369,9 +369,9 @@ const FilterItemDropdown = ({ filter, onToggle }: FilterItemDropdownProps) => {
external_webhook_data: any;
external_webhook_expect_status: any;
};
const completeFilter = await APIClient.filters.getByID(filter.id) as Partial<CompleteFilterType>;
// Extract the filter name and remove unwanted properties
const title = completeFilter.name;
delete completeFilter.name;
@ -389,7 +389,7 @@ const FilterItemDropdown = ({ filter, onToggle }: FilterItemDropdownProps) => {
delete completeFilter.external_webhook_host;
delete completeFilter.external_webhook_data;
delete completeFilter.external_webhook_expect_status;
// Remove properties with default values from the exported filter to minimize the size of the JSON string
["enabled", "priority", "smart_episode", "resolutions", "sources", "codecs", "containers", "tags_match_logic", "except_tags_match_logic"].forEach((key) => {
const value = completeFilter[key as keyof CompleteFilterType];
@ -400,7 +400,7 @@ const FilterItemDropdown = ({ filter, onToggle }: FilterItemDropdownProps) => {
} else if (["tags_match_logic", "except_tags_match_logic"].includes(key) && value === "ANY") {
delete completeFilter[key as keyof CompleteFilterType];
}
});
});
// Create a JSON string from the filter data, including a name and version
const json = JSON.stringify(
@ -412,7 +412,7 @@ const FilterItemDropdown = ({ filter, onToggle }: FilterItemDropdownProps) => {
null,
4
);
const finalJson = discordFormat ? "```JSON\n" + json + "\n```" : json;
const copyTextToClipboard = (text: string) => {
@ -423,7 +423,7 @@ const FilterItemDropdown = ({ filter, onToggle }: FilterItemDropdownProps) => {
document.body.appendChild(textarea);
textarea.focus();
textarea.select();
try {
const successful = document.execCommand("copy");
if (successful) {
@ -435,10 +435,10 @@ const FilterItemDropdown = ({ filter, onToggle }: FilterItemDropdownProps) => {
console.error("Unable to copy text", err);
toast.custom((t) => <Toast type="error" body="Failed to copy JSON to clipboard." t={t} />);
}
document.body.removeChild(textarea);
};
if (navigator.clipboard) {
navigator.clipboard.writeText(finalJson).then(() => {
toast.custom((t) => <Toast type="success" body="Filter copied to clipboard." t={t} />);
@ -448,7 +448,7 @@ const FilterItemDropdown = ({ filter, onToggle }: FilterItemDropdownProps) => {
} else {
copyTextToClipboard(finalJson);
}
} catch (error) {
console.error(error);
toast.custom((t) => <Toast type="error" body="Failed to get filter data." t={t} />);
@ -567,7 +567,7 @@ const FilterItemDropdown = ({ filter, onToggle }: FilterItemDropdownProps) => {
)}
aria-hidden="true"
/>
Export JSON to Discord
Export JSON to Discord
</button>
)}
</Menu.Item>
@ -726,7 +726,7 @@ function FilterListItem({ filter, values, idx }: FilterListItemProps) {
)
}
>
Actions: {filter.actions_count}
Actions: {filter.actions_count}
</span>
</span>
{filter.actions_count === 0 && (
@ -802,7 +802,7 @@ function FilterIndexers({ indexers }: FilterIndexersProps) {
className="mr-2 inline-flex items-center px-2.5 py-0.5 rounded-md text-sm font-medium bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-400"
title={res.map(v => v.name).toString()}
>
+{indexers.length - 2}
+{indexers.length - 2}
</span>
</>
);

View file

@ -0,0 +1,7 @@
/*
* Copyright (c) 2021 - 2023, Ludvig Lundgren and the autobrr contributors.
* SPDX-License-Identifier: GPL-2.0-or-later
*/
export { Filters } from "./List";
export { FilterDetails } from "./Details";

View file

@ -75,4 +75,4 @@ function ActionSettings() {
);
}
export default ActionSettings;
export default ActionSettings;

View file

@ -10,7 +10,7 @@ import { TrashIcon } from "@heroicons/react/24/outline";
import { KeyField } from "@components/fields/text";
import { DeleteModal } from "@components/modals";
import APIKeyAddForm from "@forms/settings/APIKeyAddForm";
import { APIKeyAddForm } from "@forms/settings/APIKeyAddForm";
import Toast from "@components/notifications/Toast";
import { APIClient } from "@api/APIClient";
import { useToggle } from "@hooks/hooks";

View file

@ -51,7 +51,7 @@ function useSort(items: ListItemProps["clients"][], config?: SortConfig) {
sortableItems.sort((a, b) => {
const aValue = sortConfig.key === "enabled" ? (a[sortConfig.key] ?? false) as number | boolean | string : a[sortConfig.key] as number | boolean | string;
const bValue = sortConfig.key === "enabled" ? (b[sortConfig.key] ?? false) as number | boolean | string : b[sortConfig.key] as number | boolean | string;
if (aValue < bValue) {
return sortConfig.direction === "ascending" ? -1 : 1;
}
@ -59,7 +59,7 @@ function useSort(items: ListItemProps["clients"][], config?: SortConfig) {
return sortConfig.direction === "ascending" ? 1 : -1;
}
return 0;
});
});
return sortableItems;
}, [items, sortConfig]);
@ -80,7 +80,7 @@ function useSort(items: ListItemProps["clients"][], config?: SortConfig) {
if (!sortConfig || sortConfig.key !== key) {
return "";
}
return sortConfig.direction === "ascending" ? "↑" : "↓";
};
@ -194,7 +194,7 @@ function DownloadClientSettings() {
onClick={() => sortedClients.requestSort("enabled")}>
Enabled <span className="sort-indicator">{sortedClients.getSortIndicator("enabled")}</span>
</div>
<div
<div
className="col-span-6 sm:col-span-4 lg:col-span-4 pl-12 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer"
onClick={() => sortedClients.requestSort("name")}
>
@ -225,4 +225,4 @@ function DownloadClientSettings() {
);
}
export default DownloadClientSettings;
export default DownloadClientSettings;

View file

@ -50,7 +50,7 @@ function useSort(items: ListItemProps["feed"][], config?: SortConfig) {
sortableItems.sort((a, b) => {
const aValue = sortConfig.key === "enabled" ? (a[sortConfig.key] ?? false) as number | boolean | string : a[sortConfig.key] as number | boolean | string;
const bValue = sortConfig.key === "enabled" ? (b[sortConfig.key] ?? false) as number | boolean | string : b[sortConfig.key] as number | boolean | string;
if (aValue < bValue) {
return sortConfig.direction === "ascending" ? -1 : 1;
}
@ -58,7 +58,7 @@ function useSort(items: ListItemProps["feed"][], config?: SortConfig) {
return sortConfig.direction === "ascending" ? 1 : -1;
}
return 0;
});
});
return sortableItems;
}, [items, sortConfig]);
@ -73,13 +73,13 @@ function useSort(items: ListItemProps["feed"][], config?: SortConfig) {
}
setSortConfig({ key, direction });
};
const getSortIndicator = (key: keyof ListItemProps["feed"]) => {
if (!sortConfig || sortConfig.key !== key) {
return "";
}
return sortConfig.direction === "ascending" ? "↑" : "↓";
};

View file

@ -40,7 +40,7 @@ function useSort(items: ListItemProps["indexer"][], config?: SortConfig) {
sortableItems.sort((a, b) => {
const aValue = sortConfig.key === "enabled" ? (a[sortConfig.key] ?? false) as number | boolean | string : a[sortConfig.key] as number | boolean | string;
const bValue = sortConfig.key === "enabled" ? (b[sortConfig.key] ?? false) as number | boolean | string : b[sortConfig.key] as number | boolean | string;
if (aValue < bValue) {
return sortConfig.direction === "ascending" ? -1 : 1;
}
@ -48,7 +48,7 @@ function useSort(items: ListItemProps["indexer"][], config?: SortConfig) {
return sortConfig.direction === "ascending" ? 1 : -1;
}
return 0;
});
});
return sortableItems;
}, [items, sortConfig]);
@ -69,7 +69,7 @@ function useSort(items: ListItemProps["indexer"][], config?: SortConfig) {
if (!sortConfig || sortConfig.key !== key) {
return "";
}
return sortConfig.direction === "ascending" ? "↑" : "↓";
};

View file

@ -11,7 +11,7 @@ import { APIClient } from "@api/APIClient";
import { GithubRelease } from "@app/types/Update";
import Toast from "@components/notifications/Toast";
import { LogLevelOptions, SelectOption } from "@domain/constants";
import { LogFiles } from "../Logs";
import { LogFiles } from "@screens/Logs";
interface RowItemProps {
label: string;
@ -195,4 +195,4 @@ function LogSettings() {
);
}
export default LogSettings;
export default LogSettings;

View file

@ -123,13 +123,13 @@ function ListItem({ notification }: ListItemProps) {
queryClient.invalidateQueries({ queryKey: notificationKeys.lists() });
}
});
const onToggleMutation = (newState: boolean) => {
mutation.mutate({
...notification,
enabled: newState
});
};
};
return (
<li key={notification.id} className="text-gray-500 dark:text-gray-400">

View file

@ -5,14 +5,14 @@
import { useRef, useState } from "react";
export const RegexPlayground = () => {
const RegexPlayground = () => {
const regexRef = useRef<HTMLInputElement>(null);
const [output, setOutput] = useState<Array<React.ReactElement>>();
const onInput = (text: string) => {
if (!regexRef || !regexRef.current)
return;
const regexp = new RegExp(regexRef.current.value, "g");
const results: Array<React.ReactElement> = [];
@ -50,7 +50,7 @@ export const RegexPlayground = () => {
</span>
);
}
if (lastIndex > 0)
results.push(<br key={`line-delim-${index}`}/>);
});
@ -107,4 +107,6 @@ export const RegexPlayground = () => {
</div>
</div>
);
};
};
export default RegexPlayground;

View file

@ -3,7 +3,7 @@
* SPDX-License-Identifier: GPL-2.0-or-later
*/
import React, { useRef, useState } from "react";
import { useRef, useState } from "react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { toast } from "react-hot-toast";
@ -147,4 +147,4 @@ function DeleteReleases() {
);
}
export default ReleaseSettings;
export default ReleaseSettings;

View file

@ -3,12 +3,13 @@
* SPDX-License-Identifier: GPL-2.0-or-later
*/
export { default as APISettings } from "./Api";
export { default as ApplicationSettings } from "./Application";
export { default as DownloadClientSettings } from "./DownloadClient";
export { default as FeedSettings } from "./Feed";
export { default as IndexerSettings } from "./Indexer";
export { default as IrcSettings } from "./Irc";
export { default as LogSettings } from "./Logs";
export { default as NotificationSettings } from "./Notifications";
export { default as ReleaseSettings } from "./Releases";
export { default as Api } from "./Api";
export { default as Application } from "./Application";
export { default as DownloadClient } from "./DownloadClient";
export { default as Feed } from "./Feed";
export { default as Indexer } from "./Indexer";
export { default as Irc } from "./Irc";
export { default as Logs } from "./Logs";
export { default as Notification } from "./Notifications";
export { default as Release } from "./Releases";
export { default as RegexPlayground } from "./RegexPlayground";

View file

@ -30,4 +30,4 @@
"./types",
"vite.config.ts"
],
}
}

View file

@ -1,15 +1,14 @@
import { fileURLToPath, URL } from "node:url";
import react from "@vitejs/plugin-react-swc";
import { defineConfig, loadEnv, ConfigEnv } from "vite";
import { VitePWA } from "vite-plugin-pwa";
import { defineConfig, loadEnv } from "vite";
import react from "@vitejs/plugin-react-swc";
import svgr from "vite-plugin-svgr";
// https://vitejs.dev/config/
export default ({ mode }: { mode: any }) => {
export default ({ mode }: ConfigEnv) => {
// early load .env file
process.env = { ...process.env, ...loadEnv(mode, process.cwd()) };
// import.meta.env.VITE_NAME available here with: process.env.VITE_NAME
process.env = { ...process.env, ...loadEnv(mode, process.cwd()) };
return defineConfig({
base: "",