mirror of
https://github.com/idanoo/autobrr
synced 2025-07-25 17:59:14 +00:00
Refactor(web): Replace final-form with Formik and cleanup (#46)
* refactor: begin to replace final-form * refactor: replace final-form with formik n cleanup
This commit is contained in:
parent
c4d580eb03
commit
5e29564f03
66 changed files with 1523 additions and 3409 deletions
|
@ -5,7 +5,7 @@ import { useTable, useFilters, useGlobalFilter, useSortBy, usePagination } from
|
|||
import APIClient from '../api/APIClient'
|
||||
import { useQuery } from 'react-query'
|
||||
import { ReleaseFindResponse, ReleaseStats } from '../domain/interfaces'
|
||||
import { EmptyListState } from '../components/EmptyListState'
|
||||
import { EmptyListState } from '../components/emptystates'
|
||||
|
||||
export function Dashboard() {
|
||||
return (
|
||||
|
|
|
@ -4,7 +4,7 @@ import IndexerSettings from "./settings/Indexer";
|
|||
import IrcSettings from "./settings/Irc";
|
||||
import ApplicationSettings from "./settings/Application";
|
||||
import DownloadClientSettings from "./settings/DownloadClient";
|
||||
import {classNames} from "../styles/utils";
|
||||
import {classNames} from "../utils";
|
||||
import ActionSettings from "./settings/Action";
|
||||
|
||||
const subNavigation = [
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import { useMutation } from "react-query";
|
||||
import APIClient from "../../api/APIClient";
|
||||
import { Form } from "react-final-form";
|
||||
import { PasswordField, TextField } from "../../components/inputs";
|
||||
import { Form, Formik } from "formik";
|
||||
import { useRecoilState } from "recoil";
|
||||
import { isLoggedIn } from "../../state/state";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { useEffect } from "react";
|
||||
import logo from "../../logo.png"
|
||||
import { TextField, PasswordField } from "../../components/inputs";
|
||||
|
||||
interface loginData {
|
||||
username: string;
|
||||
|
@ -32,9 +32,8 @@ function Login() {
|
|||
},
|
||||
})
|
||||
|
||||
const onSubmit = (data: any, form: any) => {
|
||||
const handleSubmit = (data: any) => {
|
||||
mutation.mutate(data)
|
||||
form.reset()
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -50,51 +49,34 @@ function Login() {
|
|||
<div className="sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<div className="bg-white dark:bg-gray-800 py-8 px-4 shadow sm:rounded-lg sm:px-10">
|
||||
|
||||
<Form
|
||||
<Formik
|
||||
initialValues={{
|
||||
username: "",
|
||||
password: "",
|
||||
}}
|
||||
onSubmit={onSubmit}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
{({ handleSubmit, values }) => {
|
||||
return (
|
||||
<form className="space-y-6" onSubmit={handleSubmit}>
|
||||
<TextField name="username" label="Username" autoComplete="username" />
|
||||
<PasswordField name="password" label="password" autoComplete="current-password" />
|
||||
{() => (
|
||||
<Form>
|
||||
|
||||
{/*<div className="flex items-center justify-between">*/}
|
||||
{/* <div className="flex items-center">*/}
|
||||
{/* <input*/}
|
||||
{/* id="remember-me"*/}
|
||||
{/* name="remember-me"*/}
|
||||
{/* type="checkbox"*/}
|
||||
{/* className="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded"*/}
|
||||
{/* />*/}
|
||||
{/* <label htmlFor="remember-me" className="ml-2 block text-sm text-gray-900">*/}
|
||||
{/* Remember me*/}
|
||||
{/* </label>*/}
|
||||
{/* </div>*/}
|
||||
<div className="space-y-6">
|
||||
|
||||
{/* <div className="text-sm">*/}
|
||||
{/* <a href="#" className="font-medium text-indigo-600 hover:text-indigo-500">*/}
|
||||
{/* Forgot your password?*/}
|
||||
{/* </a>*/}
|
||||
{/* </div>*/}
|
||||
{/*</div>*/}
|
||||
<TextField name="username" label="Username" columns={6} autoComplete="username" />
|
||||
<PasswordField name="password" label="Password" columns={6} autoComplete="current-password" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
||||
>
|
||||
Sign in
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}}
|
||||
</Form>
|
||||
|
||||
<div className="mt-6">
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
||||
>
|
||||
Sign in
|
||||
</button>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { 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 { EmptyListState } from "../../components/emptystates";
|
||||
|
||||
import {
|
||||
NavLink,
|
||||
|
@ -17,14 +17,12 @@ 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 { TitleSubtitle } from "../../components/headings";
|
||||
import { buildPath, classNames } from "../../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';
|
||||
|
@ -32,6 +30,7 @@ import Toast from '../../components/notifications/Toast';
|
|||
import { Field, FieldArray, Form, Formik } from "formik";
|
||||
import { AlertWarning } from "../../components/alerts";
|
||||
import { DeleteModal } from "../../components/modals";
|
||||
import { NumberField, TextField, SwitchGroup, Select, MultiSelect, DownloadClientSelect } from "../../components/inputs";
|
||||
|
||||
const tabs = [
|
||||
{ name: 'General', href: '', current: true },
|
||||
|
@ -66,93 +65,21 @@ function TabNavLink({ item, url }: any) {
|
|||
)
|
||||
}
|
||||
|
||||
const FormButtonsGroup = ({ deleteAction, reset, dirty }: any) => {
|
||||
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false)
|
||||
const FormButtonsGroup = ({ values, deleteAction, reset, dirty }: any) => {
|
||||
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
|
||||
|
||||
const cancelButtonRef = useRef(null)
|
||||
const cancelModalButtonRef = useRef(null);
|
||||
|
||||
return (
|
||||
<div className="pt-6 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
|
||||
<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>
|
||||
|
||||
<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 light:bg-white text-base font-medium text-gray-700 dark:text-red-500 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>
|
||||
<DeleteModal
|
||||
isOpen={deleteModalIsOpen}
|
||||
toggle={toggleDeleteModal}
|
||||
buttonRef={cancelModalButtonRef}
|
||||
deleteAction={deleteAction}
|
||||
title={`Remove filter: ${values.name}`}
|
||||
text="Are you sure you want to remove this filter? This action cannot be undone."
|
||||
/>
|
||||
|
||||
<div className="mt-4 pt-4 flex justify-between">
|
||||
<button
|
||||
|
@ -234,8 +161,6 @@ export default function FilterDetails() {
|
|||
}
|
||||
|
||||
const handleSubmit = (data: any) => {
|
||||
console.log("submit other");
|
||||
|
||||
updateMutation.mutate(data)
|
||||
}
|
||||
|
||||
|
@ -329,7 +254,7 @@ export default function FilterDetails() {
|
|||
}}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
{({ isSubmitting, values, dirty, resetForm }) => (
|
||||
{({ values, dirty, resetForm }) => (
|
||||
<Form>
|
||||
<RouteSwitch>
|
||||
<Route exact path={url}>
|
||||
|
@ -349,7 +274,7 @@ export default function FilterDetails() {
|
|||
</Route>
|
||||
</RouteSwitch>
|
||||
|
||||
<FormButtonsGroup deleteAction={deleteAction} dirty={dirty} reset={resetForm} />
|
||||
<FormButtonsGroup values={values} deleteAction={deleteAction} dirty={dirty} reset={resetForm} />
|
||||
|
||||
<DEBUG values={values} />
|
||||
</Form>
|
||||
|
@ -424,7 +349,7 @@ function General({ indexers }: GeneralProps) {
|
|||
<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="" />
|
||||
<NumberField name="delay" label="Delay" placeholder="" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -665,8 +590,8 @@ function FilterActions({ filter, values }: FilterActionsProps) {
|
|||
<div className="light:bg-white dark:bg-gray-800 light:shadow sm:rounded-md">
|
||||
{values.actions.length > 0 ?
|
||||
<ul className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{values.actions.map((action: any, index: any) => (
|
||||
<FilterActionsItem action={action} clients={data!} idx={index} remove={remove} />
|
||||
{values.actions.map((action: any, index: number) => (
|
||||
<FilterActionsItem action={action} clients={data!} idx={index} remove={remove} key={index} />
|
||||
))}
|
||||
</ul>
|
||||
: <EmptyListState text="No actions yet!" />
|
||||
|
|
|
@ -1,109 +0,0 @@
|
|||
import { 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 dark:text-gray-200 uppercase tracking-wide">
|
||||
Client
|
||||
</Listbox.Label>
|
||||
<div className="mt-2 relative">
|
||||
<Listbox.Button className="bg-white dark:bg-gray-800 relative w-full border border-gray-300 dark:border-gray-700 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 dark:text-gray-200 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 dark:text-gray-300"
|
||||
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 dark:bg-gray-800 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 dark:text-gray-100 bg-indigo-600 dark:bg-gray-800"
|
||||
: "text-gray-900 dark:text-gray-300",
|
||||
"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 dark:text-gray-100" : "text-indigo-600 dark:text-gray-700",
|
||||
"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>
|
||||
);
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
import React from "react";
|
||||
import { MultiSelect as 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 mb-2 text-xs font-bold tracking-wide text-gray-700 uppercase dark:text-gray-200"
|
||||
htmlFor={label}
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
|
||||
<Field name={name} type="select" multiple={true}>
|
||||
{({
|
||||
field,
|
||||
form: { setFieldValue },
|
||||
}: any) => (
|
||||
<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)
|
||||
}}
|
||||
className="dark:bg-gray-700"
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default MultiSelect;
|
|
@ -1,52 +0,0 @@
|
|||
import React from "react";
|
||||
import { Field } from "formik";
|
||||
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-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
|
||||
{label}
|
||||
</label>
|
||||
|
||||
<Field name={name} type="number">
|
||||
{({
|
||||
field,
|
||||
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 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"
|
||||
)}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
{meta.touched && meta.error && (
|
||||
<div className="error">{meta.error}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
)}
|
||||
</Field>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default NumberField;
|
|
@ -1,116 +0,0 @@
|
|||
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 dark:text-gray-200 uppercase tracking-wide">
|
||||
{label}
|
||||
</Listbox.Label>
|
||||
<div className="mt-2 relative">
|
||||
<Listbox.Button className="bg-white dark:bg-gray-800 relative w-full border border-gray-300 dark:border-gray-700 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 dark:text-gray-200 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 dark:text-gray-300"
|
||||
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 dark:bg-gray-800 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 dark:text-gray-100 bg-indigo-600 dark:bg-gray-800"
|
||||
: "text-gray-900 dark:text-gray-300",
|
||||
"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 dark:text-gray-100" : "text-indigo-600 dark:text-gray-700",
|
||||
"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;
|
|
@ -1,60 +0,0 @@
|
|||
import React, { InputHTMLAttributes } from 'react'
|
||||
import { Switch as HeadlessSwitch } from '@headlessui/react'
|
||||
import { FieldInputProps, FieldMetaProps, FieldProps, FormikProps, FormikValues } from 'formik'
|
||||
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 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-600',
|
||||
'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'
|
||||
)}
|
||||
>
|
||||
{({ 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} />
|
|
@ -1,89 +0,0 @@
|
|||
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 dark:text-gray-100"
|
||||
passive>
|
||||
{label}
|
||||
</Switch.Label>
|
||||
{description && (
|
||||
<Switch.Description className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{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 dark:bg-blue-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">{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-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;
|
|
@ -1,51 +0,0 @@
|
|||
import React from "react";
|
||||
import { Field } from "formik";
|
||||
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 dark:text-gray-200 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 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"
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
|
||||
{meta.touched && meta.error && (
|
||||
<div className="error">{meta.error}</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Field>
|
||||
</div>
|
||||
)
|
||||
|
||||
export default TextField;
|
|
@ -1,7 +0,0 @@
|
|||
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";
|
|
@ -1,6 +1,6 @@
|
|||
import { useState } from "react";
|
||||
import { Switch } from "@headlessui/react";
|
||||
import { EmptyListState } from "../../components/EmptyListState";
|
||||
import { EmptyListState } from "../../components/emptystates";
|
||||
|
||||
import {
|
||||
Link,
|
||||
|
@ -8,7 +8,7 @@ import {
|
|||
import { Filter } from "../../domain/interfaces";
|
||||
import { useToggle } from "../../hooks/hooks";
|
||||
import { useQuery } from "react-query";
|
||||
import { classNames } from "../../styles/utils";
|
||||
import { classNames } from "../../utils";
|
||||
import { FilterAddForm } from "../../forms";
|
||||
import APIClient from "../../api/APIClient";
|
||||
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
import React, {useState} from "react";
|
||||
import {Switch} from "@headlessui/react";
|
||||
import { classNames } from "../../styles/utils";
|
||||
import React, { useState } from "react";
|
||||
import { Switch } from "@headlessui/react";
|
||||
import { classNames } from "../../utils";
|
||||
// import {useRecoilState} from "recoil";
|
||||
// import {configState} from "../../state/state";
|
||||
import {useQuery} from "react-query";
|
||||
import {Config} from "../../domain/interfaces";
|
||||
import { useQuery } from "react-query";
|
||||
import { Config } from "../../domain/interfaces";
|
||||
import APIClient from "../../api/APIClient";
|
||||
|
||||
function ApplicationSettings() {
|
||||
const [isDebug, setIsDebug] = useState(true)
|
||||
// const [config] = useRecoilState(configState)
|
||||
|
||||
const {isLoading, data} = useQuery<Config, Error>(['config'], () => APIClient.config.get(),
|
||||
const { isLoading, data } = useQuery<Config, Error>(['config'], () => APIClient.config.get(),
|
||||
{
|
||||
retry: false,
|
||||
refetchOnWindowFocus: false,
|
||||
|
@ -33,49 +33,49 @@ function ApplicationSettings() {
|
|||
|
||||
{!isLoading && data && (
|
||||
|
||||
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||
<div className="col-span-6 sm:col-span-4">
|
||||
<label htmlFor="host" className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
|
||||
Host
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="host"
|
||||
id="host"
|
||||
value={data.host}
|
||||
disabled={true}
|
||||
className="mt-2 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 border border-gray-300 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"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||
<div className="col-span-6 sm:col-span-4">
|
||||
<label htmlFor="host" className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
|
||||
Host
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="host"
|
||||
id="host"
|
||||
value={data.host}
|
||||
disabled={true}
|
||||
className="mt-2 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 border border-gray-300 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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-span-6 sm:col-span-4">
|
||||
<label htmlFor="port" className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
|
||||
Port
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="port"
|
||||
id="port"
|
||||
value={data.port}
|
||||
disabled={true}
|
||||
className="mt-2 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 border border-gray-300 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"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-6 sm:col-span-4">
|
||||
<label htmlFor="port" className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
|
||||
Port
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="port"
|
||||
id="port"
|
||||
value={data.port}
|
||||
disabled={true}
|
||||
className="mt-2 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 border border-gray-300 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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-span-6 sm:col-span-4">
|
||||
<label htmlFor="base_url" className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
|
||||
Base url
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="base_url"
|
||||
id="base_url"
|
||||
value={data.base_url}
|
||||
disabled={true}
|
||||
className="mt-2 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 border border-gray-300 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"
|
||||
/>
|
||||
<div className="col-span-6 sm:col-span-4">
|
||||
<label htmlFor="base_url" className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
|
||||
Base url
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="base_url"
|
||||
id="base_url"
|
||||
value={data.base_url}
|
||||
disabled={true}
|
||||
className="mt-2 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 border border-gray-300 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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
@ -84,8 +84,7 @@ function ApplicationSettings() {
|
|||
<ul className="mt-2 divide-y divide-gray-200">
|
||||
<Switch.Group as="li" className="py-4 flex items-center justify-between">
|
||||
<div className="flex flex-col">
|
||||
<Switch.Label as="p" className="text-sm font-medium text-gray-900 dark:text-white"
|
||||
passive>
|
||||
<Switch.Label as="p" className="text-sm font-medium text-gray-900 dark:text-white" passive>
|
||||
Debug
|
||||
</Switch.Label>
|
||||
<Switch.Description className="text-sm text-gray-500 dark:text-gray-400">
|
||||
|
@ -94,6 +93,7 @@ function ApplicationSettings() {
|
|||
</div>
|
||||
<Switch
|
||||
checked={isDebug}
|
||||
disabled={true}
|
||||
onChange={setIsDebug}
|
||||
className={classNames(
|
||||
isDebug ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-700',
|
||||
|
|
|
@ -2,25 +2,24 @@ import { DownloadClient } from "../../domain/interfaces";
|
|||
import { useToggle } from "../../hooks/hooks";
|
||||
import { Switch } from "@headlessui/react";
|
||||
import { useQuery } from "react-query";
|
||||
import { classNames } from "../../styles/utils";
|
||||
import { classNames } from "../../utils";
|
||||
import { DownloadClientAddForm, DownloadClientUpdateForm } from "../../forms";
|
||||
import EmptySimple from "../../components/empty/EmptySimple";
|
||||
import { EmptySimple } from "../../components/emptystates";
|
||||
import APIClient from "../../api/APIClient";
|
||||
import { DownloadClientTypeNameMap } from "../../domain/constants";
|
||||
|
||||
interface DownloadLClientSettingsListItemProps {
|
||||
interface DLSettingsItemProps {
|
||||
client: DownloadClient;
|
||||
idx: number;
|
||||
}
|
||||
|
||||
function DownloadClientSettingsListItem({ client, idx }: DownloadLClientSettingsListItemProps) {
|
||||
function DownloadClientSettingsListItem({ client, idx }: DLSettingsItemProps) {
|
||||
const [updateClientIsOpen, toggleUpdateClient] = useToggle(false)
|
||||
|
||||
return (
|
||||
<tr key={client.name} className={idx % 2 === 0 ? 'light:bg-white' : 'light:bg-gray-50'}>
|
||||
{updateClientIsOpen &&
|
||||
<DownloadClientUpdateForm client={client} isOpen={updateClientIsOpen} toggle={toggleUpdateClient} />
|
||||
}
|
||||
<DownloadClientUpdateForm client={client} isOpen={updateClientIsOpen} toggle={toggleUpdateClient} />
|
||||
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
<Switch
|
||||
checked={client.enabled}
|
||||
|
@ -65,9 +64,7 @@ function DownloadClientSettings() {
|
|||
return (
|
||||
<div className="divide-y divide-gray-200 lg:col-span-9">
|
||||
|
||||
{addClientIsOpen &&
|
||||
<DownloadClientAddForm isOpen={addClientIsOpen} toggle={toggleAddClient} />
|
||||
}
|
||||
<DownloadClientAddForm isOpen={addClientIsOpen} toggle={toggleAddClient} />
|
||||
|
||||
<div className="py-6 px-4 sm:p-6 lg:pb-8">
|
||||
<div className="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap">
|
||||
|
@ -137,8 +134,6 @@ function DownloadClientSettings() {
|
|||
: <EmptySimple title="No download clients" subtitle="Add a new client" buttonText="New client" buttonAction={toggleAddClient} />
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -4,8 +4,8 @@ import { useQuery } from "react-query";
|
|||
import { IndexerAddForm, IndexerUpdateForm } from "../../forms";
|
||||
import { Indexer } from "../../domain/interfaces";
|
||||
import { Switch } from "@headlessui/react";
|
||||
import { classNames } from "../../styles/utils";
|
||||
import EmptySimple from "../../components/empty/EmptySimple";
|
||||
import { classNames } from "../../utils";
|
||||
import { EmptySimple } from "../../components/emptystates";
|
||||
import APIClient from "../../api/APIClient";
|
||||
|
||||
const ListItem = ({ indexer }: any) => {
|
||||
|
|
|
@ -3,8 +3,8 @@ import { IrcNetworkAddForm, IrcNetworkUpdateForm } from "../../forms";
|
|||
import { useToggle } from "../../hooks/hooks";
|
||||
import { useQuery } from "react-query";
|
||||
import { Switch } from "@headlessui/react";
|
||||
import { classNames } from "../../styles/utils";
|
||||
import EmptySimple from "../../components/empty/EmptySimple";
|
||||
import { classNames } from "../../utils";
|
||||
import { EmptySimple } from "../../components/emptystates";
|
||||
import APIClient from "../../api/APIClient";
|
||||
|
||||
interface IrcNetwork {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue