mirror of
https://github.com/idanoo/autobrr
synced 2025-07-23 08:49:13 +00:00
feat(FilterItemDropdown): added a filter dropdown component from #26 * fix(react-multi-select-component): adjusted the component to fit in with other components when comparing across multiple browsers. (where firefox consistently handles pixels and rem's, chromium doesn't agree). refactor(input): removed shadow from input components to match react-multi-select-component look where needed. refactor(SwitchGroup): added a small top margin for a less dense look. cleaned up surrounding code. refactor(DeleteModal): adjusted the background color to fit more nicely across dark/light themes. made the exclamation icon bigger and removed the circle container. refactor(Logs): adjusted text color on the light theme. cleaned up the code. refactor(FilterAddForm): adapted to conform with the changes. feat(FilterItemDropdown): added a dropdown component to the filter list as proposed in #26. could use a better look, though. also, cleaned up surrounding code and got rid of pesky negative margins. refactor(FilterListItem): made the table striped when using the dark theme. adapter for the dropdown component. refactor(filters/details): - removed needless border properties from remove button, which left artifacts after de-focusing the button. also, removed the shadow from the cancel button. - added invalidation of the filter list on a delete mutation before redirecting to /filters. - modified certain group descriptions a bit in order to make them a bit more concise. - overall, cleaned up the surrounding code further.
This commit is contained in:
parent
d4d9169210
commit
279d4ff7a3
9 changed files with 356 additions and 206 deletions
|
@ -41,7 +41,7 @@ export const TextField = ({
|
|||
id={name}
|
||||
type="text"
|
||||
autoComplete={autoComplete}
|
||||
className="mt-2 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:text-gray-100 sm:text-sm"
|
||||
className="mt-2 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-md py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:text-gray-100"
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
|
||||
|
@ -99,7 +99,7 @@ export const PasswordField = ({
|
|||
id={name}
|
||||
type={isVisible ? "text" : "password"}
|
||||
autoComplete={autoComplete}
|
||||
className={classNames(meta.touched && meta.error ? "focus:ring-red-500 focus:border-red-500 border-red-500" : "focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-700", "mt-2 block w-full dark:bg-gray-800 shadow-sm dark:text-gray-100 sm:text-sm rounded-md")}
|
||||
className={classNames(meta.touched && meta.error ? "focus:ring-red-500 focus:border-red-500 border-red-500" : "focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-700", "mt-2 block w-full dark:bg-gray-800 dark:text-gray-100 rounded-md")}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
|
||||
|
@ -150,7 +150,7 @@ export const NumberField = ({
|
|||
meta.touched && meta.error
|
||||
? "focus:ring-red-500 focus:border-red-500 border-red-500"
|
||||
: "focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300",
|
||||
"mt-2 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 shadow-sm dark:text-gray-100 sm:text-sm rounded-md"
|
||||
"mt-2 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 dark:text-gray-100 rounded-md"
|
||||
)}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
|
|
|
@ -77,7 +77,6 @@ const SwitchGroup = ({
|
|||
label,
|
||||
description
|
||||
}: SwitchGroupProps) => (
|
||||
<ul className="mt-2 divide-y divide-gray-200">
|
||||
<HeadlessSwitch.Group as="li" className="py-4 flex items-center justify-between">
|
||||
{label && <div className="flex flex-col">
|
||||
<HeadlessSwitch.Label as="p" className="text-sm font-medium text-gray-900 dark:text-gray-100"
|
||||
|
@ -85,7 +84,7 @@ const SwitchGroup = ({
|
|||
{label}
|
||||
</HeadlessSwitch.Label>
|
||||
{description && (
|
||||
<HeadlessSwitch.Description className="text-sm text-gray-500 dark:text-gray-400">
|
||||
<HeadlessSwitch.Description className="text-sm mt-1 text-gray-500 dark:text-gray-400">
|
||||
{description}
|
||||
</HeadlessSwitch.Description>
|
||||
)}
|
||||
|
@ -110,7 +109,6 @@ const SwitchGroup = ({
|
|||
'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-blue-500'
|
||||
)}
|
||||
>
|
||||
{/* <span className="sr-only">{label}</span> */}
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={classNames(
|
||||
|
@ -122,33 +120,7 @@ const SwitchGroup = ({
|
|||
|
||||
)}
|
||||
</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-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>
|
||||
)}
|
||||
/> */}
|
||||
</HeadlessSwitch.Group>
|
||||
</ul>
|
||||
)
|
||||
);
|
||||
|
||||
export { SwitchGroup }
|
|
@ -31,7 +31,7 @@ export const DeleteModal: FC<DeleteModalProps> = ({ isOpen, buttonRef, toggle, d
|
|||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Dialog.Overlay className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
|
||||
<Dialog.Overlay className="fixed inset-0 bg-gray-700/60 dark:bg-black/60 transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
|
||||
|
@ -46,12 +46,10 @@ export const DeleteModal: FC<DeleteModalProps> = ({ isOpen, buttonRef, toggle, d
|
|||
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 dark:bg-gray-700 px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||
<div className="inline-block align-bottom 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 dark:bg-gray-800 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 dark:bg-red-400 sm:mx-0 sm:h-10 sm:w-10">
|
||||
<ExclamationIcon className="h-6 w-6 text-red-600 dark:text-red-600" aria-hidden="true" />
|
||||
</div>
|
||||
<ExclamationIcon className="h-16 w-16 text-red-500 dark:text-red-500" aria-hidden="true" />
|
||||
<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 dark:text-white">
|
||||
{title}
|
||||
|
|
|
@ -12,14 +12,17 @@ import DEBUG from "../../components/debug";
|
|||
import Toast from '../../components/notifications/Toast';
|
||||
|
||||
function FilterAddForm({ isOpen, toggle }: any) {
|
||||
const mutation = useMutation((filter: Filter) => APIClient.filters.create(filter), {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries('filter');
|
||||
toast.custom((t) => <Toast type="success" body="Filter was added" t={t} />)
|
||||
const mutation = useMutation(
|
||||
(filter: Filter) => APIClient.filters.create(filter),
|
||||
{
|
||||
onSuccess: (_, filter) => {
|
||||
queryClient.invalidateQueries("filters");
|
||||
toast.custom((t) => <Toast type="success" body={`Filter ${filter.name} was added`} t={t} />);
|
||||
|
||||
toggle()
|
||||
toggle();
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
const handleSubmit = (data: any) => mutation.mutate(data);
|
||||
const validate = (values: any) => values.name ? {} : { name: "Required" };
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -5,9 +5,9 @@ import { Disclosure, Menu, Transition } from "@headlessui/react";
|
|||
import { ExternalLinkIcon } from "@heroicons/react/solid";
|
||||
import { ChevronDownIcon, MenuIcon, XIcon } from "@heroicons/react/outline";
|
||||
|
||||
import Logs from "./Logs";
|
||||
import Settings from "./Settings";
|
||||
|
||||
import { Logs } from "./Logs";
|
||||
import { Releases } from "./Releases";
|
||||
import { Dashboard } from "./Dashboard";
|
||||
import { FilterDetails, Filters } from "./filters";
|
||||
|
|
|
@ -7,10 +7,9 @@ type LogEvent = {
|
|||
message: string;
|
||||
};
|
||||
|
||||
export default function Logs() {
|
||||
const [logs, setLogs] = useState<LogEvent[]>([])
|
||||
|
||||
const messagesEndRef: any = useRef(null)
|
||||
export const Logs = () => {
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const [logs, setLogs] = useState<LogEvent[]>([]);
|
||||
|
||||
const scrollToBottom = () => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: "auto" })
|
||||
|
@ -41,12 +40,12 @@ export default function Logs() {
|
|||
<div className=" overflow-y-auto p-2 rounded-lg h-96 bg-gray-100 dark:bg-gray-900">
|
||||
{logs.map((a, idx) => (
|
||||
<p key={idx}>
|
||||
<span className="font-mono text-gray-600 mr-2">{a.time}</span>
|
||||
<span className="font-mono text-gray-500 dark:text-gray-600 mr-2">{a.time}</span>
|
||||
{a.level === "TRACE" && <span className="font-mono font-semibold text-purple-300">{a.level}</span>}
|
||||
{a.level === "DEBUG" && <span className="font-mono font-semibold text-yellow-500">{a.level}</span>}
|
||||
{a.level === "INFO" && <span className="font-mono font-semibold text-green-500">{a.level} </span>}
|
||||
{a.level === "ERROR" && <span className="font-mono font-semibold text-red-500">{a.level}</span>}
|
||||
<span className="ml-2 text-gray-300">{a.message}</span>
|
||||
<span className="ml-2 text-black dark:text-gray-300">{a.message}</span>
|
||||
</p>
|
||||
))}
|
||||
<div ref={messagesEndRef} />
|
||||
|
|
|
@ -102,7 +102,7 @@ const FormButtonsGroup = ({ values, deleteAction, reset }: any) => {
|
|||
<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 dark:text-red-500 light:bg-red-100 light:hover:bg-red-200 dark:hover:text-red-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:text-sm"
|
||||
className="inline-flex items-center justify-center px-4 py-2 rounded-md text-red-700 dark:text-red-500 light:bg-red-100 light:hover:bg-red-200 dark:hover:text-red-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:text-sm"
|
||||
onClick={toggleDeleteModal}
|
||||
>
|
||||
Remove
|
||||
|
@ -112,7 +112,7 @@ const FormButtonsGroup = ({ values, deleteAction, reset }: any) => {
|
|||
{/* {dirty && <span className="mr-4 text-sm text-gray-500">Unsaved changes..</span>} */}
|
||||
<button
|
||||
type="button"
|
||||
className="light:bg-white light:border light:border-gray-300 rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-gray-700 dark:text-gray-500 light:hover:bg-gray-50 dark:hover:text-gray-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
className="light:bg-white light:border light:border-gray-300 rounded-md py-2 px-4 inline-flex justify-center text-sm font-medium text-gray-700 dark:text-gray-500 light:hover:bg-gray-50 dark:hover:text-gray-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
onClick={reset}
|
||||
>
|
||||
Cancel
|
||||
|
@ -135,7 +135,7 @@ export default function FilterDetails() {
|
|||
const { filterId } = useParams<{ filterId: string }>();
|
||||
|
||||
const { isLoading, data: filter } = useQuery(
|
||||
['filter', +filterId],
|
||||
["filter", +filterId],
|
||||
() => APIClient.filters.getByID(parseInt(filterId)),
|
||||
{
|
||||
retry: false,
|
||||
|
@ -147,16 +147,23 @@ export default function FilterDetails() {
|
|||
const updateMutation = useMutation(
|
||||
(filter: Filter) => APIClient.filters.update(filter),
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast.custom((t) => <Toast type="success" body={`${filter?.name} was updated successfully`} t={t} />)
|
||||
queryClient.invalidateQueries(["filter", filter?.id]);
|
||||
onSuccess: (_, currentFilter) => {
|
||||
toast.custom((t) => (
|
||||
<Toast type="success" body={`${currentFilter.name} was updated successfully`} t={t} />
|
||||
));
|
||||
queryClient.invalidateQueries(["filter", currentFilter.id]);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const deleteMutation = useMutation((id: number) => APIClient.filters.delete(id), {
|
||||
onSuccess: () => {
|
||||
toast.custom((t) => <Toast type="success" body={`${filter?.name} was deleted`} t={t} />)
|
||||
toast.custom((t) => (
|
||||
<Toast type="success" body={`${filter?.name} was deleted`} t={t} />
|
||||
));
|
||||
|
||||
// Invalidate filters just in case, most likely not necessary but can't hurt.
|
||||
queryClient.invalidateQueries("filters");
|
||||
|
||||
// redirect
|
||||
history.push("/filters")
|
||||
|
@ -346,7 +353,7 @@ function General() {
|
|||
</div>
|
||||
|
||||
<div className="mt-6 lg:pb-8">
|
||||
<TitleSubtitle title="Rules" subtitle="Set rules" />
|
||||
<TitleSubtitle title="Rules" subtitle="Specify rules on how torrents should be handled/selected" />
|
||||
|
||||
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||
<TextField name="min_size" label="Min size" columns={6} placeholder="" />
|
||||
|
@ -356,7 +363,7 @@ function General() {
|
|||
</div>
|
||||
|
||||
<div className="border-t dark:border-gray-700">
|
||||
<SwitchGroup name="enabled" label="Enabled" description="Enabled or disable filter." />
|
||||
<SwitchGroup name="enabled" label="Enabled" description="Enable or disable this filter" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
@ -372,7 +379,7 @@ function MoviesTv() {
|
|||
</div>
|
||||
|
||||
<div className="mt-6 lg:pb-8">
|
||||
<TitleSubtitle title="Seasons and Episodes" subtitle="Set seaons and episodes" />
|
||||
<TitleSubtitle title="Seasons and Episodes" subtitle="Set season and episode match constraints" />
|
||||
|
||||
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||
<TextField name="seasons" label="Seasons" columns={8} placeholder="eg. 1,3,2-6" />
|
||||
|
@ -381,7 +388,7 @@ function MoviesTv() {
|
|||
</div>
|
||||
|
||||
<div className="mt-6 lg:pb-8">
|
||||
<TitleSubtitle title="Quality" subtitle="Resolution, source etc." />
|
||||
<TitleSubtitle title="Quality" subtitle="Set resolution, source, codec and related match constraints" />
|
||||
|
||||
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||
<MultiSelect name="resolutions" options={RESOLUTION_OPTIONS} label="resolutions" columns={6} />
|
||||
|
@ -467,7 +474,7 @@ function Advanced() {
|
|||
<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 dark:text-gray-200">Releases</h3>
|
||||
<p className="ml-2 mt-1 text-sm text-gray-500 dark:text-gray-400 truncate">Match or ignore</p>
|
||||
<p className="ml-2 mt-1 text-sm text-gray-500 dark:text-gray-400 truncate">Match only certain release names and/or ignore other release names</p>
|
||||
</div>
|
||||
<div className="mt-3 sm:mt-0 sm:ml-4">
|
||||
<button
|
||||
|
@ -490,7 +497,7 @@ function Advanced() {
|
|||
<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 dark:text-gray-200">Groups</h3>
|
||||
<p className="ml-2 mt-1 text-sm text-gray-500 dark:text-gray-400 truncate">Match or ignore</p>
|
||||
<p className="ml-2 mt-1 text-sm text-gray-500 dark:text-gray-400 truncate">Match only certain groups and/or ignore other groups</p>
|
||||
</div>
|
||||
<div className="mt-3 sm:mt-0 sm:ml-4">
|
||||
<button
|
||||
|
@ -594,7 +601,7 @@ interface FilterActionsProps {
|
|||
|
||||
function FilterActions({ filter, values }: FilterActionsProps) {
|
||||
const { data } = useQuery(
|
||||
['filter', 'download_clients'],
|
||||
["filter", "download_clients"],
|
||||
APIClient.download_clients.getAll,
|
||||
{ refetchOnWindowFocus: false }
|
||||
);
|
||||
|
@ -669,21 +676,6 @@ function FilterActionsItem({ action, clients, idx, remove }: FilterActionsItemPr
|
|||
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) => {
|
||||
|
|
|
@ -1,8 +1,14 @@
|
|||
import { useState } from "react";
|
||||
import { Fragment, useRef, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { Switch } from "@headlessui/react";
|
||||
import { useMutation, useQuery } from "react-query";
|
||||
import { Menu, Switch, Transition } from "@headlessui/react";
|
||||
import { useMutation, useQuery, useQueryClient } from "react-query";
|
||||
import {
|
||||
TrashIcon,
|
||||
PencilAltIcon,
|
||||
SwitchHorizontalIcon,
|
||||
DotsHorizontalIcon,
|
||||
} from "@heroicons/react/outline";
|
||||
|
||||
import { queryClient } from "../../App";
|
||||
import { classNames } from "../../utils";
|
||||
|
@ -11,19 +17,19 @@ import { useToggle } from "../../hooks/hooks";
|
|||
import { APIClient } from "../../api/APIClient";
|
||||
import Toast from "../../components/notifications/Toast";
|
||||
import { EmptyListState } from "../../components/emptystates";
|
||||
import { DeleteModal } from "../../components/modals";
|
||||
|
||||
export default function Filters() {
|
||||
const [createFilterIsOpen, toggleCreateFilter] = useToggle(false)
|
||||
|
||||
const { isLoading, error, data } = useQuery(
|
||||
'filter',
|
||||
"filters",
|
||||
APIClient.filters.getAll,
|
||||
{ refetchOnWindowFocus: false }
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return null
|
||||
}
|
||||
if (isLoading)
|
||||
return null;
|
||||
|
||||
if (error)
|
||||
return (<p>An error has occurred: </p>);
|
||||
|
@ -48,13 +54,12 @@ export default function Filters() {
|
|||
</div>
|
||||
</header>
|
||||
|
||||
<div className="max-w-7xl mx-auto pb-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="bg-white dark:bg-gray-800 light:rounded-lg shadow-lg">
|
||||
<div className="relative inset-0 light:py-3 light:px-3 light:sm:px-3 light: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 className="max-w-7xl mx-auto pb-12 px-4 sm:px-6 lg:px-8 relative">
|
||||
{data && data.length > 0 ? (
|
||||
<FilterList filters={data} />
|
||||
) : (
|
||||
<EmptyListState text="No filters here.." buttonText="Add new" buttonOnClick={toggleCreateFilter} />
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
|
@ -66,49 +71,161 @@ interface FilterListProps {
|
|||
|
||||
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="light: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 dark:border-gray-800 sm:rounded-lg">
|
||||
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead className="bg-gray-50 dark:bg-gray-800 text-gray-500 dark:text-gray-400">
|
||||
<div className="overflow-x-auto align-middle min-w-full rounded-lg shadow-lg">
|
||||
<table className="min-w-full">
|
||||
<thead className="bg-gray-50 dark:bg-gray-800 text-gray-500 dark:text-gray-400 border-b border-gray-200 dark:border-gray-700">
|
||||
<tr>
|
||||
{["Enabled", "Name", "Indexers"].map((label) => (
|
||||
<th
|
||||
key={`th-${label}`}
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider"
|
||||
className="px-6 py-2.5 text-left text-xs font-medium uppercase tracking-wider"
|
||||
>
|
||||
Enabled
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider"
|
||||
>
|
||||
Name
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider"
|
||||
>
|
||||
Indexers
|
||||
{label}
|
||||
</th>
|
||||
))}
|
||||
<th scope="col" className="relative px-6 py-3">
|
||||
<span className="sr-only">Edit</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-800">
|
||||
<tbody className="divide-y divide-gray-200 dark:divide-gray-800">
|
||||
{filters.map((filter: Filter, idx) => (
|
||||
<FilterListItem filter={filter} key={filter.id} idx={idx} />
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface FilterItemDropdownProps {
|
||||
filter: Filter;
|
||||
onToggle: (newState: boolean) => void;
|
||||
}
|
||||
|
||||
const FilterItemDropdown = ({
|
||||
filter,
|
||||
onToggle
|
||||
}: FilterItemDropdownProps) => {
|
||||
const cancelModalButtonRef = useRef(null);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
|
||||
const deleteMutation = useMutation(
|
||||
(id: number) => APIClient.filters.delete(id),
|
||||
{
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries("filters");
|
||||
queryClient.invalidateQueries(["filter", filter.id]);
|
||||
|
||||
toast.custom((t) => <Toast type="success" body={`Filter ${filter?.name} was deleted`} t={t} />);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<Menu as="div">
|
||||
<DeleteModal
|
||||
isOpen={deleteModalIsOpen}
|
||||
toggle={toggleDeleteModal}
|
||||
buttonRef={cancelModalButtonRef}
|
||||
deleteAction={() => {
|
||||
deleteMutation.mutate(filter.id);
|
||||
toggleDeleteModal();
|
||||
}}
|
||||
title={`Remove filter: ${filter.name}`}
|
||||
text="Are you sure you want to remove this filter? This action cannot be undone."
|
||||
/>
|
||||
<Menu.Button className="px-4 py-2">
|
||||
<DotsHorizontalIcon
|
||||
className="w-5 h-5 text-gray-700 hover:text-gray-900 dark:text-gray-100 dark:hover:text-gray-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Menu.Button>
|
||||
<Transition
|
||||
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
|
||||
className="absolute right-0 w-56 mt-2 origin-top-right bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700 rounded-md shadow-lg ring-1 ring-black ring-opacity-10 focus:outline-none"
|
||||
>
|
||||
<div className="px-1 py-1">
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<Link
|
||||
to={`filters/${filter.id.toString()}`}
|
||||
className={classNames(
|
||||
active ? "bg-blue-600 text-white" : "text-gray-900 dark:text-gray-300",
|
||||
"font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm"
|
||||
)}
|
||||
>
|
||||
<PencilAltIcon
|
||||
className={classNames(
|
||||
active ? "text-white" : "text-blue-500",
|
||||
"w-5 h-5 mr-2"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Edit
|
||||
</Link>
|
||||
)}
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<button
|
||||
className={classNames(
|
||||
active ? "bg-blue-600 text-white" : "text-gray-900 dark:text-gray-300",
|
||||
"font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm"
|
||||
)}
|
||||
onClick={() => onToggle(!filter.enabled)}
|
||||
>
|
||||
<SwitchHorizontalIcon
|
||||
className={classNames(
|
||||
active ? "text-white" : "text-blue-500",
|
||||
"w-5 h-5 mr-2"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Toggle
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</div>
|
||||
<div className="px-1 py-1">
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<button
|
||||
className={classNames(
|
||||
active ? "bg-blue-600 text-white" : "text-gray-900 dark:text-gray-300",
|
||||
"font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm"
|
||||
)}
|
||||
onClick={() => toggleDeleteModal()}
|
||||
>
|
||||
<TrashIcon
|
||||
className={classNames(
|
||||
active ? "text-white" : "text-blue-500",
|
||||
"w-5 h-5 mr-2"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Delete
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
|
||||
interface FilterListItemProps {
|
||||
filter: Filter;
|
||||
idx: number;
|
||||
|
@ -117,24 +234,39 @@ interface FilterListItemProps {
|
|||
function FilterListItem({ filter, idx }: FilterListItemProps) {
|
||||
const [enabled, setEnabled] = useState(filter.enabled)
|
||||
|
||||
const updateMutation = useMutation((status: boolean) => APIClient.filters.toggleEnable(filter.id, status), {
|
||||
const updateMutation = useMutation(
|
||||
(status: boolean) => APIClient.filters.toggleEnable(filter.id, status),
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast.custom((t) => <Toast type="success" body={`${filter.name} was ${enabled ? "disabled" : "enabled"} successfully`} t={t} />)
|
||||
|
||||
queryClient.invalidateQueries("filter");
|
||||
// We need to invalidate both keys here.
|
||||
// The filters key is used on the /filters page,
|
||||
// while the ["filter", filter.id] key is used on the details page.
|
||||
queryClient.invalidateQueries("filters");
|
||||
queryClient.invalidateQueries(["filter", filter?.id]);
|
||||
}
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
const toggleActive = (status: boolean) => {
|
||||
setEnabled(status)
|
||||
// call api
|
||||
updateMutation.mutate(status)
|
||||
setEnabled(status);
|
||||
updateMutation.mutate(status);
|
||||
}
|
||||
|
||||
return (
|
||||
<tr key={filter.id}
|
||||
className={idx % 2 === 0 ? 'bg-white dark:bg-gray-800' : 'bg-gray-50 dark:bg-gray-800'}>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-100">
|
||||
<tr
|
||||
key={filter.id}
|
||||
className={classNames(
|
||||
idx % 2 === 0 ?
|
||||
"bg-white dark:bg-[#2e2e31]" :
|
||||
"bg-gray-50 dark:bg-gray-800",
|
||||
"hover:bg-gray-100 dark:hover:bg-[#222225]"
|
||||
)}
|
||||
>
|
||||
<td
|
||||
className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-100"
|
||||
>
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onChange={toggleActive}
|
||||
|
@ -154,16 +286,28 @@ function FilterListItem({ filter, idx }: FilterListItemProps) {
|
|||
</Switch>
|
||||
</td>
|
||||
<td className="px-6 w-full whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
<Link to={`filters/${filter.id.toString()}`} className="dark:hover:text-gray-400 w-full py-4 flex">
|
||||
<Link
|
||||
to={`filters/${filter.id.toString()}`}
|
||||
className="hover:text-black dark:hover:text-gray-300 w-full py-4 flex"
|
||||
>
|
||||
{filter.name}
|
||||
</Link>
|
||||
</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-200 dark:bg-gray-700 text-gray-800 dark:text-gray-400">{t.name}</span>)}</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-200 dark:bg-gray-700 text-gray-800 dark:text-gray-400"
|
||||
>
|
||||
{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 dark:text-gray-200 hover:text-indigo-900 dark:hover:text-gray-400">
|
||||
Edit
|
||||
</Link>
|
||||
<FilterItemDropdown
|
||||
filter={filter}
|
||||
onToggle={toggleActive}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue