From 2fed48e0dd06f7a0b57ce517528d998fe11341a5 Mon Sep 17 00:00:00 2001 From: stacksmash76 <98354295+stacksmash76@users.noreply.github.com> Date: Sun, 10 Sep 2023 12:35:43 +0200 Subject: [PATCH] enhancement(web): add react suspense and improve DX (#1089) * add react suspense, fix broken stuff, clean up code, improve DX enhancement: added react suspense + spinner to show loading (still can be added in certain places) chore: cleaned up Header/NavBar code chore: cleaned up DeleteModal code chore: cleaned up other relevant code enhancement: changed remove button style to be much more pleasant (see e.g. filter tabs) fix: made active tab on filters page to be blue (as it should've been) when active fix: fixed ghost delimiter which was only visible when DeleteModal was active in FormButtonGroup chore: removed most of linter warnings/errors fix: fixed incorrect/double modal transition in FilterExternalItem fix: fixed incorrect z-height on Options popover in Settings/IRC (would've been visible when Add new was clicked) enhancement: improved robustness of all Context classes to support seamless new-feature expansion (#866) enhancement: improved expand logic (see #994 comments) * reverted irc expand view to previous design * forgot to propagate previous z-height fix * jinxed it * add license header to new files --------- Co-authored-by: martylukyy <35452459+martylukyy@users.noreply.github.com> Co-authored-by: Kyle Sanderson --- web/src/App.tsx | 5 +- web/src/components/SectionLoader.tsx | 32 +++ web/src/components/header/Header.tsx | 86 ++++++ web/src/components/header/LeftNav.tsx | 59 ++++ web/src/components/header/MobileNav.tsx | 45 ++++ web/src/components/header/RightNav.tsx | 98 +++++++ web/src/components/header/_shared.ts | 21 ++ web/src/components/header/index.tsx | 6 + web/src/components/modals/index.tsx | 124 +++++---- web/src/components/panels/index.tsx | 1 + web/src/domain/routes.tsx | 25 +- .../forms/settings/DownloadClientForms.tsx | 1 + web/src/screens/Base.tsx | 253 ------------------ web/src/screens/Settings.tsx | 12 +- web/src/screens/filters/Action.tsx | 19 +- web/src/screens/filters/Details.tsx | 210 ++++++++------- web/src/screens/filters/External.tsx | 210 +++++++-------- web/src/screens/filters/List.tsx | 1 + web/src/screens/settings/Api.tsx | 1 + web/src/screens/settings/Feed.tsx | 113 ++++---- web/src/screens/settings/Irc.tsx | 47 +--- web/src/screens/settings/Releases.tsx | 1 + web/src/utils/Context.ts | 212 +++++++-------- 23 files changed, 845 insertions(+), 737 deletions(-) create mode 100644 web/src/components/SectionLoader.tsx create mode 100644 web/src/components/header/Header.tsx create mode 100644 web/src/components/header/LeftNav.tsx create mode 100644 web/src/components/header/MobileNav.tsx create mode 100644 web/src/components/header/RightNav.tsx create mode 100644 web/src/components/header/_shared.ts create mode 100644 web/src/components/header/index.tsx delete mode 100644 web/src/screens/Base.tsx diff --git a/web/src/App.tsx b/web/src/App.tsx index 1ec977a..7df9afd 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -21,7 +21,8 @@ const queryClient = new QueryClient({ // See https://tanstack.com/query/v4/docs/guides/query-retries#retry-delay // delay = Math.min(1000 * 2 ** attemptIndex, 30000) retry: true, - useErrorBoundary: true + useErrorBoundary: true, + suspense: true, }, mutations: { onError: (error) => { @@ -59,4 +60,4 @@ export function App() { ); -} \ No newline at end of file +} diff --git a/web/src/components/SectionLoader.tsx b/web/src/components/SectionLoader.tsx new file mode 100644 index 0000000..dee4d0a --- /dev/null +++ b/web/src/components/SectionLoader.tsx @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2021 - 2023, Ludvig Lundgren and the autobrr contributors. + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +import { RingResizeSpinner } from "@components/Icons"; +import { classNames } from "@utils"; + +const SIZE = { + small: "w-6 h-6", + medium: "w-8 h-8", + large: "w-12 h-12", + xlarge: "w-24 h-24" +} as const; + +interface SectionLoaderProps { + $size: keyof typeof SIZE; +} + +export const SectionLoader = ({ $size }: SectionLoaderProps) => { + if ($size === "xlarge") { + return ( +
+ +
+ ); + } else { + return ( + + ); + } +} diff --git a/web/src/components/header/Header.tsx b/web/src/components/header/Header.tsx new file mode 100644 index 0000000..5ad3f2e --- /dev/null +++ b/web/src/components/header/Header.tsx @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2021 - 2023, Ludvig Lundgren and the autobrr contributors. + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +import toast from "react-hot-toast"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { Disclosure } from "@headlessui/react"; +import { Bars3Icon, XMarkIcon, MegaphoneIcon } from "@heroicons/react/24/outline"; + +import { APIClient } from "@api/APIClient"; +import { AuthContext } from "@utils/Context"; +import Toast from "@components/notifications/Toast"; + +import { LeftNav } from "./LeftNav"; +import { RightNav } from "./RightNav"; +import { MobileNav } from "./MobileNav"; + +export const Header = () => { + const { data } = useQuery({ + queryKey: ["updates"], + queryFn: () => APIClient.updates.getLatestRelease(), + retry: false, + refetchOnWindowFocus: false, + onError: err => console.log(err) + }); + + const logoutMutation = useMutation({ + mutationFn: APIClient.auth.logout, + onSuccess: () => { + AuthContext.reset(); + toast.custom((t) => ( + + )); + } + }); + + return ( + + {({ open }) => ( + <> +
+
+
+ + +
+ {/* Mobile menu button */} + + Open main menu + {open ? ( + +
+
+
+ + {data && data.html_url && ( + +
+ + New update available! + {data?.name} +
+
+ )} +
+ + + + )} +
+ ) +} diff --git a/web/src/components/header/LeftNav.tsx b/web/src/components/header/LeftNav.tsx new file mode 100644 index 0000000..53b86c5 --- /dev/null +++ b/web/src/components/header/LeftNav.tsx @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2021 - 2023, Ludvig Lundgren and the autobrr contributors. + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +import { Link, NavLink } from "react-router-dom"; +import { ArrowTopRightOnSquareIcon } from "@heroicons/react/24/solid"; + +import { classNames } from "@utils"; +import { ReactComponent as Logo } from "@app/logo.svg"; + +import { NAV_ROUTES } from "./_shared"; + +export const LeftNav = () => ( +
+
+ + + +
+
+
+ {NAV_ROUTES.map((item, itemIdx) => ( + + classNames( + "hover:bg-gray-200 dark:hover:bg-gray-800 hover:text-gray-900 dark:hover:text-white px-3 py-2 rounded-2xl text-sm font-medium", + "transition-colors duration-200", + isActive + ? "text-black dark:text-gray-50 font-bold" + : "text-gray-600 dark:text-gray-500" + ) + } + end={item.path === "/"} + > + {item.name} + + ))} + + Docs + +
+
+
+); diff --git a/web/src/components/header/MobileNav.tsx b/web/src/components/header/MobileNav.tsx new file mode 100644 index 0000000..f6504a6 --- /dev/null +++ b/web/src/components/header/MobileNav.tsx @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2021 - 2023, Ludvig Lundgren and the autobrr contributors. + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +import { NavLink } from "react-router-dom"; +import { Disclosure } from "@headlessui/react"; + +import { classNames } from "@utils"; + +import { NAV_ROUTES } from "./_shared"; +import type { RightNavProps } from "./_shared"; + +export const MobileNav = (props: RightNavProps) => ( + +
+ {NAV_ROUTES.map((item) => ( + + classNames( + "shadow-sm border bg-gray-100 border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-white block px-3 py-2 rounded-md text-base", + isActive + ? "underline underline-offset-2 decoration-2 decoration-sky-500 font-bold text-black" + : "font-medium" + ) + } + end={item.path === "/"} + > + {item.name} + + ))} + +
+
+); diff --git a/web/src/components/header/RightNav.tsx b/web/src/components/header/RightNav.tsx new file mode 100644 index 0000000..ca8aaf7 --- /dev/null +++ b/web/src/components/header/RightNav.tsx @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2021 - 2023, Ludvig Lundgren and the autobrr contributors. + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +import { Fragment } from "react"; +import { Link } from "react-router-dom"; +import { UserIcon } from "@heroicons/react/24/solid"; +import { Menu, Transition } from "@headlessui/react"; + +import { classNames } from "@utils"; +import { AuthContext } from "@utils/Context"; + +import { RightNavProps } from "./_shared"; + +export const RightNav = (props: RightNavProps) => { + const authContext = AuthContext.useValue(); + return ( +
+
+ + {({ open }) => ( + <> + + + + Open user menu for{" "} + + {authContext.username} + + + + + + {({ active }) => ( + + Settings + + )} + + + {({ active }) => ( + + )} + + + + + )} + +
+
+ ); +} diff --git a/web/src/components/header/_shared.ts b/web/src/components/header/_shared.ts new file mode 100644 index 0000000..f679cd4 --- /dev/null +++ b/web/src/components/header/_shared.ts @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2021 - 2023, Ludvig Lundgren and the autobrr contributors. + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +interface NavItem { + name: string; + path: string; +} + +export interface RightNavProps { + logoutMutation: () => void; +} + +export const NAV_ROUTES: Array = [ + { name: "Dashboard", path: "/" }, + { name: "Filters", path: "/filters" }, + { name: "Releases", path: "/releases" }, + { name: "Settings", path: "/settings" }, + { name: "Logs", path: "/logs" } +]; diff --git a/web/src/components/header/index.tsx b/web/src/components/header/index.tsx new file mode 100644 index 0000000..3316480 --- /dev/null +++ b/web/src/components/header/index.tsx @@ -0,0 +1,6 @@ +/* + * Copyright (c) 2021 - 2023, Ludvig Lundgren and the autobrr contributors. + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +export { Header } from "./Header"; diff --git a/web/src/components/modals/index.tsx b/web/src/components/modals/index.tsx index 16614b3..a55d827 100644 --- a/web/src/components/modals/index.tsx +++ b/web/src/components/modals/index.tsx @@ -7,24 +7,85 @@ import { FC, Fragment, MutableRefObject } from "react"; import { Dialog, Transition } from "@headlessui/react"; import { ExclamationTriangleIcon } from "@heroicons/react/24/solid"; -interface DeleteModalProps { - isOpen: boolean; - buttonRef: MutableRefObject | undefined; - toggle: () => void; - deleteAction: () => void; - title: string; - text: string; +import { SectionLoader } from "@components/SectionLoader"; + +interface ModalUpperProps { + title: string; + text: string; } -export const DeleteModal: FC = ({ isOpen, buttonRef, toggle, deleteAction, title, text }) => ( - +interface ModalLowerProps { + isOpen: boolean; + isLoading: boolean; + toggle: () => void; + deleteAction: () => void; +} + +interface DeleteModalProps extends ModalUpperProps, ModalLowerProps { + buttonRef: MutableRefObject | undefined; +} + +const ModalUpper = ({ title, text }: ModalUpperProps) => ( +
+
+
+
+); + +const ModalLower = ({ isOpen, isLoading, toggle, deleteAction }: ModalLowerProps) => ( +
+ {isLoading ? ( + + ) : ( + <> + + + + )} +
+); + +export const DeleteModal: FC = (props: DeleteModalProps) => ( +
= ({ isOpen, buttonRef, toggle, d leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" >
-
-
-
-
-
- - -
+ +
diff --git a/web/src/components/panels/index.tsx b/web/src/components/panels/index.tsx index efb51b6..0c0e3d0 100644 --- a/web/src/components/panels/index.tsx +++ b/web/src/components/panels/index.tsx @@ -62,6 +62,7 @@ function SlideOver({ {deleteAction && ( ( +
+
+ }> + + +
+); export const LocalRouter = ({ isLoggedIn }: { isLoggedIn: boolean }) => ( {isLoggedIn ? ( } /> - }> + }> } /> } /> } /> diff --git a/web/src/forms/settings/DownloadClientForms.tsx b/web/src/forms/settings/DownloadClientForms.tsx index b8af57e..deadc8c 100644 --- a/web/src/forms/settings/DownloadClientForms.tsx +++ b/web/src/forms/settings/DownloadClientForms.tsx @@ -833,6 +833,7 @@ export function DownloadClientUpdateForm({ client, isOpen, toggle }: updateFormP > = [ - { name: "Dashboard", path: "/" }, - { name: "Filters", path: "/filters" }, - { name: "Releases", path: "/releases" }, - { name: "Settings", path: "/settings" }, - { name: "Logs", path: "/logs" } -]; - -export const Base = () => { - const authContext = AuthContext.useValue(); - - const { data } = useQuery({ - queryKey: ["updates"], - queryFn: () => APIClient.updates.getLatestRelease(), - retry: false, - refetchOnWindowFocus: false, - onError: err => console.log(err) - }); - - const logoutMutation = useMutation( { - mutationFn: APIClient.auth.logout, - onSuccess: () => { - AuthContext.reset(); - - toast.custom((t) => ( - - )); - } - }); - - const logoutAction = () => { - logoutMutation.mutate(); - }; - - return ( -
- - {({ open }) => ( - <> -
-
-
-
-
- - - -
-
-
- {nav.map((item, itemIdx) => ( - - classNames( - "hover:bg-gray-200 dark:hover:bg-gray-800 hover:text-gray-900 dark:hover:text-white px-3 py-2 rounded-2xl text-sm font-medium", - "transition-colors duration-200", - isActive - ? "text-black dark:text-gray-50 font-bold" - : "text-gray-600 dark:text-gray-500" - ) - } - end={item.path === "/"} - > - {item.name} - - ))} - - Docs - -
-
-
-
-
- - {({ open }) => ( - <> - - - - Open user menu for{" "} - - {authContext.username} - - - - - - {({ active }) => ( - - Settings - - )} - - - {({ active }) => ( - - )} - - - - - )} - -
-
-
- {/* Mobile menu button */} - - Open main menu - {open ? ( - -
-
-
- - {data && data.html_url && ( - -
- - New update available! - {data?.name} -
-
- )} -
- - -
- {nav.map((item) => ( - - classNames( - "shadow-sm border bg-gray-100 border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-white block px-3 py-2 rounded-md text-base", - isActive - ? "underline underline-offset-2 decoration-2 decoration-sky-500 font-bold text-black" - : "font-medium" - ) - } - end={item.path === "/"} - > - {item.name} - - ))} - -
-
- - )} -
- -
- ); -}; diff --git a/web/src/screens/Settings.tsx b/web/src/screens/Settings.tsx index 2789d87..71c24ad 100644 --- a/web/src/screens/Settings.tsx +++ b/web/src/screens/Settings.tsx @@ -3,6 +3,7 @@ * SPDX-License-Identifier: GPL-2.0-or-later */ +import { Suspense } from "react"; import { NavLink, Outlet, useLocation } from "react-router-dom"; import { BellIcon, @@ -16,6 +17,7 @@ import { } from "@heroicons/react/24/outline"; import { classNames } from "@utils"; +import { SectionLoader } from "@components/SectionLoader"; interface NavTabType { name: string; @@ -96,7 +98,15 @@ export function Settings() {
- + + +
+ } + > + +
diff --git a/web/src/screens/filters/Action.tsx b/web/src/screens/filters/Action.tsx index 6251ac1..a49d92f 100644 --- a/web/src/screens/filters/Action.tsx +++ b/web/src/screens/filters/Action.tsx @@ -102,10 +102,10 @@ export function FilterActions({ filter, values }: FilterActionsProps) { {values.actions.length > 0 ?
    {values.actions.map((action: Action, index: number) => ( - + ))}
- : + : } @@ -122,7 +122,7 @@ interface TypeFormProps { } const TypeForm = ({ action, idx, clients }: TypeFormProps) => { - const { setFieldValue } = useFormikContext(); + const { setFieldValue } = useFormikContext(); const resetClientField = (action: Action, idx: number, prevActionType: string): void => { const fieldName = `actions.${idx}.client_id`; @@ -544,9 +544,9 @@ const TypeForm = ({ action, idx, clients }: TypeFormProps) => { clients={clients} /> Override Download client Id from the one set in Clients. Useful if you have multiple clients inside the arr.

} + name={`actions.${idx}.external_download_client_id`} + label="Override download client id for arr" + tooltip={

Override Download client Id from the one set in Clients. Useful if you have multiple clients inside the arr.

} /> ); @@ -648,7 +648,7 @@ function FilterActionsItem({ action, clients, idx, initialEdit, remove }: Filter
-

+

{action.name}

@@ -683,6 +683,7 @@ function FilterActionsItem({ action, clients, idx, initialEdit, remove }: Filter > removeAction(action.id)} @@ -706,13 +707,13 @@ function FilterActionsItem({ action, clients, idx, initialEdit, remove }: Filter
- +
} /> -

Number of seconds to wait before running actions.

https://autobrr.com/filters#rules
} /> +

Supports units such as MB, MiB, GB, etc.

https://autobrr.com/filters#rules
} /> +

Number of seconds to wait before running actions.

https://autobrr.com/filters#rules} />

Filters are checked in order of priority. Higher number = higher priority.

https://autobrr.com/filters#rules} />

Number of max downloads as specified by the respective unit.

https://autobrr.com/filters#rules} /> -

The unit of time for counting the maximum downloads per filter.

https://autobrr.com/filters#rules} /> @@ -474,19 +480,19 @@ export function MoviesTv() { return (
-

You can use basic filtering like wildcards * or replace single characters with ?

https://autobrr.com/filters#tvmovies
} /> -

This field takes a range of years and/or comma separated single years.

https://autobrr.com/filters#tvmovies
} /> +

You can use basic filtering like wildcards * or replace single characters with ?

https://autobrr.com/filters#tvmovies} /> +

This field takes a range of years and/or comma separated single years.

https://autobrr.com/filters#tvmovies} />
-

See docs for information about how to only grab season packs:

https://autobrr.com/filters/examples#only-season-packs
} /> -

See docs for information about how to only grab episodes:

https://autobrr.com/filters/examples/#skip-season-packs
} /> +

See docs for information about how to only grab season packs:

https://autobrr.com/filters/examples#only-season-packs} /> +

See docs for information about how to only grab episodes:

https://autobrr.com/filters/examples/#skip-season-packs} />
- {/*Do not match older or already existing episodes.*/} + {/*Do not match older or already existing episodes.*/}
@@ -494,23 +500,23 @@ export function MoviesTv() {
-

Will match releases which contain any of the selected resolutions.

https://autobrr.com/filters#quality
} /> -

Will match releases which contain any of the selected sources.

https://autobrr.com/filters#quality} /> +

Will match releases which contain any of the selected resolutions.

https://autobrr.com/filters#quality} /> +

Will match releases which contain any of the selected sources.

https://autobrr.com/filters#quality} />
-

Will match releases which contain any of the selected codecs.

https://autobrr.com/filters#quality
} /> -

Will match releases which contain any of the selected containers.

https://autobrr.com/filters#quality} /> +

Will match releases which contain any of the selected codecs.

https://autobrr.com/filters#quality} /> +

Will match releases which contain any of the selected containers.

https://autobrr.com/filters#quality} />
-

Will match releases which contain any of the selected HDR designations.

https://autobrr.com/filters#quality
} /> -

Won't match releases which contain any of the selected HDR designations (takes priority over Match HDR).

https://autobrr.com/filters#quality} /> +

Will match releases which contain any of the selected HDR designations.

https://autobrr.com/filters#quality} /> +

Won't match releases which contain any of the selected HDR designations (takes priority over Match HDR).

https://autobrr.com/filters#quality} />
-

Will match releases which contain any of the selected designations.

https://autobrr.com/filters#quality
} /> -

Won't match releases which contain any of the selected Other designations (takes priority over Match Other).

https://autobrr.com/filters#quality} /> +

Will match releases which contain any of the selected designations.

https://autobrr.com/filters#quality} /> +

Won't match releases which contain any of the selected Other designations (takes priority over Match Other).

https://autobrr.com/filters#quality} /> @@ -580,8 +586,8 @@ export function Advanced({ values }: AdvancedProps) {
-

This field has full regex support (Golang flavour).

https://autobrr.com/filters#advanced

Remember to tick Use Regex below if using more than * and ?.

} /> -

This field has full regex support (Golang flavour).

https://autobrr.com/filters#advanced

Remember to tick Use Regex below if using more than * and ?.

} /> +

This field has full regex support (Golang flavour).

https://autobrr.com/filters#advanced

Remember to tick Use Regex below if using more than * and ?.

} /> +

This field has full regex support (Golang flavour).

https://autobrr.com/filters#advanced

Remember to tick Use Regex below if using more than * and ?.

} /> {values.match_releases ? (

Comma separated list of categories to ignore (takes priority over Match releases).

https://autobrr.com/filters/categories} />

Comma separated list of tags to match.

https://autobrr.com/filters#advanced} /> -

Logic used to match filter tags.

https://autobrr.com/filters#advanced} />

Comma separated list of tags to ignore (takes priority over Match releases).

hhttps://autobrr.com/filters#advanced} /> -

Logic used to match except tags.

https://autobrr.com/filters#advanced} /> @@ -652,8 +658,8 @@ export function Advanced({ values }: AdvancedProps) { {/*
*/} -

This field has full regex support (Golang flavour).

https://autobrr.com/filters#advanced

Remember to tick Use Regex below if using more than * and ?.

} /> -

This field has full regex support (Golang flavour).

https://autobrr.com/filters#advanced

Remember to tick Use Regex below if using more than * and ?.

} /> +

This field has full regex support (Golang flavour).

https://autobrr.com/filters#advanced

Remember to tick Use Regex below if using more than * and ?.

} /> +

This field has full regex support (Golang flavour).

https://autobrr.com/filters#advanced

Remember to tick Use Regex below if using more than * and ?.

} /> {/**/}
@@ -702,10 +708,10 @@ function WarningAlert({ text, alert, colors }: WarningAlertProps) { } interface CollapsableSectionProps { - title: string; - subtitle: string; - children: ReactNode; - defaultOpen?: boolean; + title: string; + subtitle: string; + children: ReactNode; + defaultOpen?: boolean; } export function CollapsableSection({ title, subtitle, children, defaultOpen }: CollapsableSectionProps) { diff --git a/web/src/screens/filters/External.tsx b/web/src/screens/filters/External.tsx index fec5d02..efc455c 100644 --- a/web/src/screens/filters/External.tsx +++ b/web/src/screens/filters/External.tsx @@ -6,11 +6,11 @@ import { Field, FieldArray, FieldArrayRenderProps, FieldProps, useFormikContext } from "formik"; import { NumberField, Select, TextField } from "@components/inputs"; import { TextArea } from "@components/inputs/input"; -import { Fragment, useEffect, useRef, useState } from "react"; +import { Fragment, useRef } from "react"; import { EmptyListState } from "@components/emptystates"; import { useToggle } from "@hooks/hooks"; import { classNames } from "@utils"; -import { Dialog, Switch as SwitchBasic, Transition } from "@headlessui/react"; +import { Switch as SwitchBasic } from "@headlessui/react"; import { ExternalFilterTypeNameMap, ExternalFilterTypeOptions, @@ -21,20 +21,20 @@ import { DeleteModal } from "@components/modals"; import { ArrowDownIcon, ArrowUpIcon } from "@heroicons/react/24/outline"; export function External() { - const {values} = useFormikContext(); + const { values } = useFormikContext(); const newItem: ExternalFilter = { id: values.external.length + 1, index: values.external.length, name: `External ${values.external.length + 1}`, enabled: false, - type: "EXEC", - } + type: "EXEC" + }; return (
- {({remove, push, move}: FieldArrayRenderProps) => ( + {({ remove, push, move }: FieldArrayRenderProps) => (
@@ -58,10 +58,10 @@ export function External() { {values.external.length > 0 ?
    {values.external.map((f, index: number) => ( - + ))}
- : + : }
@@ -80,7 +80,7 @@ interface FilterExternalItemProps { } function FilterExternalItem({ idx, external, initialEdit, remove, move }: FilterExternalItemProps) { - const {values, setFieldValue} = useFormikContext(); + const { values, setFieldValue } = useFormikContext(); const cancelButtonRef = useRef(null); const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false); @@ -91,13 +91,13 @@ function FilterExternalItem({ idx, external, initialEdit, remove, move }: Filter }; const moveUp = () => { - move(idx, idx - 1) - setFieldValue(`external.${idx}.index`, idx - 1) + move(idx, idx - 1); + setFieldValue(`external.${idx}.index`, idx - 1); }; const moveDown = () => { - move(idx, idx + 1) - setFieldValue(`external.${idx}.index`, idx + 1) + move(idx, idx + 1); + setFieldValue(`external.${idx}.index`, idx + 1); }; return ( @@ -130,9 +130,9 @@ function FilterExternalItem({ idx, external, initialEdit, remove, move }: Filter {({ - field, - form: {setFieldValue} - }: FieldProps) => ( + field, + form: { setFieldValue } + }: FieldProps) => (
-

+

{external.name}

@@ -183,28 +183,17 @@ function FilterExternalItem({ idx, external, initialEdit, remove, move }: Filter
{edit && (
- - - - - +
-

Select the HTTP method for this webhook. Defaults to POST

} + placeholder="Absolute path to executable eg. /bin/test" + tooltip={ +
+

+ For custom commands you should specify the full path to the binary/program + you want to run. And you can include your own static variables: +

+ https://autobrr.com/filters/actions#custom-commands--exec + +
+ } /> -