refactor(web): update to react 18 and other deps (#285)

* Started refactoring codebase for React 18.

* Migrated to react-router v6 fully

* Removed useless test setup along with relevant packages.

* Removed leftover console.log statement

* feat: use status forbidden for onboarding

* refactor(web): use react hook form on login

* fix: comply with r18 shenanigans

* chore: update packages
This commit is contained in:
stacksmash76 2022-06-10 19:31:46 +02:00 committed by GitHub
parent d065fee16b
commit 4d753b76ed
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 2252 additions and 2145 deletions

View file

@ -1,16 +1,9 @@
import { Fragment } from "react";
import type { match } from "react-router-dom";
import { Link, NavLink, Route, Switch } from "react-router-dom";
import { Link, NavLink, Outlet } from "react-router-dom";
import { Disclosure, Menu, Transition } from "@headlessui/react";
import { ExternalLinkIcon } from "@heroicons/react/solid";
import { ChevronDownIcon, MenuIcon, XIcon } from "@heroicons/react/outline";
import Settings from "./Settings";
import { Logs } from "./Logs";
import { Releases } from "./releases";
import { Dashboard } from "./dashboard";
import { FilterDetails, Filters } from "./filters";
import { AuthContext } from "../utils/Context";
import logo from "../logo.png";
@ -24,23 +17,6 @@ function classNames(...classes: string[]) {
return classes.filter(Boolean).join(" ");
}
const isActiveMatcher = (
match: match | null,
location: { pathname: string },
item: NavItem
) => {
if (!match)
return false;
if (match?.url === "/" && item.path === "/" && location.pathname === "/")
return true;
if (match.url === "/")
return false;
return true;
};
export default function Base() {
const authContext = AuthContext.useValue();
const nav: Array<NavItem> = [
@ -81,13 +57,11 @@ export default function Base() {
<NavLink
key={item.name + itemIdx}
to={item.path}
strict
className={classNames(
"text-gray-600 dark:text-gray-500 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"
className={({ isActive }) => 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"
)}
activeClassName="text-black dark:text-gray-50 !font-bold"
isActive={(match, location) => isActiveMatcher(match, location, item)}
>
{item.name}
</NavLink>
@ -199,10 +173,11 @@ export default function Base() {
<NavLink
key={item.path}
to={item.path}
strict
className="dark:bg-gray-900 dark:text-white block px-3 py-2 rounded-md text-base font-medium"
activeClassName="font-bold bg-gray-300 text-black"
isActive={(match, location) => isActiveMatcher(match, location, item)}
className={({ isActive }) => classNames(
// TODO: Double check whether this is correct
"dark:bg-gray-900 dark:text-white block px-3 py-2 rounded-md text-base",
isActive ? "font-bold bg-gray-300 text-black" : "font-medium"
)}
>
{item.name}
</NavLink>
@ -219,32 +194,7 @@ export default function Base() {
</>
)}
</Disclosure>
<Switch>
<Route path="/logs">
<Logs/>
</Route>
<Route path="/settings">
<Settings/>
</Route>
<Route path="/releases">
<Releases/>
</Route>
<Route exact={true} path="/filters">
<Filters/>
</Route>
<Route path="/filters/:filterId">
<FilterDetails/>
</Route>
<Route exact path="/">
<Dashboard/>
</Route>
</Switch>
<Outlet />
</div>
);
}

View file

@ -1,56 +1,52 @@
import { BellIcon, ChatAlt2Icon, CogIcon, CollectionIcon, DownloadIcon, KeyIcon, RssIcon } from "@heroicons/react/outline";
import { NavLink, Route, Switch as RouteSwitch, useLocation, useRouteMatch } from "react-router-dom";
import { NavLink, Outlet, useLocation } from "react-router-dom";
import {
BellIcon,
ChatAlt2Icon,
CogIcon,
CollectionIcon,
DownloadIcon,
KeyIcon,
RssIcon
} from "@heroicons/react/outline";
import { classNames } from "../utils";
import IndexerSettings from "./settings/Indexer";
import { IrcSettings } from "./settings/Irc";
import ApplicationSettings from "./settings/Application";
import DownloadClientSettings from "./settings/DownloadClient";
import { RegexPlayground } from "./settings/RegexPlayground";
import ReleaseSettings from "./settings/Releases";
import NotificationSettings from "./settings/Notifications";
import FeedSettings from "./settings/Feed";
interface NavTabType {
name: string;
href: string;
icon: typeof CogIcon;
current: boolean;
}
const subNavigation: NavTabType[] = [
{ name: "Application", href: "", icon: CogIcon, current: true },
{ name: "Indexers", href: "indexers", icon: KeyIcon, current: false },
{ name: "IRC", href: "irc", icon: ChatAlt2Icon, current: false },
{ name: "Feeds", href: "feeds", icon: RssIcon, current: false },
{ name: "Clients", href: "clients", icon: DownloadIcon, current: false },
{ name: "Notifications", href: "notifications", icon: BellIcon, current: false },
{ name: "Releases", href: "releases", icon: CollectionIcon, current: false }
{ name: "Application", href: "", icon: CogIcon },
{ name: "Indexers", href: "indexers", icon: KeyIcon },
{ name: "IRC", href: "irc", icon: ChatAlt2Icon },
{ name: "Feeds", href: "feeds", icon: RssIcon },
{ name: "Clients", href: "clients", icon: DownloadIcon },
{ name: "Notifications", href: "notifications", icon: BellIcon },
{ name: "Releases", href: "releases", icon: CollectionIcon }
// {name: 'Regex Playground', href: 'regex-playground', icon: CogIcon, current: false}
// {name: 'Rules', href: 'rules', icon: ClipboardCheckIcon, current: false},
];
interface NavLinkProps {
item: NavTabType;
url: string;
}
function SubNavLink({ item, url }: NavLinkProps) {
const location = useLocation();
const { pathname } = location;
function SubNavLink({ item }: NavLinkProps) {
const { pathname } = useLocation();
const splitLocation = pathname.split("/");
// we need to clean the / if it's a base root path
const too = item.href ? `${url}/${item.href}` : url;
return (
<NavLink
key={item.name}
to={too}
exact={true}
activeClassName="bg-teal-50 dark:bg-gray-700 border-teal-500 dark:border-blue-500 text-teal-700 dark:text-white hover:bg-teal-50 dark:hover:bg-gray-500 hover:text-teal-700 dark:hover:text-gray-200"
className={classNames(
"border-transparent text-gray-900 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-gray-300 group border-l-4 px-3 py-2 flex items-center text-sm font-medium"
to={item.href}
end
className={({ isActive }) => classNames(
"border-transparent text-gray-900 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-gray-300 group border-l-4 px-3 py-2 flex items-center text-sm font-medium",
isActive ?
"bg-teal-50 dark:bg-gray-700 border-teal-500 dark:border-blue-500 text-teal-700 dark:text-white hover:bg-teal-50 dark:hover:bg-gray-500 hover:text-teal-700 dark:hover:text-gray-200" : ""
)}
aria-current={splitLocation[2] === item.href ? "page" : undefined}
>
@ -65,15 +61,14 @@ function SubNavLink({ item, url }: NavLinkProps) {
interface SidebarNavProps {
subNavigation: NavTabType[];
url: string;
}
function SidebarNav({ subNavigation, url }: SidebarNavProps) {
function SidebarNav({ subNavigation }: SidebarNavProps) {
return (
<aside className="py-2 lg:col-span-3">
<nav className="space-y-1">
{subNavigation.map((item) => (
<SubNavLink item={item} url={url} key={item.href}/>
<SubNavLink item={item} key={item.href}/>
))}
</nav>
</aside>
@ -81,7 +76,6 @@ function SidebarNav({ subNavigation, url }: SidebarNavProps) {
}
export default function Settings() {
const { url } = useRouteMatch();
return (
<main>
<header className="py-10">
@ -93,41 +87,8 @@ export default function Settings() {
<div className="max-w-screen-xl mx-auto pb-6 px-4 sm:px-6 lg:pb-16 lg:px-8">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-lg">
<div className="divide-y divide-gray-200 dark:divide-gray-700 lg:grid lg:grid-cols-12 lg:divide-y-0 lg:divide-x">
<SidebarNav url={url} subNavigation={subNavigation}/>
<RouteSwitch>
<Route exact path={url}>
<ApplicationSettings/>
</Route>
<Route path={`${url}/indexers`}>
<IndexerSettings/>
</Route>
<Route path={`${url}/feeds`}>
<FeedSettings/>
</Route>
<Route path={`${url}/irc`}>
<IrcSettings/>
</Route>
<Route path={`${url}/clients`}>
<DownloadClientSettings/>
</Route>
<Route path={`${url}/notifications`}>
<NotificationSettings />
</Route>
<Route path={`${url}/releases`}>
<ReleaseSettings/>
</Route>
<Route path={`${url}/regex-playground`}>
<RegexPlayground />
</Route>
</RouteSwitch>
<SidebarNav subNavigation={subNavigation}/>
<Outlet />
</div>
</div>
</div>

View file

@ -1,44 +1,48 @@
import { useHistory } from "react-router-dom";
import { useNavigate } from "react-router-dom";
import { useMutation } from "react-query";
import { Form, Formik } from "formik";
import { APIClient } from "../../api/APIClient";
import { TextField, PasswordField } from "../../components/inputs";
import logo from "../../logo.png";
import { AuthContext } from "../../utils/Context";
import { useEffect } from "react";
import { SubmitHandler, useForm } from "react-hook-form";
import { PasswordInput, TextInput } from "../../components/inputs/text";
interface LoginData {
export type LoginFormFields = {
username: string;
password: string;
}
};
export const Login = () => {
const history = useHistory();
const { handleSubmit, register, formState: { errors } } = useForm<LoginFormFields>({
defaultValues: { username: "", password: "" },
mode: "onBlur"
});
const navigate = useNavigate();
const [, setAuthContext] = AuthContext.use();
useEffect(() => {
// Check if onboarding is available for this instance
// and redirect if needed
APIClient.auth.canOnboard()
.then(() => history.push("/onboard"));
.then(() => navigate("/onboard"));
}, [history]);
const mutation = useMutation(
(data: LoginData) => APIClient.auth.login(data.username, data.password),
const loginMutation = useMutation(
(data: LoginFormFields) => APIClient.auth.login(data.username, data.password),
{
onSuccess: (_, variables: LoginData) => {
onSuccess: (_, variables: LoginFormFields) => {
setAuthContext({
username: variables.username,
isLoggedIn: true
});
history.push("/");
navigate("/");
}
}
);
const handleSubmit = (data: LoginData) => mutation.mutate(data);
const onSubmit: SubmitHandler<LoginFormFields> = (data: LoginFormFields) => loginMutation.mutate(data);
return (
<div className="min-h-screen flex flex-col justify-center py-12 sm:px-6 lg:px-8">
@ -52,25 +56,39 @@ export const Login = () => {
<div className="sm:mx-auto sm:w-full sm:max-w-md shadow-lg">
<div className="bg-white dark:bg-gray-800 py-8 px-4 sm:rounded-lg sm:px-10">
<Formik
initialValues={{ username: "", password: "" }}
onSubmit={handleSubmit}
>
<Form>
<div className="space-y-6">
<TextField name="username" label="Username" columns={6} autoComplete="username" />
<PasswordField name="password" label="Password" columns={6} autoComplete="current-password" />
</div>
<div className="mt-6">
<button
type="submit"
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
>
Sign in
</button>
</div>
</Form>
</Formik>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="space-y-6">
<TextInput<LoginFormFields>
name="username"
id="username"
label="username"
type="text"
register={register}
rules={{ required: "Username is required" }}
errors={errors}
autoComplete="username"
/>
<PasswordInput<LoginFormFields>
name="password"
id="password"
label="password"
register={register}
rules={{ required: "Password is required" }}
errors={errors}
autoComplete="current-password"
/>
</div>
<div className="mt-6">
<button
type="submit"
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
>
Sign in
</button>
</div>
</form>
</div>
</div>
</div>

View file

@ -1,12 +1,12 @@
import { useEffect } from "react";
import { useCookies } from "react-cookie";
import { useHistory } from "react-router-dom";
import { useNavigate } from "react-router-dom";
import { APIClient } from "../../api/APIClient";
import { AuthContext } from "../../utils/Context";
export const Logout = () => {
const history = useHistory();
const navigate = useNavigate();
const [, setAuthContext] = AuthContext.use();
const [,, removeCookie] = useCookies(["user_session"]);
@ -15,10 +15,10 @@ export const Logout = () => {
() => {
APIClient.auth.logout()
.then(() => {
setAuthContext({ username: "", isLoggedIn: false });
removeCookie("user_session");
setAuthContext({ username: "", isLoggedIn: false });
history.push("/login");
navigate("/login");
});
},
[history, removeCookie, setAuthContext]

View file

@ -1,6 +1,6 @@
import { Form, Formik } from "formik";
import { useMutation } from "react-query";
import { useHistory } from "react-router-dom";
import { useNavigate } from "react-router-dom";
import { APIClient } from "../../api/APIClient";
import { TextField, PasswordField } from "../../components/inputs";
@ -30,15 +30,11 @@ export const Onboarding = () => {
return obj;
};
const history = useHistory();
const navigate = useNavigate();
const mutation = useMutation(
(data: InputValues) => APIClient.auth.onboard(data.username, data.password1),
{
onSuccess: () => {
history.push("/login");
}
}
{ onSuccess: () => navigate("/login") }
);
return (

View file

@ -13,6 +13,7 @@ import { EmptyListState } from "../../components/emptystates";
import * as Icons from "../../components/Icons";
import * as DataTable from "../../components/data-table";
import { Fragment } from "react";
// This is a custom filter UI for selecting
// a unique option from a list
@ -32,7 +33,7 @@ function SelectColumnFilter({
// Render a multi-select box
return (
<label className="flex items-baseline gap-x-2">
<span className="text-gray-700">{render("Header")}: </span>
<span className="text-gray-700"><>{render("Header")}:</></span>
<select
className="border-gray-300 rounded-md shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
name={id}
@ -99,7 +100,7 @@ function Table({ columns, data }: TableProps) {
{...columnRest}
>
<div className="flex items-center justify-between">
{column.render("Header")}
<>{column.render("Header")}</>
{/* Add a sort direction indicator */}
<span>
{column.isSorted ? (
@ -138,7 +139,7 @@ function Table({ columns, data }: TableProps) {
role="cell"
{...cellRowRest}
>
{cell.render("Cell")}
<>{cell.render("Cell")}</>
</td>
);
})}

View file

@ -3,11 +3,10 @@ import { useMutation, useQuery } from "react-query";
import {
NavLink,
Route,
Switch as RouteSwitch,
useHistory,
Routes,
useLocation,
useParams,
useRouteMatch
useNavigate,
useParams
} from "react-router-dom";
import { toast } from "react-hot-toast";
import { Field, FieldArray, FieldProps, Form, Formik, FormikValues } from "formik";
@ -55,24 +54,21 @@ import { EmptyListState } from "../../components/emptystates";
interface tabType {
name: string;
href: string;
current: boolean;
}
const tabs: tabType[] = [
{ name: "General", href: "", current: true },
{ name: "Movies and TV", href: "movies-tv", current: false },
{ name: "Music", href: "music", current: false },
// { name: 'P2P', href: 'p2p', current: false },
{ name: "Advanced", href: "advanced", current: false },
{ name: "Actions", href: "actions", current: false }
{ name: "General", href: "" },
{ name: "Movies and TV", href: "movies-tv" },
{ name: "Music", href: "music" },
{ name: "Advanced", href: "advanced" },
{ name: "Actions", href: "actions" }
];
export interface NavLinkProps {
item: tabType;
url: string;
}
function TabNavLink({ item, url }: NavLinkProps) {
function TabNavLink({ item }: NavLinkProps) {
const location = useLocation();
const splitLocation = location.pathname.split("/");
@ -80,11 +76,11 @@ function TabNavLink({ item, url }: NavLinkProps) {
return (
<NavLink
key={item.name}
to={item.href ? `${url}/${item.href}` : url}
exact
activeClassName="border-purple-600 dark:border-blue-500 text-purple-600 dark:text-white"
className={classNames(
"border-transparent text-gray-500 hover:text-purple-600 dark:hover:text-white hover:border-purple-600 dark:hover:border-blue-500 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm"
to={item.href}
end
className={({ isActive }) => classNames(
"text-gray-500 hover:text-purple-600 dark:hover:text-white hover:border-purple-600 dark:hover:border-blue-500 whitespace-nowrap py-4 px-1 font-medium text-sm",
isActive ? "border-b-2 border-purple-600 dark:border-blue-500 text-purple-600 dark:text-white" : ""
)}
aria-current={splitLocation[2] === item.href ? "page" : undefined}
>
@ -132,13 +128,13 @@ const FormButtonsGroup = ({ values, deleteAction, reset }: FormButtonsGroupProps
className="light:bg-white light:border light:border-gray-300 rounded-md py-2 px-4 inline-flex justify-center text-sm font-medium text-gray-700 dark:text-gray-500 light:hover:bg-gray-50 dark:hover:text-gray-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
onClick={reset}
>
Cancel
Cancel
</button>
<button
type="submit"
className="ml-4 relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
Save
Save
</button>
</div>
</div>
@ -147,17 +143,16 @@ const FormButtonsGroup = ({ values, deleteAction, reset }: FormButtonsGroupProps
};
export default function FilterDetails() {
const history = useHistory();
const { url } = useRouteMatch();
const navigate = useNavigate();
const { filterId } = useParams<{ filterId: string }>();
const { isLoading, data: filter } = useQuery(
["filters", filterId],
() => APIClient.filters.getByID(parseInt(filterId)),
() => APIClient.filters.getByID(parseInt(filterId ?? "0")),
{
retry: false,
refetchOnWindowFocus: false,
onError: () => history.push("./")
onError: () => navigate("./")
}
);
@ -184,7 +179,7 @@ export default function FilterDetails() {
queryClient.invalidateQueries(["filters"]);
// redirect
history.push("/filters");
navigate("/filters");
}
});
@ -218,8 +213,8 @@ export default function FilterDetails() {
<header className="py-10">
<div className="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8 flex items-center">
<h1 className="text-3xl font-bold text-black dark:text-white">
<NavLink to="/filters" exact>
Filters
<NavLink to="/filters">
Filters
</NavLink>
</h1>
<ChevronRightIcon className="h-6 w-6 text-gray-500" aria-hidden="true" />
@ -229,102 +224,82 @@ export default function FilterDetails() {
<div className="max-w-screen-xl mx-auto pb-12 px-4 sm:px-6 lg:px-8">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow">
<div className="relative mx-auto md:px-6 xl:px-4">
<div className="px-4 sm:px-6 md:px-0">
<div className="pt-1 pb-6">
<div className="block overflow-auto">
<div className="border-b border-gray-200 dark:border-gray-700">
<nav className="-mb-px flex space-x-6 sm:space-x-8">
{tabs.map((tab) => (
<TabNavLink item={tab} url={url} key={tab.href} />
))}
</nav>
</div>
</div>
<Formik
initialValues={{
id: filter.id,
name: filter.name,
enabled: filter.enabled || false,
min_size: filter.min_size,
max_size: filter.max_size,
delay: filter.delay,
priority: filter.priority,
max_downloads: filter.max_downloads,
max_downloads_unit: filter.max_downloads_unit,
use_regex: filter.use_regex || false,
shows: filter.shows,
years: filter.years,
resolutions: filter.resolutions || [],
sources: filter.sources || [],
codecs: filter.codecs || [],
containers: filter.containers || [],
match_hdr: filter.match_hdr || [],
except_hdr: filter.except_hdr || [],
match_other: filter.match_other || [],
except_other: filter.except_other || [],
seasons: filter.seasons,
episodes: filter.episodes,
match_releases: filter.match_releases,
except_releases: filter.except_releases,
match_release_groups: filter.match_release_groups,
except_release_groups: filter.except_release_groups,
match_categories: filter.match_categories,
except_categories: filter.except_categories,
tags: filter.tags,
except_tags: filter.except_tags,
match_uploaders: filter.match_uploaders,
except_uploaders: filter.except_uploaders,
freeleech: filter.freeleech,
freeleech_percent: filter.freeleech_percent,
formats: filter.formats || [],
quality: filter.quality || [],
media: filter.media || [],
match_release_types: filter.match_release_types || [],
log_score: filter.log_score,
log: filter.log,
cue: filter.cue,
perfect_flac: filter.perfect_flac,
artists: filter.artists,
albums: filter.albums,
origins: filter.origins || [],
indexers: filter.indexers || [],
actions: filter.actions || []
} as Filter}
onSubmit={handleSubmit}
>
{({ values, dirty, resetForm }) => (
<Form>
<RouteSwitch>
<Route exact path={url}>
<General />
</Route>
<Route path={`${url}/movies-tv`}>
<MoviesTv />
</Route>
<Route path={`${url}/music`}>
<Music />
</Route>
<Route path={`${url}/advanced`}>
<Advanced />
</Route>
<Route path={`${url}/actions`}>
<FilterActions filter={filter} values={values} />
</Route>
</RouteSwitch>
<FormButtonsGroup values={values} deleteAction={deleteAction} dirty={dirty} reset={resetForm} />
<DEBUG values={values} />
</Form>
)}
</Formik>
<div className="pt-1 pb-6 block overflow-auto">
<div className="border-b border-gray-200 dark:border-gray-700">
<nav className="-mb-px flex space-x-6 sm:space-x-8">
{tabs.map((tab) => (
<TabNavLink item={tab} key={tab.href} />
))}
</nav>
</div>
<Formik
initialValues={{
id: filter.id,
name: filter.name,
enabled: filter.enabled || false,
min_size: filter.min_size,
max_size: filter.max_size,
delay: filter.delay,
priority: filter.priority,
max_downloads: filter.max_downloads,
max_downloads_unit: filter.max_downloads_unit,
use_regex: filter.use_regex || false,
shows: filter.shows,
years: filter.years,
resolutions: filter.resolutions || [],
sources: filter.sources || [],
codecs: filter.codecs || [],
containers: filter.containers || [],
match_hdr: filter.match_hdr || [],
except_hdr: filter.except_hdr || [],
match_other: filter.match_other || [],
except_other: filter.except_other || [],
seasons: filter.seasons,
episodes: filter.episodes,
match_releases: filter.match_releases,
except_releases: filter.except_releases,
match_release_groups: filter.match_release_groups,
except_release_groups: filter.except_release_groups,
match_categories: filter.match_categories,
except_categories: filter.except_categories,
tags: filter.tags,
except_tags: filter.except_tags,
match_uploaders: filter.match_uploaders,
except_uploaders: filter.except_uploaders,
freeleech: filter.freeleech,
freeleech_percent: filter.freeleech_percent,
formats: filter.formats || [],
quality: filter.quality || [],
media: filter.media || [],
match_release_types: filter.match_release_types || [],
log_score: filter.log_score,
log: filter.log,
cue: filter.cue,
perfect_flac: filter.perfect_flac,
artists: filter.artists,
albums: filter.albums,
origins: filter.origins || [],
indexers: filter.indexers || [],
actions: filter.actions || []
} as Filter}
onSubmit={handleSubmit}
>
{({ values, dirty, resetForm }) => (
<Form>
<Routes>
<Route index element={<General />} />
<Route path="movies-tv" element={<MoviesTv />} />
<Route path="music" element={<Music />} />
<Route path="advanced" element={<Advanced />} />
<Route path="actions" element={<FilterActions filter={filter} values={values} />}
/>
</Routes>
<FormButtonsGroup values={values} deleteAction={deleteAction} dirty={dirty} reset={resetForm} />
<DEBUG values={values} />
</Form>
)}
</Formik>
</div>
</div>
</div>
@ -333,13 +308,13 @@ export default function FilterDetails() {
);
}
function General() {
export function General() {
const { isLoading, data: indexers } = useQuery(
["filters", "indexer_list"],
APIClient.indexers.getOptions,
{ refetchOnWindowFocus: false }
);
const opts = indexers && indexers.length > 0 ? indexers.map(v => ({
label: v.name,
value: v.id
@ -380,7 +355,7 @@ function General() {
);
}
function MoviesTv() {
export function MoviesTv() {
return (
<div>
<div className="mt-6 grid grid-cols-12 gap-6">
@ -424,7 +399,7 @@ function MoviesTv() {
);
}
function Music() {
export function Music() {
return (
<div>
<div className="mt-6 grid grid-cols-12 gap-6">
@ -476,7 +451,7 @@ function Music() {
);
}
function Advanced() {
export function Advanced() {
return (
<div>
<CollapsableSection title="Releases" subtitle="Match only certain release names and/or ignore other release names">
@ -559,7 +534,7 @@ interface FilterActionsProps {
values: FormikValues;
}
function FilterActions({ filter, values }: FilterActionsProps) {
export function FilterActions({ filter, values }: FilterActionsProps) {
const { data } = useQuery(
["filters", "download_clients"],
APIClient.download_clients.getAll,
@ -973,7 +948,7 @@ function FilterActionsItem({ action, clients, idx, remove }: FilterActionsItemPr
className="inline-flex items-center justify-center py-2 border border-transparent font-medium rounded-md text-red-700 dark:text-red-500 hover:text-red-500 dark:hover:text-red-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:text-sm"
onClick={toggleDeleteModal}
>
Remove
Remove
</button>
<div>
@ -982,7 +957,7 @@ function FilterActionsItem({ action, clients, idx, remove }: FilterActionsItemPr
className="light:bg-white light:border light:border-gray-300 rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-gray-700 dark:text-gray-500 light:hover:bg-gray-50 dark:hover:text-gray-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
onClick={toggleEdit}
>
Close
Close
</button>
</div>
</div>

View file

@ -41,7 +41,7 @@ export default function Filters() {
<header className="py-10">
<div className="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8 flex justify-between">
<h1 className="text-3xl font-bold text-black dark:text-white">
Filters
Filters
</h1>
<div className="flex-shrink-0">
<button
@ -49,7 +49,7 @@ export default function Filters() {
className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
onClick={toggleCreateFilter}
>
Add new
Add new
</button>
</div>
</div>
@ -101,8 +101,8 @@ function FilterList({ filters }: FilterListProps) {
}
interface FilterItemDropdownProps {
filter: Filter;
onToggle: (newState: boolean) => void;
filter: Filter;
onToggle: (newState: boolean) => void;
}
const FilterItemDropdown = ({
@ -172,7 +172,7 @@ const FilterItemDropdown = ({
<Menu.Item>
{({ active }) => (
<Link
to={`filters/${filter.id.toString()}`}
to={filter.id.toString()}
className={classNames(
active ? "bg-blue-600 text-white" : "text-gray-900 dark:text-gray-300",
"font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm"
@ -185,7 +185,7 @@ const FilterItemDropdown = ({
)}
aria-hidden="true"
/>
Edit
Edit
</Link>
)}
</Menu.Item>
@ -205,7 +205,7 @@ const FilterItemDropdown = ({
)}
aria-hidden="true"
/>
Toggle
Toggle
</button>
)}
</Menu.Item>
@ -225,7 +225,7 @@ const FilterItemDropdown = ({
)}
aria-hidden="true"
/>
Duplicate
Duplicate
</button>
)}
</Menu.Item>
@ -247,7 +247,7 @@ const FilterItemDropdown = ({
)}
aria-hidden="true"
/>
Delete
Delete
</button>
)}
</Menu.Item>
@ -319,7 +319,7 @@ function FilterListItem({ filter, idx }: FilterListItemProps) {
</td>
<td className="px-6 w-full whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-100">
<Link
to={`filters/${filter.id.toString()}`}
to={filter.id.toString()}
className="hover:text-black dark:hover:text-gray-300 w-full py-4 flex"
>
{filter.name}

View file

@ -186,7 +186,7 @@ export const ReleaseTable = () => {
headerGroup.headers.map((column) => (
column.Filter ? (
<div className="mt-2 sm:mt-0" key={column.id}>
{column.render("Filter")}
<>{column.render("Filter")}</>
</div>
) : null
))
@ -211,7 +211,7 @@ export const ReleaseTable = () => {
{...columnRest}
>
<div className="flex items-center justify-between">
{column.render("Header")}
<>{column.render("Header")}</>
{/* Add a sort direction indicator */}
<span>
{column.isSorted ? (
@ -251,7 +251,7 @@ export const ReleaseTable = () => {
role="cell"
{...cellRowRest}
>
{cell.render("Cell")}
<>{cell.render("Cell")}</>
</td>
);
})}

View file

@ -103,10 +103,10 @@ const ListItem = ({ idx, network }: ListItemProps) => {
<div className="col-span-4 flex justify-between items-center sm:px-6 text-sm text-gray-500 dark:text-gray-400 cursor-pointer" onClick={toggleEdit}>{network.server}:{network.port} {network.tls && <span className="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 dark:bg-green-300 text-green-800 dark:text-green-900">TLS</span>}</div>
{network.nickserv && network.nickserv.account ? (
<div className="col-span-4 items-center sm:px-6 text-sm text-gray-500 dark:text-gray-400 cursor-pointer" onClick={toggleEdit}>{network.nickserv.account}</div>
) : null}
) : <div className="col-span-4" />}
<div className="col-span-1 text-sm text-gray-500 dark:text-gray-400">
<span className="text-indigo-600 dark:text-gray-300 hover:text-indigo-900 cursor-pointer" onClick={toggleUpdate}>
Edit
Edit
</span>
</div>
</div>