mirror of
https://github.com/idanoo/autobrr
synced 2025-07-23 08:49:13 +00:00
Refactor: Filter form (#31)
* chore: add packages * chore: disable tailwind jit * refactor: update base layout structure * refactor: filter to single form with formik * feat: save filter actions on save * feat: disable form dirty
This commit is contained in:
parent
c092ffc62d
commit
974ca95d80
19 changed files with 1915 additions and 1112 deletions
|
@ -189,6 +189,16 @@ func (s *service) Update(filter domain.Filter) (*domain.Filter, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// store actions
|
||||||
|
if filter.Actions != nil {
|
||||||
|
for _, action := range filter.Actions {
|
||||||
|
if _, err := s.actionRepo.Store(action); err != nil {
|
||||||
|
log.Error().Err(err).Msgf("could not store filter actions: %v", filter.Name)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return f, nil
|
return f, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
"@types/react-dom": "^17.0.0",
|
"@types/react-dom": "^17.0.0",
|
||||||
"final-form": "^4.20.2",
|
"final-form": "^4.20.2",
|
||||||
"final-form-arrays": "^3.0.2",
|
"final-form-arrays": "^3.0.2",
|
||||||
|
"formik": "^2.2.9",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-cookie": "^4.1.1",
|
"react-cookie": "^4.1.1",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
|
|
|
@ -22,11 +22,9 @@ function App() {
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<Router basename={baseUrl()}>
|
<Router basename={baseUrl()}>
|
||||||
<Switch>
|
|
||||||
<Route exact={true} path="/login" component={Login}/>
|
<Route exact={true} path="/login" component={Login}/>
|
||||||
<Route exact={true} path="/logout" component={Logout}/>
|
<Route exact={true} path="/logout" component={Logout}/>
|
||||||
<Route exact={true} path="/*" component={Protected}/>
|
<Route exact={true} path="/*" component={Protected}/>
|
||||||
</Switch>
|
|
||||||
</Router>
|
</Router>
|
||||||
<ReactQueryDevtools initialIsOpen={false}/>
|
<ReactQueryDevtools initialIsOpen={false}/>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
|
|
|
@ -32,6 +32,7 @@ const TextField: React.FC<Props> = ({ name, label, placeholder, columns , classN
|
||||||
{...input}
|
{...input}
|
||||||
id={name}
|
id={name}
|
||||||
type="text"
|
type="text"
|
||||||
|
value={input.value}
|
||||||
autoComplete={autoComplete}
|
autoComplete={autoComplete}
|
||||||
className="mt-2 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-light-blue-500 focus:border-light-blue-500 sm:text-sm"
|
className="mt-2 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-light-blue-500 focus:border-light-blue-500 sm:text-sm"
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
|
|
|
@ -1,207 +1,207 @@
|
||||||
import {Fragment} from 'react'
|
import { Fragment } from 'react'
|
||||||
import {Disclosure, Menu, Transition} from '@headlessui/react'
|
import { Disclosure, Menu, Transition } from '@headlessui/react'
|
||||||
import {BellIcon, ChevronDownIcon, MenuIcon, XIcon} from '@heroicons/react/outline'
|
import { ChevronDownIcon, MenuIcon, XIcon } from '@heroicons/react/outline'
|
||||||
import {NavLink,Link, Route, Switch} from "react-router-dom";
|
import { NavLink, Link, Route, Switch } from "react-router-dom";
|
||||||
import Settings from "./Settings";
|
import Settings from "./Settings";
|
||||||
import { Dashboard } from "./Dashboard";
|
import { Dashboard } from "./Dashboard";
|
||||||
import { FilterDetails, Filters} from "./Filters";
|
import { FilterDetails, Filters } from "./filters";
|
||||||
import Logs from './Logs';
|
import Logs from './Logs';
|
||||||
|
import logo from '../logo.png';
|
||||||
const profile = ['Settings', 'Sign out']
|
|
||||||
|
|
||||||
function classNames(...classes: string[]) {
|
function classNames(...classes: string[]) {
|
||||||
return classes.filter(Boolean).join(' ')
|
return classes.filter(Boolean).join(' ')
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Base() {
|
export default function Base() {
|
||||||
const nav = [{name: 'Dashboard', path: "/"}, {name: 'Filters', path: "/filters"}, {name: "Settings", path: "/settings"},{name: "Logs", path: "/logs"}]
|
const nav = [{ name: 'Dashboard', path: "/" }, { name: 'Filters', path: "/filters" }, { name: "Settings", path: "/settings" }, { name: "Logs", path: "/logs" }]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="">
|
||||||
<div className="">
|
<Disclosure as="nav" className="bg-gray-800 pb-48">
|
||||||
<Disclosure as="nav" className="bg-gray-800 pb-48">
|
{({ open }) => (
|
||||||
{({open}) => (
|
<>
|
||||||
<>
|
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||||
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
<div className="border-b border-gray-700">
|
||||||
<div className="border-b border-gray-700">
|
<div className="flex items-center justify-between h-16 px-4 sm:px-0">
|
||||||
<div className="flex items-center justify-between h-16 px-4 sm:px-0">
|
<div className="flex items-center">
|
||||||
<div className="flex items-center">
|
<div className="flex-shrink-0 flex items-center">
|
||||||
<div className="hidden md:block">
|
<img
|
||||||
<div className="flex items-baseline space-x-4">
|
className="block lg:hidden h-8 w-auto"
|
||||||
{nav.map((item, itemIdx) =>
|
src={logo}
|
||||||
<NavLink
|
alt="Logo"
|
||||||
key={itemIdx}
|
/>
|
||||||
to={item.path}
|
<img
|
||||||
exact={true}
|
className="hidden lg:block h-8 w-auto"
|
||||||
activeClassName="bg-gray-900 text-white "
|
src={logo}
|
||||||
className="text-gray-300 hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium"
|
alt="Logo"
|
||||||
>
|
/>
|
||||||
{item.name}
|
|
||||||
</NavLink>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden md:block">
|
<div className="sm:ml-6 hidden sm:block">
|
||||||
<div className="ml-4 flex items-center md:ml-6">
|
<div className="flex items-baseline space-x-4">
|
||||||
{/*<button*/}
|
{nav.map((item, itemIdx) =>
|
||||||
{/* className="bg-gray-800 p-1 text-gray-400 rounded-full hover:text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white">*/}
|
<NavLink
|
||||||
{/* <span className="sr-only">View notifications</span>*/}
|
key={itemIdx}
|
||||||
{/* <BellIcon className="h-6 w-6" aria-hidden="true"/>*/}
|
to={item.path}
|
||||||
{/*</button>*/}
|
exact={true}
|
||||||
|
activeClassName="bg-gray-900 text-white "
|
||||||
<Menu as="div" className="ml-3 relative">
|
className="text-gray-300 hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium"
|
||||||
{({open}) => (
|
>
|
||||||
<>
|
{item.name}
|
||||||
<div>
|
</NavLink>
|
||||||
<Menu.Button
|
|
||||||
className="max-w-xs bg-gray-800 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white">
|
|
||||||
<span
|
|
||||||
className="hidden text-gray-300 text-sm font-medium lg:block">
|
|
||||||
<span className="sr-only">Open user menu for </span>User
|
|
||||||
</span>
|
|
||||||
<ChevronDownIcon
|
|
||||||
className="hidden flex-shrink-0 ml-1 h-5 w-5 text-gray-400 lg:block"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</Menu.Button>
|
|
||||||
</div>
|
|
||||||
<Transition
|
|
||||||
show={open}
|
|
||||||
as={Fragment}
|
|
||||||
enter="transition ease-out duration-100"
|
|
||||||
enterFrom="transform opacity-0 scale-95"
|
|
||||||
enterTo="transform opacity-100 scale-100"
|
|
||||||
leave="transition ease-in duration-75"
|
|
||||||
leaveFrom="transform opacity-100 scale-100"
|
|
||||||
leaveTo="transform opacity-0 scale-95"
|
|
||||||
>
|
|
||||||
<Menu.Items
|
|
||||||
static
|
|
||||||
className="origin-top-right absolute right-0 mt-2 w-48 z-10 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none"
|
|
||||||
>
|
|
||||||
<Menu.Item>
|
|
||||||
{({active}) => (
|
|
||||||
<Link
|
|
||||||
to="settings"
|
|
||||||
className={classNames(
|
|
||||||
active ? 'bg-gray-100' : '',
|
|
||||||
'block px-4 py-2 text-sm text-gray-700'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
Settings
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item>
|
|
||||||
{({active}) => (
|
|
||||||
<Link
|
|
||||||
to="/logout"
|
|
||||||
className={classNames(
|
|
||||||
active ? 'bg-gray-100' : '',
|
|
||||||
'block px-4 py-2 text-sm text-gray-700'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
Logout
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</Menu.Item>
|
|
||||||
</Menu.Items>
|
|
||||||
</Transition>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Menu>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="-mr-2 flex md:hidden">
|
|
||||||
{/* Mobile menu button */}
|
|
||||||
<Disclosure.Button
|
|
||||||
className="bg-gray-800 inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white">
|
|
||||||
<span className="sr-only">Open main menu</span>
|
|
||||||
{open ? (
|
|
||||||
<XIcon className="block h-6 w-6" aria-hidden="true"/>
|
|
||||||
) : (
|
|
||||||
<MenuIcon className="block h-6 w-6" aria-hidden="true"/>
|
|
||||||
)}
|
)}
|
||||||
</Disclosure.Button>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="hidden sm:block">
|
||||||
|
<div className="ml-4 flex items-center sm:ml-6">
|
||||||
|
<Menu as="div" className="ml-3 relative">
|
||||||
|
{({ open }) => (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<Menu.Button
|
||||||
|
className="max-w-xs bg-gray-800 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white">
|
||||||
|
<span
|
||||||
|
className="hidden text-gray-300 text-sm font-medium sm:block">
|
||||||
|
<span className="sr-only">Open user menu for </span>User
|
||||||
|
</span>
|
||||||
|
<ChevronDownIcon
|
||||||
|
className="hidden flex-shrink-0 ml-1 h-5 w-5 text-gray-400 sm:block"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</Menu.Button>
|
||||||
|
</div>
|
||||||
|
<Transition
|
||||||
|
show={open}
|
||||||
|
as={Fragment}
|
||||||
|
enter="transition ease-out duration-100"
|
||||||
|
enterFrom="transform opacity-0 scale-95"
|
||||||
|
enterTo="transform opacity-100 scale-100"
|
||||||
|
leave="transition ease-in duration-75"
|
||||||
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
|
leaveTo="transform opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
<Menu.Items
|
||||||
|
static
|
||||||
|
className="origin-top-right absolute right-0 mt-2 w-48 z-10 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none"
|
||||||
|
>
|
||||||
|
<Menu.Item>
|
||||||
|
{({ active }) => (
|
||||||
|
<Link
|
||||||
|
to="settings"
|
||||||
|
className={classNames(
|
||||||
|
active ? 'bg-gray-100' : '',
|
||||||
|
'block px-4 py-2 text-sm text-gray-700'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Settings
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item>
|
||||||
|
{({ active }) => (
|
||||||
|
<Link
|
||||||
|
to="/logout"
|
||||||
|
className={classNames(
|
||||||
|
active ? 'bg-gray-100' : '',
|
||||||
|
'block px-4 py-2 text-sm text-gray-700'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Logout
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu.Items>
|
||||||
|
</Transition>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="-mr-2 flex sm:hidden">
|
||||||
|
{/* Mobile menu button */}
|
||||||
|
<Disclosure.Button
|
||||||
|
className="bg-gray-800 inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white">
|
||||||
|
<span className="sr-only">Open main menu</span>
|
||||||
|
{open ? (
|
||||||
|
<XIcon className="block h-6 w-6" aria-hidden="true" />
|
||||||
|
) : (
|
||||||
|
<MenuIcon className="block h-6 w-6" aria-hidden="true" />
|
||||||
|
)}
|
||||||
|
</Disclosure.Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Disclosure.Panel className="border-b border-gray-700 md:hidden">
|
<Disclosure.Panel className="border-b border-gray-700 md:hidden">
|
||||||
<div className="px-2 py-3 space-y-1 sm:px-3">
|
<div className="px-2 py-3 space-y-1 sm:px-3">
|
||||||
{nav.map((item, itemIdx) =>
|
{nav.map((item, itemIdx) =>
|
||||||
itemIdx === 0 ? (
|
itemIdx === 0 ? (
|
||||||
<Fragment key={item.path}>
|
<Fragment key={item.path}>
|
||||||
{/* Current: "bg-gray-900 text-white", Default: "text-gray-300 hover:bg-gray-700 hover:text-white" */}
|
{/* Current: "bg-gray-900 text-white", Default: "text-gray-300 hover:bg-gray-700 hover:text-white" */}
|
||||||
<Link to={item.path}
|
<Link to={item.path}
|
||||||
className="bg-gray-900 text-white block px-3 py-2 rounded-md text-base font-medium">
|
className="bg-gray-900 text-white block px-3 py-2 rounded-md text-base font-medium">
|
||||||
{item.name}
|
|
||||||
</Link>
|
|
||||||
</Fragment>
|
|
||||||
) : (
|
|
||||||
<Link
|
|
||||||
key={item.path}
|
|
||||||
to={item.path}
|
|
||||||
className="text-gray-300 hover:bg-gray-700 hover:text-white block px-3 py-2 rounded-md text-base font-medium"
|
|
||||||
>
|
|
||||||
{item.name}
|
{item.name}
|
||||||
</Link>
|
</Link>
|
||||||
)
|
</Fragment>
|
||||||
)}
|
) : (
|
||||||
</div>
|
<Link
|
||||||
<div className="pt-4 pb-3 border-t border-gray-700">
|
key={item.path}
|
||||||
<div className="flex items-center px-5">
|
to={item.path}
|
||||||
<div>
|
className="text-gray-300 hover:bg-gray-700 hover:text-white block px-3 py-2 rounded-md text-base font-medium"
|
||||||
<div className="text-base font-medium leading-none text-white">User</div>
|
>
|
||||||
{/*<div className="text-sm font-medium leading-none text-gray-400">tom@example.com</div>*/}
|
{item.name}
|
||||||
</div>
|
</Link>
|
||||||
<button
|
)
|
||||||
className="ml-auto bg-gray-800 flex-shrink-0 p-1 text-gray-400 rounded-full hover:text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white">
|
)}
|
||||||
<span className="sr-only">View notifications</span>
|
</div>
|
||||||
<BellIcon className="h-6 w-6" aria-hidden="true"/>
|
<div className="pt-4 pb-3 border-t border-gray-700">
|
||||||
</button>
|
<div className="flex items-center px-5">
|
||||||
</div>
|
<div>
|
||||||
<div className="mt-3 px-2 space-y-1">
|
<div className="text-base font-medium leading-none text-white">User</div>
|
||||||
{profile.map((item) => (
|
|
||||||
<Link
|
|
||||||
key={item}
|
|
||||||
to={item}
|
|
||||||
className="block px-3 py-2 rounded-md text-base font-medium text-gray-400 hover:text-white hover:bg-gray-700"
|
|
||||||
>
|
|
||||||
{item}
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Disclosure.Panel>
|
<div className="mt-3 px-2 space-y-1">
|
||||||
</>
|
<Link
|
||||||
)}
|
to="settings"
|
||||||
</Disclosure>
|
className="block px-3 py-2 rounded-md text-base font-medium text-gray-400 hover:text-white hover:bg-gray-700"
|
||||||
|
>
|
||||||
|
Settings
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="/logout"
|
||||||
|
className="block px-3 py-2 rounded-md text-base font-medium text-gray-400 hover:text-white hover:bg-gray-700"
|
||||||
|
>
|
||||||
|
Logout
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Disclosure.Panel>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Disclosure>
|
||||||
|
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path="/logs">
|
<Route path="/logs">
|
||||||
<Logs />
|
<Logs />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
<Route path="/settings">
|
<Route path="/settings">
|
||||||
<Settings/>
|
<Settings />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
<Route exact={true} path="/filters">
|
<Route exact={true} path="/filters">
|
||||||
<Filters/>
|
<Filters />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
<Route path="/filters/:filterId">
|
<Route path="/filters/:filterId">
|
||||||
<FilterDetails />
|
<FilterDetails />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
<Route exact path="/">
|
<Route exact path="/">
|
||||||
<Dashboard />
|
<Dashboard />
|
||||||
</Route>
|
</Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
|
@ -1,930 +0,0 @@
|
||||||
import React, {Fragment, useRef, useState} from "react";
|
|
||||||
import {Dialog, Switch, Transition} from "@headlessui/react";
|
|
||||||
import {ChevronDownIcon, ChevronRightIcon, ExclamationIcon,} from '@heroicons/react/solid'
|
|
||||||
import {EmptyListState} from "../components/EmptyListState";
|
|
||||||
|
|
||||||
import {
|
|
||||||
Link,
|
|
||||||
NavLink,
|
|
||||||
Route,
|
|
||||||
Switch as RouteSwitch,
|
|
||||||
useHistory,
|
|
||||||
useLocation,
|
|
||||||
useParams,
|
|
||||||
useRouteMatch
|
|
||||||
} from "react-router-dom";
|
|
||||||
import {FilterActionList} from "../components/FilterActionList";
|
|
||||||
import {DownloadClient, Filter, Indexer} from "../domain/interfaces";
|
|
||||||
import {useToggle} from "../hooks/hooks";
|
|
||||||
import {useMutation, useQuery} from "react-query";
|
|
||||||
import {queryClient} from "../App";
|
|
||||||
import {CONTAINER_OPTIONS, CODECS_OPTIONS, RESOLUTION_OPTIONS, SOURCES_OPTIONS} from "../domain/constants";
|
|
||||||
import {Field, Form} from "react-final-form";
|
|
||||||
import {MultiSelectField, TextField} from "../components/inputs";
|
|
||||||
import DEBUG from "../components/debug";
|
|
||||||
import TitleSubtitle from "../components/headings/TitleSubtitle";
|
|
||||||
import { SwitchGroup } from "../components/inputs";
|
|
||||||
import {classNames} from "../styles/utils";
|
|
||||||
import { FilterAddForm, FilterActionAddForm} from "../forms";
|
|
||||||
import Select from "react-select";
|
|
||||||
import APIClient from "../api/APIClient";
|
|
||||||
|
|
||||||
import { toast } from 'react-hot-toast'
|
|
||||||
import Toast from '../components/notifications/Toast';
|
|
||||||
|
|
||||||
|
|
||||||
const tabs = [
|
|
||||||
{name: 'General', href: '', current: true},
|
|
||||||
// { name: 'TV', href: 'tv', current: false },
|
|
||||||
// { name: 'Movies', href: 'movies', current: false },
|
|
||||||
{name: 'Movies and TV', href: 'movies-tv', current: false},
|
|
||||||
// { name: 'P2P', href: 'p2p', current: false },
|
|
||||||
{name: 'Advanced', href: 'advanced', current: false},
|
|
||||||
{name: 'Actions', href: 'actions', current: false},
|
|
||||||
]
|
|
||||||
|
|
||||||
function TabNavLink({item, url}: any) {
|
|
||||||
const location = useLocation();
|
|
||||||
|
|
||||||
const {pathname} = location;
|
|
||||||
const splitLocation = pathname.split("/");
|
|
||||||
|
|
||||||
// we need to clean the / if it's a base root path
|
|
||||||
let too = item.href ? `${url}/${item.href}` : url
|
|
||||||
|
|
||||||
return (
|
|
||||||
<NavLink
|
|
||||||
key={item.name}
|
|
||||||
to={too}
|
|
||||||
exact={true}
|
|
||||||
activeClassName="border-purple-500 text-purple-600"
|
|
||||||
className={classNames(
|
|
||||||
'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm'
|
|
||||||
)}
|
|
||||||
aria-current={splitLocation[2] === item.href ? 'page' : undefined}
|
|
||||||
>
|
|
||||||
{item.name}
|
|
||||||
</NavLink>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Filters() {
|
|
||||||
const [createFilterIsOpen, toggleCreateFilter] = useToggle(false)
|
|
||||||
|
|
||||||
const {isLoading, error, data} = useQuery<Filter[], Error>('filter', APIClient.filters.getAll,
|
|
||||||
{
|
|
||||||
refetchOnWindowFocus: false
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) return (<p>'An error has occurred: '</p>)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<main className="-mt-48 ">
|
|
||||||
<FilterAddForm isOpen={createFilterIsOpen} toggle={toggleCreateFilter}/>
|
|
||||||
|
|
||||||
<header className="py-10">
|
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex justify-between">
|
|
||||||
<h1 className="text-3xl font-bold text-white capitalize">Filters</h1>
|
|
||||||
|
|
||||||
<div className="flex-shrink-0">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
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 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
|
||||||
onClick={toggleCreateFilter}
|
|
||||||
>
|
|
||||||
Add new
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div className="max-w-7xl mx-auto pb-12 px-4 sm:px-6 lg:px-8">
|
|
||||||
<div className="bg-white rounded-lg shadow">
|
|
||||||
<div className="relative inset-0 py-3 px-3 sm:px-3 lg:px-3 h-full">
|
|
||||||
{data && data.length > 0 ? <FilterList filters={data}/> :
|
|
||||||
<EmptyListState text="No filters here.." buttonText="Add new" buttonOnClick={toggleCreateFilter}/>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FilterListProps {
|
|
||||||
filters: Filter[];
|
|
||||||
}
|
|
||||||
|
|
||||||
function FilterList({filters}: FilterListProps) {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
|
||||||
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
|
|
||||||
<div className="shadow overflow-hidden border-b border-gray-200 sm:rounded-lg">
|
|
||||||
<table className="min-w-full divide-y divide-gray-200">
|
|
||||||
<thead className="bg-gray-50">
|
|
||||||
<tr>
|
|
||||||
<th
|
|
||||||
scope="col"
|
|
||||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
|
||||||
>
|
|
||||||
Enabled
|
|
||||||
</th>
|
|
||||||
<th
|
|
||||||
scope="col"
|
|
||||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
|
||||||
>
|
|
||||||
Name
|
|
||||||
</th>
|
|
||||||
<th
|
|
||||||
scope="col"
|
|
||||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
|
||||||
>
|
|
||||||
Indexers
|
|
||||||
</th>
|
|
||||||
<th scope="col" className="relative px-6 py-3">
|
|
||||||
<span className="sr-only">Edit</span>
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="bg-white divide-y divide-gray-200">
|
|
||||||
{filters.map((filter: Filter, idx) => (
|
|
||||||
<FilterListItem filter={filter} key={idx} idx={idx}/>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FilterListItemProps {
|
|
||||||
filter: Filter;
|
|
||||||
idx: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
function FilterListItem({filter, idx}: FilterListItemProps) {
|
|
||||||
const [enabled, setEnabled] = useState(filter.enabled)
|
|
||||||
|
|
||||||
const toggleActive = (status: boolean) => {
|
|
||||||
console.log(status)
|
|
||||||
setEnabled(status)
|
|
||||||
// call api
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<tr key={filter.name}
|
|
||||||
className={idx % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
||||||
<Switch
|
|
||||||
checked={enabled}
|
|
||||||
onChange={toggleActive}
|
|
||||||
className={classNames(
|
|
||||||
enabled ? 'bg-teal-500' : 'bg-gray-200',
|
|
||||||
'relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-light-blue-500'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span className="sr-only">Use setting</span>
|
|
||||||
<span
|
|
||||||
aria-hidden="true"
|
|
||||||
className={classNames(
|
|
||||||
enabled ? 'translate-x-5' : 'translate-x-0',
|
|
||||||
'inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200'
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Switch>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 w-full whitespace-nowrap text-sm font-medium text-gray-900">{filter.name}</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{filter.indexers && filter.indexers.map(t =>
|
|
||||||
<span key={t.id} className="mr-2 inline-flex items-center px-2.5 py-0.5 rounded-md text-sm font-medium bg-gray-100 text-gray-800">{t.name}</span>)}</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
|
||||||
<Link to={`filters/${filter.id.toString()}`} className="text-indigo-600 hover:text-indigo-900">
|
|
||||||
Edit
|
|
||||||
</Link>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const FormButtonsGroup = ({deleteAction}: any) => {
|
|
||||||
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false)
|
|
||||||
|
|
||||||
const cancelButtonRef = useRef(null)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="pt-6 divide-y divide-gray-200">
|
|
||||||
|
|
||||||
<Transition.Root show={deleteModalIsOpen} as={Fragment}>
|
|
||||||
<Dialog
|
|
||||||
as="div"
|
|
||||||
static
|
|
||||||
className="fixed z-10 inset-0 overflow-y-auto"
|
|
||||||
initialFocus={cancelButtonRef}
|
|
||||||
open={deleteModalIsOpen}
|
|
||||||
onClose={toggleDeleteModal}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
|
||||||
<Transition.Child
|
|
||||||
as={Fragment}
|
|
||||||
enter="ease-out duration-300"
|
|
||||||
enterFrom="opacity-0"
|
|
||||||
enterTo="opacity-100"
|
|
||||||
leave="ease-in duration-200"
|
|
||||||
leaveFrom="opacity-100"
|
|
||||||
leaveTo="opacity-0"
|
|
||||||
>
|
|
||||||
<Dialog.Overlay className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"/>
|
|
||||||
</Transition.Child>
|
|
||||||
|
|
||||||
{/* This element is to trick the browser into centering the modal contents. */}
|
|
||||||
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
|
|
||||||
​
|
|
||||||
</span>
|
|
||||||
<Transition.Child
|
|
||||||
as={Fragment}
|
|
||||||
enter="ease-out duration-300"
|
|
||||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
|
||||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
|
||||||
leave="ease-in duration-200"
|
|
||||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
|
||||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
|
|
||||||
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
|
||||||
<div className="sm:flex sm:items-start">
|
|
||||||
<div
|
|
||||||
className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
|
|
||||||
<ExclamationIcon className="h-6 w-6 text-red-600" aria-hidden="true"/>
|
|
||||||
</div>
|
|
||||||
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
|
||||||
<Dialog.Title as="h3"
|
|
||||||
className="text-lg leading-6 font-medium text-gray-900">
|
|
||||||
Remove filter
|
|
||||||
</Dialog.Title>
|
|
||||||
<div className="mt-2">
|
|
||||||
<p className="text-sm text-gray-500">
|
|
||||||
Are you sure you want to remove this filter?
|
|
||||||
This action cannot be undone.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm"
|
|
||||||
onClick={deleteAction}
|
|
||||||
>
|
|
||||||
Remove
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
|
|
||||||
onClick={toggleDeleteModal}
|
|
||||||
ref={cancelButtonRef}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Transition.Child>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
</Transition.Root>
|
|
||||||
|
|
||||||
<div className="mt-4 pt-4 flex justify-between">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="inline-flex items-center justify-center px-4 py-2 border border-transparent font-medium rounded-md text-red-700 bg-red-100 hover:bg-red-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:text-sm"
|
|
||||||
onClick={toggleDeleteModal}
|
|
||||||
>
|
|
||||||
Remove
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="bg-white border border-gray-300 rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-light-blue-500"
|
|
||||||
>
|
|
||||||
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 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function FilterDetails() {
|
|
||||||
let {url} = useRouteMatch();
|
|
||||||
let history = useHistory();
|
|
||||||
let {filterId}: any = useParams();
|
|
||||||
|
|
||||||
const {isLoading, data} = useQuery<Filter, Error>(['filter', parseInt(filterId)], () => APIClient.filters.getByID(parseInt(filterId)),
|
|
||||||
{
|
|
||||||
retry: false,
|
|
||||||
refetchOnWindowFocus: false,
|
|
||||||
onError: err => {
|
|
||||||
history.push("./")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<main className="-mt-48 ">
|
|
||||||
<header className="py-10">
|
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex items-center">
|
|
||||||
<h1 className="text-3xl font-bold text-white capitalize">
|
|
||||||
<NavLink to="/filters" exact={true}>
|
|
||||||
Filters
|
|
||||||
</NavLink>
|
|
||||||
</h1>
|
|
||||||
<ChevronRightIcon className="h-6 w-6 text-gray-500" aria-hidden="true"/>
|
|
||||||
<h1 className="text-3xl font-bold text-white capitalize">{data.name}</h1>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<div className="max-w-7xl mx-auto pb-12 px-4 sm:px-6 lg:px-8">
|
|
||||||
<div className="bg-white 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="py-6">
|
|
||||||
{/* Tabs */}
|
|
||||||
<div className="lg:hidden">
|
|
||||||
<label htmlFor="selected-tab" className="sr-only">
|
|
||||||
Select a tab
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
id="selected-tab"
|
|
||||||
name="selected-tab"
|
|
||||||
className="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-purple-500 focus:border-purple-500 sm:text-sm rounded-md"
|
|
||||||
>
|
|
||||||
{tabs.map((tab) => (
|
|
||||||
<option key={tab.name}>{tab.name}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div className="hidden lg:block">
|
|
||||||
<div className="border-b border-gray-200">
|
|
||||||
<nav className="-mb-px flex space-x-8">
|
|
||||||
{tabs.map((tab) => (
|
|
||||||
<TabNavLink item={tab} url={url} key={tab.href}/>
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<RouteSwitch>
|
|
||||||
<Route exact path={url}>
|
|
||||||
<FilterTabGeneral filter={data}/>
|
|
||||||
</Route>
|
|
||||||
|
|
||||||
<Route path={`${url}/movies-tv`}>
|
|
||||||
{/*<FilterTabMoviesTv filter={data}/>*/}
|
|
||||||
<FilterTabMoviesTvNew2 filter={data}/>
|
|
||||||
</Route>
|
|
||||||
|
|
||||||
{/*<Route path={`${path}/movies`}>*/}
|
|
||||||
{/* <p>movies</p>*/}
|
|
||||||
{/*</Route>*/}
|
|
||||||
|
|
||||||
{/*<Route path={`${path}/p2p`}>*/}
|
|
||||||
{/* <p>p2p</p>*/}
|
|
||||||
{/*</Route>*/}
|
|
||||||
|
|
||||||
<Route path={`${url}/advanced`}>
|
|
||||||
<FilterTabAdvanced filter={data}/>
|
|
||||||
</Route>
|
|
||||||
|
|
||||||
<Route path={`${url}/actions`}>
|
|
||||||
<FilterTabActions filter={data}/>
|
|
||||||
</Route>
|
|
||||||
|
|
||||||
</RouteSwitch>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FilterTabGeneralProps {
|
|
||||||
filter: Filter;
|
|
||||||
}
|
|
||||||
|
|
||||||
function FilterTabGeneral({filter}: FilterTabGeneralProps) {
|
|
||||||
const history = useHistory();
|
|
||||||
|
|
||||||
const { data } = useQuery<Indexer[], Error>('indexerList', APIClient.indexers.getOptions,
|
|
||||||
{
|
|
||||||
refetchOnWindowFocus: false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const updateMutation = useMutation((filter: Filter) => APIClient.filters.update(filter), {
|
|
||||||
onSuccess: () => {
|
|
||||||
// queryClient.setQueryData(['filter', filter.id], data)
|
|
||||||
toast.custom((t) => <Toast type="success" body={`${filter.name} was updated successfully`} t={t} />)
|
|
||||||
|
|
||||||
queryClient.invalidateQueries(["filter",filter.id]);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const deleteMutation = useMutation((id: number) => APIClient.filters.delete(id), {
|
|
||||||
onSuccess: () => {
|
|
||||||
// invalidate filters
|
|
||||||
queryClient.invalidateQueries("filter");
|
|
||||||
toast.custom((t) => <Toast type="success" body={`${filter.name} was deleted`} t={t} />)
|
|
||||||
|
|
||||||
// redirect
|
|
||||||
history.push("/filters")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const submitOther = (data: Filter) => {
|
|
||||||
updateMutation.mutate(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteAction = () => {
|
|
||||||
deleteMutation.mutate(filter.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Form
|
|
||||||
initialValues={{
|
|
||||||
id: filter.id,
|
|
||||||
name: filter.name,
|
|
||||||
enabled: filter.enabled,
|
|
||||||
min_size: filter.min_size,
|
|
||||||
max_size: filter.max_size,
|
|
||||||
delay: filter.delay,
|
|
||||||
shows: filter.shows,
|
|
||||||
years: filter.years,
|
|
||||||
resolutions: filter.resolutions || [],
|
|
||||||
sources: filter.sources || [],
|
|
||||||
codecs: filter.codecs || [],
|
|
||||||
containers: filter.containers || [],
|
|
||||||
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,
|
|
||||||
match_tags: filter.match_tags,
|
|
||||||
except_tags: filter.except_tags,
|
|
||||||
match_uploaders: filter.match_uploaders,
|
|
||||||
except_uploaders: filter.except_uploaders,
|
|
||||||
freeleech: filter.freeleech,
|
|
||||||
freeleech_percent: filter.freeleech_percent,
|
|
||||||
indexers: filter.indexers || [],
|
|
||||||
}}
|
|
||||||
// validate={validate}
|
|
||||||
onSubmit={submitOther}
|
|
||||||
>
|
|
||||||
{({handleSubmit, submitting, values, valid}) => {
|
|
||||||
return (
|
|
||||||
<form onSubmit={handleSubmit}>
|
|
||||||
<div>
|
|
||||||
<div className="mt-6 lg:pb-8">
|
|
||||||
|
|
||||||
<div className="mt-6 grid grid-cols-12 gap-6">
|
|
||||||
<TextField name="name" label="Filter name" columns={6} placeholder="eg. Filter 1"/>
|
|
||||||
|
|
||||||
<div className="col-span-6">
|
|
||||||
<label htmlFor="indexers" className="block text-xs font-bold text-gray-700 uppercase tracking-wide">
|
|
||||||
Indexers
|
|
||||||
</label>
|
|
||||||
<Field
|
|
||||||
name="indexers"
|
|
||||||
multiple={true}
|
|
||||||
parse={val => val && val.map((item: any) => ({ id: item.value.id, name: item.value.name, enabled: item.value.enabled, identifier: item.value.identifier}))}
|
|
||||||
format={values => values.map((val: any) => ({ label: val.name, value: val}))}
|
|
||||||
render={({input, meta}) => (
|
|
||||||
<Select {...input}
|
|
||||||
isClearable={true}
|
|
||||||
isMulti={true}
|
|
||||||
placeholder="Choose indexers"
|
|
||||||
className="mt-2 block w-full focus:outline-none focus:ring-light-blue-500 focus:border-light-blue-500 sm:text-sm"
|
|
||||||
options={data ? data.map(v => ({
|
|
||||||
label: v.name,
|
|
||||||
value: v
|
|
||||||
})) : []}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-6 lg:pb-8">
|
|
||||||
<TitleSubtitle title="Rules" subtitle="Set rules"/>
|
|
||||||
|
|
||||||
<div className="mt-6 grid grid-cols-12 gap-6">
|
|
||||||
<TextField name="min_size" label="Min size" columns={6} placeholder=""/>
|
|
||||||
<TextField name="max_size" label="Max size" columns={6} placeholder=""/>
|
|
||||||
<TextField name="delay" label="Delay" columns={6} placeholder=""/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="border-t">
|
|
||||||
<SwitchGroup name="enabled" label="Enabled" description="Enabled or disable filter."/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<FormButtonsGroup deleteAction={deleteAction}/>
|
|
||||||
|
|
||||||
<DEBUG values={values}/>
|
|
||||||
</form>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function FilterTabMoviesTvNew2({filter}: FilterTabGeneralProps) {
|
|
||||||
const history = useHistory();
|
|
||||||
|
|
||||||
const updateMutation = useMutation((filter: Filter) => APIClient.filters.update(filter), {
|
|
||||||
onSuccess: () => {
|
|
||||||
// queryClient.setQueryData(['filter', filter.id], data)
|
|
||||||
toast.custom((t) => <Toast type="success" body={`${filter.name} was updated successfully`} t={t} />)
|
|
||||||
queryClient.invalidateQueries(["filter",filter.id]);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const deleteMutation = useMutation((id: number) => APIClient.filters.delete(id), {
|
|
||||||
onSuccess: () => {
|
|
||||||
// invalidate filters
|
|
||||||
toast.custom((t) => <Toast type="success" body={`${filter.name} was deleted`} t={t} />)
|
|
||||||
queryClient.invalidateQueries("filter");
|
|
||||||
// redirect
|
|
||||||
history.push("/filters")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const deleteAction = () => {
|
|
||||||
deleteMutation.mutate(filter.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
const submitOther = (data: Filter) => {
|
|
||||||
updateMutation.mutate(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Form
|
|
||||||
initialValues={{
|
|
||||||
id: filter.id,
|
|
||||||
name: filter.name,
|
|
||||||
enabled: filter.enabled,
|
|
||||||
min_size: filter.min_size,
|
|
||||||
max_size: filter.max_size,
|
|
||||||
delay: filter.delay,
|
|
||||||
shows: filter.shows,
|
|
||||||
years: filter.years,
|
|
||||||
resolutions: filter.resolutions || [],
|
|
||||||
sources: filter.sources || [],
|
|
||||||
codecs: filter.codecs || [],
|
|
||||||
containers: filter.containers || [],
|
|
||||||
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,
|
|
||||||
match_tags: filter.match_tags,
|
|
||||||
except_tags: filter.except_tags,
|
|
||||||
match_uploaders: filter.match_uploaders,
|
|
||||||
except_uploaders: filter.except_uploaders,
|
|
||||||
freeleech: filter.freeleech,
|
|
||||||
freeleech_percent: filter.freeleech_percent,
|
|
||||||
indexers: filter.indexers || [],
|
|
||||||
}}
|
|
||||||
// validate={validate}
|
|
||||||
onSubmit={submitOther}
|
|
||||||
>
|
|
||||||
{({handleSubmit, submitting, values, valid}) => {
|
|
||||||
return (
|
|
||||||
<form onSubmit={handleSubmit}>
|
|
||||||
<div className="mt-6 grid grid-cols-12 gap-6">
|
|
||||||
<TextField name="shows" label="Movies / Shows" columns={8} placeholder="eg. Movie, Show 1, Show?2"/>
|
|
||||||
<TextField name="years" label="Years" columns={4} placeholder="eg. 2018,2019-2021"/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-6 lg:pb-8">
|
|
||||||
<TitleSubtitle title="Seasons and Episodes" subtitle="Set seaons and episodes"/>
|
|
||||||
|
|
||||||
<div className="mt-6 grid grid-cols-12 gap-6">
|
|
||||||
<TextField name="seasons" label="Seasons" columns={8} placeholder="eg. 1, 3, 2-6"/>
|
|
||||||
<TextField name="episodes" label="Episodes" columns={4} placeholder="eg. 2, 4, 10-20"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-6 lg:pb-8">
|
|
||||||
<TitleSubtitle title="Quality" subtitle="Resolution, source etc."/>
|
|
||||||
|
|
||||||
<div className="mt-6 grid grid-cols-12 gap-6">
|
|
||||||
<MultiSelectField name="resolutions" options={RESOLUTION_OPTIONS} label="resolutions" columns={6}/>
|
|
||||||
<MultiSelectField name="sources" options={SOURCES_OPTIONS} label="sources" columns={6}/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-6 grid grid-cols-12 gap-6">
|
|
||||||
<MultiSelectField name="codecs" options={CODECS_OPTIONS} label="codecs" columns={6}/>
|
|
||||||
<MultiSelectField name="containers" options={CONTAINER_OPTIONS} label="containers" columns={6}/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<FormButtonsGroup deleteAction={deleteAction}/>
|
|
||||||
|
|
||||||
<DEBUG values={values}/>
|
|
||||||
</form>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function FilterTabAdvanced({filter}: FilterTabGeneralProps) {
|
|
||||||
const history = useHistory();
|
|
||||||
const [releasesIsOpen, toggleReleases] = useToggle(false)
|
|
||||||
const [groupsIsOpen, toggleGroups] = useToggle(false)
|
|
||||||
const [categoriesIsOpen, toggleCategories] = useToggle(false)
|
|
||||||
const [uploadersIsOpen, toggleUploaders] = useToggle(false)
|
|
||||||
const [freeleechIsOpen, toggleFreeleech] = useToggle(false)
|
|
||||||
|
|
||||||
const updateMutation = useMutation((filter: Filter) => APIClient.filters.update(filter), {
|
|
||||||
onSuccess: () => {
|
|
||||||
// queryClient.setQueryData(['filter', filter.id], data)
|
|
||||||
toast.custom((t) => <Toast type="success" body={`${filter.name} was updated successfully`} t={t} />)
|
|
||||||
|
|
||||||
queryClient.invalidateQueries(["filter",filter.id]);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const deleteMutation = useMutation((id: number) => APIClient.filters.delete(id), {
|
|
||||||
onSuccess: () => {
|
|
||||||
// invalidate filters
|
|
||||||
queryClient.invalidateQueries("filter");
|
|
||||||
toast.custom((t) => <Toast type="success" body={`${filter.name} was deleted`} t={t} />)
|
|
||||||
|
|
||||||
// redirect
|
|
||||||
history.push("/filters")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const deleteAction = () => {
|
|
||||||
deleteMutation.mutate(filter.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
const submitOther = (data: Filter) => {
|
|
||||||
updateMutation.mutate(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Form
|
|
||||||
initialValues={{
|
|
||||||
id: filter.id,
|
|
||||||
name: filter.name,
|
|
||||||
enabled: filter.enabled,
|
|
||||||
min_size: filter.min_size,
|
|
||||||
max_size: filter.max_size,
|
|
||||||
delay: filter.delay,
|
|
||||||
shows: filter.shows,
|
|
||||||
years: filter.years,
|
|
||||||
resolutions: filter.resolutions || [],
|
|
||||||
sources: filter.sources || [],
|
|
||||||
codecs: filter.codecs || [],
|
|
||||||
containers: filter.containers || [],
|
|
||||||
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,
|
|
||||||
match_tags: filter.match_tags,
|
|
||||||
except_tags: filter.except_tags,
|
|
||||||
match_uploaders: filter.match_uploaders,
|
|
||||||
except_uploaders: filter.except_uploaders,
|
|
||||||
freeleech: filter.freeleech,
|
|
||||||
freeleech_percent: filter.freeleech_percent,
|
|
||||||
indexers: filter.indexers || [],
|
|
||||||
}}
|
|
||||||
// validate={validate}
|
|
||||||
onSubmit={submitOther}
|
|
||||||
>
|
|
||||||
{({handleSubmit, submitting, values, valid}) => {
|
|
||||||
return (
|
|
||||||
<form onSubmit={handleSubmit}>
|
|
||||||
<div className="mt-6 lg:pb-8 border-b border-gray-200">
|
|
||||||
<div className="flex justify-between items-center cursor-pointer" onClick={toggleReleases}>
|
|
||||||
<div className="-ml-2 -mt-2 flex flex-wrap items-baseline">
|
|
||||||
<h3 className="ml-2 mt-2 text-lg leading-6 font-medium text-gray-900">Releases</h3>
|
|
||||||
<p className="ml-2 mt-1 text-sm text-gray-500 truncate">Match or ignore</p>
|
|
||||||
</div>
|
|
||||||
<div className="mt-3 sm:mt-0 sm:ml-4">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="inline-flex items-center px-4 py-2 border-transparent text-sm font-medium text-white"
|
|
||||||
>
|
|
||||||
{releasesIsOpen ? <ChevronDownIcon className="h-6 w-6 text-gray-500" aria-hidden="true"/> : <ChevronRightIcon className="h-6 w-6 text-gray-500" aria-hidden="true"/>}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{releasesIsOpen && (
|
|
||||||
<div className="mt-6 grid grid-cols-12 gap-6">
|
|
||||||
<TextField name="match_releases" label="Match releases" columns={6} placeholder=""/>
|
|
||||||
<TextField name="except_releases" label="Except releases" columns={6}
|
|
||||||
placeholder=""/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-6 lg:pb-8 border-b border-gray-200">
|
|
||||||
<div className="flex justify-between items-center cursor-pointer" onClick={toggleGroups}>
|
|
||||||
<div className="-ml-2 -mt-2 flex flex-wrap items-baseline">
|
|
||||||
<h3 className="ml-2 mt-2 text-lg leading-6 font-medium text-gray-900">Groups</h3>
|
|
||||||
<p className="ml-2 mt-1 text-sm text-gray-500 truncate">Match or ignore</p>
|
|
||||||
</div>
|
|
||||||
<div className="mt-3 sm:mt-0 sm:ml-4">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="inline-flex items-center px-4 py-2 border-transparent text-sm font-medium text-white"
|
|
||||||
>
|
|
||||||
{groupsIsOpen ? <ChevronDownIcon className="h-6 w-6 text-gray-500" aria-hidden="true"/> : <ChevronRightIcon className="h-6 w-6 text-gray-500" aria-hidden="true"/>}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{groupsIsOpen && (
|
|
||||||
<div className="mt-6 grid grid-cols-12 gap-6">
|
|
||||||
<TextField name="match_release_groups" label="Match release groups" columns={6} placeholder=""/>
|
|
||||||
<TextField name="except_release_groups" label="Except release groups" columns={6} placeholder=""/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-6 lg:pb-8 border-b border-gray-200">
|
|
||||||
<div className="flex justify-between items-center cursor-pointer" onClick={toggleCategories}>
|
|
||||||
<div className="-ml-2 -mt-2 flex flex-wrap items-baseline">
|
|
||||||
<h3 className="ml-2 mt-2 text-lg leading-6 font-medium text-gray-900">Categories and tags</h3>
|
|
||||||
<p className="ml-2 mt-1 text-sm text-gray-500 truncate">Match or ignore categories or tags</p>
|
|
||||||
</div>
|
|
||||||
<div className="mt-3 sm:mt-0 sm:ml-4">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="inline-flex items-center px-4 py-2 border-transparent text-sm font-medium text-white"
|
|
||||||
>
|
|
||||||
{categoriesIsOpen ? <ChevronDownIcon className="h-6 w-6 text-gray-500" aria-hidden="true"/> : <ChevronRightIcon className="h-6 w-6 text-gray-500" aria-hidden="true"/>}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{categoriesIsOpen && (
|
|
||||||
<div className="mt-6 grid grid-cols-12 gap-6">
|
|
||||||
<TextField name="match_categories" label="Match categories" columns={6}
|
|
||||||
placeholder=""/>
|
|
||||||
<TextField name="except_categories" label="Except categories" columns={6}
|
|
||||||
placeholder=""/>
|
|
||||||
|
|
||||||
<TextField name="match_tags" label="Match tags" columns={6} placeholder=""/>
|
|
||||||
<TextField name="except_tags" label="Except tags" columns={6} placeholder=""/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-6 lg:pb-8 border-b border-gray-200">
|
|
||||||
<div className="flex justify-between items-center cursor-pointer" onClick={toggleUploaders}>
|
|
||||||
<div className="-ml-2 -mt-2 flex flex-wrap items-baseline">
|
|
||||||
<h3 className="ml-2 mt-2 text-lg leading-6 font-medium text-gray-900">Uploaders</h3>
|
|
||||||
<p className="ml-2 mt-1 text-sm text-gray-500 truncate">Match or ignore uploaders</p>
|
|
||||||
</div>
|
|
||||||
<div className="mt-3 sm:mt-0 sm:ml-4">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="inline-flex items-center px-4 py-2 border-transparent text-sm font-medium text-white"
|
|
||||||
>
|
|
||||||
{uploadersIsOpen ? <ChevronDownIcon className="h-6 w-6 text-gray-500" aria-hidden="true"/> : <ChevronRightIcon className="h-6 w-6 text-gray-500" aria-hidden="true"/>}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{uploadersIsOpen && (
|
|
||||||
<div className="mt-6 grid grid-cols-12 gap-6">
|
|
||||||
<TextField name="match_uploaders" label="Match uploaders" columns={6}
|
|
||||||
placeholder=""/>
|
|
||||||
<TextField name="except_uploaders" label="Except uploaders" columns={6}
|
|
||||||
placeholder=""/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-6 lg:pb-8 border-b border-gray-200">
|
|
||||||
<div className="flex justify-between items-center cursor-pointer" onClick={toggleFreeleech}>
|
|
||||||
<div className="-ml-2 -mt-2 flex flex-wrap items-baseline">
|
|
||||||
<h3 className="ml-2 mt-2 text-lg leading-6 font-medium text-gray-900">Freeleech</h3>
|
|
||||||
<p className="ml-2 mt-1 text-sm text-gray-500 truncate">Match only freeleech and freeleech percent</p>
|
|
||||||
</div>
|
|
||||||
<div className="mt-3 sm:mt-0 sm:ml-4">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="inline-flex items-center px-4 py-2 border-transparent text-sm font-medium text-white"
|
|
||||||
>
|
|
||||||
{freeleechIsOpen ? <ChevronDownIcon className="h-6 w-6 text-gray-500" aria-hidden="true"/> : <ChevronRightIcon className="h-6 w-6 text-gray-500" aria-hidden="true"/>}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{freeleechIsOpen && (
|
|
||||||
<div className="mt-6 grid grid-cols-12 gap-6">
|
|
||||||
<div className="col-span-6">
|
|
||||||
<SwitchGroup name="freeleech" label="Freeleech" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<TextField name="freeleech_percent" label="Freeleech percent" columns={6} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<FormButtonsGroup deleteAction={deleteAction}/>
|
|
||||||
|
|
||||||
<DEBUG values={values}/>
|
|
||||||
|
|
||||||
</form>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function FilterTabActions({filter}: FilterTabGeneralProps) {
|
|
||||||
const [addActionIsOpen, toggleAddAction] = useToggle(false)
|
|
||||||
|
|
||||||
const {data} = useQuery<DownloadClient[], Error>('downloadClients', APIClient.download_clients.getAll,
|
|
||||||
{
|
|
||||||
refetchOnWindowFocus: false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mt-10">
|
|
||||||
{addActionIsOpen &&
|
|
||||||
<FilterActionAddForm filter={filter} clients={data || []} isOpen={addActionIsOpen} toggle={toggleAddAction}/>
|
|
||||||
}
|
|
||||||
<div>
|
|
||||||
<div className="-ml-4 -mt-4 mb-6 flex justify-between items-center flex-wrap sm:flex-nowrap">
|
|
||||||
<div className="ml-4 mt-4">
|
|
||||||
<h3 className="text-lg leading-6 font-medium text-gray-900">Actions</h3>
|
|
||||||
<p className="mt-1 text-sm text-gray-500">
|
|
||||||
Actions
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="ml-4 mt-4 flex-shrink-0">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
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 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
|
||||||
onClick={toggleAddAction}
|
|
||||||
>
|
|
||||||
Add new
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{filter.actions ? <FilterActionList actions={filter.actions} clients={data || []} filterID={filter.id}/> :
|
|
||||||
<EmptyListState text="No actions yet!"/>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
994
web/src/screens/filters/details.tsx
Normal file
994
web/src/screens/filters/details.tsx
Normal file
|
@ -0,0 +1,994 @@
|
||||||
|
import React, { Fragment, useRef } from "react";
|
||||||
|
import { Dialog, Transition, Switch as SwitchBasic } from "@headlessui/react";
|
||||||
|
import { ChevronDownIcon, ChevronRightIcon, ExclamationIcon, } from '@heroicons/react/solid'
|
||||||
|
import { EmptyListState } from "../../components/EmptyListState";
|
||||||
|
|
||||||
|
import {
|
||||||
|
NavLink,
|
||||||
|
Route,
|
||||||
|
Switch as RouteSwitch,
|
||||||
|
useHistory,
|
||||||
|
useLocation,
|
||||||
|
useParams,
|
||||||
|
useRouteMatch
|
||||||
|
} from "react-router-dom";
|
||||||
|
import { Action, ActionType, DownloadClient, Filter, Indexer } from "../../domain/interfaces";
|
||||||
|
import { useToggle } from "../../hooks/hooks";
|
||||||
|
import { useMutation, useQuery } from "react-query";
|
||||||
|
import { queryClient } from "../../App";
|
||||||
|
import { CONTAINER_OPTIONS, CODECS_OPTIONS, RESOLUTION_OPTIONS, SOURCES_OPTIONS, ActionTypeNameMap, ActionTypeOptions } from "../../domain/constants";
|
||||||
|
import { TextField, SwitchGroup, Select, MultiSelect, NumberField, DownloadClientSelect } from "./inputs";
|
||||||
|
|
||||||
|
import DEBUG from "../../components/debug";
|
||||||
|
import TitleSubtitle from "../../components/headings/TitleSubtitle";
|
||||||
|
import { classNames } from "../../styles/utils";
|
||||||
|
import SelectM from "react-select";
|
||||||
|
import APIClient from "../../api/APIClient";
|
||||||
|
import { buildPath } from "../../utils/utils"
|
||||||
|
|
||||||
|
import { toast } from 'react-hot-toast'
|
||||||
|
import Toast from '../../components/notifications/Toast';
|
||||||
|
|
||||||
|
import { Field, FieldArray, Form, Formik } from "formik";
|
||||||
|
import { AlertWarning } from "../../components/alerts";
|
||||||
|
import { DeleteModal } from "../../components/modals";
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ name: 'General', href: '', current: true },
|
||||||
|
{ name: 'Movies and TV', href: 'movies-tv', current: false },
|
||||||
|
// { name: 'P2P', href: 'p2p', current: false },
|
||||||
|
{ name: 'Advanced', href: 'advanced', current: false },
|
||||||
|
{ name: 'Actions', href: 'actions', current: false },
|
||||||
|
]
|
||||||
|
|
||||||
|
function TabNavLink({ item, url }: any) {
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
const { pathname } = location;
|
||||||
|
const splitLocation = pathname.split("/");
|
||||||
|
|
||||||
|
// we need to clean the / if it's a base root path
|
||||||
|
let too = item.href ? `${url}/${item.href}` : url
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NavLink
|
||||||
|
key={item.name}
|
||||||
|
to={too}
|
||||||
|
exact={true}
|
||||||
|
activeClassName="border-purple-600 text-purple-600"
|
||||||
|
className={classNames(
|
||||||
|
'border-transparent text-gray-500 hover:text-purple-600 hover:border-purple-600 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm'
|
||||||
|
)}
|
||||||
|
aria-current={splitLocation[2] === item.href ? 'page' : undefined}
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</NavLink>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const FormButtonsGroup = ({ deleteAction, reset, dirty }: any) => {
|
||||||
|
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false)
|
||||||
|
|
||||||
|
const cancelButtonRef = useRef(null)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="pt-6 divide-y divide-gray-200">
|
||||||
|
|
||||||
|
<Transition.Root show={deleteModalIsOpen} as={Fragment}>
|
||||||
|
<Dialog
|
||||||
|
as="div"
|
||||||
|
static
|
||||||
|
className="fixed z-10 inset-0 overflow-y-auto"
|
||||||
|
initialFocus={cancelButtonRef}
|
||||||
|
open={deleteModalIsOpen}
|
||||||
|
onClose={toggleDeleteModal}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||||
|
<Transition.Child
|
||||||
|
as={Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0"
|
||||||
|
enterTo="opacity-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<Dialog.Overlay className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
|
||||||
|
</Transition.Child>
|
||||||
|
|
||||||
|
{/* This element is to trick the browser into centering the modal contents. */}
|
||||||
|
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
|
||||||
|
​
|
||||||
|
</span>
|
||||||
|
<Transition.Child
|
||||||
|
as={Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
|
||||||
|
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||||
|
<div className="sm:flex sm:items-start">
|
||||||
|
<div
|
||||||
|
className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
|
||||||
|
<ExclamationIcon className="h-6 w-6 text-red-600" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||||
|
<Dialog.Title as="h3"
|
||||||
|
className="text-lg leading-6 font-medium text-gray-900">
|
||||||
|
Remove filter
|
||||||
|
</Dialog.Title>
|
||||||
|
<div className="mt-2">
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
Are you sure you want to remove this filter?
|
||||||
|
This action cannot be undone.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm"
|
||||||
|
onClick={deleteAction}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
|
||||||
|
onClick={toggleDeleteModal}
|
||||||
|
ref={cancelButtonRef}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition.Child>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</Transition.Root>
|
||||||
|
|
||||||
|
<div className="mt-4 pt-4 flex justify-between">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex items-center justify-center px-4 py-2 border border-transparent font-medium rounded-md text-red-700 bg-red-100 hover:bg-red-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:text-sm"
|
||||||
|
onClick={toggleDeleteModal}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{/* {dirty && <span className="mr-4 text-sm text-gray-500">Unsaved changes..</span>} */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="bg-white border border-gray-300 rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-light-blue-500"
|
||||||
|
onClick={reset}
|
||||||
|
>
|
||||||
|
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 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FilterDetails() {
|
||||||
|
let { url } = useRouteMatch();
|
||||||
|
let history = useHistory();
|
||||||
|
let { filterId }: any = useParams();
|
||||||
|
|
||||||
|
const { isLoading, data } = useQuery<Filter, Error>(['filter', parseInt(filterId)], () => APIClient.filters.getByID(parseInt(filterId)),
|
||||||
|
{
|
||||||
|
retry: false,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
onError: err => {
|
||||||
|
history.push("./")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const { data: indexers } = useQuery<Indexer[], Error>('indexerList', APIClient.indexers.getOptions,
|
||||||
|
{
|
||||||
|
refetchOnWindowFocus: false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const updateMutation = useMutation((filter: Filter) => APIClient.filters.update(filter), {
|
||||||
|
onSuccess: (filter) => {
|
||||||
|
// queryClient.setQueryData(['filter', filter.id], data)
|
||||||
|
toast.custom((t) => <Toast type="success" body={`${filter.name} was updated successfully`} t={t} />)
|
||||||
|
|
||||||
|
queryClient.invalidateQueries(["filter", filter.id]);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const deleteMutation = useMutation((id: number) => APIClient.filters.delete(id), {
|
||||||
|
onSuccess: (filter) => {
|
||||||
|
// invalidate filters
|
||||||
|
queryClient.invalidateQueries("filter");
|
||||||
|
toast.custom((t) => <Toast type="success" body={`${filter.name} was deleted`} t={t} />)
|
||||||
|
|
||||||
|
// redirect
|
||||||
|
history.push("/filters")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = (data: any) => {
|
||||||
|
console.log("submit other");
|
||||||
|
|
||||||
|
updateMutation.mutate(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteAction = () => {
|
||||||
|
deleteMutation.mutate(data.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMobileNav = (e: any, href: string) => {
|
||||||
|
let s = history.location.pathname.split(/((?:\/.*?)?\/filters\/\d)/gi)
|
||||||
|
|
||||||
|
let p = buildPath(s[1], href)
|
||||||
|
|
||||||
|
history.push(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="-mt-48 ">
|
||||||
|
<header className="py-10">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex items-center">
|
||||||
|
<h1 className="text-3xl font-bold text-white capitalize">
|
||||||
|
<NavLink to="/filters" exact={true}>
|
||||||
|
Filters
|
||||||
|
</NavLink>
|
||||||
|
</h1>
|
||||||
|
<ChevronRightIcon className="h-6 w-6 text-gray-500" aria-hidden="true" />
|
||||||
|
<h1 className="text-3xl font-bold text-white capitalize">{data.name}</h1>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div className="max-w-7xl mx-auto pb-12 px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="bg-white 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-2 pb-6">
|
||||||
|
|
||||||
|
<div className="sm:hidden">
|
||||||
|
<label htmlFor="selected-tab" className="sr-only">
|
||||||
|
Select a tab
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="selected-tab"
|
||||||
|
name="selected-tab"
|
||||||
|
className="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-purple-500 focus:border-purple-500 sm:text-sm rounded-md"
|
||||||
|
>
|
||||||
|
{tabs.map((tab) => (
|
||||||
|
<option key={tab.name} onClick={(e) => handleMobileNav(e, tab.href)}>
|
||||||
|
{tab.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="hidden sm:block">
|
||||||
|
<div className="border-b border-gray-200">
|
||||||
|
<nav className="-mb-px flex space-x-8">
|
||||||
|
{tabs.map((tab) => (
|
||||||
|
<TabNavLink item={tab} url={url} key={tab.href} />
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
id: data.id,
|
||||||
|
name: data.name,
|
||||||
|
enabled: data.enabled || false,
|
||||||
|
min_size: data.min_size,
|
||||||
|
max_size: data.max_size,
|
||||||
|
delay: data.delay,
|
||||||
|
shows: data.shows,
|
||||||
|
years: data.years,
|
||||||
|
resolutions: data.resolutions || [],
|
||||||
|
sources: data.sources || [],
|
||||||
|
codecs: data.codecs || [],
|
||||||
|
containers: data.containers || [],
|
||||||
|
seasons: data.seasons,
|
||||||
|
episodes: data.episodes,
|
||||||
|
match_releases: data.match_releases,
|
||||||
|
except_releases: data.except_releases,
|
||||||
|
match_release_groups: data.match_release_groups,
|
||||||
|
except_release_groups: data.except_release_groups,
|
||||||
|
match_categories: data.match_categories,
|
||||||
|
except_categories: data.except_categories,
|
||||||
|
match_tags: data.match_tags,
|
||||||
|
except_tags: data.except_tags,
|
||||||
|
match_uploaders: data.match_uploaders,
|
||||||
|
except_uploaders: data.except_uploaders,
|
||||||
|
freeleech: data.freeleech,
|
||||||
|
freeleech_percent: data.freeleech_percent,
|
||||||
|
indexers: data.indexers || [],
|
||||||
|
actions: data.actions || [],
|
||||||
|
}}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
>
|
||||||
|
{({ isSubmitting, values, dirty, resetForm }) => (
|
||||||
|
<Form>
|
||||||
|
<RouteSwitch>
|
||||||
|
<Route exact path={url}>
|
||||||
|
<General indexers={indexers as any} />
|
||||||
|
</Route>
|
||||||
|
|
||||||
|
<Route path={`${url}/movies-tv`}>
|
||||||
|
<MoviesTv />
|
||||||
|
</Route>
|
||||||
|
|
||||||
|
<Route path={`${url}/advanced`}>
|
||||||
|
<Advanced />
|
||||||
|
</Route>
|
||||||
|
|
||||||
|
<Route path={`${url}/actions`}>
|
||||||
|
<FilterActions filter={data} values={values} />
|
||||||
|
</Route>
|
||||||
|
</RouteSwitch>
|
||||||
|
|
||||||
|
<FormButtonsGroup deleteAction={deleteAction} dirty={dirty} reset={resetForm} />
|
||||||
|
|
||||||
|
<DEBUG values={values} />
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GeneralProps {
|
||||||
|
indexers: Indexer[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function General({ indexers }: GeneralProps) {
|
||||||
|
|
||||||
|
let opts = indexers ? indexers.map(v => ({
|
||||||
|
label: v.name,
|
||||||
|
value: v
|
||||||
|
})) : [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="mt-6 lg:pb-8">
|
||||||
|
|
||||||
|
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||||
|
<TextField name="name" label="Filter name" columns={6} placeholder="eg. Filter 1" />
|
||||||
|
|
||||||
|
<div className="col-span-6">
|
||||||
|
<label htmlFor="indexers" className="block text-xs font-bold text-gray-700 uppercase tracking-wide">
|
||||||
|
Indexers
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<Field name="indexers" type="select" multiple={true}>
|
||||||
|
{({
|
||||||
|
field,
|
||||||
|
form: { setFieldValue },
|
||||||
|
}: any) => {
|
||||||
|
return (
|
||||||
|
<SelectM
|
||||||
|
{...field}
|
||||||
|
value={field.value && field.value.map((v: any) => ({
|
||||||
|
label: v.name,
|
||||||
|
value: v
|
||||||
|
}))}
|
||||||
|
onChange={(values: any) => {
|
||||||
|
let am = values && values.map((i: any) => i.value)
|
||||||
|
|
||||||
|
setFieldValue(field.name, am)
|
||||||
|
}}
|
||||||
|
isClearable={true}
|
||||||
|
isMulti={true}
|
||||||
|
placeholder="Choose indexers"
|
||||||
|
className="mt-2 block w-full focus:outline-none focus:ring-light-blue-500 focus:border-light-blue-500 sm:text-sm"
|
||||||
|
options={opts}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 lg:pb-8">
|
||||||
|
<TitleSubtitle title="Rules" subtitle="Set rules" />
|
||||||
|
|
||||||
|
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||||
|
<TextField name="min_size" label="Min size" columns={6} placeholder="" />
|
||||||
|
<TextField name="max_size" label="Max size" columns={6} placeholder="" />
|
||||||
|
<TextField name="delay" label="Delay" columns={6} placeholder="" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t">
|
||||||
|
<SwitchGroup name="enabled" label="Enabled" description="Enabled or disable filter." />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// interface FilterTabGeneralProps {
|
||||||
|
// filter: Filter;
|
||||||
|
// }
|
||||||
|
|
||||||
|
function MoviesTv() {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||||
|
<TextField name="shows" label="Movies / Shows" columns={8} placeholder="eg. Movie,Show 1,Show?2" />
|
||||||
|
<TextField name="years" label="Years" columns={4} placeholder="eg. 2018,2019-2021" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 lg:pb-8">
|
||||||
|
<TitleSubtitle title="Seasons and Episodes" subtitle="Set seaons and episodes" />
|
||||||
|
|
||||||
|
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||||
|
<TextField name="seasons" label="Seasons" columns={8} placeholder="eg. 1,3,2-6" />
|
||||||
|
<TextField name="episodes" label="Episodes" columns={4} placeholder="eg. 2,4,10-20" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 lg:pb-8">
|
||||||
|
<TitleSubtitle title="Quality" subtitle="Resolution, source etc." />
|
||||||
|
|
||||||
|
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||||
|
<MultiSelect name="resolutions" options={RESOLUTION_OPTIONS} label="resolutions" columns={6} />
|
||||||
|
<MultiSelect name="sources" options={SOURCES_OPTIONS} label="sources" columns={6} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||||
|
<MultiSelect name="codecs" options={CODECS_OPTIONS} label="codecs" columns={6} />
|
||||||
|
<MultiSelect name="containers" options={CONTAINER_OPTIONS} label="containers" columns={6} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Advanced() {
|
||||||
|
const [releasesIsOpen, toggleReleases] = useToggle(false)
|
||||||
|
const [groupsIsOpen, toggleGroups] = useToggle(false)
|
||||||
|
const [categoriesIsOpen, toggleCategories] = useToggle(false)
|
||||||
|
const [uploadersIsOpen, toggleUploaders] = useToggle(false)
|
||||||
|
const [freeleechIsOpen, toggleFreeleech] = useToggle(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="mt-6 lg:pb-8 border-b border-gray-200">
|
||||||
|
<div className="flex justify-between items-center cursor-pointer" onClick={toggleReleases}>
|
||||||
|
<div className="-ml-2 -mt-2 flex flex-wrap items-baseline">
|
||||||
|
<h3 className="ml-2 mt-2 text-lg leading-6 font-medium text-gray-900">Releases</h3>
|
||||||
|
<p className="ml-2 mt-1 text-sm text-gray-500 truncate">Match or ignore</p>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 sm:mt-0 sm:ml-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex items-center px-4 py-2 border-transparent text-sm font-medium text-white"
|
||||||
|
>
|
||||||
|
{releasesIsOpen ? <ChevronDownIcon className="h-6 w-6 text-gray-500" aria-hidden="true" /> : <ChevronRightIcon className="h-6 w-6 text-gray-500" aria-hidden="true" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{releasesIsOpen && (
|
||||||
|
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||||
|
<TextField name="match_releases" label="Match releases" columns={6} placeholder="eg. *some?movie*,*some?show*s01*" />
|
||||||
|
<TextField name="except_releases" label="Except releases" columns={6} placeholder="" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 lg:pb-8 border-b border-gray-200">
|
||||||
|
<div className="flex justify-between items-center cursor-pointer" onClick={toggleGroups}>
|
||||||
|
<div className="-ml-2 -mt-2 flex flex-wrap items-baseline">
|
||||||
|
<h3 className="ml-2 mt-2 text-lg leading-6 font-medium text-gray-900">Groups</h3>
|
||||||
|
<p className="ml-2 mt-1 text-sm text-gray-500 truncate">Match or ignore</p>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 sm:mt-0 sm:ml-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex items-center px-4 py-2 border-transparent text-sm font-medium text-white"
|
||||||
|
>
|
||||||
|
{groupsIsOpen ? <ChevronDownIcon className="h-6 w-6 text-gray-500" aria-hidden="true" /> : <ChevronRightIcon className="h-6 w-6 text-gray-500" aria-hidden="true" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{groupsIsOpen && (
|
||||||
|
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||||
|
<TextField name="match_release_groups" label="Match release groups" columns={6} placeholder="eg. group1,group2" />
|
||||||
|
<TextField name="except_release_groups" label="Except release groups" columns={6} placeholder="eg. badgroup1,badgroup2" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 lg:pb-8 border-b border-gray-200">
|
||||||
|
<div className="flex justify-between items-center cursor-pointer" onClick={toggleCategories}>
|
||||||
|
<div className="-ml-2 -mt-2 flex flex-wrap items-baseline">
|
||||||
|
<h3 className="ml-2 mt-2 text-lg leading-6 font-medium text-gray-900">Categories and tags</h3>
|
||||||
|
<p className="ml-2 mt-1 text-sm text-gray-500 truncate">Match or ignore categories or tags</p>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 sm:mt-0 sm:ml-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex items-center px-4 py-2 border-transparent text-sm font-medium text-white"
|
||||||
|
>
|
||||||
|
{categoriesIsOpen ? <ChevronDownIcon className="h-6 w-6 text-gray-500" aria-hidden="true" /> : <ChevronRightIcon className="h-6 w-6 text-gray-500" aria-hidden="true" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{categoriesIsOpen && (
|
||||||
|
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||||
|
<TextField name="match_categories" label="Match categories" columns={6} placeholder="eg. *category*,category1" />
|
||||||
|
<TextField name="except_categories" label="Except categories" columns={6} placeholder="eg. *category*" />
|
||||||
|
|
||||||
|
<TextField name="match_tags" label="Match tags" columns={6} placeholder="eg. tag1,tag2" />
|
||||||
|
<TextField name="except_tags" label="Except tags" columns={6} placeholder="eg. tag1,tag2" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 lg:pb-8 border-b border-gray-200">
|
||||||
|
<div className="flex justify-between items-center cursor-pointer" onClick={toggleUploaders}>
|
||||||
|
<div className="-ml-2 -mt-2 flex flex-wrap items-baseline">
|
||||||
|
<h3 className="ml-2 mt-2 text-lg leading-6 font-medium text-gray-900">Uploaders</h3>
|
||||||
|
<p className="ml-2 mt-1 text-sm text-gray-500 truncate">Match or ignore uploaders</p>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 sm:mt-0 sm:ml-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex items-center px-4 py-2 border-transparent text-sm font-medium text-white"
|
||||||
|
>
|
||||||
|
{uploadersIsOpen ? <ChevronDownIcon className="h-6 w-6 text-gray-500" aria-hidden="true" /> : <ChevronRightIcon className="h-6 w-6 text-gray-500" aria-hidden="true" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{uploadersIsOpen && (
|
||||||
|
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||||
|
<TextField name="match_uploaders" label="Match uploaders" columns={6} placeholder="eg. uploader1" />
|
||||||
|
<TextField name="except_uploaders" label="Except uploaders" columns={6} placeholder="eg. anonymous" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 lg:pb-8 border-b border-gray-200">
|
||||||
|
<div className="flex justify-between items-center cursor-pointer" onClick={toggleFreeleech}>
|
||||||
|
<div className="-ml-2 -mt-2 flex flex-wrap items-baseline">
|
||||||
|
<h3 className="ml-2 mt-2 text-lg leading-6 font-medium text-gray-900">Freeleech</h3>
|
||||||
|
<p className="ml-2 mt-1 text-sm text-gray-500 truncate">Match only freeleech and freeleech percent</p>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 sm:mt-0 sm:ml-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex items-center px-4 py-2 border-transparent text-sm font-medium text-white"
|
||||||
|
>
|
||||||
|
{freeleechIsOpen ? <ChevronDownIcon className="h-6 w-6 text-gray-500" aria-hidden="true" /> : <ChevronRightIcon className="h-6 w-6 text-gray-500" aria-hidden="true" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{freeleechIsOpen && (
|
||||||
|
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||||
|
<div className="col-span-6">
|
||||||
|
<SwitchGroup name="freeleech" label="Freeleech" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TextField name="freeleech_percent" label="Freeleech percent" columns={6} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FilterActionsProps {
|
||||||
|
filter: Filter;
|
||||||
|
values: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
function FilterActions({ filter, values }: FilterActionsProps) {
|
||||||
|
// const [addActionIsOpen, toggleAddAction] = useToggle(false)
|
||||||
|
|
||||||
|
const { data } = useQuery<DownloadClient[], Error>('downloadClients', APIClient.download_clients.getAll,
|
||||||
|
{
|
||||||
|
refetchOnWindowFocus: false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
let newAction = {
|
||||||
|
name: "new action",
|
||||||
|
enabled: true,
|
||||||
|
type: "TEST",
|
||||||
|
watch_folder: "",
|
||||||
|
exec_cmd: "",
|
||||||
|
exec_args: "",
|
||||||
|
category: "",
|
||||||
|
tags: "",
|
||||||
|
label: "",
|
||||||
|
save_path: "",
|
||||||
|
paused: false,
|
||||||
|
ignore_rules: false,
|
||||||
|
limit_upload_speed: 0,
|
||||||
|
limit_download_speed: 0,
|
||||||
|
filter_id: filter.id,
|
||||||
|
// client_id: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-10">
|
||||||
|
{/* {addActionIsOpen &&
|
||||||
|
<FilterActionAddForm filter={filter} clients={data || []} isOpen={addActionIsOpen} toggle={toggleAddAction} />
|
||||||
|
} */}
|
||||||
|
<FieldArray name="actions">
|
||||||
|
{({ remove, push }) => (
|
||||||
|
<Fragment>
|
||||||
|
<div className="-ml-4 -mt-4 mb-6 flex justify-between items-center flex-wrap sm:flex-nowrap">
|
||||||
|
<div className="ml-4 mt-4">
|
||||||
|
<h3 className="text-lg leading-6 font-medium text-gray-900">Actions</h3>
|
||||||
|
<p className="mt-1 text-sm text-gray-500">
|
||||||
|
Add to download clients or run custom commands.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="ml-4 mt-4 flex-shrink-0">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
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 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||||
|
onClick={() => push(newAction)}
|
||||||
|
>
|
||||||
|
Add new
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white shadow sm:rounded-md">
|
||||||
|
{values.actions.length > 0 ?
|
||||||
|
values.actions.map((action: any, index: any) => (
|
||||||
|
<ul className="divide-y divide-gray-200" key={index}>
|
||||||
|
<FilterActionsItem action={action} clients={data!} idx={index} remove={remove} />
|
||||||
|
</ul>
|
||||||
|
)) : <EmptyListState text="No actions yet!" />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
|
</FieldArray>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FilterActionsItemProps {
|
||||||
|
action: Action;
|
||||||
|
clients: DownloadClient[];
|
||||||
|
idx: number;
|
||||||
|
remove: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
function FilterActionsItem({ action, clients, idx, remove }: FilterActionsItemProps) {
|
||||||
|
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
|
||||||
|
const [edit, toggleEdit] = useToggle(false);
|
||||||
|
|
||||||
|
// const enabledMutation = useMutation(
|
||||||
|
// (actionID: number) => APIClient.actions.toggleEnable(actionID),
|
||||||
|
// {
|
||||||
|
// onSuccess: () => {
|
||||||
|
// // queryClient.invalidateQueries(["filter", filterID]);
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
// );
|
||||||
|
|
||||||
|
// const toggleActive = () => {
|
||||||
|
// console.log("action: ", action);
|
||||||
|
|
||||||
|
// enabledMutation.mutate(action.id);
|
||||||
|
// };
|
||||||
|
|
||||||
|
const cancelButtonRef = useRef(null);
|
||||||
|
|
||||||
|
const TypeForm = (actionType: ActionType) => {
|
||||||
|
switch (actionType) {
|
||||||
|
case "TEST":
|
||||||
|
return (
|
||||||
|
<AlertWarning
|
||||||
|
title="Notice"
|
||||||
|
text="The test action does nothing except to show if the filter works."
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case "EXEC":
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||||
|
<TextField
|
||||||
|
name={`actions.${idx}.exec_cmd`}
|
||||||
|
label="Command"
|
||||||
|
columns={6}
|
||||||
|
placeholder="Path to program eg. /bin/test"
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
name={`actions.${idx}.exec_args`}
|
||||||
|
label="Arguments"
|
||||||
|
columns={6}
|
||||||
|
placeholder="Arguments eg. --test"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case "WATCH_FOLDER":
|
||||||
|
return (
|
||||||
|
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||||
|
<TextField
|
||||||
|
name={`actions.${idx}.watch_folder`}
|
||||||
|
label="Watch folder"
|
||||||
|
columns={6}
|
||||||
|
placeholder="Watch directory eg. /home/user/rwatch"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case "QBITTORRENT":
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||||
|
<DownloadClientSelect
|
||||||
|
name={`actions.${idx}.client_id`}
|
||||||
|
action={action}
|
||||||
|
clients={clients}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="col-span-6 sm:col-span-6">
|
||||||
|
<TextField
|
||||||
|
name={`actions.${idx}.save_path`}
|
||||||
|
label="Save path"
|
||||||
|
columns={6}
|
||||||
|
placeholder="eg. /full/path/to/watch_folder"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||||
|
<TextField name={`actions.${idx}.category`} label="Category" columns={6} placeholder="eg. category" />
|
||||||
|
<TextField name={`actions.${idx}.tags`} label="Tags" columns={6} placeholder="eg. tag1,tag2" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||||
|
<NumberField
|
||||||
|
name={`actions.${idx}.limit_download_speed`}
|
||||||
|
label="Limit download speed (KB/s)"
|
||||||
|
/>
|
||||||
|
<NumberField
|
||||||
|
name={`actions.${idx}.limit_upload_speed`}
|
||||||
|
label="Limit upload speed (KB/s)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||||
|
<div className="col-span-6">
|
||||||
|
<SwitchGroup
|
||||||
|
name={`actions.${idx}.paused`}
|
||||||
|
label="Add paused"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case "DELUGE_V1":
|
||||||
|
case "DELUGE_V2":
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||||
|
<DownloadClientSelect
|
||||||
|
name={`actions.${idx}.client_id`}
|
||||||
|
action={action}
|
||||||
|
clients={clients}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="col-span-12 sm:col-span-6">
|
||||||
|
<TextField
|
||||||
|
name={`actions.${idx}.save_path`}
|
||||||
|
label="Save path"
|
||||||
|
columns={6}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 col-span-12 sm:col-span-6">
|
||||||
|
<TextField
|
||||||
|
name={`actions.${idx}.label`}
|
||||||
|
label="Label"
|
||||||
|
columns={6}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||||
|
<NumberField
|
||||||
|
name={`actions.${idx}.limit_download_speed`}
|
||||||
|
label="Limit download speed (KB/s)"
|
||||||
|
/>
|
||||||
|
<NumberField
|
||||||
|
name={`actions.${idx}.limit_upload_speed`}
|
||||||
|
label="Limit upload speed (KB/s)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||||
|
<div className="col-span-6">
|
||||||
|
<SwitchGroup
|
||||||
|
name={`actions.${idx}.paused`}
|
||||||
|
label="Add paused"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case "RADARR":
|
||||||
|
case "SONARR":
|
||||||
|
case "LIDARR":
|
||||||
|
return (
|
||||||
|
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||||
|
<DownloadClientSelect
|
||||||
|
name={`actions.${idx}.client_id`}
|
||||||
|
action={action}
|
||||||
|
clients={clients}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li>
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
idx % 2 === 0 ? "bg-white" : "bg-gray-50",
|
||||||
|
"flex items-center sm:px-6 hover:bg-gray-50"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Field name={`actions.${idx}.enabled`} type="checkbox">
|
||||||
|
{({
|
||||||
|
field,
|
||||||
|
form: { setFieldValue },
|
||||||
|
}: any) => (
|
||||||
|
<SwitchBasic
|
||||||
|
{...field}
|
||||||
|
type="button"
|
||||||
|
value={field.value}
|
||||||
|
checked={field.checked}
|
||||||
|
onChange={value => {
|
||||||
|
setFieldValue(field?.name ?? '', value)
|
||||||
|
}}
|
||||||
|
className={classNames(
|
||||||
|
field.value ? 'bg-teal-500' : 'bg-gray-200',
|
||||||
|
'relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-light-blue-500'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="sr-only">toggle enabled</span>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className={classNames(
|
||||||
|
field.value ? 'translate-x-5' : 'translate-x-0',
|
||||||
|
'inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</SwitchBasic>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<button className="px-4 py-4 w-full flex block" type="button" onClick={toggleEdit}>
|
||||||
|
<div className="min-w-0 flex-1 sm:flex sm:items-center sm:justify-between">
|
||||||
|
<div className="truncate">
|
||||||
|
<div className="flex text-sm">
|
||||||
|
<p className="ml-4 font-medium text-indigo-600 truncate">
|
||||||
|
{action.name}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 flex-shrink-0 sm:mt-0 sm:ml-5">
|
||||||
|
<div className="flex overflow-hidden -space-x-1">
|
||||||
|
<span className="text-sm font-normal text-gray-500">
|
||||||
|
{ActionTypeNameMap[action.type]}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="ml-5 flex-shrink-0">
|
||||||
|
<ChevronRightIcon
|
||||||
|
className="h-5 w-5 text-gray-400"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{edit && (
|
||||||
|
<div className="px-4 py-4 flex items-center sm:px-6">
|
||||||
|
<Transition.Root show={deleteModalIsOpen} as={Fragment}>
|
||||||
|
<Dialog
|
||||||
|
as="div"
|
||||||
|
static
|
||||||
|
className="fixed inset-0 overflow-y-auto"
|
||||||
|
initialFocus={cancelButtonRef}
|
||||||
|
open={deleteModalIsOpen}
|
||||||
|
onClose={toggleDeleteModal}
|
||||||
|
>
|
||||||
|
<DeleteModal
|
||||||
|
isOpen={deleteModalIsOpen}
|
||||||
|
buttonRef={cancelButtonRef}
|
||||||
|
toggle={toggleDeleteModal}
|
||||||
|
deleteAction={() => remove(idx)}
|
||||||
|
title="Remove filter action"
|
||||||
|
text="Are you sure you want to remove this action? This action cannot be undone."
|
||||||
|
/>
|
||||||
|
</Dialog>
|
||||||
|
</Transition.Root>
|
||||||
|
|
||||||
|
<div className="w-full">
|
||||||
|
|
||||||
|
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||||
|
<Select
|
||||||
|
name={`actions.${idx}.type`}
|
||||||
|
label="Type"
|
||||||
|
optionDefaultText="Select type"
|
||||||
|
options={ActionTypeOptions}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField name={`actions.${idx}.name`} label="Name" columns={6} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{TypeForm(action.type)}
|
||||||
|
|
||||||
|
<div className="pt-6 divide-y divide-gray-200">
|
||||||
|
<div className="mt-4 pt-4 flex justify-between">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex items-center justify-center py-2 border border-transparent font-medium rounded-md text-red-700 hover:text-red-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:text-sm"
|
||||||
|
onClick={toggleDeleteModal}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="bg-white border border-gray-300 rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-light-blue-500"
|
||||||
|
onClick={toggleEdit}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
2
web/src/screens/filters/index.tsx
Normal file
2
web/src/screens/filters/index.tsx
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export { default as Filters } from "./list";
|
||||||
|
export { default as FilterDetails } from "./details";
|
109
web/src/screens/filters/inputs/DownloadClientSelect.tsx
Normal file
109
web/src/screens/filters/inputs/DownloadClientSelect.tsx
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
import React, { Fragment } from "react";
|
||||||
|
import { Transition, Listbox } from "@headlessui/react";
|
||||||
|
import { CheckIcon, SelectorIcon } from '@heroicons/react/solid';
|
||||||
|
import { Action, DownloadClient } from "../../../domain/interfaces";
|
||||||
|
import { classNames } from "../../../styles/utils";
|
||||||
|
import { Field } from "formik";
|
||||||
|
|
||||||
|
interface DownloadClientSelectProps {
|
||||||
|
name: string;
|
||||||
|
action: Action;
|
||||||
|
clients: DownloadClient[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DownloadClientSelect({
|
||||||
|
name, action, clients,
|
||||||
|
}: DownloadClientSelectProps) {
|
||||||
|
return (
|
||||||
|
<div className="col-span-6 sm:col-span-6">
|
||||||
|
<Field name={name} type="select">
|
||||||
|
{({
|
||||||
|
field,
|
||||||
|
form: { setFieldValue },
|
||||||
|
}: any) => (
|
||||||
|
<Listbox
|
||||||
|
value={field.value}
|
||||||
|
onChange={(value: any) => setFieldValue(field?.name, value)}
|
||||||
|
>
|
||||||
|
{({ open }) => (
|
||||||
|
<>
|
||||||
|
<Listbox.Label className="block text-xs font-bold text-gray-700 uppercase tracking-wide">
|
||||||
|
Client
|
||||||
|
</Listbox.Label>
|
||||||
|
<div className="mt-2 relative">
|
||||||
|
<Listbox.Button className="bg-white relative w-full border border-gray-300 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||||
|
<span className="block truncate">
|
||||||
|
{field.value
|
||||||
|
? clients.find((c) => c.id === field.value)!.name
|
||||||
|
: "Choose a client"}
|
||||||
|
</span>
|
||||||
|
{/*<span className="block truncate">Choose a client</span>*/}
|
||||||
|
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||||
|
<SelectorIcon
|
||||||
|
className="h-5 w-5 text-gray-400"
|
||||||
|
aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
</Listbox.Button>
|
||||||
|
|
||||||
|
<Transition
|
||||||
|
show={open}
|
||||||
|
as={Fragment}
|
||||||
|
leave="transition ease-in duration-100"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<Listbox.Options
|
||||||
|
static
|
||||||
|
className="absolute z-10 mt-1 w-full bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm"
|
||||||
|
>
|
||||||
|
{clients
|
||||||
|
.filter((c) => c.type === action.type)
|
||||||
|
.map((client: any) => (
|
||||||
|
<Listbox.Option
|
||||||
|
key={client.id}
|
||||||
|
className={({ active }) => classNames(
|
||||||
|
active
|
||||||
|
? "text-white bg-indigo-600"
|
||||||
|
: "text-gray-900",
|
||||||
|
"cursor-default select-none relative py-2 pl-3 pr-9"
|
||||||
|
)}
|
||||||
|
value={client.id}
|
||||||
|
>
|
||||||
|
{({ selected, active }) => (
|
||||||
|
<>
|
||||||
|
<span
|
||||||
|
className={classNames(
|
||||||
|
selected ? "font-semibold" : "font-normal",
|
||||||
|
"block truncate"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{client.name}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{selected ? (
|
||||||
|
<span
|
||||||
|
className={classNames(
|
||||||
|
active ? "text-white" : "text-indigo-600",
|
||||||
|
"absolute inset-y-0 right-0 flex items-center pr-4"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<CheckIcon
|
||||||
|
className="h-5 w-5"
|
||||||
|
aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Listbox.Option>
|
||||||
|
))}
|
||||||
|
</Listbox.Options>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Listbox>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
57
web/src/screens/filters/inputs/MultiSelect.tsx
Normal file
57
web/src/screens/filters/inputs/MultiSelect.tsx
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
import React from "react";
|
||||||
|
import RMSC from "react-multi-select-component";
|
||||||
|
import { Field } from "formik";
|
||||||
|
import { classNames, COL_WIDTHS } from "../../../styles/utils";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
label?: string;
|
||||||
|
options?: [] | any;
|
||||||
|
name: string;
|
||||||
|
className?: string;
|
||||||
|
columns?: COL_WIDTHS;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MultiSelect: React.FC<Props> = ({
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
options,
|
||||||
|
className,
|
||||||
|
columns
|
||||||
|
}) => (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
columns ? `col-span-${columns}` : "col-span-12"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
className="block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2"
|
||||||
|
htmlFor={label}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<Field name={name} type="select" multiple={true}>
|
||||||
|
{({
|
||||||
|
field,
|
||||||
|
form: { setFieldValue },
|
||||||
|
}: any) => {
|
||||||
|
return (
|
||||||
|
<RMSC
|
||||||
|
{...field}
|
||||||
|
type="select"
|
||||||
|
options={options}
|
||||||
|
labelledBy={name}
|
||||||
|
value={field.value && field.value.map((item: any) => options.find((o: any) => o.value === item))}
|
||||||
|
onChange={(values: any) => {
|
||||||
|
let am = values && values.map((i: any) => i.value)
|
||||||
|
|
||||||
|
setFieldValue(field.name, am)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default MultiSelect;
|
53
web/src/screens/filters/inputs/NumberField.tsx
Normal file
53
web/src/screens/filters/inputs/NumberField.tsx
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import { Field } from "formik";
|
||||||
|
import React from "react";
|
||||||
|
import { classNames } from "../../../styles/utils";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
name: string;
|
||||||
|
label?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
className?: string;
|
||||||
|
required?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NumberField: React.FC<Props> = ({
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
placeholder,
|
||||||
|
required,
|
||||||
|
className,
|
||||||
|
}) => (
|
||||||
|
<div className="col-span-12 sm:col-span-6">
|
||||||
|
<label htmlFor={name} className="block text-sm font-medium text-gray-700">
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<Field name={name} type="number">
|
||||||
|
{({
|
||||||
|
field,
|
||||||
|
form: { touched, errors },
|
||||||
|
meta,
|
||||||
|
}: any) => (
|
||||||
|
<div className="sm:col-span-2">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
{...field}
|
||||||
|
className={classNames(
|
||||||
|
meta.touched && meta.error
|
||||||
|
? "focus:ring-red-500 focus:border-red-500 border-red-500"
|
||||||
|
: "focus:ring-indigo-500 focus:border-indigo-500 border-gray-300",
|
||||||
|
"block w-full shadow-sm sm:text-sm rounded-md"
|
||||||
|
)}
|
||||||
|
placeholder={placeholder}
|
||||||
|
/>
|
||||||
|
{meta.touched && meta.error && (
|
||||||
|
<div className="error">{meta.error}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default NumberField;
|
116
web/src/screens/filters/inputs/Select.tsx
Normal file
116
web/src/screens/filters/inputs/Select.tsx
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
import { Fragment } from "react";
|
||||||
|
import { Field } from "formik";
|
||||||
|
import { Listbox, Transition } from "@headlessui/react";
|
||||||
|
import { CheckIcon, SelectorIcon } from "@heroicons/react/solid";
|
||||||
|
import { classNames } from "../../../styles/utils";
|
||||||
|
|
||||||
|
interface Option {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface props {
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
optionDefaultText: string;
|
||||||
|
options: Option[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function Select({ name, label, optionDefaultText, options }: props) {
|
||||||
|
return (
|
||||||
|
<div className="col-span-6">
|
||||||
|
<Field name={name} type="select">
|
||||||
|
{({
|
||||||
|
field,
|
||||||
|
form: { setFieldValue },
|
||||||
|
}: any) => (
|
||||||
|
<Listbox
|
||||||
|
value={field.value}
|
||||||
|
onChange={(value: any) => setFieldValue(field?.name, value)}
|
||||||
|
>
|
||||||
|
{({ open }) => (
|
||||||
|
<>
|
||||||
|
<Listbox.Label className="block text-xs font-bold text-gray-700 uppercase tracking-wide">
|
||||||
|
{label}
|
||||||
|
</Listbox.Label>
|
||||||
|
<div className="mt-2 relative">
|
||||||
|
<Listbox.Button className="bg-white relative w-full border border-gray-300 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||||
|
<span className="block truncate">
|
||||||
|
{field.value
|
||||||
|
? options.find((c) => c.value === field.value)!.label
|
||||||
|
: optionDefaultText
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||||
|
<SelectorIcon
|
||||||
|
className="h-5 w-5 text-gray-400"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</Listbox.Button>
|
||||||
|
|
||||||
|
<Transition
|
||||||
|
show={open}
|
||||||
|
as={Fragment}
|
||||||
|
leave="transition ease-in duration-100"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<Listbox.Options
|
||||||
|
static
|
||||||
|
className="absolute z-10 mt-1 w-full bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm"
|
||||||
|
>
|
||||||
|
{options.map((opt) => (
|
||||||
|
<Listbox.Option
|
||||||
|
key={opt.value}
|
||||||
|
className={({ active }) =>
|
||||||
|
classNames(
|
||||||
|
active
|
||||||
|
? "text-white bg-indigo-600"
|
||||||
|
: "text-gray-900",
|
||||||
|
"cursor-default select-none relative py-2 pl-3 pr-9"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
value={opt.value}
|
||||||
|
>
|
||||||
|
{({ selected, active }) => (
|
||||||
|
<>
|
||||||
|
<span
|
||||||
|
className={classNames(
|
||||||
|
selected ? "font-semibold" : "font-normal",
|
||||||
|
"block truncate"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{opt.label}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{selected ? (
|
||||||
|
<span
|
||||||
|
className={classNames(
|
||||||
|
active ? "text-white" : "text-indigo-600",
|
||||||
|
"absolute inset-y-0 right-0 flex items-center pr-4"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<CheckIcon
|
||||||
|
className="h-5 w-5"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Listbox.Option>
|
||||||
|
))}
|
||||||
|
</Listbox.Options>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Listbox>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Select;
|
60
web/src/screens/filters/inputs/Switch.tsx
Normal file
60
web/src/screens/filters/inputs/Switch.tsx
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
import { Switch as HeadlessSwitch } from '@headlessui/react'
|
||||||
|
import { FieldInputProps, FieldMetaProps, FieldProps, FormikProps, FormikValues } from 'formik'
|
||||||
|
import React, { InputHTMLAttributes } from 'react'
|
||||||
|
import { classNames } from "../../../styles/utils";
|
||||||
|
|
||||||
|
type SwitchProps<V = any> = {
|
||||||
|
label: string
|
||||||
|
checked: boolean
|
||||||
|
disabled?: boolean
|
||||||
|
onChange: (value: boolean) => void
|
||||||
|
field?: FieldInputProps<V>
|
||||||
|
form?: FormikProps<FormikValues>
|
||||||
|
meta?: FieldMetaProps<V>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Switch: React.FC<SwitchProps> = ({
|
||||||
|
label,
|
||||||
|
checked: $checked,
|
||||||
|
disabled = false,
|
||||||
|
onChange: $onChange,
|
||||||
|
field,
|
||||||
|
form,
|
||||||
|
}) => {
|
||||||
|
const checked = field?.checked ?? $checked
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HeadlessSwitch.Group as="div" className="flex items-center space-x-4">
|
||||||
|
<HeadlessSwitch.Label>{label}</HeadlessSwitch.Label>
|
||||||
|
<HeadlessSwitch
|
||||||
|
as="button"
|
||||||
|
name={field?.name}
|
||||||
|
disabled={disabled}
|
||||||
|
checked={checked}
|
||||||
|
onChange={value => {
|
||||||
|
form?.setFieldValue(field?.name ?? '', value)
|
||||||
|
$onChange && $onChange(value)
|
||||||
|
}}
|
||||||
|
|
||||||
|
className={classNames(
|
||||||
|
checked ? 'bg-teal-500' : 'bg-gray-200',
|
||||||
|
'ml-4 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-light-blue-500'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{({ checked }) => (
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className={classNames(
|
||||||
|
checked ? 'translate-x-5' : 'translate-x-0',
|
||||||
|
'inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</HeadlessSwitch>
|
||||||
|
</HeadlessSwitch.Group>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SwitchFormikProps = SwitchProps & FieldProps & InputHTMLAttributes<HTMLInputElement>
|
||||||
|
|
||||||
|
export const SwitchFormik: React.FC<SwitchProps> = args => <Switch {...args} />
|
89
web/src/screens/filters/inputs/SwitchGroup.tsx
Normal file
89
web/src/screens/filters/inputs/SwitchGroup.tsx
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
import React from "react";
|
||||||
|
import { Switch } from "@headlessui/react";
|
||||||
|
import { Field } from "formik";
|
||||||
|
import { classNames } from "../../../styles/utils";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
name: string;
|
||||||
|
label?: string;
|
||||||
|
description?: string;
|
||||||
|
defaultValue?: boolean;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SwitchGroup: React.FC<Props> = ({ name, label, description, defaultValue }) => (
|
||||||
|
<ul className="mt-2 divide-y divide-gray-200">
|
||||||
|
<Switch.Group as="li" className="py-4 flex items-center justify-between">
|
||||||
|
{label && <div className="flex flex-col">
|
||||||
|
<Switch.Label as="p" className="text-sm font-medium text-gray-900"
|
||||||
|
passive>
|
||||||
|
{label}
|
||||||
|
</Switch.Label>
|
||||||
|
{description && (
|
||||||
|
<Switch.Description className="text-sm text-gray-500">
|
||||||
|
{description}
|
||||||
|
</Switch.Description>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<Field name={name} type="checkbox">
|
||||||
|
{({
|
||||||
|
field,
|
||||||
|
form: { setFieldValue },
|
||||||
|
}: any) => (
|
||||||
|
<Switch
|
||||||
|
{...field}
|
||||||
|
type="button"
|
||||||
|
value={field.value}
|
||||||
|
checked={field.checked}
|
||||||
|
onChange={value => {
|
||||||
|
setFieldValue(field?.name ?? '', value)
|
||||||
|
}}
|
||||||
|
className={classNames(
|
||||||
|
field.value ? 'bg-teal-500' : 'bg-gray-200',
|
||||||
|
'ml-4 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-light-blue-500'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* <span className="sr-only">{label}</span> */}
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className={classNames(
|
||||||
|
field.value ? 'translate-x-5' : 'translate-x-0',
|
||||||
|
'inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Switch>
|
||||||
|
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
{/* <Field
|
||||||
|
name={name}
|
||||||
|
defaultValue={defaultValue as any}
|
||||||
|
render={({input: {onChange, checked, value}}) => (
|
||||||
|
<Switch
|
||||||
|
value={value}
|
||||||
|
checked={value}
|
||||||
|
onChange={onChange}
|
||||||
|
className={classNames(
|
||||||
|
value ? 'bg-teal-500' : 'bg-gray-200',
|
||||||
|
'ml-4 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-light-blue-500'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="sr-only">Use setting</span>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className={classNames(
|
||||||
|
value ? 'translate-x-5' : 'translate-x-0',
|
||||||
|
'inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Switch>
|
||||||
|
)}
|
||||||
|
/> */}
|
||||||
|
</Switch.Group>
|
||||||
|
</ul>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default SwitchGroup;
|
51
web/src/screens/filters/inputs/TextField.tsx
Normal file
51
web/src/screens/filters/inputs/TextField.tsx
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import { Field } from "formik";
|
||||||
|
import React from "react";
|
||||||
|
import { classNames } from "../../../styles/utils";
|
||||||
|
|
||||||
|
type COL_WIDTHS = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
name: string;
|
||||||
|
label?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
columns?: COL_WIDTHS;
|
||||||
|
className?: string;
|
||||||
|
autoComplete?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TextField: React.FC<Props> = ({ name, label, placeholder, columns, className, autoComplete }) => (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
columns ? `col-span-${columns}` : "col-span-12"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{label && (
|
||||||
|
<label htmlFor={name} className="block text-xs font-bold text-gray-700 uppercase tracking-wide">
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
<Field name={name}>
|
||||||
|
{({
|
||||||
|
field,
|
||||||
|
meta,
|
||||||
|
}: any) => (
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
{...field}
|
||||||
|
id={name}
|
||||||
|
type="text"
|
||||||
|
autoComplete={autoComplete}
|
||||||
|
className="mt-2 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-light-blue-500 focus:border-light-blue-500 sm:text-sm"
|
||||||
|
placeholder={placeholder}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{meta.touched && meta.error && (
|
||||||
|
<div className="error">{meta.error}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default TextField;
|
7
web/src/screens/filters/inputs/index.ts
Normal file
7
web/src/screens/filters/inputs/index.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
export { default as DownloadClientSelect } from "./DownloadClientSelect";
|
||||||
|
export { default as TextField } from "./TextField";
|
||||||
|
export { default as Select } from "./Select";
|
||||||
|
export { default as SwitchGroup } from "./SwitchGroup";
|
||||||
|
export { default as MultiSelect } from "./MultiSelect";
|
||||||
|
export { default as NumberField } from "./NumberField";
|
||||||
|
export { Switch } from "./Switch";
|
157
web/src/screens/filters/list.tsx
Normal file
157
web/src/screens/filters/list.tsx
Normal file
|
@ -0,0 +1,157 @@
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { Switch } from "@headlessui/react";
|
||||||
|
import { EmptyListState } from "../../components/EmptyListState";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Link,
|
||||||
|
} from "react-router-dom";
|
||||||
|
import { Filter } from "../../domain/interfaces";
|
||||||
|
import { useToggle } from "../../hooks/hooks";
|
||||||
|
import { useQuery } from "react-query";
|
||||||
|
import { classNames } from "../../styles/utils";
|
||||||
|
import { FilterAddForm } from "../../forms";
|
||||||
|
import APIClient from "../../api/APIClient";
|
||||||
|
|
||||||
|
export default function Filters() {
|
||||||
|
const [createFilterIsOpen, toggleCreateFilter] = useToggle(false)
|
||||||
|
|
||||||
|
const { isLoading, error, data } = useQuery<Filter[], Error>('filter', APIClient.filters.getAll,
|
||||||
|
{
|
||||||
|
refetchOnWindowFocus: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) return (<p>'An error has occurred: '</p>)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="-mt-48 ">
|
||||||
|
<FilterAddForm isOpen={createFilterIsOpen} toggle={toggleCreateFilter} />
|
||||||
|
|
||||||
|
<header className="py-10">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex justify-between">
|
||||||
|
<h1 className="text-3xl font-bold text-white capitalize">Filters</h1>
|
||||||
|
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
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 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||||
|
onClick={toggleCreateFilter}
|
||||||
|
>
|
||||||
|
Add new
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="max-w-7xl mx-auto pb-12 px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="bg-white rounded-lg shadow">
|
||||||
|
<div className="relative inset-0 py-3 px-3 sm:px-3 lg:px-3 h-full">
|
||||||
|
{data && data.length > 0 ? <FilterList filters={data} /> :
|
||||||
|
<EmptyListState text="No filters here.." buttonText="Add new" buttonOnClick={toggleCreateFilter} />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FilterListProps {
|
||||||
|
filters: Filter[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function FilterList({ filters }: FilterListProps) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
||||||
|
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
|
||||||
|
<div className="shadow overflow-hidden border-b border-gray-200 sm:rounded-lg">
|
||||||
|
<table className="min-w-full divide-y divide-gray-200">
|
||||||
|
<thead className="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||||
|
>
|
||||||
|
Enabled
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||||
|
>
|
||||||
|
Name
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||||
|
>
|
||||||
|
Indexers
|
||||||
|
</th>
|
||||||
|
<th scope="col" className="relative px-6 py-3">
|
||||||
|
<span className="sr-only">Edit</span>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="bg-white divide-y divide-gray-200">
|
||||||
|
{filters.map((filter: Filter, idx) => (
|
||||||
|
<FilterListItem filter={filter} key={idx} idx={idx} />
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FilterListItemProps {
|
||||||
|
filter: Filter;
|
||||||
|
idx: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function FilterListItem({ filter, idx }: FilterListItemProps) {
|
||||||
|
const [enabled, setEnabled] = useState(filter.enabled)
|
||||||
|
|
||||||
|
const toggleActive = (status: boolean) => {
|
||||||
|
console.log(status)
|
||||||
|
setEnabled(status)
|
||||||
|
// call api
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr key={filter.name}
|
||||||
|
className={idx % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||||
|
<Switch
|
||||||
|
checked={enabled}
|
||||||
|
onChange={toggleActive}
|
||||||
|
className={classNames(
|
||||||
|
enabled ? 'bg-teal-500' : 'bg-gray-200',
|
||||||
|
'relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-light-blue-500'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="sr-only">Use setting</span>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className={classNames(
|
||||||
|
enabled ? 'translate-x-5' : 'translate-x-0',
|
||||||
|
'inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Switch>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 w-full whitespace-nowrap text-sm font-medium text-gray-900">{filter.name}</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{filter.indexers && filter.indexers.map(t =>
|
||||||
|
<span key={t.id} className="mr-2 inline-flex items-center px-2.5 py-0.5 rounded-md text-sm font-medium bg-gray-100 text-gray-800">{t.name}</span>)}</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||||
|
<Link to={`filters/${filter.id.toString()}`} className="text-indigo-600 hover:text-indigo-900">
|
||||||
|
Edit
|
||||||
|
</Link>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
const colors = require('tailwindcss/colors')
|
const colors = require('tailwindcss/colors')
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
mode: 'jit',
|
// mode: 'jit',
|
||||||
purge: {
|
purge: {
|
||||||
content: [
|
content: [
|
||||||
'./src/**/*.{tsx,ts,html,css}',
|
'./src/**/*.{tsx,ts,html,css}',
|
||||||
|
|
|
@ -4203,6 +4203,11 @@ deep-is@^0.1.3, deep-is@~0.1.3:
|
||||||
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
|
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
|
||||||
integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=
|
integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=
|
||||||
|
|
||||||
|
deepmerge@^2.1.1:
|
||||||
|
version "2.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.2.1.tgz#5d3ff22a01c00f645405a2fbc17d0778a1801170"
|
||||||
|
integrity sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==
|
||||||
|
|
||||||
deepmerge@^4.2.2:
|
deepmerge@^4.2.2:
|
||||||
version "4.2.2"
|
version "4.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955"
|
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955"
|
||||||
|
@ -5347,6 +5352,19 @@ form-data@^3.0.0:
|
||||||
combined-stream "^1.0.8"
|
combined-stream "^1.0.8"
|
||||||
mime-types "^2.1.12"
|
mime-types "^2.1.12"
|
||||||
|
|
||||||
|
formik@^2.2.9:
|
||||||
|
version "2.2.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/formik/-/formik-2.2.9.tgz#8594ba9c5e2e5cf1f42c5704128e119fc46232d0"
|
||||||
|
integrity sha512-LQLcISMmf1r5at4/gyJigGn0gOwFbeEAlji+N9InZF6LIMXnFNkO42sCI8Jt84YZggpD4cPWObAZaxpEFtSzNA==
|
||||||
|
dependencies:
|
||||||
|
deepmerge "^2.1.1"
|
||||||
|
hoist-non-react-statics "^3.3.0"
|
||||||
|
lodash "^4.17.21"
|
||||||
|
lodash-es "^4.17.21"
|
||||||
|
react-fast-compare "^2.0.1"
|
||||||
|
tiny-warning "^1.0.2"
|
||||||
|
tslib "^1.10.0"
|
||||||
|
|
||||||
forwarded@0.2.0:
|
forwarded@0.2.0:
|
||||||
version "0.2.0"
|
version "0.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
|
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
|
||||||
|
@ -7252,6 +7270,11 @@ locate-path@^5.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
p-locate "^4.1.0"
|
p-locate "^4.1.0"
|
||||||
|
|
||||||
|
lodash-es@^4.17.21:
|
||||||
|
version "4.17.21"
|
||||||
|
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
|
||||||
|
integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
|
||||||
|
|
||||||
lodash._reinterpolate@^3.0.0:
|
lodash._reinterpolate@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
|
resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
|
||||||
|
@ -9497,6 +9520,11 @@ react-error-overlay@^6.0.9:
|
||||||
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.9.tgz#3c743010c9359608c375ecd6bc76f35d93995b0a"
|
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.9.tgz#3c743010c9359608c375ecd6bc76f35d93995b0a"
|
||||||
integrity sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew==
|
integrity sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew==
|
||||||
|
|
||||||
|
react-fast-compare@^2.0.1:
|
||||||
|
version "2.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"
|
||||||
|
integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==
|
||||||
|
|
||||||
react-final-form-arrays@^3.1.3:
|
react-final-form-arrays@^3.1.3:
|
||||||
version "3.1.3"
|
version "3.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/react-final-form-arrays/-/react-final-form-arrays-3.1.3.tgz#d3594c500495a4cf5e437070ada989da9624bba2"
|
resolved "https://registry.yarnpkg.com/react-final-form-arrays/-/react-final-form-arrays-3.1.3.tgz#d3594c500495a4cf5e437070ada989da9624bba2"
|
||||||
|
@ -11087,7 +11115,7 @@ tiny-invariant@^1.0.2:
|
||||||
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875"
|
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875"
|
||||||
integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==
|
integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==
|
||||||
|
|
||||||
tiny-warning@^1.0.0, tiny-warning@^1.0.3:
|
tiny-warning@^1.0.0, tiny-warning@^1.0.2, tiny-warning@^1.0.3:
|
||||||
version "1.0.3"
|
version "1.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
|
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
|
||||||
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
|
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
|
||||||
|
@ -11186,7 +11214,7 @@ tsconfig-paths@^3.9.0:
|
||||||
minimist "^1.2.0"
|
minimist "^1.2.0"
|
||||||
strip-bom "^3.0.0"
|
strip-bom "^3.0.0"
|
||||||
|
|
||||||
tslib@^1.8.1:
|
tslib@^1.10.0, tslib@^1.8.1:
|
||||||
version "1.14.1"
|
version "1.14.1"
|
||||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
|
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
|
||||||
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue