mirror of
https://github.com/idanoo/autobrr
synced 2025-07-23 08:49:13 +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
|
@ -2,7 +2,7 @@
|
||||||
"name": "web",
|
"name": "web",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"proxy": "http://localhost:8989",
|
"proxy": "http://127.0.0.1:8989",
|
||||||
"homepage": ".",
|
"homepage": ".",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@craco/craco": "^6.1.2",
|
"@craco/craco": "^6.1.2",
|
||||||
|
@ -16,14 +16,10 @@
|
||||||
"@types/react": "^17.0.0",
|
"@types/react": "^17.0.0",
|
||||||
"@types/react-dom": "^17.0.0",
|
"@types/react-dom": "^17.0.0",
|
||||||
"date-fns": "^2.25.0",
|
"date-fns": "^2.25.0",
|
||||||
"final-form": "^4.20.2",
|
|
||||||
"final-form-arrays": "^3.0.2",
|
|
||||||
"formik": "^2.2.9",
|
"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",
|
||||||
"react-final-form": "^6.5.3",
|
|
||||||
"react-final-form-arrays": "^3.1.3",
|
|
||||||
"react-hot-toast": "^2.1.1",
|
"react-hot-toast": "^2.1.1",
|
||||||
"react-multi-select-component": "^4.0.2",
|
"react-multi-select-component": "^4.0.2",
|
||||||
"react-query": "^3.18.1",
|
"react-query": "^3.18.1",
|
||||||
|
|
|
@ -5,7 +5,7 @@ import Logout from "./screens/auth/logout";
|
||||||
import Base from "./screens/Base";
|
import Base from "./screens/Base";
|
||||||
import { ReactQueryDevtools } from "react-query/devtools";
|
import { ReactQueryDevtools } from "react-query/devtools";
|
||||||
import Layout from "./components/Layout";
|
import Layout from "./components/Layout";
|
||||||
import { baseUrl } from "./utils/utils";
|
import { baseUrl } from "./utils";
|
||||||
|
|
||||||
function Protected() {
|
function Protected() {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import {Action, DownloadClient, Filter, Indexer, Network} from "../domain/interfaces";
|
import {Action, DownloadClient, Filter, Indexer, Network} from "../domain/interfaces";
|
||||||
import {baseUrl, sseBaseUrl} from "../utils/utils";
|
import {baseUrl, sseBaseUrl} from "../utils";
|
||||||
|
|
||||||
function baseClient(endpoint: string, method: string, { body, ...customConfig}: any = {}) {
|
function baseClient(endpoint: string, method: string, { body, ...customConfig}: any = {}) {
|
||||||
let baseURL = baseUrl()
|
let baseURL = baseUrl()
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
interface props {
|
|
||||||
text: string;
|
|
||||||
buttonText?: string;
|
|
||||||
buttonOnClick?: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function EmptyListState({ text, buttonText, buttonOnClick }: props) {
|
|
||||||
return (
|
|
||||||
<div className="px-4 py-12 flex flex-col items-center">
|
|
||||||
<p className="text-center text-gray-500 dark:text-white">{text}</p>
|
|
||||||
{buttonText && buttonOnClick && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="relative inline-flex items-center px-4 py-2 mt-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
|
||||||
onClick={buttonOnClick}
|
|
||||||
>
|
|
||||||
{buttonText}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,495 +0,0 @@
|
||||||
import { Action, DownloadClient } from "../domain/interfaces";
|
|
||||||
import { Fragment, useEffect, useRef } from "react";
|
|
||||||
import { Dialog, Listbox, Switch, Transition } from "@headlessui/react";
|
|
||||||
import { classNames } from "../styles/utils";
|
|
||||||
import {
|
|
||||||
CheckIcon,
|
|
||||||
ChevronRightIcon,
|
|
||||||
SelectorIcon,
|
|
||||||
} from "@heroicons/react/solid";
|
|
||||||
import { useToggle } from "../hooks/hooks";
|
|
||||||
import { useMutation } from "react-query";
|
|
||||||
import { Field, Form } from "react-final-form";
|
|
||||||
import { SwitchGroup, TextField } from "./inputs";
|
|
||||||
import { NumberField, SelectField } from "./inputs/compact";
|
|
||||||
import DEBUG from "./debug";
|
|
||||||
import APIClient from "../api/APIClient";
|
|
||||||
import { queryClient } from "../App";
|
|
||||||
import { ActionTypeNameMap, ActionTypeOptions } from "../domain/constants";
|
|
||||||
import { AlertWarning } from "./alerts";
|
|
||||||
import { DeleteModal } from "./modals";
|
|
||||||
|
|
||||||
interface DownloadClientSelectProps {
|
|
||||||
name: string;
|
|
||||||
action: Action;
|
|
||||||
clients: DownloadClient[];
|
|
||||||
}
|
|
||||||
|
|
||||||
function DownloadClientSelect({
|
|
||||||
name,
|
|
||||||
action,
|
|
||||||
clients,
|
|
||||||
}: DownloadClientSelectProps) {
|
|
||||||
return (
|
|
||||||
<div className="col-span-6 sm:col-span-6">
|
|
||||||
<Field
|
|
||||||
name={name}
|
|
||||||
type="select"
|
|
||||||
render={({ input }) => (
|
|
||||||
<Listbox value={input.value} onChange={input.onChange}>
|
|
||||||
{({ 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">
|
|
||||||
{input.value
|
|
||||||
? clients.find((c) => c.id === input.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>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FilterListProps {
|
|
||||||
actions: Action[];
|
|
||||||
clients: DownloadClient[];
|
|
||||||
filterID: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function FilterActionList({
|
|
||||||
actions,
|
|
||||||
clients,
|
|
||||||
filterID,
|
|
||||||
}: FilterListProps) {
|
|
||||||
useEffect(() => {
|
|
||||||
// console.log("render list")
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="bg-white shadow overflow-hidden sm:rounded-md">
|
|
||||||
<ul className="divide-y divide-gray-200">
|
|
||||||
{actions.map((action, idx) => (
|
|
||||||
<ListItem
|
|
||||||
action={action}
|
|
||||||
clients={clients}
|
|
||||||
filterID={filterID}
|
|
||||||
key={action.id}
|
|
||||||
idx={idx}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ListItemProps {
|
|
||||||
action: Action;
|
|
||||||
clients: DownloadClient[];
|
|
||||||
filterID: number;
|
|
||||||
idx: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ListItem({ action, clients, filterID, idx }: ListItemProps) {
|
|
||||||
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
|
|
||||||
const [edit, toggleEdit] = useToggle(false);
|
|
||||||
|
|
||||||
const deleteMutation = useMutation(
|
|
||||||
(actionID: number) => APIClient.actions.delete(actionID),
|
|
||||||
{
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries(["filter", filterID]);
|
|
||||||
toggleDeleteModal();
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const enabledMutation = useMutation(
|
|
||||||
(actionID: number) => APIClient.actions.toggleEnable(actionID),
|
|
||||||
{
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries(["filter", filterID]);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const updateMutation = useMutation(
|
|
||||||
(action: Action) => APIClient.actions.update(action),
|
|
||||||
{
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries(["filter", filterID]);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const toggleActive = () => {
|
|
||||||
enabledMutation.mutate(action.id);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => { }, [action]);
|
|
||||||
|
|
||||||
const cancelButtonRef = useRef(null);
|
|
||||||
|
|
||||||
const deleteAction = () => {
|
|
||||||
deleteMutation.mutate(action.id);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSubmit = (action: Action) => {
|
|
||||||
// TODO clear data depending on type
|
|
||||||
updateMutation.mutate(action);
|
|
||||||
};
|
|
||||||
|
|
||||||
const TypeForm = (action: Action) => {
|
|
||||||
switch (action.type) {
|
|
||||||
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="exec_cmd"
|
|
||||||
label="Command"
|
|
||||||
columns={6}
|
|
||||||
placeholder="Path to program eg. /bin/test"
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
name="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="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="client_id"
|
|
||||||
action={action}
|
|
||||||
clients={clients}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="col-span-6 sm:col-span-6">
|
|
||||||
<TextField name="save_path" label="Save path" columns={6} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-6 grid grid-cols-12 gap-6">
|
|
||||||
<TextField name="category" label="Category" columns={6} />
|
|
||||||
<TextField name="tags" label="Tags" columns={6} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-6 grid grid-cols-12 gap-6">
|
|
||||||
<NumberField
|
|
||||||
name="limit_download_speed"
|
|
||||||
label="Limit download speed (KB/s)"
|
|
||||||
/>
|
|
||||||
<NumberField
|
|
||||||
name="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="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="client_id"
|
|
||||||
action={action}
|
|
||||||
clients={clients}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="col-span-12 sm:col-span-6">
|
|
||||||
<TextField name="save_path" label="Save path" columns={6} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-6 col-span-12 sm:col-span-6">
|
|
||||||
<TextField name="label" label="Label" columns={6} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-6 grid grid-cols-12 gap-6">
|
|
||||||
<NumberField
|
|
||||||
name="limit_download_speed"
|
|
||||||
label="Limit download speed (KB/s)"
|
|
||||||
/>
|
|
||||||
<NumberField
|
|
||||||
name="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="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="client_id"
|
|
||||||
action={action}
|
|
||||||
clients={clients}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<li key={action.id}>
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
idx % 2 === 0 ? "bg-white" : "bg-gray-50",
|
|
||||||
"flex items-center sm:px-6 hover:bg-gray-50"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Switch
|
|
||||||
checked={action.enabled}
|
|
||||||
onChange={toggleActive}
|
|
||||||
className={classNames(
|
|
||||||
action.enabled ? "bg-teal-500" : "bg-gray-200",
|
|
||||||
"z-10 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(
|
|
||||||
action.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>
|
|
||||||
<button className="px-4 py-4 w-full flex block" 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={deleteAction}
|
|
||||||
title="Remove filter action"
|
|
||||||
text="Are you sure you want to remove this action? This action cannot be undone."
|
|
||||||
/>
|
|
||||||
</Dialog>
|
|
||||||
</Transition.Root>
|
|
||||||
|
|
||||||
<Form
|
|
||||||
initialValues={{
|
|
||||||
id: action.id,
|
|
||||||
name: action.name,
|
|
||||||
enabled: action.enabled,
|
|
||||||
type: action.type,
|
|
||||||
watch_folder: action.watch_folder,
|
|
||||||
exec_cmd: action.exec_cmd,
|
|
||||||
exec_args: action.exec_args,
|
|
||||||
category: action.category,
|
|
||||||
tags: action.tags,
|
|
||||||
label: action.label,
|
|
||||||
save_path: action.save_path,
|
|
||||||
paused: action.paused,
|
|
||||||
ignore_rules: action.ignore_rules,
|
|
||||||
limit_upload_speed: action.limit_upload_speed || 0,
|
|
||||||
limit_download_speed: action.limit_download_speed || 0,
|
|
||||||
filter_id: action.filter_id,
|
|
||||||
client_id: action.client_id,
|
|
||||||
}}
|
|
||||||
onSubmit={onSubmit}
|
|
||||||
>
|
|
||||||
{({ handleSubmit, values }) => {
|
|
||||||
return (
|
|
||||||
<form onSubmit={handleSubmit} className="w-full">
|
|
||||||
<div className="mt-6 grid grid-cols-12 gap-6">
|
|
||||||
<SelectField
|
|
||||||
name="type"
|
|
||||||
label="Type"
|
|
||||||
optionDefaultText="Select yype"
|
|
||||||
options={ActionTypeOptions}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextField name="name" label="Name" columns={6} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{TypeForm(values)}
|
|
||||||
|
|
||||||
<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 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-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>
|
|
||||||
|
|
||||||
<DEBUG values={values} />
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
export { default as AlertWarning } from "./warning";
|
|
|
@ -1,12 +1,12 @@
|
||||||
|
|
||||||
import { ExclamationIcon } from "@heroicons/react/solid";
|
import { ExclamationIcon } from "@heroicons/react/solid";
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
interface props {
|
interface props {
|
||||||
title: string;
|
title: string;
|
||||||
text: string;
|
text: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function AlertWarning({ title, text }: props) {
|
export function AlertWarning({ title, text }: props) {
|
||||||
return (
|
return (
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<div className="rounded-md bg-yellow-50 p-4">
|
<div className="rounded-md bg-yellow-50 p-4">
|
||||||
|
@ -28,5 +28,3 @@ function AlertWarning({ title, text }: props) {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AlertWarning;
|
|
|
@ -1,4 +1,10 @@
|
||||||
const DEBUG = ({ values }: any) => {
|
import { FC } from "react"
|
||||||
|
|
||||||
|
interface DebugProps {
|
||||||
|
values: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEBUG: FC<DebugProps> = ({ values }) => {
|
||||||
if (process.env.NODE_ENV !== "development") {
|
if (process.env.NODE_ENV !== "development") {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,27 +0,0 @@
|
||||||
import { PlusIcon } from "@heroicons/react/solid";
|
|
||||||
|
|
||||||
interface props {
|
|
||||||
title: string;
|
|
||||||
subtitle: string;
|
|
||||||
buttonText: string;
|
|
||||||
buttonAction: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
const EmptySimple = ({ title, subtitle, buttonText, buttonAction }: props) => (
|
|
||||||
<div className="text-center py-8">
|
|
||||||
<h3 className="mt-2 text-sm font-medium text-gray-900 dark:text-white">{title}</h3>
|
|
||||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-200">{subtitle}</p>
|
|
||||||
<div className="mt-6">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={buttonAction}
|
|
||||||
className="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
|
||||||
>
|
|
||||||
<PlusIcon className="-ml-1 mr-2 h-5 w-5" aria-hidden="true" />
|
|
||||||
{buttonText}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
export default EmptySimple;
|
|
48
web/src/components/emptystates/index.tsx
Normal file
48
web/src/components/emptystates/index.tsx
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import { PlusIcon } from "@heroicons/react/solid";
|
||||||
|
|
||||||
|
interface EmptySimpleProps {
|
||||||
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
buttonText: string;
|
||||||
|
buttonAction: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EmptySimple = ({ title, subtitle, buttonText, buttonAction }: EmptySimpleProps) => (
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<h3 className="mt-2 text-sm font-medium text-gray-900 dark:text-white">{title}</h3>
|
||||||
|
<p className="mt-1 text-sm text-gray-500 dark:text-gray-200">{subtitle}</p>
|
||||||
|
<div className="mt-6">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={buttonAction}
|
||||||
|
className="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
||||||
|
>
|
||||||
|
<PlusIcon className="-ml-1 mr-2 h-5 w-5" aria-hidden="true" />
|
||||||
|
{buttonText}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
interface EmptyListStateProps {
|
||||||
|
text: string;
|
||||||
|
buttonText?: string;
|
||||||
|
buttonOnClick?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EmptyListState({ text, buttonText, buttonOnClick }: EmptyListStateProps) {
|
||||||
|
return (
|
||||||
|
<div className="px-4 py-12 flex flex-col items-center">
|
||||||
|
<p className="text-center text-gray-500 dark:text-white">{text}</p>
|
||||||
|
{buttonText && buttonOnClick && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="relative inline-flex items-center px-4 py-2 mt-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
||||||
|
onClick={buttonOnClick}
|
||||||
|
>
|
||||||
|
{buttonText}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,15 +1,13 @@
|
||||||
import React from "react";
|
import { FC } from "react";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title: string;
|
title: string;
|
||||||
subtitle: string;
|
subtitle: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TitleSubtitle: React.FC<Props> = ({ title, subtitle }) => (
|
export const TitleSubtitle: FC<Props> = ({ title, subtitle }) => (
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg leading-6 font-medium text-gray-900 dark:text-gray-100">{title}</h2>
|
<h2 className="text-lg leading-6 font-medium text-gray-900 dark:text-gray-100">{title}</h2>
|
||||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">{subtitle}</p>
|
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">{subtitle}</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
export default TitleSubtitle;
|
|
|
@ -1,20 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import { Field } from "react-final-form";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
name: string;
|
|
||||||
classNames?: string;
|
|
||||||
subscribe?: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Error: React.FC<Props> = ({ name, classNames }) => (
|
|
||||||
<Field
|
|
||||||
name={name}
|
|
||||||
subscribe={{ touched: true, error: true }}
|
|
||||||
render={({ meta: { touched, error } }) =>
|
|
||||||
touched && error ? <span className={classNames}>{error}</span> : null
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default Error;
|
|
|
@ -1,50 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import {Field} from "react-final-form";
|
|
||||||
import { MultiSelect } from "react-multi-select-component";
|
|
||||||
import {classNames, COL_WIDTHS} from "../../styles/utils";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
label?: string;
|
|
||||||
options?: [] | any;
|
|
||||||
name: string;
|
|
||||||
className?: string;
|
|
||||||
columns?: COL_WIDTHS;
|
|
||||||
}
|
|
||||||
|
|
||||||
const MultiSelectField: 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"
|
|
||||||
htmlFor={label}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</label>
|
|
||||||
<Field
|
|
||||||
name={name}
|
|
||||||
parse={val => val && val.map((item: any) => item.value)}
|
|
||||||
format={val =>
|
|
||||||
val &&
|
|
||||||
val.map((item: any) => options.find((o: any) => o.value === item))
|
|
||||||
}
|
|
||||||
render={({input, meta}) => (
|
|
||||||
<MultiSelect
|
|
||||||
{...input}
|
|
||||||
options={options}
|
|
||||||
labelledBy={name}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default MultiSelectField;
|
|
|
@ -1,47 +0,0 @@
|
||||||
import { Field } from "react-final-form";
|
|
||||||
import React from "react";
|
|
||||||
import Error from "./Error";
|
|
||||||
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 PasswordField: 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-white uppercase tracking-wide">
|
|
||||||
{label}
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
<Field
|
|
||||||
name={name}
|
|
||||||
render={({ input }) => (
|
|
||||||
<input
|
|
||||||
{...input}
|
|
||||||
id={name}
|
|
||||||
type="password"
|
|
||||||
autoComplete={autoComplete}
|
|
||||||
className="mt-2 block w-full border border-gray-300 dark:border-gray-700 dark:bg-gray-800 dark:text-white rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
|
||||||
placeholder={placeholder}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<Error name={name} classNames="text-red mt-2" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
export default PasswordField;
|
|
|
@ -1,60 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import {Field} from "react-final-form";
|
|
||||||
|
|
||||||
export interface radioFieldsetOption {
|
|
||||||
label: string;
|
|
||||||
description: string;
|
|
||||||
value: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface props {
|
|
||||||
name: string;
|
|
||||||
legend: string;
|
|
||||||
options: radioFieldsetOption[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const RadioFieldset: React.FC<props> = ({ name, legend,options }) => (
|
|
||||||
<fieldset>
|
|
||||||
<div className="space-y-2 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:px-6 sm:py-5">
|
|
||||||
<div>
|
|
||||||
<legend className="text-sm font-medium text-gray-900">{legend}</legend>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-5 sm:col-span-2">
|
|
||||||
<div className="space-y-5 sm:mt-0">
|
|
||||||
|
|
||||||
{options.map((opt, idx) => (
|
|
||||||
<div className="relative flex items-start" key={idx}>
|
|
||||||
<div className="absolute flex items-center h-5">
|
|
||||||
<Field
|
|
||||||
name={name}
|
|
||||||
type="radio"
|
|
||||||
render={({input}) => (
|
|
||||||
<input
|
|
||||||
{...input}
|
|
||||||
id={name}
|
|
||||||
value={opt.value}
|
|
||||||
// type="radio"
|
|
||||||
checked={input.checked}
|
|
||||||
className="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="pl-7 text-sm">
|
|
||||||
<label htmlFor={opt.value} className="font-medium text-gray-900">
|
|
||||||
{opt.label}
|
|
||||||
</label>
|
|
||||||
<p id={opt.value+"_description"} className="text-gray-500">
|
|
||||||
{opt.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
)
|
|
||||||
|
|
||||||
export default RadioFieldset;
|
|
|
@ -1,57 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import { Switch } from "@headlessui/react";
|
|
||||||
import { Field } from "react-final-form";
|
|
||||||
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 dark:divide-gray-700">
|
|
||||||
<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>
|
|
||||||
{label}
|
|
||||||
</Switch.Label>
|
|
||||||
{description && (
|
|
||||||
<Switch.Description className="text-sm text-gray-500 dark:text-gray-700">
|
|
||||||
{description}
|
|
||||||
</Switch.Description>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-500',
|
|
||||||
'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,40 +0,0 @@
|
||||||
import {Field} from "react-final-form";
|
|
||||||
import React from "react";
|
|
||||||
import Error from "./Error";
|
|
||||||
import {classNames} from "../../styles/utils";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
name: string;
|
|
||||||
label?: string;
|
|
||||||
placeholder?: string;
|
|
||||||
className?: string;
|
|
||||||
required?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const TextAreaWide: React.FC<Props> = ({name, label, placeholder, required, className}) => (
|
|
||||||
<div
|
|
||||||
className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
|
|
||||||
<div>
|
|
||||||
|
|
||||||
<label htmlFor={name} className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2">
|
|
||||||
{label} {required && <span className="text-gray-500">*</span>}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className="sm:col-span-2">
|
|
||||||
<Field
|
|
||||||
name={name}
|
|
||||||
render={({input, meta}) => (
|
|
||||||
<textarea
|
|
||||||
{...input}
|
|
||||||
id={name}
|
|
||||||
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}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Error name={name} classNames="block text-red-500 mt-2"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
export default TextAreaWide;
|
|
|
@ -1,48 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import { Field } from "react-final-form";
|
|
||||||
import Error from "./Error";
|
|
||||||
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-white uppercase tracking-wide">
|
|
||||||
{label}
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
<Field
|
|
||||||
name={name}
|
|
||||||
render={({ input }) => (
|
|
||||||
<input
|
|
||||||
{...input}
|
|
||||||
id={name}
|
|
||||||
type="text"
|
|
||||||
value={input.value}
|
|
||||||
autoComplete={autoComplete}
|
|
||||||
className="mt-2 block w-full border border-gray-300 dark:border-gray-700 dark:bg-gray-800 dark:text-white rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
|
||||||
placeholder={placeholder}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<Error name={name} classNames="text-red mt-2" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
export default TextField;
|
|
|
@ -1,49 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import { Field } from "react-final-form";
|
|
||||||
import Error from "./Error";
|
|
||||||
import { classNames } from "../../styles/utils";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
name: string;
|
|
||||||
label?: string;
|
|
||||||
help?: string;
|
|
||||||
placeholder?: string;
|
|
||||||
defaultValue?: string;
|
|
||||||
className?: string;
|
|
||||||
required?: boolean;
|
|
||||||
hidden?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const TextFieldWide: React.FC<Props> = ({ name, label, help, placeholder, defaultValue, required, hidden, className }) => (
|
|
||||||
<div hidden={hidden}
|
|
||||||
className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
|
|
||||||
<div>
|
|
||||||
|
|
||||||
<label htmlFor={name} className="block text-sm font-medium text-gray-900 dark:text-white sm:mt-px sm:pt-2">
|
|
||||||
{label} {required && <span className="text-gray-500">*</span>}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className="sm:col-span-2">
|
|
||||||
<Field
|
|
||||||
name={name}
|
|
||||||
defaultValue={defaultValue}
|
|
||||||
render={({ input, meta }) => (
|
|
||||||
<input
|
|
||||||
{...input}
|
|
||||||
id={name}
|
|
||||||
type="text"
|
|
||||||
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", "block w-full shadow-sm dark:bg-gray-800 sm:text-sm dark:text-white rounded-md")}
|
|
||||||
placeholder={placeholder}
|
|
||||||
hidden={hidden}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{help && (
|
|
||||||
<p className="mt-2 text-sm text-gray-500" id="email-description">{help}</p>
|
|
||||||
)}
|
|
||||||
<Error name={name} classNames="block text-red-500 mt-2" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
export default TextFieldWide;
|
|
17
web/src/components/inputs/common.tsx
Normal file
17
web/src/components/inputs/common.tsx
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import React from "react";
|
||||||
|
import { Field } from "formik";
|
||||||
|
|
||||||
|
interface ErrorFieldProps {
|
||||||
|
name: string;
|
||||||
|
classNames?: string;
|
||||||
|
subscribe?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ErrorField: React.FC<ErrorFieldProps> = ({ name, classNames }) => (
|
||||||
|
<Field name={name} subscribe={{ touched: true, error: true }}>
|
||||||
|
{({ meta: { touched, error } }: any) =>
|
||||||
|
touched && error ? <span className={classNames}>{error}</span> : null
|
||||||
|
}
|
||||||
|
</Field>
|
||||||
|
);
|
||||||
|
export { ErrorField }
|
|
@ -1,47 +0,0 @@
|
||||||
import { Field } from "react-final-form";
|
|
||||||
import React from "react";
|
|
||||||
import Error from "../Error";
|
|
||||||
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} parse={(v) => v & parseInt(v, 10)}>
|
|
||||||
{({ input, meta }) => (
|
|
||||||
<div className="sm:col-span-2">
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
{...input}
|
|
||||||
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}
|
|
||||||
/>
|
|
||||||
<Error name={name} classNames="block text-red-500 mt-2" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default NumberField;
|
|
|
@ -1,111 +0,0 @@
|
||||||
import { Field } from "react-final-form";
|
|
||||||
import { Listbox, Transition } from "@headlessui/react";
|
|
||||||
import { CheckIcon, SelectorIcon } from "@heroicons/react/solid";
|
|
||||||
import { Fragment } from "react";
|
|
||||||
import { classNames } from "../../../styles/utils";
|
|
||||||
|
|
||||||
interface SelectOption {
|
|
||||||
label: string;
|
|
||||||
value: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface props {
|
|
||||||
name: string;
|
|
||||||
label: string;
|
|
||||||
optionDefaultText: string;
|
|
||||||
options: SelectOption[];
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectField({ name, label, optionDefaultText, options }: props) {
|
|
||||||
return (
|
|
||||||
<div className="col-span-6">
|
|
||||||
<Field
|
|
||||||
name={name}
|
|
||||||
type="select"
|
|
||||||
render={({ input }) => (
|
|
||||||
<Listbox value={input.value} onChange={input.onChange}>
|
|
||||||
{({ 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">
|
|
||||||
{input.value
|
|
||||||
? options.find((c) => c.value === input.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>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SelectField;
|
|
|
@ -1,2 +0,0 @@
|
||||||
export { default as NumberField } from "./NumberField";
|
|
||||||
export { default as SelectField } from "./SelectField";
|
|
|
@ -1,7 +1,7 @@
|
||||||
export { default as TextField } from "./TextField";
|
export { ErrorField } from "./common";
|
||||||
export { default as TextFieldWide } from "./TextFieldWide";
|
export { TextField, NumberField, PasswordField } from "./input";
|
||||||
export { default as PasswordField } from "./PasswordField";
|
export { NumberFieldWide, PasswordFieldWide, SwitchGroupWide, TextFieldWide } from "./input_wide";
|
||||||
export { default as TextAreaWide } from "./TextAreaWide";
|
export { RadioFieldsetWide } from "./radio";
|
||||||
export { default as MultiSelectField } from "./MultiSelectField";
|
export { DownloadClientSelect, MultiSelect, Select} from "./select";
|
||||||
export { default as RadioFieldset } from "./RadioFieldset";
|
export { SwitchGroup } from "./switch";
|
||||||
export { default as SwitchGroup } from "./SwitchGroup";
|
|
||||||
|
|
159
web/src/components/inputs/input.tsx
Normal file
159
web/src/components/inputs/input.tsx
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
import React from "react";
|
||||||
|
import { Field } from "formik";
|
||||||
|
import { classNames } from "../../utils";
|
||||||
|
import { EyeIcon, EyeOffIcon } from "@heroicons/react/solid";
|
||||||
|
import { useToggle } from "../../hooks/hooks";
|
||||||
|
|
||||||
|
type COL_WIDTHS = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
|
||||||
|
|
||||||
|
interface TextFieldProps {
|
||||||
|
name: string;
|
||||||
|
label?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
columns?: COL_WIDTHS;
|
||||||
|
className?: string;
|
||||||
|
autoComplete?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TextField: React.FC<TextFieldProps> = ({ 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>
|
||||||
|
)
|
||||||
|
|
||||||
|
interface PasswordFieldProps {
|
||||||
|
name: string;
|
||||||
|
label?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
columns?: COL_WIDTHS;
|
||||||
|
className?: string;
|
||||||
|
autoComplete?: string;
|
||||||
|
defaultValue?: string;
|
||||||
|
help?: string;
|
||||||
|
required?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PasswordField: React.FC<PasswordFieldProps> = ({ name, label, placeholder, defaultValue, columns, className, autoComplete, help, required }) => {
|
||||||
|
const [isVisible, toggleVisibility] = useToggle(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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} {required && <span className="text-gray-500">*</span>}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
<Field name={name} defaultValue={defaultValue}>
|
||||||
|
{({
|
||||||
|
field,
|
||||||
|
meta,
|
||||||
|
}: any) => (
|
||||||
|
<div className="sm:col-span-2 relative">
|
||||||
|
<input
|
||||||
|
{...field}
|
||||||
|
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")}
|
||||||
|
placeholder={placeholder}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="absolute inset-y-0 right-0 px-3 flex items-center" onClick={toggleVisibility}>
|
||||||
|
{!isVisible ? <EyeIcon className="h-5 w-5 text-gray-400 hover:text-gray-500" aria-hidden="true" /> : <EyeOffIcon className="h-5 w-5 text-gray-400 hover:text-gray-500" aria-hidden="true" />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{help && (
|
||||||
|
<p className="mt-2 text-sm text-gray-500" id="email-description">{help}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{meta.touched && meta.error && (
|
||||||
|
<div className="error">{meta.error}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NumberFieldProps {
|
||||||
|
name: string;
|
||||||
|
label?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
className?: string;
|
||||||
|
required?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NumberField: React.FC<NumberFieldProps> = ({
|
||||||
|
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 { TextField, PasswordField, NumberField };
|
217
web/src/components/inputs/input_wide.tsx
Normal file
217
web/src/components/inputs/input_wide.tsx
Normal file
|
@ -0,0 +1,217 @@
|
||||||
|
import React from "react";
|
||||||
|
import { Field, FieldProps } from "formik";
|
||||||
|
import { classNames } from "../../utils";
|
||||||
|
import { useToggle } from "../../hooks/hooks";
|
||||||
|
import { EyeIcon, EyeOffIcon } from "@heroicons/react/solid";
|
||||||
|
import { Switch } from "@headlessui/react";
|
||||||
|
import { ErrorField } from "./common"
|
||||||
|
|
||||||
|
interface TextFieldWideProps {
|
||||||
|
name: string;
|
||||||
|
label?: string;
|
||||||
|
help?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
defaultValue?: string;
|
||||||
|
className?: string;
|
||||||
|
required?: boolean;
|
||||||
|
hidden?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TextFieldWide: React.FC<TextFieldWideProps> = ({ name, label, help, placeholder, defaultValue, required, hidden, className }) => (
|
||||||
|
<div hidden={hidden} className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
|
||||||
|
<div>
|
||||||
|
<label htmlFor={name} className="block text-sm font-medium text-gray-900 dark:text-white sm:mt-px sm:pt-2">
|
||||||
|
{label} {required && <span className="text-gray-500">*</span>}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="sm:col-span-2">
|
||||||
|
<Field name={name} value={defaultValue}>
|
||||||
|
{({ field, meta }: FieldProps) => (
|
||||||
|
<input
|
||||||
|
{...field}
|
||||||
|
id={name}
|
||||||
|
type="text"
|
||||||
|
value={field.value ? field.value : defaultValue ?? ""}
|
||||||
|
onChange={field.onChange}
|
||||||
|
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", "block w-full shadow-sm dark:bg-gray-800 sm:text-sm dark:text-white rounded-md")}
|
||||||
|
placeholder={placeholder}
|
||||||
|
hidden={hidden}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
{help && (
|
||||||
|
<p className="mt-2 text-sm text-gray-500" id={`${name}-description`}>{help}</p>
|
||||||
|
)}
|
||||||
|
<ErrorField name={name} classNames="block text-red-500 mt-2" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
interface PasswordFieldWideProps {
|
||||||
|
name: string;
|
||||||
|
label?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
defaultValue?: string;
|
||||||
|
help?: string;
|
||||||
|
required?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function PasswordFieldWide({ name, label, placeholder, defaultValue, help, required }: PasswordFieldWideProps) {
|
||||||
|
const [isVisible, toggleVisibility] = useToggle(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
|
||||||
|
<div>
|
||||||
|
<label htmlFor={name} className="block text-sm font-medium text-gray-900 dark:text-white sm:mt-px sm:pt-2">
|
||||||
|
{label} {required && <span className="text-gray-500">*</span>}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="sm:col-span-2">
|
||||||
|
<Field
|
||||||
|
name={name}
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
>
|
||||||
|
{({ field, meta }: FieldProps) => (
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
{...field}
|
||||||
|
id={name}
|
||||||
|
value={field.value ? field.value : defaultValue ?? ""}
|
||||||
|
onChange={field.onChange}
|
||||||
|
type={isVisible ? "text" : "password"}
|
||||||
|
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", "block w-full dark:bg-gray-800 shadow-sm dark:text-gray-100 sm:text-sm rounded-md")}
|
||||||
|
placeholder={placeholder}
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-y-0 right-0 px-3 flex items-center" onClick={toggleVisibility}>
|
||||||
|
{!isVisible ? <EyeIcon className="h-5 w-5 text-gray-400 hover:text-gray-500" aria-hidden="true" /> : <EyeOffIcon className="h-5 w-5 text-gray-400 hover:text-gray-500" aria-hidden="true" />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
{help && (
|
||||||
|
<p className="mt-2 text-sm text-gray-500" id={`${name}-description`}>{help}</p>
|
||||||
|
)}
|
||||||
|
<ErrorField name={name} classNames="block text-red-500 mt-2" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NumberFieldWideProps {
|
||||||
|
name: string;
|
||||||
|
label?: string;
|
||||||
|
help?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
defaultValue?: number;
|
||||||
|
className?: string;
|
||||||
|
required?: boolean;
|
||||||
|
hidden?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NumberFieldWide: React.FC<NumberFieldWideProps> = ({
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
placeholder,
|
||||||
|
help,
|
||||||
|
defaultValue,
|
||||||
|
required,
|
||||||
|
hidden,
|
||||||
|
className,
|
||||||
|
}) => (
|
||||||
|
<div className="px-4 space-y-1 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor={name}
|
||||||
|
className="block text-sm font-medium text-gray-900 dark:text-white sm:mt-px sm:pt-2"
|
||||||
|
>
|
||||||
|
{label} {required && <span className="text-gray-500">*</span>}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="sm:col-span-2">
|
||||||
|
<Field
|
||||||
|
name={name}
|
||||||
|
defaultValue={defaultValue ?? 0}
|
||||||
|
>
|
||||||
|
{({ field, meta, form }: FieldProps) => (
|
||||||
|
<input
|
||||||
|
{...field}
|
||||||
|
id={name}
|
||||||
|
type="number"
|
||||||
|
value={field.value ? field.value : defaultValue ?? 0}
|
||||||
|
onChange={(e) => { form.setFieldValue(field.name, parseInt(e.target.value)) }}
|
||||||
|
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",
|
||||||
|
"block w-full shadow-sm dark:bg-gray-800 sm:text-sm dark:text-white rounded-md"
|
||||||
|
)}
|
||||||
|
placeholder={placeholder}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
{help && (
|
||||||
|
<p className="mt-2 text-sm text-gray-500 dark:text-gray-200" id={`${name}-description`}>{help}</p>
|
||||||
|
)}
|
||||||
|
<ErrorField name={name} classNames="block text-red-500 mt-2" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
interface SwitchGroupWideProps {
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
description?: string;
|
||||||
|
defaultValue?: boolean;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SwitchGroupWide: React.FC<SwitchGroupWideProps> = ({ name, label, description, defaultValue }) => (
|
||||||
|
<ul className="mt-2 divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
<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>
|
||||||
|
{label}
|
||||||
|
</Switch.Label>
|
||||||
|
{description && (
|
||||||
|
<Switch.Description className="text-sm text-gray-500 dark:text-gray-700">
|
||||||
|
{description}
|
||||||
|
</Switch.Description>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Field
|
||||||
|
name={name}
|
||||||
|
defaultValue={defaultValue as boolean}
|
||||||
|
type="checkbox"
|
||||||
|
>
|
||||||
|
{({ field, form }: FieldProps) => (
|
||||||
|
<Switch
|
||||||
|
{...field}
|
||||||
|
type="button"
|
||||||
|
value={field.value}
|
||||||
|
checked={field.checked ?? false}
|
||||||
|
onChange={value => {
|
||||||
|
form.setFieldValue(field?.name ?? '', value)
|
||||||
|
}}
|
||||||
|
className={classNames(
|
||||||
|
field.value ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-500',
|
||||||
|
'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(
|
||||||
|
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>
|
||||||
|
</Switch.Group>
|
||||||
|
</ul>
|
||||||
|
)
|
||||||
|
|
||||||
|
export { NumberFieldWide, TextFieldWide, PasswordFieldWide, SwitchGroupWide };
|
115
web/src/components/inputs/radio.tsx
Normal file
115
web/src/components/inputs/radio.tsx
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
import { Fragment } from "react";
|
||||||
|
import { Field, useFormikContext } from "formik";
|
||||||
|
import { RadioGroup } from "@headlessui/react";
|
||||||
|
import { classNames } from "../../utils";
|
||||||
|
|
||||||
|
export interface radioFieldsetOption {
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface props {
|
||||||
|
name: string;
|
||||||
|
legend: string;
|
||||||
|
options: radioFieldsetOption[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function RadioFieldsetWide({ name, legend, options }: props) {
|
||||||
|
const {
|
||||||
|
values,
|
||||||
|
setFieldValue,
|
||||||
|
} = useFormikContext<any>();
|
||||||
|
|
||||||
|
const onChange = (value: string) => {
|
||||||
|
setFieldValue(name, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<fieldset>
|
||||||
|
<div className="space-y-2 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:px-6 sm:py-5">
|
||||||
|
<div>
|
||||||
|
<legend className="text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
{legend}
|
||||||
|
</legend>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-5 sm:col-span-2">
|
||||||
|
<div className="space-y-5 sm:mt-0">
|
||||||
|
<Field name={name} type="radio">
|
||||||
|
{() => (
|
||||||
|
<RadioGroup value={values[name]} onChange={onChange}>
|
||||||
|
<RadioGroup.Label className="sr-only">
|
||||||
|
{legend}
|
||||||
|
</RadioGroup.Label>
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-md -space-y-px">
|
||||||
|
{options.map((setting, settingIdx) => (
|
||||||
|
<RadioGroup.Option
|
||||||
|
key={setting.value}
|
||||||
|
value={setting.value}
|
||||||
|
className={({ checked }) =>
|
||||||
|
classNames(
|
||||||
|
settingIdx === 0
|
||||||
|
? "rounded-tl-md rounded-tr-md"
|
||||||
|
: "",
|
||||||
|
settingIdx === options.length - 1
|
||||||
|
? "rounded-bl-md rounded-br-md"
|
||||||
|
: "",
|
||||||
|
checked
|
||||||
|
? "bg-indigo-50 dark:bg-gray-700 border-indigo-200 dark:border-blue-600 z-10"
|
||||||
|
: "border-gray-200 dark:border-gray-700",
|
||||||
|
"relative border p-4 flex cursor-pointer focus:outline-none"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{({ active, checked }) => (
|
||||||
|
<Fragment>
|
||||||
|
<span
|
||||||
|
className={classNames(
|
||||||
|
checked
|
||||||
|
? "bg-indigo-600 dark:bg-blue-600 border-transparent"
|
||||||
|
: "bg-white border-gray-300 dark:border-gray-300",
|
||||||
|
active
|
||||||
|
? "ring-2 ring-offset-2 ring-indigo-500 dark:ring-blue-500"
|
||||||
|
: "",
|
||||||
|
"h-4 w-4 mt-0.5 cursor-pointer rounded-full border flex items-center justify-center"
|
||||||
|
)}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<span className="rounded-full bg-white w-1.5 h-1.5" />
|
||||||
|
</span>
|
||||||
|
<div className="ml-3 flex flex-col">
|
||||||
|
<RadioGroup.Label
|
||||||
|
as="span"
|
||||||
|
className={classNames(
|
||||||
|
checked ? "text-indigo-900 dark:text-blue-500" : "text-gray-900 dark:text-gray-300",
|
||||||
|
"block text-sm font-medium"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{setting.label}
|
||||||
|
</RadioGroup.Label>
|
||||||
|
<RadioGroup.Description
|
||||||
|
as="span"
|
||||||
|
className={classNames(
|
||||||
|
checked ? "text-indigo-700 dark:text-blue-500" : "text-gray-500",
|
||||||
|
"block text-sm"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{setting.description}
|
||||||
|
</RadioGroup.Description>
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
|
</RadioGroup.Option>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { RadioFieldsetWide };
|
271
web/src/components/inputs/select.tsx
Normal file
271
web/src/components/inputs/select.tsx
Normal file
|
@ -0,0 +1,271 @@
|
||||||
|
import { Fragment } from "react";
|
||||||
|
import { MultiSelect as RMSC} from "react-multi-select-component";
|
||||||
|
import { Transition, Listbox } from "@headlessui/react";
|
||||||
|
import { CheckIcon, SelectorIcon } from '@heroicons/react/solid';
|
||||||
|
import { Action, DownloadClient } from "../../domain/interfaces";
|
||||||
|
import { classNames, COL_WIDTHS } from "../../utils";
|
||||||
|
import { Field } from "formik";
|
||||||
|
|
||||||
|
interface MultiSelectProps {
|
||||||
|
label?: string;
|
||||||
|
options?: [] | any;
|
||||||
|
name: string;
|
||||||
|
className?: string;
|
||||||
|
columns?: COL_WIDTHS;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MultiSelect: React.FC<MultiSelectProps> = ({
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SelectFieldOption {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SelectFieldProps {
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
optionDefaultText: string;
|
||||||
|
options: SelectFieldOption[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function Select({ name, label, optionDefaultText, options }: SelectFieldProps) {
|
||||||
|
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 { MultiSelect, DownloadClientSelect, Select }
|
|
@ -1,9 +1,65 @@
|
||||||
import React from "react";
|
import React, { InputHTMLAttributes } from 'react'
|
||||||
import { Switch } from "@headlessui/react";
|
import { Switch as HeadlessSwitch } from '@headlessui/react'
|
||||||
import { Field } from "formik";
|
import { FieldInputProps, FieldMetaProps, FieldProps, FormikProps, FormikValues, Field } from 'formik'
|
||||||
import { classNames } from "../../../styles/utils";
|
import { classNames } from "../../utils";
|
||||||
|
|
||||||
interface Props {
|
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} />
|
||||||
|
|
||||||
|
interface SwitchGroupProps {
|
||||||
name: string;
|
name: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
@ -11,18 +67,18 @@ interface Props {
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SwitchGroup: React.FC<Props> = ({ name, label, description, defaultValue }) => (
|
const SwitchGroup: React.FC<SwitchGroupProps> = ({ name, label, description, defaultValue }) => (
|
||||||
<ul className="mt-2 divide-y divide-gray-200">
|
<ul className="mt-2 divide-y divide-gray-200">
|
||||||
<Switch.Group as="li" className="py-4 flex items-center justify-between">
|
<HeadlessSwitch.Group as="li" className="py-4 flex items-center justify-between">
|
||||||
{label && <div className="flex flex-col">
|
{label && <div className="flex flex-col">
|
||||||
<Switch.Label as="p" className="text-sm font-medium text-gray-900 dark:text-gray-100"
|
<HeadlessSwitch.Label as="p" className="text-sm font-medium text-gray-900 dark:text-gray-100"
|
||||||
passive>
|
passive>
|
||||||
{label}
|
{label}
|
||||||
</Switch.Label>
|
</HeadlessSwitch.Label>
|
||||||
{description && (
|
{description && (
|
||||||
<Switch.Description className="text-sm text-gray-500 dark:text-gray-400">
|
<HeadlessSwitch.Description className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
{description}
|
{description}
|
||||||
</Switch.Description>
|
</HeadlessSwitch.Description>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
@ -82,8 +138,8 @@ const SwitchGroup: React.FC<Props> = ({ name, label, description, defaultValue }
|
||||||
</Switch>
|
</Switch>
|
||||||
)}
|
)}
|
||||||
/> */}
|
/> */}
|
||||||
</Switch.Group>
|
</HeadlessSwitch.Group>
|
||||||
</ul>
|
</ul>
|
||||||
)
|
)
|
||||||
|
|
||||||
export default SwitchGroup;
|
export { SwitchGroup }
|
|
@ -1,64 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import { Field } from "react-final-form";
|
|
||||||
import Error from "../Error";
|
|
||||||
import { classNames } from "../../../styles/utils";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
name: string;
|
|
||||||
label?: string;
|
|
||||||
help?: string;
|
|
||||||
placeholder?: string;
|
|
||||||
defaultValue?: number;
|
|
||||||
className?: string;
|
|
||||||
required?: boolean;
|
|
||||||
hidden?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const NumberFieldWide: React.FC<Props> = ({
|
|
||||||
name,
|
|
||||||
label,
|
|
||||||
placeholder,
|
|
||||||
help,
|
|
||||||
defaultValue,
|
|
||||||
required,
|
|
||||||
hidden,
|
|
||||||
className,
|
|
||||||
}) => (
|
|
||||||
<div className="px-4 space-y-1 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
htmlFor={name}
|
|
||||||
className="block text-sm font-medium text-gray-900 dark:text-white sm:mt-px sm:pt-2"
|
|
||||||
>
|
|
||||||
{label} {required && <span className="text-gray-500">*</span>}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className="sm:col-span-2">
|
|
||||||
<Field
|
|
||||||
name={name}
|
|
||||||
defaultValue={defaultValue}
|
|
||||||
parse={(v: any) => v & parseInt(v, 10)}
|
|
||||||
render={({ input, meta }) => (
|
|
||||||
<input
|
|
||||||
{...input}
|
|
||||||
id={name}
|
|
||||||
type="number"
|
|
||||||
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",
|
|
||||||
"block w-full shadow-sm dark:bg-gray-800 sm:text-sm dark:text-white rounded-md"
|
|
||||||
)}
|
|
||||||
placeholder={placeholder}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{help && (
|
|
||||||
<p className="mt-2 text-sm text-gray-500 dark:text-gray-200" id={`${name}-description`}>{help}</p>
|
|
||||||
)}
|
|
||||||
<Error name={name} classNames="block text-red-500 mt-2" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default NumberFieldWide;
|
|
|
@ -1,56 +0,0 @@
|
||||||
import { Field } from "react-final-form";
|
|
||||||
import Error from "../Error";
|
|
||||||
import { classNames } from "../../../styles/utils";
|
|
||||||
import { useToggle } from "../../../hooks/hooks";
|
|
||||||
import { EyeIcon, EyeOffIcon } from "@heroicons/react/solid";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
name: string;
|
|
||||||
label?: string;
|
|
||||||
placeholder?: string;
|
|
||||||
defaultValue?: string;
|
|
||||||
help?: string;
|
|
||||||
required?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
function PasswordField({ name, label, placeholder, defaultValue, help, required }: Props) {
|
|
||||||
const [isVisible, toggleVisibility] = useToggle(false)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
|
|
||||||
<div>
|
|
||||||
|
|
||||||
<label htmlFor={name} className="block text-sm font-medium text-gray-900 dark:text-white sm:mt-px sm:pt-2">
|
|
||||||
{label} {required && <span className="text-gray-500">*</span>}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className="sm:col-span-2">
|
|
||||||
<Field
|
|
||||||
name={name}
|
|
||||||
defaultValue={defaultValue}
|
|
||||||
render={({ input, meta }) => (
|
|
||||||
<div className="relative">
|
|
||||||
<input
|
|
||||||
{...input}
|
|
||||||
id={name}
|
|
||||||
type={isVisible ? "text" : "password"}
|
|
||||||
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", "block w-full dark:bg-gray-800 shadow-sm dark:text-gray-100 sm:text-sm rounded-md")}
|
|
||||||
placeholder={placeholder}
|
|
||||||
/>
|
|
||||||
<div className="absolute inset-y-0 right-0 px-3 flex items-center" onClick={toggleVisibility}>
|
|
||||||
{!isVisible ? <EyeIcon className="h-5 w-5 text-gray-400 hover:text-gray-500" aria-hidden="true" /> : <EyeOffIcon className="h-5 w-5 text-gray-400 hover:text-gray-500" aria-hidden="true" />}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{help && (
|
|
||||||
<p className="mt-2 text-sm text-gray-500" id="email-description">{help}</p>
|
|
||||||
)}
|
|
||||||
<Error name={name} classNames="block text-red-500 mt-2" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PasswordField;
|
|
|
@ -1,105 +0,0 @@
|
||||||
import { Field, useFormState } from "react-final-form";
|
|
||||||
import { RadioGroup } from "@headlessui/react";
|
|
||||||
import { classNames } from "../../../styles/utils";
|
|
||||||
import { Fragment } from "react";
|
|
||||||
import { radioFieldsetOption } from "../RadioFieldset";
|
|
||||||
|
|
||||||
interface props {
|
|
||||||
name: string;
|
|
||||||
legend: string;
|
|
||||||
options: radioFieldsetOption[];
|
|
||||||
}
|
|
||||||
|
|
||||||
function RadioFieldsetWide({ name, legend, options }: props) {
|
|
||||||
const { values } = useFormState();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<fieldset>
|
|
||||||
<div className="space-y-2 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:px-6 sm:py-5">
|
|
||||||
<div>
|
|
||||||
<legend className="text-sm font-medium text-gray-900 dark:text-white">
|
|
||||||
{legend}
|
|
||||||
</legend>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-5 sm:col-span-2">
|
|
||||||
<div className="space-y-5 sm:mt-0">
|
|
||||||
<Field
|
|
||||||
name={name}
|
|
||||||
type="radio"
|
|
||||||
render={({ input }) => (
|
|
||||||
<RadioGroup value={values[name]} onChange={input.onChange}>
|
|
||||||
<RadioGroup.Label className="sr-only">
|
|
||||||
Privacy setting
|
|
||||||
</RadioGroup.Label>
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-md -space-y-px">
|
|
||||||
{options.map((setting, settingIdx) => (
|
|
||||||
<RadioGroup.Option
|
|
||||||
key={setting.value}
|
|
||||||
value={setting.value}
|
|
||||||
className={({ checked }) =>
|
|
||||||
classNames(
|
|
||||||
settingIdx === 0
|
|
||||||
? "rounded-tl-md rounded-tr-md"
|
|
||||||
: "",
|
|
||||||
settingIdx === options.length - 1
|
|
||||||
? "rounded-bl-md rounded-br-md"
|
|
||||||
: "",
|
|
||||||
checked
|
|
||||||
? "bg-indigo-50 dark:bg-gray-700 border-indigo-200 dark:border-blue-600 z-10"
|
|
||||||
: "border-gray-200 dark:border-gray-700",
|
|
||||||
"relative border p-4 flex cursor-pointer focus:outline-none"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{({ active, checked }) => (
|
|
||||||
<Fragment>
|
|
||||||
<span
|
|
||||||
className={classNames(
|
|
||||||
checked
|
|
||||||
? "bg-indigo-600 dark:bg-blue-600 border-transparent"
|
|
||||||
: "bg-white border-gray-300 dark:border-gray-300",
|
|
||||||
active
|
|
||||||
? "ring-2 ring-offset-2 ring-indigo-500 dark:ring-blue-500"
|
|
||||||
: "",
|
|
||||||
"h-4 w-4 mt-0.5 cursor-pointer rounded-full border flex items-center justify-center"
|
|
||||||
)}
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<span className="rounded-full bg-white w-1.5 h-1.5" />
|
|
||||||
</span>
|
|
||||||
<div className="ml-3 flex flex-col">
|
|
||||||
<RadioGroup.Label
|
|
||||||
as="span"
|
|
||||||
className={classNames(
|
|
||||||
checked ? "text-indigo-900 dark:text-blue-500" : "text-gray-900 dark:text-gray-300",
|
|
||||||
"block text-sm font-medium"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{setting.label}
|
|
||||||
</RadioGroup.Label>
|
|
||||||
<RadioGroup.Description
|
|
||||||
as="span"
|
|
||||||
className={classNames(
|
|
||||||
checked ? "text-indigo-700 dark:text-blue-500" : "text-gray-500",
|
|
||||||
"block text-sm"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{setting.description}
|
|
||||||
</RadioGroup.Description>
|
|
||||||
</div>
|
|
||||||
</Fragment>
|
|
||||||
)}
|
|
||||||
</RadioGroup.Option>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</RadioGroup>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default RadioFieldsetWide;
|
|
|
@ -1,111 +0,0 @@
|
||||||
import { Field } from "react-final-form";
|
|
||||||
import { Listbox, Transition } from "@headlessui/react";
|
|
||||||
import { CheckIcon, SelectorIcon } from "@heroicons/react/solid";
|
|
||||||
import React, { Fragment } from "react";
|
|
||||||
import { classNames } from "../../../styles/utils";
|
|
||||||
|
|
||||||
interface SelectOption {
|
|
||||||
label: string;
|
|
||||||
value: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface props {
|
|
||||||
name: string;
|
|
||||||
label: string;
|
|
||||||
optionDefaultText: string;
|
|
||||||
options: SelectOption[];
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectField({ name, label, optionDefaultText, options }: props) {
|
|
||||||
return (
|
|
||||||
<div className="col-span-6 sm:col-span-6">
|
|
||||||
<Field
|
|
||||||
name={name}
|
|
||||||
type="select"
|
|
||||||
render={({ input }) => (
|
|
||||||
<Listbox value={input.value} onChange={input.onChange}>
|
|
||||||
{({ open }) => (
|
|
||||||
<div className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
|
|
||||||
<Listbox.Label className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2">
|
|
||||||
{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">
|
|
||||||
{input.value
|
|
||||||
? options.find((c) => c.value === input.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>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Listbox>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SelectField;
|
|
|
@ -1,4 +0,0 @@
|
||||||
export { default as NumberFieldWide } from "./NumberField";
|
|
||||||
export { default as PasswordFieldWide } from "./PasswordField";
|
|
||||||
export { default as RadioFieldsetWide } from "./RadioFieldsetWide";
|
|
||||||
export { default as SelectFieldWide } from "./SelectField";
|
|
|
@ -1 +0,0 @@
|
||||||
export { default as DeleteModal } from "./Delete";
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { Fragment } from "react";
|
import { Fragment, FC } from "react";
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
import { ExclamationIcon } from "@heroicons/react/solid";
|
import { ExclamationIcon } from "@heroicons/react/solid";
|
||||||
|
|
||||||
interface props {
|
interface DeleteModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
buttonRef: any;
|
buttonRef: any;
|
||||||
toggle: any;
|
toggle: any;
|
||||||
|
@ -11,7 +11,7 @@ interface props {
|
||||||
text: string;
|
text: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DeleteModal = ({ isOpen, buttonRef, toggle, deleteAction, title, text }: props) => (
|
export const DeleteModal: FC<DeleteModalProps> = ({ isOpen, buttonRef, toggle, deleteAction, title, text }) => (
|
||||||
<Transition.Root show={isOpen} as={Fragment}>
|
<Transition.Root show={isOpen} as={Fragment}>
|
||||||
<Dialog
|
<Dialog
|
||||||
as="div"
|
as="div"
|
||||||
|
@ -87,5 +87,3 @@ const DeleteModal = ({ isOpen, buttonRef, toggle, deleteAction, title, text }: p
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</Transition.Root>
|
</Transition.Root>
|
||||||
)
|
)
|
||||||
|
|
||||||
export default DeleteModal;
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import { XIcon, CheckCircleIcon, ExclamationIcon, ExclamationCircleIcon } from '@heroicons/react/solid'
|
import { XIcon, CheckCircleIcon, ExclamationIcon, ExclamationCircleIcon } from '@heroicons/react/solid'
|
||||||
import { toast } from 'react-hot-toast'
|
import { toast } from 'react-hot-toast'
|
||||||
import { classNames } from '../../styles/utils'
|
import { classNames } from '../../utils'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
type: 'error' | 'success' | 'warning'
|
type: 'error' | 'success' | 'warning'
|
||||||
|
@ -9,12 +9,7 @@ type Props = {
|
||||||
t?: any;
|
t?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Toast: FC<Props> = ({
|
const Toast: FC<Props> = ({ type, body, t }) => (
|
||||||
type,
|
|
||||||
body,
|
|
||||||
t
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<div className={classNames(
|
<div className={classNames(
|
||||||
t.visible ? 'animate-enter' : 'animate-leave',
|
t.visible ? 'animate-enter' : 'animate-leave',
|
||||||
"max-w-sm w-full bg-white dark:bg-gray-800 shadow-lg rounded-lg pointer-events-auto ring-1 ring-black ring-opacity-5 overflow-hidden transition-all")}>
|
"max-w-sm w-full bg-white dark:bg-gray-800 shadow-lg rounded-lg pointer-events-auto ring-1 ring-black ring-opacity-5 overflow-hidden transition-all")}>
|
||||||
|
@ -47,7 +42,6 @@ const Toast: FC<Props> = ({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
export default Toast;
|
export default Toast;
|
|
@ -1 +0,0 @@
|
||||||
export { default as SlideOver } from "./SlideOver";
|
|
|
@ -1,16 +1,15 @@
|
||||||
import { Fragment, useRef } from "react";
|
import { Fragment, useRef } from "react";
|
||||||
import { XIcon } from "@heroicons/react/solid";
|
import { XIcon } from "@heroicons/react/solid";
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
import { Form } from "react-final-form";
|
import { Form, Formik } from "formik";
|
||||||
import DEBUG from "../../components/debug";
|
import DEBUG from "../debug";
|
||||||
import { useToggle } from "../../hooks/hooks";
|
import { useToggle } from "../../hooks/hooks";
|
||||||
import { DeleteModal } from "../../components/modals";
|
import { DeleteModal } from "../modals";
|
||||||
import { classNames } from "../../styles/utils";
|
import { classNames } from "../../utils";
|
||||||
|
|
||||||
interface props {
|
interface SlideOverProps {
|
||||||
title: string;
|
title: string;
|
||||||
initialValues: any;
|
initialValues: any;
|
||||||
mutators?: any;
|
|
||||||
validate?: any;
|
validate?: any;
|
||||||
onSubmit: any;
|
onSubmit: any;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
@ -20,7 +19,7 @@ interface props {
|
||||||
type: "CREATE" | "UPDATE";
|
type: "CREATE" | "UPDATE";
|
||||||
}
|
}
|
||||||
|
|
||||||
function SlideOver({ title, initialValues, mutators, validate, onSubmit, deleteAction, isOpen, toggle, type, children }: props) {
|
function SlideOver({ title, initialValues, validate, onSubmit, deleteAction, isOpen, toggle, type, children }: SlideOverProps) {
|
||||||
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false)
|
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false)
|
||||||
|
|
||||||
const cancelModalButtonRef = useRef(null)
|
const cancelModalButtonRef = useRef(null)
|
||||||
|
@ -54,15 +53,13 @@ function SlideOver({ title, initialValues, mutators, validate, onSubmit, deleteA
|
||||||
>
|
>
|
||||||
<div className="w-screen max-w-2xl dark:border-gray-700 border-l">
|
<div className="w-screen max-w-2xl dark:border-gray-700 border-l">
|
||||||
|
|
||||||
<Form
|
<Formik
|
||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
mutators={mutators}
|
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
validate={validate}
|
validate={validate}
|
||||||
>
|
>
|
||||||
{({ handleSubmit, values }) => {
|
{({ handleSubmit, values }) => (
|
||||||
return (
|
<Form className="h-full flex flex-col bg-white dark:bg-gray-800 shadow-xl overflow-y-scroll"
|
||||||
<form className="h-full flex flex-col bg-white dark:bg-gray-800 shadow-xl overflow-y-scroll"
|
|
||||||
onSubmit={handleSubmit}>
|
onSubmit={handleSubmit}>
|
||||||
|
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
|
@ -90,7 +87,6 @@ function SlideOver({ title, initialValues, mutators, validate, onSubmit, deleteA
|
||||||
{children !== undefined && children(values)}
|
{children !== undefined && children(values)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div className="flex-shrink-0 px-4 border-t border-gray-200 dark:border-gray-700 py-5 sm:px-6">
|
<div className="flex-shrink-0 px-4 border-t border-gray-200 dark:border-gray-700 py-5 sm:px-6">
|
||||||
<div className={classNames(type === "CREATE" ? "justify-end" : "justify-between", "space-x-3 flex")}>
|
<div className={classNames(type === "CREATE" ? "justify-end" : "justify-between", "space-x-3 flex")}>
|
||||||
{type === "UPDATE" && (
|
{type === "UPDATE" && (
|
||||||
|
@ -122,10 +118,9 @@ function SlideOver({ title, initialValues, mutators, validate, onSubmit, deleteA
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DEBUG values={values} />
|
<DEBUG values={values} />
|
||||||
</form>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</Form>
|
</Form>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -137,4 +132,4 @@ function SlideOver({ title, initialValues, mutators, validate, onSubmit, deleteA
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SlideOver;
|
export { SlideOver };
|
|
@ -152,6 +152,7 @@ export interface Network {
|
||||||
|
|
||||||
export interface Channel {
|
export interface Channel {
|
||||||
name: string;
|
name: string;
|
||||||
|
password: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SASL {
|
export interface SASL {
|
||||||
|
|
|
@ -1,415 +0,0 @@
|
||||||
import { Fragment, useEffect } from "react";
|
|
||||||
import { useMutation } from "react-query";
|
|
||||||
import { Action, DownloadClient, Filter } from "../../domain/interfaces";
|
|
||||||
import { queryClient } from "../../App";
|
|
||||||
import { sleep } from "../../utils/utils";
|
|
||||||
import { CheckIcon, SelectorIcon, XIcon } from "@heroicons/react/solid";
|
|
||||||
import { Dialog, Listbox, Transition } from "@headlessui/react";
|
|
||||||
import { classNames } from "../../styles/utils";
|
|
||||||
import { Field, Form } from "react-final-form";
|
|
||||||
import DEBUG from "../../components/debug";
|
|
||||||
import APIClient from "../../api/APIClient";
|
|
||||||
import { ActionTypeOptions } from "../../domain/constants";
|
|
||||||
import { SwitchGroup, TextFieldWide } from "../../components/inputs";
|
|
||||||
import { AlertWarning } from "../../components/alerts";
|
|
||||||
import {
|
|
||||||
NumberFieldWide,
|
|
||||||
RadioFieldsetWide,
|
|
||||||
} from "../../components/inputs/wide";
|
|
||||||
|
|
||||||
import { toast } from 'react-hot-toast'
|
|
||||||
import Toast from '../../components/notifications/Toast';
|
|
||||||
|
|
||||||
interface DownloadClientSelectProps {
|
|
||||||
name: string;
|
|
||||||
clients: DownloadClient[];
|
|
||||||
values: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DownloadClientSelect({
|
|
||||||
name,
|
|
||||||
clients,
|
|
||||||
values,
|
|
||||||
}: DownloadClientSelectProps) {
|
|
||||||
return (
|
|
||||||
<div className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
|
|
||||||
<Field
|
|
||||||
name={name}
|
|
||||||
type="select"
|
|
||||||
render={({ input }) => (
|
|
||||||
<Listbox value={input.value} onChange={input.onChange}>
|
|
||||||
{({ open }) => (
|
|
||||||
<>
|
|
||||||
<Listbox.Label className="block text-sm font-medium text-gray-700">
|
|
||||||
Client
|
|
||||||
</Listbox.Label>
|
|
||||||
<div className="mt-1 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">
|
|
||||||
{input.value
|
|
||||||
? clients.find((c) => c.id === input.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 === values.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>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface props {
|
|
||||||
filter: Filter;
|
|
||||||
isOpen: boolean;
|
|
||||||
toggle: any;
|
|
||||||
clients: DownloadClient[];
|
|
||||||
}
|
|
||||||
|
|
||||||
function FilterActionAddForm({ filter, isOpen, toggle, clients }: props) {
|
|
||||||
const mutation = useMutation(
|
|
||||||
(action: Action) => APIClient.actions.create(action),
|
|
||||||
{
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries(["filter", filter.id]);
|
|
||||||
toast.custom((t) => <Toast type="success" body="Action was added" t={t} />)
|
|
||||||
|
|
||||||
sleep(500).then(() => toggle());
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// console.log("render add action form", clients)
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const onSubmit = (data: any) => {
|
|
||||||
// TODO clear data depending on type
|
|
||||||
mutation.mutate(data);
|
|
||||||
};
|
|
||||||
|
|
||||||
const TypeForm = (values: any) => {
|
|
||||||
switch (values.type) {
|
|
||||||
case "TEST":
|
|
||||||
return (
|
|
||||||
<AlertWarning
|
|
||||||
title="Notice"
|
|
||||||
text="The test action does nothing except to show if the filter works."
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case "WATCH_FOLDER":
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<TextFieldWide
|
|
||||||
name="watch_folder"
|
|
||||||
label="Watch dir"
|
|
||||||
placeholder="Watch directory eg. /home/user/watch_folder"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
case "EXEC":
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<TextFieldWide
|
|
||||||
name="exec_cmd"
|
|
||||||
label="Program"
|
|
||||||
placeholder="Path to program eg. /bin/test"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextFieldWide
|
|
||||||
name="exec_args"
|
|
||||||
label="Arguments"
|
|
||||||
placeholder="Arguments eg. --test"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
case "QBITTORRENT":
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<DownloadClientSelect
|
|
||||||
name="client_id"
|
|
||||||
clients={clients}
|
|
||||||
values={values}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextFieldWide name="category" label="Category" placeholder="" />
|
|
||||||
<TextFieldWide
|
|
||||||
name="tags"
|
|
||||||
label="Tags"
|
|
||||||
placeholder="Comma separated eg. 4k,remux"
|
|
||||||
/>
|
|
||||||
<TextFieldWide name="save_path" label="Save path" />
|
|
||||||
|
|
||||||
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
|
||||||
<SwitchGroup name="paused" label="Add paused" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="divide-y divide-gray-200 pt-8 space-y-6 sm:pt-10 sm:space-y-5">
|
|
||||||
<div className="px-4">
|
|
||||||
<h3 className="text-lg leading-6 font-medium text-gray-900">
|
|
||||||
Limit speeds
|
|
||||||
</h3>
|
|
||||||
<p className="mt-1 max-w-2xl text-sm text-gray-500">
|
|
||||||
Limit download and upload speed for torrents in this filter.
|
|
||||||
In KB/s.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<NumberFieldWide
|
|
||||||
name="limit_download_speed"
|
|
||||||
label="Limit download speed"
|
|
||||||
/>
|
|
||||||
<NumberFieldWide
|
|
||||||
name="limit_upload_speed"
|
|
||||||
label="Limit upload speed"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
case "DELUGE_V1":
|
|
||||||
case "DELUGE_V2":
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<DownloadClientSelect
|
|
||||||
name="client_id"
|
|
||||||
clients={clients}
|
|
||||||
values={values}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextFieldWide name="label" label="Label" />
|
|
||||||
<TextFieldWide name="save_path" label="Save path" />
|
|
||||||
|
|
||||||
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
|
||||||
<SwitchGroup name="paused" label="Add paused" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="divide-y divide-gray-200 pt-8 space-y-6 sm:pt-10 sm:space-y-5">
|
|
||||||
<div className="px-4">
|
|
||||||
<h3 className="text-lg leading-6 font-medium text-gray-900">
|
|
||||||
Limit speeds
|
|
||||||
</h3>
|
|
||||||
<p className="mt-1 max-w-2xl text-sm text-gray-500">
|
|
||||||
Limit download and upload speed for torrents in this filter.
|
|
||||||
In KB/s.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<NumberFieldWide
|
|
||||||
name="limit_download_speed"
|
|
||||||
label="Limit download speed"
|
|
||||||
/>
|
|
||||||
<NumberFieldWide
|
|
||||||
name="limit_upload_speed"
|
|
||||||
label="Limit upload speed"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
case "RADARR":
|
|
||||||
case "SONARR":
|
|
||||||
case "LIDARR":
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<DownloadClientSelect
|
|
||||||
name="client_id"
|
|
||||||
clients={clients}
|
|
||||||
values={values}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return (
|
|
||||||
<AlertWarning
|
|
||||||
title="Notice"
|
|
||||||
text="The test action does nothing except to show if the filter works."
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Transition.Root show={isOpen} as={Fragment}>
|
|
||||||
<Dialog
|
|
||||||
as="div"
|
|
||||||
static
|
|
||||||
className="fixed inset-0 overflow-hidden"
|
|
||||||
open={isOpen}
|
|
||||||
onClose={toggle}
|
|
||||||
>
|
|
||||||
<div className="absolute inset-0 overflow-hidden">
|
|
||||||
<Dialog.Overlay className="absolute inset-0" />
|
|
||||||
|
|
||||||
<div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16">
|
|
||||||
<Transition.Child
|
|
||||||
as={Fragment}
|
|
||||||
enter="transform transition ease-in-out duration-500 sm:duration-700"
|
|
||||||
enterFrom="translate-x-full"
|
|
||||||
enterTo="translate-x-0"
|
|
||||||
leave="transform transition ease-in-out duration-500 sm:duration-700"
|
|
||||||
leaveFrom="translate-x-0"
|
|
||||||
leaveTo="translate-x-full"
|
|
||||||
>
|
|
||||||
<div className="w-screen max-w-2xl">
|
|
||||||
<Form
|
|
||||||
initialValues={{
|
|
||||||
name: "",
|
|
||||||
enabled: false,
|
|
||||||
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: null,
|
|
||||||
}}
|
|
||||||
onSubmit={onSubmit}
|
|
||||||
>
|
|
||||||
{({ handleSubmit, values }) => {
|
|
||||||
return (
|
|
||||||
<form
|
|
||||||
className="h-full flex flex-col bg-white shadow-xl overflow-y-scroll"
|
|
||||||
onSubmit={handleSubmit}
|
|
||||||
>
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="px-4 py-6 bg-gray-50 sm:px-6">
|
|
||||||
<div className="flex items-start justify-between space-x-3">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Dialog.Title className="text-lg font-medium text-gray-900">
|
|
||||||
Add action
|
|
||||||
</Dialog.Title>
|
|
||||||
<p className="text-sm text-gray-500">
|
|
||||||
Add filter action.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="h-7 flex items-center">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="bg-white rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
|
||||||
onClick={toggle}
|
|
||||||
>
|
|
||||||
<span className="sr-only">Close panel</span>
|
|
||||||
<XIcon
|
|
||||||
className="h-6 w-6"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
|
||||||
<TextFieldWide name="name" label="Action name" />
|
|
||||||
<RadioFieldsetWide
|
|
||||||
name="type"
|
|
||||||
legend="Type"
|
|
||||||
options={ActionTypeOptions}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{TypeForm(values)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex-shrink-0 px-4 border-t border-gray-200 py-5 sm:px-6">
|
|
||||||
<div className="space-x-3 flex justify-end">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
|
||||||
onClick={toggle}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="inline-flex justify-center py-2 px-4 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>
|
|
||||||
|
|
||||||
<DEBUG values={values} />
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
</Transition.Child>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
</Transition.Root>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default FilterActionAddForm;
|
|
|
@ -1,318 +0,0 @@
|
||||||
import { Fragment, useEffect } from "react";
|
|
||||||
import { useMutation } from "react-query";
|
|
||||||
import { Action, DownloadClient, Filter } from "../../domain/interfaces";
|
|
||||||
import { queryClient } from "../../App";
|
|
||||||
import { sleep } from "../../utils/utils";
|
|
||||||
import { XIcon } from "@heroicons/react/solid";
|
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
|
||||||
import { Form } from "react-final-form";
|
|
||||||
import DEBUG from "../../components/debug";
|
|
||||||
import APIClient from "../../api/APIClient";
|
|
||||||
import { ActionTypeOptions } from "../../domain/constants";
|
|
||||||
import { AlertWarning } from "../../components/alerts";
|
|
||||||
import { SwitchGroup, TextFieldWide } from "../../components/inputs";
|
|
||||||
import {
|
|
||||||
NumberFieldWide,
|
|
||||||
RadioFieldsetWide,
|
|
||||||
} from "../../components/inputs/wide";
|
|
||||||
import { DownloadClientSelect } from "./FilterActionAddForm";
|
|
||||||
|
|
||||||
import { toast } from 'react-hot-toast'
|
|
||||||
import Toast from '../../components/notifications/Toast';
|
|
||||||
|
|
||||||
|
|
||||||
interface props {
|
|
||||||
filter: Filter;
|
|
||||||
isOpen: boolean;
|
|
||||||
toggle: any;
|
|
||||||
clients: DownloadClient[];
|
|
||||||
action: Action;
|
|
||||||
}
|
|
||||||
|
|
||||||
function FilterActionUpdateForm({
|
|
||||||
filter,
|
|
||||||
isOpen,
|
|
||||||
toggle,
|
|
||||||
clients,
|
|
||||||
action,
|
|
||||||
}: props) {
|
|
||||||
const mutation = useMutation(
|
|
||||||
(action: Action) => APIClient.actions.update(action),
|
|
||||||
{
|
|
||||||
onSuccess: () => {
|
|
||||||
// console.log("add action");
|
|
||||||
queryClient.invalidateQueries(["filter", filter.id]);
|
|
||||||
toast.custom((t) => <Toast type="success" body={`${filter.name} was updated successfully`} t={t} />)
|
|
||||||
|
|
||||||
sleep(1500);
|
|
||||||
|
|
||||||
toggle();
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// console.log("render add action form", clients)
|
|
||||||
}, [clients]);
|
|
||||||
|
|
||||||
const onSubmit = (data: any) => {
|
|
||||||
// TODO clear data depending on type
|
|
||||||
|
|
||||||
console.log(data);
|
|
||||||
mutation.mutate(data);
|
|
||||||
};
|
|
||||||
|
|
||||||
const TypeForm = (values: any) => {
|
|
||||||
switch (values.type) {
|
|
||||||
case "TEST":
|
|
||||||
return (
|
|
||||||
<AlertWarning
|
|
||||||
title="Notice"
|
|
||||||
text="The test action does nothing except to show if the filter works."
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case "WATCH_FOLDER":
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<TextFieldWide
|
|
||||||
name="watch_folder"
|
|
||||||
label="Watch dir"
|
|
||||||
placeholder="Watch directory eg. /home/user/watch_folder"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
case "EXEC":
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<TextFieldWide
|
|
||||||
name="exec_cmd"
|
|
||||||
label="Program"
|
|
||||||
placeholder="Path to program eg. /bin/test"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextFieldWide
|
|
||||||
name="exec_args"
|
|
||||||
label="Arguments"
|
|
||||||
placeholder="Arguments eg. --test"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
case "QBITTORRENT":
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<DownloadClientSelect
|
|
||||||
name="client_id"
|
|
||||||
clients={clients}
|
|
||||||
values={values}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextFieldWide name="category" label="Category" placeholder="" />
|
|
||||||
<TextFieldWide
|
|
||||||
name="tags"
|
|
||||||
label="Tags"
|
|
||||||
placeholder="Comma separated eg. 4k,remux"
|
|
||||||
/>
|
|
||||||
<TextFieldWide name="save_path" label="Save path" />
|
|
||||||
|
|
||||||
<div className="divide-y divide-gray-200 pt-8 space-y-6 sm:pt-10 sm:space-y-5">
|
|
||||||
<div className="px-4">
|
|
||||||
<h3 className="text-lg leading-6 font-medium text-gray-900">
|
|
||||||
Limit speeds
|
|
||||||
</h3>
|
|
||||||
<p className="mt-1 max-w-2xl text-sm text-gray-500">
|
|
||||||
Limit download and upload speed for torrents in this filter.
|
|
||||||
In KB/s.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<NumberFieldWide
|
|
||||||
name="limit_download_speed"
|
|
||||||
label="Limit download speed"
|
|
||||||
/>
|
|
||||||
<NumberFieldWide
|
|
||||||
name="limit_upload_speed"
|
|
||||||
label="Limit upload speed"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="col-span-6">
|
|
||||||
<SwitchGroup name="paused" label="Add paused" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
case "DELUGE_V1":
|
|
||||||
case "DELUGE_V2":
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<DownloadClientSelect
|
|
||||||
name="client_id"
|
|
||||||
clients={clients}
|
|
||||||
values={values}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextFieldWide name="label" label="Label" />
|
|
||||||
<TextFieldWide name="save_path" label="Save path" />
|
|
||||||
|
|
||||||
<div className="divide-y divide-gray-200 pt-8 space-y-6 sm:pt-10 sm:space-y-5">
|
|
||||||
<div className="px-4">
|
|
||||||
<h3 className="text-lg leading-6 font-medium text-gray-900">
|
|
||||||
Limit speeds
|
|
||||||
</h3>
|
|
||||||
<p className="mt-1 max-w-2xl text-sm text-gray-500">
|
|
||||||
Limit download and upload speed for torrents in this filter.
|
|
||||||
In KB/s.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<NumberFieldWide
|
|
||||||
name="limit_download_speed"
|
|
||||||
label="Limit download speed"
|
|
||||||
/>
|
|
||||||
<NumberFieldWide
|
|
||||||
name="limit_upload_speed"
|
|
||||||
label="Limit upload speed"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
case "RADARR":
|
|
||||||
case "SONARR":
|
|
||||||
case "LIDARR":
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<DownloadClientSelect
|
|
||||||
name="client_id"
|
|
||||||
clients={clients}
|
|
||||||
values={values}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return (
|
|
||||||
<AlertWarning
|
|
||||||
title="Notice"
|
|
||||||
text="The test action does nothing except to show if the filter works."
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Transition.Root show={isOpen} as={Fragment}>
|
|
||||||
<Dialog
|
|
||||||
as="div"
|
|
||||||
static
|
|
||||||
className="fixed inset-0 overflow-hidden"
|
|
||||||
open={isOpen}
|
|
||||||
onClose={toggle}
|
|
||||||
>
|
|
||||||
<div className="absolute inset-0 overflow-hidden">
|
|
||||||
<Dialog.Overlay className="absolute inset-0" />
|
|
||||||
|
|
||||||
<div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16">
|
|
||||||
<Transition.Child
|
|
||||||
as={Fragment}
|
|
||||||
enter="transform transition ease-in-out duration-500 sm:duration-700"
|
|
||||||
enterFrom="translate-x-full"
|
|
||||||
enterTo="translate-x-0"
|
|
||||||
leave="transform transition ease-in-out duration-500 sm:duration-700"
|
|
||||||
leaveFrom="translate-x-0"
|
|
||||||
leaveTo="translate-x-full"
|
|
||||||
>
|
|
||||||
<div className="w-screen max-w-2xl">
|
|
||||||
<Form
|
|
||||||
initialValues={{
|
|
||||||
name: "",
|
|
||||||
enabled: false,
|
|
||||||
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: null,
|
|
||||||
}}
|
|
||||||
onSubmit={onSubmit}
|
|
||||||
>
|
|
||||||
{({ handleSubmit, values }) => {
|
|
||||||
return (
|
|
||||||
<form
|
|
||||||
className="h-full flex flex-col bg-white shadow-xl overflow-y-scroll"
|
|
||||||
onSubmit={handleSubmit}
|
|
||||||
>
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="px-4 py-6 bg-gray-50 sm:px-6">
|
|
||||||
<div className="flex items-start justify-between space-x-3">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Dialog.Title className="text-lg font-medium text-gray-900">
|
|
||||||
Update action
|
|
||||||
</Dialog.Title>
|
|
||||||
<p className="text-sm text-gray-500">
|
|
||||||
Add filter action.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="h-7 flex items-center">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="bg-white rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
|
||||||
onClick={toggle}
|
|
||||||
>
|
|
||||||
<span className="sr-only">Close panel</span>
|
|
||||||
<XIcon
|
|
||||||
className="h-6 w-6"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
|
||||||
<TextFieldWide name="name" label="Action name" />
|
|
||||||
<RadioFieldsetWide
|
|
||||||
name="type"
|
|
||||||
legend="Type"
|
|
||||||
options={ActionTypeOptions}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{TypeForm(values)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex-shrink-0 px-4 border-t border-gray-200 py-5 sm:px-6">
|
|
||||||
<div className="space-x-3 flex justify-end">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
|
||||||
onClick={toggle}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="inline-flex justify-center py-2 px-4 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>
|
|
||||||
|
|
||||||
<DEBUG values={values} />
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
</Transition.Child>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
</Transition.Root>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default FilterActionUpdateForm;
|
|
|
@ -4,12 +4,12 @@ import { Filter } from "../../domain/interfaces";
|
||||||
import { queryClient } from "../../App";
|
import { queryClient } from "../../App";
|
||||||
import { XIcon } from "@heroicons/react/solid";
|
import { XIcon } from "@heroicons/react/solid";
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
import { Field, Form } from "react-final-form";
|
|
||||||
import DEBUG from "../../components/debug";
|
import DEBUG from "../../components/debug";
|
||||||
import APIClient from "../../api/APIClient";
|
import APIClient from "../../api/APIClient";
|
||||||
|
|
||||||
import { toast } from 'react-hot-toast'
|
import { toast } from 'react-hot-toast'
|
||||||
import Toast from '../../components/notifications/Toast';
|
import Toast from '../../components/notifications/Toast';
|
||||||
|
import { Field, FieldProps, Form, Formik } from "formik";
|
||||||
|
|
||||||
function FilterAddForm({ isOpen, toggle }: any) {
|
function FilterAddForm({ isOpen, toggle }: any) {
|
||||||
const mutation = useMutation((filter: Filter) => APIClient.filters.create(filter), {
|
const mutation = useMutation((filter: Filter) => APIClient.filters.create(filter), {
|
||||||
|
@ -25,7 +25,7 @@ function FilterAddForm({ isOpen, toggle }: any) {
|
||||||
// console.log("render add action form")
|
// console.log("render add action form")
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onSubmit = (data: any) => {
|
const handleSubmit = (data: any) => {
|
||||||
mutation.mutate(data)
|
mutation.mutate(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,7 +57,7 @@ function FilterAddForm({ isOpen, toggle }: any) {
|
||||||
>
|
>
|
||||||
<div className="w-screen max-w-2xl border-l dark:border-gray-700">
|
<div className="w-screen max-w-2xl border-l dark:border-gray-700">
|
||||||
|
|
||||||
<Form
|
<Formik
|
||||||
initialValues={{
|
initialValues={{
|
||||||
name: "",
|
name: "",
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
@ -66,12 +66,11 @@ function FilterAddForm({ isOpen, toggle }: any) {
|
||||||
sources: [],
|
sources: [],
|
||||||
containers: []
|
containers: []
|
||||||
}}
|
}}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
validate={validate}
|
validate={validate}
|
||||||
onSubmit={onSubmit}
|
|
||||||
>
|
>
|
||||||
{({ handleSubmit, values }) => {
|
{({ values }) => (
|
||||||
return (
|
<Form className="h-full flex flex-col bg-white dark:bg-gray-800 shadow-xl overflow-y-scroll">
|
||||||
<form className="h-full flex flex-col bg-white dark:bg-gray-800 shadow-xl overflow-y-scroll" onSubmit={handleSubmit}>
|
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="px-4 py-6 bg-gray-50 dark:bg-gray-900 sm:px-6">
|
<div className="px-4 py-6 bg-gray-50 dark:bg-gray-900 sm:px-6">
|
||||||
<div className="flex items-start justify-between space-x-3">
|
<div className="flex items-start justify-between space-x-3">
|
||||||
|
@ -107,20 +106,25 @@ function FilterAddForm({ isOpen, toggle }: any) {
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<Field name="name">
|
<Field name="name">
|
||||||
{({ input, meta }) => (
|
{({
|
||||||
|
field,
|
||||||
|
meta,
|
||||||
|
}: FieldProps ) => (
|
||||||
<div className="sm:col-span-2">
|
<div className="sm:col-span-2">
|
||||||
<input
|
<input
|
||||||
|
{...field}
|
||||||
|
id="name"
|
||||||
type="text"
|
type="text"
|
||||||
{...input}
|
|
||||||
className="block w-full shadow-sm dark:bg-gray-800 border-gray-300 dark:border-gray-700 sm:text-sm dark:text-white focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 rounded-md"
|
className="block w-full shadow-sm dark:bg-gray-800 border-gray-300 dark:border-gray-700 sm:text-sm dark:text-white focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 rounded-md"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{meta.touched && meta.error &&
|
{meta.touched && meta.error &&
|
||||||
<span className="block mt-2 text-red-500">{meta.error}</span>}
|
<span className="block mt-2 text-red-500">{meta.error}</span>}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -143,12 +147,10 @@ function FilterAddForm({ isOpen, toggle }: any) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DEBUG values={values} />
|
<DEBUG values={values} />
|
||||||
</form>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</Form>
|
</Form>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
export { default as FilterAddForm } from "./filters/FilterAddForm";
|
export { default as FilterAddForm } from "./filters/FilterAddForm";
|
||||||
export { default as FilterActionAddForm } from "./filters/FilterActionAddForm";
|
|
||||||
export { default as FilterActionUpdateForm } from "./filters/FilterActionUpdateForm";
|
|
||||||
|
|
||||||
export { DownloadClientAddForm, DownloadClientUpdateForm } from "./settings/DownloadClientForms";
|
export { DownloadClientAddForm, DownloadClientUpdateForm } from "./settings/DownloadClientForms";
|
||||||
export { IndexerAddForm, IndexerUpdateForm } from "./settings/IndexerForms";
|
export { IndexerAddForm, IndexerUpdateForm } from "./settings/IndexerForms";
|
||||||
|
|
|
@ -6,20 +6,47 @@ import {
|
||||||
} from "../../domain/interfaces";
|
} from "../../domain/interfaces";
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
import { XIcon } from "@heroicons/react/solid";
|
import { XIcon } from "@heroicons/react/solid";
|
||||||
import { classNames } from "../../styles/utils";
|
import { sleep, classNames } from "../../utils";
|
||||||
import { Form, useField } from "react-final-form";
|
|
||||||
|
import { Form, Formik, useFormikContext } from "formik";
|
||||||
import DEBUG from "../../components/debug";
|
import DEBUG from "../../components/debug";
|
||||||
import { SwitchGroup, TextFieldWide } from "../../components/inputs";
|
|
||||||
import { queryClient } from "../../App";
|
import { queryClient } from "../../App";
|
||||||
import APIClient from "../../api/APIClient";
|
import APIClient from "../../api/APIClient";
|
||||||
import { sleep } from "../../utils/utils";
|
|
||||||
import { DownloadClientTypeOptions } from "../../domain/constants";
|
import { DownloadClientTypeOptions } from "../../domain/constants";
|
||||||
import { NumberFieldWide, PasswordFieldWide, RadioFieldsetWide } from "../../components/inputs/wide";
|
|
||||||
|
|
||||||
import { toast } from 'react-hot-toast'
|
import { toast } from 'react-hot-toast'
|
||||||
import Toast from '../../components/notifications/Toast';
|
import Toast from '../../components/notifications/Toast';
|
||||||
import { useToggle } from "../../hooks/hooks";
|
import { useToggle } from "../../hooks/hooks";
|
||||||
import { DeleteModal } from "../../components/modals";
|
import { DeleteModal } from "../../components/modals";
|
||||||
|
import { NumberFieldWide, PasswordFieldWide, SwitchGroupWide, TextFieldWide } from "../../components/inputs/input_wide";
|
||||||
|
import { RadioFieldsetWide } from "../../components/inputs/radio";
|
||||||
|
|
||||||
|
interface InitialValuesSettings {
|
||||||
|
basic?: {
|
||||||
|
auth: boolean;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
rules?: {
|
||||||
|
enabled?: boolean;
|
||||||
|
ignore_slow_torrents?: boolean;
|
||||||
|
download_speed_threshold?: number;
|
||||||
|
max_active_downloads?: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InitialValues {
|
||||||
|
name: string;
|
||||||
|
type: DOWNLOAD_CLIENT_TYPES;
|
||||||
|
enabled: boolean;
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
ssl: boolean;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
settings: InitialValuesSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function FormFieldsDefault() {
|
function FormFieldsDefault() {
|
||||||
return (
|
return (
|
||||||
|
@ -29,7 +56,7 @@ function FormFieldsDefault() {
|
||||||
<NumberFieldWide name="port" label="Port" />
|
<NumberFieldWide name="port" label="Port" />
|
||||||
|
|
||||||
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
||||||
<SwitchGroup name="ssl" label="SSL" />
|
<SwitchGroupWide name="ssl" label="SSL" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TextFieldWide name="username" label="Username" />
|
<TextFieldWide name="username" label="Username" />
|
||||||
|
@ -39,7 +66,10 @@ function FormFieldsDefault() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function FormFieldsArr() {
|
function FormFieldsArr() {
|
||||||
const { input } = useField("settings.basic.auth");
|
const {
|
||||||
|
values: { settings },
|
||||||
|
} = useFormikContext<InitialValues>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<TextFieldWide name="host" label="Host" help="Full url like http(s)://domain.ltd/" />
|
<TextFieldWide name="host" label="Host" help="Full url like http(s)://domain.ltd/" />
|
||||||
|
@ -47,10 +77,10 @@ function FormFieldsArr() {
|
||||||
<PasswordFieldWide name="settings.apikey" label="API key" />
|
<PasswordFieldWide name="settings.apikey" label="API key" />
|
||||||
|
|
||||||
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
||||||
<SwitchGroup name="settings.basic.auth" label="Basic auth" />
|
<SwitchGroupWide name="settings.basic.auth" label="Basic auth" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{input.value === true && (
|
{settings.basic?.auth === true && (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<TextFieldWide name="settings.basic.username" label="Username" />
|
<TextFieldWide name="settings.basic.username" label="Username" />
|
||||||
<PasswordFieldWide name="settings.basic.password" label="Password" />
|
<PasswordFieldWide name="settings.basic.password" label="Password" />
|
||||||
|
@ -71,7 +101,9 @@ export const componentMap: any = {
|
||||||
|
|
||||||
|
|
||||||
function FormFieldsRulesBasic() {
|
function FormFieldsRulesBasic() {
|
||||||
const { input: enabled } = useField("settings.rules.enabled");
|
const {
|
||||||
|
values: { settings },
|
||||||
|
} = useFormikContext<InitialValues>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border-t border-gray-200 dark:border-gray-700 py-5">
|
<div className="border-t border-gray-200 dark:border-gray-700 py-5">
|
||||||
|
@ -84,10 +116,10 @@ function FormFieldsRulesBasic() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
||||||
<SwitchGroup name="settings.rules.enabled" label="Enabled" />
|
<SwitchGroupWide name="settings.rules.enabled" label="Enabled" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{enabled.value === true && (
|
{settings && settings.rules?.enabled === true && (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<NumberFieldWide name="settings.rules.max_active_downloads" label="Max active downloads" />
|
<NumberFieldWide name="settings.rules.max_active_downloads" label="Max active downloads" />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
|
@ -97,8 +129,9 @@ function FormFieldsRulesBasic() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function FormFieldsRules() {
|
function FormFieldsRules() {
|
||||||
const { input } = useField("settings.rules.ignore_slow_torrents");
|
const {
|
||||||
const { input: enabled } = useField("settings.rules.enabled");
|
values: { settings },
|
||||||
|
} = useFormikContext<InitialValues>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border-t border-gray-200 dark:border-gray-700 py-5">
|
<div className="border-t border-gray-200 dark:border-gray-700 py-5">
|
||||||
|
@ -111,17 +144,17 @@ function FormFieldsRules() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
||||||
<SwitchGroup name="settings.rules.enabled" label="Enabled" />
|
<SwitchGroupWide name="settings.rules.enabled" label="Enabled" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{enabled.value === true && (
|
{settings.rules?.enabled === true && (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<NumberFieldWide name="settings.rules.max_active_downloads" label="Max active downloads" />
|
<NumberFieldWide name="settings.rules.max_active_downloads" label="Max active downloads" />
|
||||||
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
||||||
<SwitchGroup name="settings.rules.ignore_slow_torrents" label="Ignore slow torrents" />
|
<SwitchGroupWide name="settings.rules.ignore_slow_torrents" label="Ignore slow torrents" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{input.value === true && (
|
{settings.rules?.ignore_slow_torrents === true && (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<NumberFieldWide name="settings.rules.download_speed_threshold" label="Download speed threshold" placeholder="in KB/s" help="If download speed is below this when max active downloads is hit, download anyways. KB/s" />
|
<NumberFieldWide name="settings.rules.download_speed_threshold" label="Download speed threshold" placeholder="in KB/s" help="If download speed is below this when max active downloads is hit, download anyways. KB/s" />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
|
@ -138,6 +171,100 @@ export const rulesComponentMap: any = {
|
||||||
QBITTORRENT: <FormFieldsRules />,
|
QBITTORRENT: <FormFieldsRules />,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface formButtonsProps {
|
||||||
|
isSuccessfulTest: boolean;
|
||||||
|
isErrorTest: boolean;
|
||||||
|
isTesting: boolean;
|
||||||
|
cancelFn: any;
|
||||||
|
testFn: any;
|
||||||
|
values: any;
|
||||||
|
type: "CREATE" | "UPDATE";
|
||||||
|
toggleDeleteModal?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DownloadClientFormButtons({ type, isSuccessfulTest, isErrorTest, isTesting, cancelFn, testFn, values, toggleDeleteModal }: formButtonsProps) {
|
||||||
|
|
||||||
|
const test = () => {
|
||||||
|
testFn(values)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex-shrink-0 px-4 border-t border-gray-200 dark:border-gray-700 py-5 sm:px-6">
|
||||||
|
<div className={classNames(type === "CREATE" ? "justify-end" : "justify-between", "space-x-3 flex")}>
|
||||||
|
{type === "UPDATE" && (
|
||||||
|
<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 className="flex">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={classNames(
|
||||||
|
isSuccessfulTest
|
||||||
|
? "text-green-500 border-green-500 bg-green-50"
|
||||||
|
: isErrorTest
|
||||||
|
? "text-red-500 border-red-500 bg-red-50"
|
||||||
|
: "border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-400 bg-white dark:bg-gray-700 hover:bg-gray-50 focus:border-rose-700 active:bg-rose-700",
|
||||||
|
isTesting ? "cursor-not-allowed" : "",
|
||||||
|
"mr-2 inline-flex items-center px-4 py-2 border font-medium rounded-md shadow-sm text-sm transition ease-in-out duration-150 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
||||||
|
)}
|
||||||
|
disabled={isTesting}
|
||||||
|
// onClick={() => testClient(values)}
|
||||||
|
onClick={test}
|
||||||
|
>
|
||||||
|
{isTesting ? (
|
||||||
|
<svg
|
||||||
|
className="animate-spin h-5 w-5 text-green-500"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
className="opacity-25"
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="10"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="4"
|
||||||
|
></circle>
|
||||||
|
<path
|
||||||
|
className="opacity-75"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
) : isSuccessfulTest ? (
|
||||||
|
"OK!"
|
||||||
|
) : isErrorTest ? (
|
||||||
|
"ERROR"
|
||||||
|
) : (
|
||||||
|
"Test"
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="mr-4 bg-white dark:bg-gray-700 py-2 px-4 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
||||||
|
onClick={cancelFn}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
||||||
|
>
|
||||||
|
{type === "CREATE" ? "Create" : "Save"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function DownloadClientAddForm({ isOpen, toggle }: any) {
|
export function DownloadClientAddForm({ isOpen, toggle }: any) {
|
||||||
const [isTesting, setIsTesting] = useState(false);
|
const [isTesting, setIsTesting] = useState(false);
|
||||||
const [isSuccessfulTest, setIsSuccessfulTest] = useState(false);
|
const [isSuccessfulTest, setIsSuccessfulTest] = useState(false);
|
||||||
|
@ -197,6 +324,18 @@ export function DownloadClientAddForm({ isOpen, toggle }: any) {
|
||||||
testClientMutation.mutate(data);
|
testClientMutation.mutate(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let initialValues: InitialValues = {
|
||||||
|
name: "",
|
||||||
|
type: DOWNLOAD_CLIENT_TYPES.qBittorrent,
|
||||||
|
enabled: true,
|
||||||
|
host: "",
|
||||||
|
port: 10000,
|
||||||
|
ssl: false,
|
||||||
|
username: "",
|
||||||
|
password: "",
|
||||||
|
settings: {}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Transition.Root show={isOpen} as={Fragment}>
|
<Transition.Root show={isOpen} as={Fragment}>
|
||||||
<Dialog
|
<Dialog
|
||||||
|
@ -220,22 +359,12 @@ export function DownloadClientAddForm({ isOpen, toggle }: any) {
|
||||||
leaveTo="translate-x-full"
|
leaveTo="translate-x-full"
|
||||||
>
|
>
|
||||||
<div className="w-screen max-w-2xl border-l dark:border-gray-700">
|
<div className="w-screen max-w-2xl border-l dark:border-gray-700">
|
||||||
<Form
|
<Formik
|
||||||
initialValues={{
|
initialValues={initialValues}
|
||||||
name: "",
|
|
||||||
type: DOWNLOAD_CLIENT_TYPES.qBittorrent,
|
|
||||||
enabled: true,
|
|
||||||
host: "",
|
|
||||||
port: 10000,
|
|
||||||
ssl: false,
|
|
||||||
username: "",
|
|
||||||
password: "",
|
|
||||||
}}
|
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
>
|
>
|
||||||
{({ handleSubmit, values }) => {
|
{({ handleSubmit, values }) => (
|
||||||
return (
|
<Form
|
||||||
<form
|
|
||||||
className="h-full flex flex-col bg-white dark:bg-gray-800 shadow-xl overflow-y-scroll"
|
className="h-full flex flex-col bg-white dark:bg-gray-800 shadow-xl overflow-y-scroll"
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
>
|
>
|
||||||
|
@ -270,7 +399,7 @@ export function DownloadClientAddForm({ isOpen, toggle }: any) {
|
||||||
<TextFieldWide name="name" label="Name" />
|
<TextFieldWide name="name" label="Name" />
|
||||||
|
|
||||||
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200 dark:divide-gray-700">
|
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200 dark:divide-gray-700">
|
||||||
<SwitchGroup name="enabled" label="Enabled" />
|
<SwitchGroupWide name="enabled" label="Enabled" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<RadioFieldsetWide
|
<RadioFieldsetWide
|
||||||
|
@ -285,72 +414,20 @@ export function DownloadClientAddForm({ isOpen, toggle }: any) {
|
||||||
|
|
||||||
{rulesComponentMap[values.type]}
|
{rulesComponentMap[values.type]}
|
||||||
|
|
||||||
<div className="flex-shrink-0 px-4 border-t border-gray-200 dark:border-gray-700 py-5 sm:px-6">
|
<DownloadClientFormButtons
|
||||||
<div className="space-x-3 flex justify-end">
|
type="CREATE"
|
||||||
<button
|
isTesting={isTesting}
|
||||||
type="button"
|
isSuccessfulTest={isSuccessfulTest}
|
||||||
className={classNames(
|
isErrorTest={isErrorTest}
|
||||||
isSuccessfulTest
|
cancelFn={toggle}
|
||||||
? "text-green-500 border-green-500 bg-green-50"
|
testFn={testClient}
|
||||||
: isErrorTest
|
values={values}
|
||||||
? "text-red-500 border-red-500 bg-red-50"
|
/>
|
||||||
: "border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-400 bg-white dark:bg-gray-700 hover:bg-gray-50 focus:border-rose-700 active:bg-rose-700",
|
|
||||||
isTesting ? "cursor-not-allowed" : "",
|
|
||||||
"mr-2 inline-flex items-center px-4 py-2 border font-medium rounded-md shadow-sm text-sm transition ease-in-out duration-150 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
|
||||||
)}
|
|
||||||
disabled={isTesting}
|
|
||||||
onClick={() => testClient(values)}
|
|
||||||
>
|
|
||||||
{isTesting ? (
|
|
||||||
<svg
|
|
||||||
className="animate-spin h-5 w-5 text-green-500"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<circle
|
|
||||||
className="opacity-25"
|
|
||||||
cx="12"
|
|
||||||
cy="12"
|
|
||||||
r="10"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="4"
|
|
||||||
></circle>
|
|
||||||
<path
|
|
||||||
className="opacity-75"
|
|
||||||
fill="currentColor"
|
|
||||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
) : isSuccessfulTest ? (
|
|
||||||
"OK!"
|
|
||||||
) : isErrorTest ? (
|
|
||||||
"ERROR"
|
|
||||||
) : (
|
|
||||||
"Test"
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="bg-white dark:bg-gray-700 py-2 px-4 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
|
||||||
onClick={toggle}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
|
||||||
>
|
|
||||||
Create
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DEBUG values={values} />
|
<DEBUG values={values} />
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Form>
|
</Form>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
</div>
|
</div>
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
</div>
|
</div>
|
||||||
|
@ -433,6 +510,19 @@ export function DownloadClientUpdateForm({ client, isOpen, toggle }: any) {
|
||||||
testClientMutation.mutate(data);
|
testClientMutation.mutate(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let initialValues = {
|
||||||
|
id: client.id,
|
||||||
|
name: client.name,
|
||||||
|
type: client.type,
|
||||||
|
enabled: client.enabled,
|
||||||
|
host: client.host,
|
||||||
|
port: client.port,
|
||||||
|
ssl: client.ssl,
|
||||||
|
username: client.username,
|
||||||
|
password: client.password,
|
||||||
|
settings: client.settings,
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Transition.Root show={isOpen} as={Fragment}>
|
<Transition.Root show={isOpen} as={Fragment}>
|
||||||
<Dialog
|
<Dialog
|
||||||
|
@ -465,24 +555,13 @@ export function DownloadClientUpdateForm({ client, isOpen, toggle }: any) {
|
||||||
leaveTo="translate-x-full"
|
leaveTo="translate-x-full"
|
||||||
>
|
>
|
||||||
<div className="w-screen max-w-2xl border-l dark:border-gray-700">
|
<div className="w-screen max-w-2xl border-l dark:border-gray-700">
|
||||||
<Form
|
<Formik
|
||||||
initialValues={{
|
initialValues={initialValues}
|
||||||
id: client.id,
|
|
||||||
name: client.name,
|
|
||||||
type: client.type,
|
|
||||||
enabled: client.enabled,
|
|
||||||
host: client.host,
|
|
||||||
port: client.port,
|
|
||||||
ssl: client.ssl,
|
|
||||||
username: client.username,
|
|
||||||
password: client.password,
|
|
||||||
settings: client.settings,
|
|
||||||
}}
|
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
>
|
>
|
||||||
{({ handleSubmit, values }) => {
|
{({ handleSubmit, values }) => {
|
||||||
return (
|
return (
|
||||||
<form
|
<Form
|
||||||
className="h-full flex flex-col bg-white dark:bg-gray-800 shadow-xl overflow-y-scroll"
|
className="h-full flex flex-col bg-white dark:bg-gray-800 shadow-xl overflow-y-scroll"
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
>
|
>
|
||||||
|
@ -517,7 +596,7 @@ export function DownloadClientUpdateForm({ client, isOpen, toggle }: any) {
|
||||||
<TextFieldWide name="name" label="Name" />
|
<TextFieldWide name="name" label="Name" />
|
||||||
|
|
||||||
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
||||||
<SwitchGroup name="enabled" label="Enabled" />
|
<SwitchGroupWide name="enabled" label="Enabled" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<RadioFieldsetWide
|
<RadioFieldsetWide
|
||||||
|
@ -532,81 +611,22 @@ export function DownloadClientUpdateForm({ client, isOpen, toggle }: any) {
|
||||||
|
|
||||||
{rulesComponentMap[values.type]}
|
{rulesComponentMap[values.type]}
|
||||||
|
|
||||||
<div className="flex-shrink-0 px-4 border-t border-gray-200 dark:border-gray-700 py-5 sm:px-6">
|
<DownloadClientFormButtons
|
||||||
<div className="space-x-3 flex justify-between">
|
type="UPDATE"
|
||||||
<button
|
toggleDeleteModal={toggleDeleteModal}
|
||||||
type="button"
|
isTesting={isTesting}
|
||||||
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"
|
isSuccessfulTest={isSuccessfulTest}
|
||||||
onClick={toggleDeleteModal}
|
isErrorTest={isErrorTest}
|
||||||
>
|
cancelFn={toggle}
|
||||||
Remove
|
testFn={testClient}
|
||||||
</button>
|
values={values}
|
||||||
<div className="flex">
|
/>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={classNames(
|
|
||||||
isSuccessfulTest
|
|
||||||
? "text-green-500 border-green-500 bg-green-50"
|
|
||||||
: isErrorTest
|
|
||||||
? "text-red-500 border-red-500 bg-red-50"
|
|
||||||
: "border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-400 bg-white dark:bg-gray-700 hover:bg-gray-50 focus:border-rose-700 active:bg-rose-700",
|
|
||||||
isTesting ? "cursor-not-allowed" : "",
|
|
||||||
"mr-2 inline-flex items-center px-4 py-2 border font-medium rounded-md shadow-sm text-sm transition ease-in-out duration-150 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
|
||||||
)}
|
|
||||||
disabled={isTesting}
|
|
||||||
onClick={() => testClient(values)}
|
|
||||||
>
|
|
||||||
{isTesting ? (
|
|
||||||
<svg
|
|
||||||
className="animate-spin h-5 w-5 text-green-500"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<circle
|
|
||||||
className="opacity-25"
|
|
||||||
cx="12"
|
|
||||||
cy="12"
|
|
||||||
r="10"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="4"
|
|
||||||
></circle>
|
|
||||||
<path
|
|
||||||
className="opacity-75"
|
|
||||||
fill="currentColor"
|
|
||||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
) : isSuccessfulTest ? (
|
|
||||||
"OK!"
|
|
||||||
) : isErrorTest ? (
|
|
||||||
"ERROR"
|
|
||||||
) : (
|
|
||||||
"Test"
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="mr-4 bg-white dark:bg-gray-700 py-2 px-4 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
|
||||||
onClick={toggle}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
|
||||||
>
|
|
||||||
Create
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<DEBUG values={values} />
|
<DEBUG values={values} />
|
||||||
</form>
|
</Form>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</Form>
|
</Formik>
|
||||||
</div>
|
</div>
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,20 +1,22 @@
|
||||||
import React, { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
import { useMutation, useQuery } from "react-query";
|
import { useMutation, useQuery } from "react-query";
|
||||||
import { Channel, Indexer, IndexerSchema, IndexerSchemaSettings, Network } from "../../domain/interfaces";
|
import { Channel, Indexer, IndexerSchema, IndexerSchemaSettings, Network } from "../../domain/interfaces";
|
||||||
import { sleep } from "../../utils/utils";
|
import { sleep } from "../../utils";
|
||||||
import { XIcon } from "@heroicons/react/solid";
|
import { XIcon } from "@heroicons/react/solid";
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
import { Field, Form } from "react-final-form";
|
import { Field, FieldProps, Form, Formik } from "formik";
|
||||||
import DEBUG from "../../components/debug";
|
import DEBUG from "../../components/debug";
|
||||||
import Select from "react-select";
|
import Select, { components, InputProps } from "react-select";
|
||||||
import { queryClient } from "../../App";
|
import { queryClient } from "../../App";
|
||||||
import { SwitchGroup, TextFieldWide } from "../../components/inputs";
|
|
||||||
import APIClient from "../../api/APIClient";
|
import APIClient from "../../api/APIClient";
|
||||||
import { NumberFieldWide, PasswordFieldWide } from "../../components/inputs/wide";
|
import { TextFieldWide, PasswordFieldWide, SwitchGroupWide } from "../../components/inputs/input_wide";
|
||||||
|
|
||||||
import { toast } from 'react-hot-toast'
|
import { toast } from 'react-hot-toast'
|
||||||
import Toast from '../../components/notifications/Toast';
|
import Toast from '../../components/notifications/Toast';
|
||||||
import { SlideOver } from "../../components/panels";
|
import { SlideOver } from "../../components/panels";
|
||||||
|
|
||||||
|
const Input = ({ type, ...rest }: InputProps) => <components.Input {...rest} />;
|
||||||
|
|
||||||
interface AddProps {
|
interface AddProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
toggle: any;
|
toggle: any;
|
||||||
|
@ -44,7 +46,7 @@ export function IndexerAddForm({ isOpen, toggle }: AddProps) {
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
// console.log("irc mutation: ", data);
|
// console.log("irc mutation: ", data);
|
||||||
|
|
||||||
// queryClient.invalidateQueries(['indexer']);
|
// queryClient.invalidateQueries(['networks']);
|
||||||
// sleep(1500)
|
// sleep(1500)
|
||||||
|
|
||||||
// toggle()
|
// toggle()
|
||||||
|
@ -61,24 +63,24 @@ export function IndexerAddForm({ isOpen, toggle }: AddProps) {
|
||||||
let channels: Channel[] = []
|
let channels: Channel[] = []
|
||||||
if (ind.irc.channels.length) {
|
if (ind.irc.channels.length) {
|
||||||
ind.irc.channels.forEach(element => {
|
ind.irc.channels.forEach(element => {
|
||||||
channels.push({ name: element })
|
channels.push({ name: element, password: "" })
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const network: Network = {
|
const network: Network = {
|
||||||
name: ind.name,
|
name: ind.name,
|
||||||
enabled: false,
|
enabled: false,
|
||||||
server: formData.irc.server,
|
server: ind.irc.server,
|
||||||
port: formData.irc.port,
|
port: ind.irc.port,
|
||||||
tls: formData.irc.tls,
|
tls: ind.irc.tls,
|
||||||
nickserv: formData.irc.nickserv,
|
nickserv: formData.irc.nickserv,
|
||||||
invite_command: formData.irc.invite_command,
|
invite_command: formData.irc.invite_command,
|
||||||
settings: formData.irc.settings,
|
settings: formData.irc.settings,
|
||||||
channels: channels,
|
channels: channels,
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("network: ", network);
|
// console.log("network: ", network);
|
||||||
|
// console.log("formData: ", formData);
|
||||||
|
|
||||||
mutation.mutate(formData, {
|
mutation.mutate(formData, {
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
|
@ -86,7 +88,6 @@ export function IndexerAddForm({ isOpen, toggle }: AddProps) {
|
||||||
ircMutation.mutate(network)
|
ircMutation.mutate(network)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderSettingFields = (indexer: string) => {
|
const renderSettingFields = (indexer: string) => {
|
||||||
|
@ -109,7 +110,7 @@ export function IndexerAddForm({ isOpen, toggle }: AddProps) {
|
||||||
return null
|
return null
|
||||||
})}
|
})}
|
||||||
<div hidden={true}>
|
<div hidden={true}>
|
||||||
<TextFieldWide name={`name`} label="Name" defaultValue={ind?.name} />
|
<TextFieldWide name="name" label="Name" defaultValue={ind?.name} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -141,11 +142,11 @@ export function IndexerAddForm({ isOpen, toggle }: AddProps) {
|
||||||
return null
|
return null
|
||||||
})}
|
})}
|
||||||
|
|
||||||
<div hidden={true}>
|
{/* <div hidden={false}>
|
||||||
<TextFieldWide name={`irc.server`} label="Server" defaultValue={ind.irc.server} />
|
<TextFieldWide name="irc.server" label="Server" defaultValue={ind.irc.server} />
|
||||||
<NumberFieldWide name={`irc.port`} label="Port" defaultValue={ind.irc.port} />
|
<NumberFieldWide name="irc.port" label="Port" defaultValue={ind.irc.port} />
|
||||||
<SwitchGroup name="irc.tls" label="TLS" defaultValue={ind.irc.tls} />
|
<SwitchGroupWide name="irc.tls" label="TLS" defaultValue={ind.irc.tls} />
|
||||||
</div>
|
</div> */}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
|
@ -170,18 +171,20 @@ export function IndexerAddForm({ isOpen, toggle }: AddProps) {
|
||||||
leaveTo="translate-x-full"
|
leaveTo="translate-x-full"
|
||||||
>
|
>
|
||||||
<div className="w-screen max-w-2xl dark:border-gray-700 border-l">
|
<div className="w-screen max-w-2xl dark:border-gray-700 border-l">
|
||||||
<Form
|
<Formik
|
||||||
|
enableReinitialize={true}
|
||||||
initialValues={{
|
initialValues={{
|
||||||
enabled: true,
|
enabled: true,
|
||||||
identifier: "",
|
identifier: "",
|
||||||
irc: {}
|
irc: {},
|
||||||
|
settings: {},
|
||||||
}}
|
}}
|
||||||
|
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
>
|
>
|
||||||
{({ handleSubmit, values }) => {
|
{({ values }) => {
|
||||||
return (
|
return (
|
||||||
<form className="h-full flex flex-col bg-white dark:bg-gray-800 shadow-xl overflow-y-scroll"
|
<Form className="h-full flex flex-col bg-white dark:bg-gray-800 shadow-xl overflow-y-scroll">
|
||||||
onSubmit={handleSubmit}>
|
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="px-4 py-6 bg-gray-50 dark:bg-gray-900 sm:px-6">
|
<div className="px-4 py-6 bg-gray-50 dark:bg-gray-900 sm:px-6">
|
||||||
<div className="flex items-start justify-between space-x-3">
|
<div className="flex items-start justify-between space-x-3">
|
||||||
|
@ -206,11 +209,8 @@ export function IndexerAddForm({ isOpen, toggle }: AddProps) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div className="py-6 space-y-6 py-0 space-y-0 divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
className="py-6 space-y-6 py-0 space-y-0 divide-y divide-gray-200 dark:divide-gray-700">
|
<div className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
|
||||||
|
|
||||||
<div
|
|
||||||
className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
|
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor="identifier"
|
htmlFor="identifier"
|
||||||
|
@ -220,28 +220,29 @@ export function IndexerAddForm({ isOpen, toggle }: AddProps) {
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="sm:col-span-2">
|
<div className="sm:col-span-2">
|
||||||
<Field
|
<Field name="identifier" type="select">
|
||||||
name="identifier"
|
{({ field, form: { setFieldValue } }: FieldProps) => (
|
||||||
parse={val => val && val.value}
|
<Select {...field}
|
||||||
format={val => data && data.find((o: any) => o.value === val)}
|
|
||||||
render={({ input, meta }) => (
|
|
||||||
<React.Fragment>
|
|
||||||
<Select {...input}
|
|
||||||
isClearable={true}
|
isClearable={true}
|
||||||
|
isSearchable={true}
|
||||||
|
components={{ Input }}
|
||||||
placeholder="Choose an indexer"
|
placeholder="Choose an indexer"
|
||||||
|
value={field?.value && field.value.value}
|
||||||
|
onChange={(option: any) => {
|
||||||
|
setFieldValue(field.name, option?.value ?? "")
|
||||||
|
}}
|
||||||
options={data && data.sort((a, b): any => a.name.localeCompare(b.name)).map(v => ({
|
options={data && data.sort((a, b): any => a.name.localeCompare(b.name)).map(v => ({
|
||||||
label: v.name,
|
label: v.name,
|
||||||
value: v.identifier
|
value: v.identifier
|
||||||
}))} />
|
}))} />
|
||||||
</React.Fragment>
|
|
||||||
)}
|
)}
|
||||||
/>
|
</Field>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
||||||
<SwitchGroup name="enabled" label="Enabled" />
|
<SwitchGroupWide name="enabled" label="Enabled" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
@ -272,10 +273,10 @@ export function IndexerAddForm({ isOpen, toggle }: AddProps) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DEBUG values={values} />
|
<DEBUG values={values} />
|
||||||
</form>
|
</Form>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</Form>
|
</Formik>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
|
@ -361,11 +362,8 @@ export function IndexerUpdateForm({ isOpen, toggle, indexer }: UpdateProps) {
|
||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
>
|
>
|
||||||
{({ values }: any) => (
|
{({ values }: any) => (
|
||||||
<>
|
<div className="py-6 space-y-6 sm:py-0 sm:space-y-0 divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
<div
|
<div className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
|
||||||
className="py-6 space-y-6 sm:py-0 sm:space-y-0 divide-y divide-gray-200 dark:divide-gray-700">
|
|
||||||
<div
|
|
||||||
className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
|
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor="name"
|
htmlFor="name"
|
||||||
|
@ -375,11 +373,11 @@ export function IndexerUpdateForm({ isOpen, toggle, indexer }: UpdateProps) {
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<Field name="name">
|
<Field name="name">
|
||||||
{({ input, meta }) => (
|
{({ field, meta }: FieldProps) => (
|
||||||
<div className="sm:col-span-2">
|
<div className="sm:col-span-2">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
{...input}
|
{...field}
|
||||||
className="block w-full shadow-sm dark:bg-gray-800 sm:text-sm dark:text-white focus:ring-indigo-500 focus:border-indigo-500 border-gray-300 dark:border-gray-700 rounded-md"
|
className="block w-full shadow-sm dark:bg-gray-800 sm:text-sm dark:text-white focus:ring-indigo-500 focus:border-indigo-500 border-gray-300 dark:border-gray-700 rounded-md"
|
||||||
/>
|
/>
|
||||||
{meta.touched && meta.error &&
|
{meta.touched && meta.error &&
|
||||||
|
@ -390,16 +388,12 @@ export function IndexerUpdateForm({ isOpen, toggle, indexer }: UpdateProps) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200 dark:sm:divide-gray-700">
|
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200 dark:sm:divide-gray-700">
|
||||||
<SwitchGroup name="enabled" label="Enabled" />
|
<SwitchGroupWide name="enabled" label="Enabled" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{renderSettingFields(indexer.settings)}
|
{renderSettingFields(indexer.settings)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</SlideOver>
|
</SlideOver>
|
||||||
)
|
)
|
||||||
}
|
}
|
|
@ -1,19 +1,83 @@
|
||||||
import { useMutation } from "react-query";
|
import { useMutation } from "react-query";
|
||||||
import { Network } from "../../domain/interfaces";
|
import { Channel, Network } from "../../domain/interfaces";
|
||||||
import { XIcon } from "@heroicons/react/solid";
|
import { XIcon } from "@heroicons/react/solid";
|
||||||
import { Field } from "react-final-form";
|
|
||||||
import { SwitchGroup, TextFieldWide } from "../../components/inputs";
|
|
||||||
import { queryClient } from "../../App";
|
import { queryClient } from "../../App";
|
||||||
|
|
||||||
import arrayMutators from "final-form-arrays";
|
import { Field, FieldArray, FieldProps } from "formik";
|
||||||
import { FieldArray } from "react-final-form-arrays";
|
|
||||||
import APIClient from "../../api/APIClient";
|
import APIClient from "../../api/APIClient";
|
||||||
import { NumberFieldWide, PasswordFieldWide } from "../../components/inputs/wide";
|
|
||||||
|
import { TextFieldWide, PasswordFieldWide, SwitchGroupWide, NumberFieldWide } from "../../components/inputs/input_wide";
|
||||||
|
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import Toast from '../../components/notifications/Toast';
|
import Toast from '../../components/notifications/Toast';
|
||||||
import { SlideOver } from "../../components/panels";
|
import { SlideOver } from "../../components/panels";
|
||||||
|
|
||||||
|
function ChannelsFieldArray({ values }: any) {
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<FieldArray name="channels">
|
||||||
|
{({ remove, push }) => (
|
||||||
|
<div className="flex flex-col border-2 border-dashed dark:border-gray-700 p-4">
|
||||||
|
{values && values.channels.length > 0 ? (
|
||||||
|
values.channels.map((_channel: Channel, index: number) => (
|
||||||
|
<div key={index} className="flex justify-between">
|
||||||
|
<div className="flex">
|
||||||
|
<Field name={`channels.${index}.name`}>
|
||||||
|
{({ field }: FieldProps) => (
|
||||||
|
<input
|
||||||
|
{...field}
|
||||||
|
type="text"
|
||||||
|
value={field.value ?? ""}
|
||||||
|
onChange={field.onChange}
|
||||||
|
placeholder="#Channel"
|
||||||
|
className="mr-4 dark:bg-gray-700 focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-600 block w-full shadow-sm sm:text-sm dark:text-white rounded-md"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field name={`channels.${index}.password`}>
|
||||||
|
{({ field }: FieldProps) => (
|
||||||
|
<input
|
||||||
|
{...field}
|
||||||
|
type="text"
|
||||||
|
value={field.value ?? ""}
|
||||||
|
onChange={field.onChange}
|
||||||
|
placeholder="Password"
|
||||||
|
className="mr-4 dark:bg-gray-700 focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-600 block w-full shadow-sm sm:text-sm dark:text-white rounded-md"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="bg-white dark:bg-gray-700 rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
||||||
|
onClick={() => remove(index)}
|
||||||
|
>
|
||||||
|
<span className="sr-only">Remove</span>
|
||||||
|
<XIcon className="h-6 w-6" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<span className="text-center text-sm text-grey-darker dark:text-white">
|
||||||
|
No channels!
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="border dark:border-gray-600 dark:bg-gray-700 my-4 px-4 py-2 text-sm text-gray-700 dark:text-white hover:bg-gray-50 dark:hover:bg-gray-600 rounded self-center text-center"
|
||||||
|
onClick={() => push({ name: "", password: "" })}
|
||||||
|
>
|
||||||
|
Add Channel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</FieldArray>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function IrcNetworkAddForm({ isOpen, toggle }: any) {
|
export function IrcNetworkAddForm({ isOpen, toggle }: any) {
|
||||||
const mutation = useMutation((network: Network) => APIClient.irc.createNetwork(network), {
|
const mutation = useMutation((network: Network) => APIClient.irc.createNetwork(network), {
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
|
@ -66,15 +130,13 @@ export function IrcNetworkAddForm({ isOpen, toggle }: any) {
|
||||||
name: "",
|
name: "",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
server: "",
|
server: "",
|
||||||
|
port: 6667,
|
||||||
tls: false,
|
tls: false,
|
||||||
pass: "",
|
pass: "",
|
||||||
nickserv: {
|
nickserv: {
|
||||||
account: ""
|
account: ""
|
||||||
}
|
},
|
||||||
}
|
channels: [],
|
||||||
|
|
||||||
const mutators = {
|
|
||||||
...arrayMutators
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -85,19 +147,16 @@ export function IrcNetworkAddForm({ isOpen, toggle }: any) {
|
||||||
toggle={toggle}
|
toggle={toggle}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
mutators={mutators}
|
|
||||||
validate={validate}
|
validate={validate}
|
||||||
>
|
>
|
||||||
{() => (
|
{(values) => (
|
||||||
<>
|
<>
|
||||||
|
|
||||||
|
|
||||||
<TextFieldWide name="name" label="Name" placeholder="Name" required={true} />
|
<TextFieldWide name="name" label="Name" placeholder="Name" required={true} />
|
||||||
|
|
||||||
<div className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y dark:divide-gray-700">
|
<div className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y dark:divide-gray-700">
|
||||||
|
|
||||||
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200 dark:sm:divide-gray-700">
|
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200 dark:sm:divide-gray-700">
|
||||||
<SwitchGroup name="enabled" label="Enabled" />
|
<SwitchGroupWide name="enabled" label="Enabled" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
@ -105,7 +164,7 @@ export function IrcNetworkAddForm({ isOpen, toggle }: any) {
|
||||||
<NumberFieldWide name="port" label="Port" placeholder="Eg 6667" required={true} />
|
<NumberFieldWide name="port" label="Port" placeholder="Eg 6667" required={true} />
|
||||||
|
|
||||||
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
||||||
<SwitchGroup name="tls" label="TLS" />
|
<SwitchGroupWide name="tls" label="TLS" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PasswordFieldWide name="pass" label="Password" help="Network password" />
|
<PasswordFieldWide name="pass" label="Password" help="Network password" />
|
||||||
|
@ -117,57 +176,7 @@ export function IrcNetworkAddForm({ isOpen, toggle }: any) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-6">
|
<ChannelsFieldArray values={values} />
|
||||||
|
|
||||||
<FieldArray name="channels">
|
|
||||||
{({ fields }) => (
|
|
||||||
<div className="flex flex-col border-2 border-dashed dark:border-gray-700 p-4">
|
|
||||||
{fields && (fields.length as any) > 0 ? (
|
|
||||||
fields.map((name, index) => (
|
|
||||||
<div key={name} className="flex justify-between">
|
|
||||||
<div className="flex">
|
|
||||||
<Field
|
|
||||||
name={`${name}.name`}
|
|
||||||
component="input"
|
|
||||||
type="text"
|
|
||||||
placeholder="#Channel"
|
|
||||||
className="mr-4 dark:bg-gray-700 focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-600 block w-full shadow-sm sm:text-sm dark:text-white rounded-md"
|
|
||||||
/>
|
|
||||||
<Field
|
|
||||||
name={`${name}.password`}
|
|
||||||
component="input"
|
|
||||||
type="text"
|
|
||||||
placeholder="Password"
|
|
||||||
className="dark:bg-gray-700 focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-600 block w-full shadow-sm sm:text-sm dark:text-white rounded-md"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="bg-white dark:bg-gray-700 rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
|
||||||
onClick={() => fields.remove(index)}
|
|
||||||
>
|
|
||||||
<span className="sr-only">Remove</span>
|
|
||||||
<XIcon className="h-6 w-6" aria-hidden="true" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<span className="text-center text-sm text-grey-darker dark:text-white">
|
|
||||||
No channels!
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="border dark:border-gray-600 dark:bg-gray-700 my-4 px-4 py-2 text-sm text-gray-700 dark:text-white hover:bg-gray-50 dark:hover:bg-gray-600 rounded self-center text-center"
|
|
||||||
onClick={() => fields.push({ name: "", password: "" })}
|
|
||||||
>
|
|
||||||
Add Channel
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</FieldArray>
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</SlideOver>
|
</SlideOver>
|
||||||
|
@ -193,8 +202,6 @@ export function IrcNetworkUpdateForm({ isOpen, toggle, network }: any) {
|
||||||
})
|
})
|
||||||
|
|
||||||
const onSubmit = (data: any) => {
|
const onSubmit = (data: any) => {
|
||||||
console.log(data)
|
|
||||||
|
|
||||||
// easy way to split textarea lines into array of strings for each newline.
|
// easy way to split textarea lines into array of strings for each newline.
|
||||||
// parse on the field didn't really work.
|
// parse on the field didn't really work.
|
||||||
// TODO fix connect_commands on network update
|
// TODO fix connect_commands on network update
|
||||||
|
@ -241,14 +248,9 @@ export function IrcNetworkUpdateForm({ isOpen, toggle, network }: any) {
|
||||||
nickserv: network.nickserv,
|
nickserv: network.nickserv,
|
||||||
pass: network.pass,
|
pass: network.pass,
|
||||||
invite_command: network.invite_command,
|
invite_command: network.invite_command,
|
||||||
// connect_commands: network.connect_commands,
|
|
||||||
channels: network.channels
|
channels: network.channels
|
||||||
}
|
}
|
||||||
|
|
||||||
const mutators = {
|
|
||||||
...arrayMutators
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SlideOver
|
<SlideOver
|
||||||
type="UPDATE"
|
type="UPDATE"
|
||||||
|
@ -258,18 +260,16 @@ export function IrcNetworkUpdateForm({ isOpen, toggle, network }: any) {
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
deleteAction={deleteAction}
|
deleteAction={deleteAction}
|
||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
mutators={mutators}
|
|
||||||
validate={validate}
|
validate={validate}
|
||||||
>
|
>
|
||||||
{() => (
|
{(values) => (
|
||||||
<>
|
<>
|
||||||
|
|
||||||
<TextFieldWide name="name" label="Name" placeholder="Name" required={true} />
|
<TextFieldWide name="name" label="Name" placeholder="Name" required={true} />
|
||||||
|
|
||||||
<div className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y dark:divide-gray-700">
|
<div className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y dark:divide-gray-700">
|
||||||
|
|
||||||
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0">
|
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0">
|
||||||
<SwitchGroup name="enabled" label="Enabled" />
|
<SwitchGroupWide name="enabled" label="Enabled" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
@ -277,7 +277,7 @@ export function IrcNetworkUpdateForm({ isOpen, toggle, network }: any) {
|
||||||
<NumberFieldWide name="port" label="Port" placeholder="Eg 6667" required={true} />
|
<NumberFieldWide name="port" label="Port" placeholder="Eg 6667" required={true} />
|
||||||
|
|
||||||
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
||||||
<SwitchGroup name="tls" label="TLS" />
|
<SwitchGroupWide name="tls" label="TLS" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PasswordFieldWide name="pass" label="Password" help="Network password" />
|
<PasswordFieldWide name="pass" label="Password" help="Network password" />
|
||||||
|
@ -289,57 +289,7 @@ export function IrcNetworkUpdateForm({ isOpen, toggle, network }: any) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-6">
|
<ChannelsFieldArray values={values} />
|
||||||
|
|
||||||
<FieldArray name="channels">
|
|
||||||
{({ fields }) => (
|
|
||||||
<div className="flex flex-col border-2 border-dashed dark:border-gray-700 p-4">
|
|
||||||
{fields && (fields.length as any) > 0 ? (
|
|
||||||
fields.map((name, index) => (
|
|
||||||
<div key={name} className="flex justify-between">
|
|
||||||
<div className="flex">
|
|
||||||
<Field
|
|
||||||
name={`${name}.name`}
|
|
||||||
component="input"
|
|
||||||
type="text"
|
|
||||||
placeholder="#Channel"
|
|
||||||
className="mr-4 dark:bg-gray-700 focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-600 block w-full shadow-sm sm:text-sm dark:text-white rounded-md"
|
|
||||||
/>
|
|
||||||
<Field
|
|
||||||
name={`${name}.password`}
|
|
||||||
component="input"
|
|
||||||
type="text"
|
|
||||||
placeholder="Password"
|
|
||||||
className="dark:bg-gray-700 focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-600 block w-full shadow-sm sm:text-sm dark:text-white rounded-md"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="bg-white dark:bg-gray-700 rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
|
||||||
onClick={() => fields.remove(index)}
|
|
||||||
>
|
|
||||||
<span className="sr-only">Remove</span>
|
|
||||||
<XIcon className="h-6 w-6" aria-hidden="true" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<span className="text-center text-sm text-grey-darker dark:text-white">
|
|
||||||
No channels!
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="border dark:border-gray-600 dark:bg-gray-700 my-4 px-4 py-2 text-sm text-gray-700 dark:text-white hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-blue-500 rounded self-center text-center"
|
|
||||||
onClick={() => fields.push({ name: "", password: "" })}
|
|
||||||
>
|
|
||||||
Add Channel
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</FieldArray>
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</SlideOver>
|
</SlideOver>
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { useTable, useFilters, useGlobalFilter, useSortBy, usePagination } from
|
||||||
import APIClient from '../api/APIClient'
|
import APIClient from '../api/APIClient'
|
||||||
import { useQuery } from 'react-query'
|
import { useQuery } from 'react-query'
|
||||||
import { ReleaseFindResponse, ReleaseStats } from '../domain/interfaces'
|
import { ReleaseFindResponse, ReleaseStats } from '../domain/interfaces'
|
||||||
import { EmptyListState } from '../components/EmptyListState'
|
import { EmptyListState } from '../components/emptystates'
|
||||||
|
|
||||||
export function Dashboard() {
|
export function Dashboard() {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -4,7 +4,7 @@ import IndexerSettings from "./settings/Indexer";
|
||||||
import IrcSettings from "./settings/Irc";
|
import IrcSettings from "./settings/Irc";
|
||||||
import ApplicationSettings from "./settings/Application";
|
import ApplicationSettings from "./settings/Application";
|
||||||
import DownloadClientSettings from "./settings/DownloadClient";
|
import DownloadClientSettings from "./settings/DownloadClient";
|
||||||
import {classNames} from "../styles/utils";
|
import {classNames} from "../utils";
|
||||||
import ActionSettings from "./settings/Action";
|
import ActionSettings from "./settings/Action";
|
||||||
|
|
||||||
const subNavigation = [
|
const subNavigation = [
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import { useMutation } from "react-query";
|
import { useMutation } from "react-query";
|
||||||
import APIClient from "../../api/APIClient";
|
import APIClient from "../../api/APIClient";
|
||||||
import { Form } from "react-final-form";
|
import { Form, Formik } from "formik";
|
||||||
import { PasswordField, TextField } from "../../components/inputs";
|
|
||||||
import { useRecoilState } from "recoil";
|
import { useRecoilState } from "recoil";
|
||||||
import { isLoggedIn } from "../../state/state";
|
import { isLoggedIn } from "../../state/state";
|
||||||
import { useHistory } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import logo from "../../logo.png"
|
import logo from "../../logo.png"
|
||||||
|
import { TextField, PasswordField } from "../../components/inputs";
|
||||||
|
|
||||||
interface loginData {
|
interface loginData {
|
||||||
username: string;
|
username: string;
|
||||||
|
@ -32,9 +32,8 @@ function Login() {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const onSubmit = (data: any, form: any) => {
|
const handleSubmit = (data: any) => {
|
||||||
mutation.mutate(data)
|
mutation.mutate(data)
|
||||||
form.reset()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -50,40 +49,24 @@ function Login() {
|
||||||
<div className="sm:mx-auto sm:w-full sm:max-w-md">
|
<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">
|
<div className="bg-white dark:bg-gray-800 py-8 px-4 shadow sm:rounded-lg sm:px-10">
|
||||||
|
|
||||||
<Form
|
<Formik
|
||||||
initialValues={{
|
initialValues={{
|
||||||
username: "",
|
username: "",
|
||||||
password: "",
|
password: "",
|
||||||
}}
|
}}
|
||||||
onSubmit={onSubmit}
|
onSubmit={handleSubmit}
|
||||||
>
|
>
|
||||||
{({ handleSubmit, values }) => {
|
{() => (
|
||||||
return (
|
<Form>
|
||||||
<form className="space-y-6" onSubmit={handleSubmit}>
|
|
||||||
<TextField name="username" label="Username" autoComplete="username" />
|
|
||||||
<PasswordField name="password" label="password" autoComplete="current-password" />
|
|
||||||
|
|
||||||
{/*<div className="flex items-center justify-between">*/}
|
<div className="space-y-6">
|
||||||
{/* <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="text-sm">*/}
|
<TextField name="username" label="Username" columns={6} autoComplete="username" />
|
||||||
{/* <a href="#" className="font-medium text-indigo-600 hover:text-indigo-500">*/}
|
<PasswordField name="password" label="Password" columns={6} autoComplete="current-password" />
|
||||||
{/* Forgot your password?*/}
|
</div>
|
||||||
{/* </a>*/}
|
|
||||||
{/* </div>*/}
|
|
||||||
{/*</div>*/}
|
|
||||||
|
|
||||||
<div>
|
|
||||||
|
<div className="mt-6">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
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"
|
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"
|
||||||
|
@ -91,10 +74,9 @@ function Login() {
|
||||||
Sign in
|
Sign in
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</Form>
|
</Form>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Fragment, useRef } from "react";
|
import { Fragment, useRef } from "react";
|
||||||
import { Dialog, Transition, Switch as SwitchBasic } from "@headlessui/react";
|
import { Dialog, Transition, Switch as SwitchBasic } from "@headlessui/react";
|
||||||
import { ChevronDownIcon, ChevronRightIcon, ExclamationIcon, } from '@heroicons/react/solid'
|
import { ChevronDownIcon, ChevronRightIcon, ExclamationIcon, } from '@heroicons/react/solid'
|
||||||
import { EmptyListState } from "../../components/EmptyListState";
|
import { EmptyListState } from "../../components/emptystates";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
NavLink,
|
NavLink,
|
||||||
|
@ -17,14 +17,12 @@ import { useToggle } from "../../hooks/hooks";
|
||||||
import { useMutation, useQuery } from "react-query";
|
import { useMutation, useQuery } from "react-query";
|
||||||
import { queryClient } from "../../App";
|
import { queryClient } from "../../App";
|
||||||
import { CONTAINER_OPTIONS, CODECS_OPTIONS, RESOLUTION_OPTIONS, SOURCES_OPTIONS, ActionTypeNameMap, ActionTypeOptions } from "../../domain/constants";
|
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 DEBUG from "../../components/debug";
|
||||||
import TitleSubtitle from "../../components/headings/TitleSubtitle";
|
import { TitleSubtitle } from "../../components/headings";
|
||||||
import { classNames } from "../../styles/utils";
|
import { buildPath, classNames } from "../../utils";
|
||||||
import SelectM from "react-select";
|
import SelectM from "react-select";
|
||||||
import APIClient from "../../api/APIClient";
|
import APIClient from "../../api/APIClient";
|
||||||
import { buildPath } from "../../utils/utils"
|
|
||||||
|
|
||||||
import { toast } from 'react-hot-toast'
|
import { toast } from 'react-hot-toast'
|
||||||
import Toast from '../../components/notifications/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 { Field, FieldArray, Form, Formik } from "formik";
|
||||||
import { AlertWarning } from "../../components/alerts";
|
import { AlertWarning } from "../../components/alerts";
|
||||||
import { DeleteModal } from "../../components/modals";
|
import { DeleteModal } from "../../components/modals";
|
||||||
|
import { NumberField, TextField, SwitchGroup, Select, MultiSelect, DownloadClientSelect } from "../../components/inputs";
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ name: 'General', href: '', current: true },
|
{ name: 'General', href: '', current: true },
|
||||||
|
@ -66,93 +65,21 @@ function TabNavLink({ item, url }: any) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const FormButtonsGroup = ({ deleteAction, reset, dirty }: any) => {
|
const FormButtonsGroup = ({ values, deleteAction, reset, dirty }: any) => {
|
||||||
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false)
|
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
|
||||||
|
|
||||||
const cancelButtonRef = useRef(null)
|
const cancelModalButtonRef = useRef(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pt-6 divide-y divide-gray-200 dark:divide-gray-700">
|
<div className="pt-6 divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
<DeleteModal
|
||||||
<Transition.Root show={deleteModalIsOpen} as={Fragment}>
|
isOpen={deleteModalIsOpen}
|
||||||
<Dialog
|
toggle={toggleDeleteModal}
|
||||||
as="div"
|
buttonRef={cancelModalButtonRef}
|
||||||
static
|
deleteAction={deleteAction}
|
||||||
className="fixed z-10 inset-0 overflow-y-auto"
|
title={`Remove filter: ${values.name}`}
|
||||||
initialFocus={cancelButtonRef}
|
text="Are you sure you want to remove this filter? This action cannot be undone."
|
||||||
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>
|
|
||||||
|
|
||||||
<div className="mt-4 pt-4 flex justify-between">
|
<div className="mt-4 pt-4 flex justify-between">
|
||||||
<button
|
<button
|
||||||
|
@ -234,8 +161,6 @@ export default function FilterDetails() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSubmit = (data: any) => {
|
const handleSubmit = (data: any) => {
|
||||||
console.log("submit other");
|
|
||||||
|
|
||||||
updateMutation.mutate(data)
|
updateMutation.mutate(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -329,7 +254,7 @@ export default function FilterDetails() {
|
||||||
}}
|
}}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
>
|
>
|
||||||
{({ isSubmitting, values, dirty, resetForm }) => (
|
{({ values, dirty, resetForm }) => (
|
||||||
<Form>
|
<Form>
|
||||||
<RouteSwitch>
|
<RouteSwitch>
|
||||||
<Route exact path={url}>
|
<Route exact path={url}>
|
||||||
|
@ -349,7 +274,7 @@ export default function FilterDetails() {
|
||||||
</Route>
|
</Route>
|
||||||
</RouteSwitch>
|
</RouteSwitch>
|
||||||
|
|
||||||
<FormButtonsGroup deleteAction={deleteAction} dirty={dirty} reset={resetForm} />
|
<FormButtonsGroup values={values} deleteAction={deleteAction} dirty={dirty} reset={resetForm} />
|
||||||
|
|
||||||
<DEBUG values={values} />
|
<DEBUG values={values} />
|
||||||
</Form>
|
</Form>
|
||||||
|
@ -424,7 +349,7 @@ function General({ indexers }: GeneralProps) {
|
||||||
<div className="mt-6 grid grid-cols-12 gap-6">
|
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||||
<TextField name="min_size" label="Min size" columns={6} placeholder="" />
|
<TextField name="min_size" label="Min size" columns={6} placeholder="" />
|
||||||
<TextField name="max_size" label="Max 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>
|
||||||
</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">
|
<div className="light:bg-white dark:bg-gray-800 light:shadow sm:rounded-md">
|
||||||
{values.actions.length > 0 ?
|
{values.actions.length > 0 ?
|
||||||
<ul className="divide-y divide-gray-200 dark:divide-gray-700">
|
<ul className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
{values.actions.map((action: any, index: any) => (
|
{values.actions.map((action: any, index: number) => (
|
||||||
<FilterActionsItem action={action} clients={data!} idx={index} remove={remove} />
|
<FilterActionsItem action={action} clients={data!} idx={index} remove={remove} key={index} />
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
: <EmptyListState text="No actions yet!" />
|
: <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,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 { useState } from "react";
|
||||||
import { Switch } from "@headlessui/react";
|
import { Switch } from "@headlessui/react";
|
||||||
import { EmptyListState } from "../../components/EmptyListState";
|
import { EmptyListState } from "../../components/emptystates";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Link,
|
Link,
|
||||||
|
@ -8,7 +8,7 @@ import {
|
||||||
import { Filter } from "../../domain/interfaces";
|
import { Filter } from "../../domain/interfaces";
|
||||||
import { useToggle } from "../../hooks/hooks";
|
import { useToggle } from "../../hooks/hooks";
|
||||||
import { useQuery } from "react-query";
|
import { useQuery } from "react-query";
|
||||||
import { classNames } from "../../styles/utils";
|
import { classNames } from "../../utils";
|
||||||
import { FilterAddForm } from "../../forms";
|
import { FilterAddForm } from "../../forms";
|
||||||
import APIClient from "../../api/APIClient";
|
import APIClient from "../../api/APIClient";
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
import React, {useState} from "react";
|
import React, { useState } from "react";
|
||||||
import {Switch} from "@headlessui/react";
|
import { Switch } from "@headlessui/react";
|
||||||
import { classNames } from "../../styles/utils";
|
import { classNames } from "../../utils";
|
||||||
// import {useRecoilState} from "recoil";
|
// import {useRecoilState} from "recoil";
|
||||||
// import {configState} from "../../state/state";
|
// import {configState} from "../../state/state";
|
||||||
import {useQuery} from "react-query";
|
import { useQuery } from "react-query";
|
||||||
import {Config} from "../../domain/interfaces";
|
import { Config } from "../../domain/interfaces";
|
||||||
import APIClient from "../../api/APIClient";
|
import APIClient from "../../api/APIClient";
|
||||||
|
|
||||||
function ApplicationSettings() {
|
function ApplicationSettings() {
|
||||||
const [isDebug, setIsDebug] = useState(true)
|
const [isDebug, setIsDebug] = useState(true)
|
||||||
// const [config] = useRecoilState(configState)
|
// 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,
|
retry: false,
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
|
@ -84,8 +84,7 @@ function ApplicationSettings() {
|
||||||
<ul className="mt-2 divide-y divide-gray-200">
|
<ul className="mt-2 divide-y divide-gray-200">
|
||||||
<Switch.Group as="li" className="py-4 flex items-center justify-between">
|
<Switch.Group as="li" className="py-4 flex items-center justify-between">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<Switch.Label as="p" className="text-sm font-medium text-gray-900 dark:text-white"
|
<Switch.Label as="p" className="text-sm font-medium text-gray-900 dark:text-white" passive>
|
||||||
passive>
|
|
||||||
Debug
|
Debug
|
||||||
</Switch.Label>
|
</Switch.Label>
|
||||||
<Switch.Description className="text-sm text-gray-500 dark:text-gray-400">
|
<Switch.Description className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
@ -94,6 +93,7 @@ function ApplicationSettings() {
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
checked={isDebug}
|
checked={isDebug}
|
||||||
|
disabled={true}
|
||||||
onChange={setIsDebug}
|
onChange={setIsDebug}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
isDebug ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-700',
|
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 { useToggle } from "../../hooks/hooks";
|
||||||
import { Switch } from "@headlessui/react";
|
import { Switch } from "@headlessui/react";
|
||||||
import { useQuery } from "react-query";
|
import { useQuery } from "react-query";
|
||||||
import { classNames } from "../../styles/utils";
|
import { classNames } from "../../utils";
|
||||||
import { DownloadClientAddForm, DownloadClientUpdateForm } from "../../forms";
|
import { DownloadClientAddForm, DownloadClientUpdateForm } from "../../forms";
|
||||||
import EmptySimple from "../../components/empty/EmptySimple";
|
import { EmptySimple } from "../../components/emptystates";
|
||||||
import APIClient from "../../api/APIClient";
|
import APIClient from "../../api/APIClient";
|
||||||
import { DownloadClientTypeNameMap } from "../../domain/constants";
|
import { DownloadClientTypeNameMap } from "../../domain/constants";
|
||||||
|
|
||||||
interface DownloadLClientSettingsListItemProps {
|
interface DLSettingsItemProps {
|
||||||
client: DownloadClient;
|
client: DownloadClient;
|
||||||
idx: number;
|
idx: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DownloadClientSettingsListItem({ client, idx }: DownloadLClientSettingsListItemProps) {
|
function DownloadClientSettingsListItem({ client, idx }: DLSettingsItemProps) {
|
||||||
const [updateClientIsOpen, toggleUpdateClient] = useToggle(false)
|
const [updateClientIsOpen, toggleUpdateClient] = useToggle(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr key={client.name} className={idx % 2 === 0 ? 'light:bg-white' : 'light:bg-gray-50'}>
|
<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">
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||||
<Switch
|
<Switch
|
||||||
checked={client.enabled}
|
checked={client.enabled}
|
||||||
|
@ -65,9 +64,7 @@ function DownloadClientSettings() {
|
||||||
return (
|
return (
|
||||||
<div className="divide-y divide-gray-200 lg:col-span-9">
|
<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="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">
|
<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} />
|
: <EmptySimple title="No download clients" subtitle="Add a new client" buttonText="New client" buttonAction={toggleAddClient} />
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,8 @@ import { useQuery } from "react-query";
|
||||||
import { IndexerAddForm, IndexerUpdateForm } from "../../forms";
|
import { IndexerAddForm, IndexerUpdateForm } from "../../forms";
|
||||||
import { Indexer } from "../../domain/interfaces";
|
import { Indexer } from "../../domain/interfaces";
|
||||||
import { Switch } from "@headlessui/react";
|
import { Switch } from "@headlessui/react";
|
||||||
import { classNames } from "../../styles/utils";
|
import { classNames } from "../../utils";
|
||||||
import EmptySimple from "../../components/empty/EmptySimple";
|
import { EmptySimple } from "../../components/emptystates";
|
||||||
import APIClient from "../../api/APIClient";
|
import APIClient from "../../api/APIClient";
|
||||||
|
|
||||||
const ListItem = ({ indexer }: any) => {
|
const ListItem = ({ indexer }: any) => {
|
||||||
|
|
|
@ -3,8 +3,8 @@ import { IrcNetworkAddForm, IrcNetworkUpdateForm } from "../../forms";
|
||||||
import { useToggle } from "../../hooks/hooks";
|
import { useToggle } from "../../hooks/hooks";
|
||||||
import { useQuery } from "react-query";
|
import { useQuery } from "react-query";
|
||||||
import { Switch } from "@headlessui/react";
|
import { Switch } from "@headlessui/react";
|
||||||
import { classNames } from "../../styles/utils";
|
import { classNames } from "../../utils";
|
||||||
import EmptySimple from "../../components/empty/EmptySimple";
|
import { EmptySimple } from "../../components/emptystates";
|
||||||
import APIClient from "../../api/APIClient";
|
import APIClient from "../../api/APIClient";
|
||||||
|
|
||||||
interface IrcNetwork {
|
interface IrcNetwork {
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
// concatenate classes
|
|
||||||
export function classNames(...classes: string[]) {
|
|
||||||
return classes.filter(Boolean).join(' ')
|
|
||||||
}
|
|
||||||
|
|
||||||
// column widths for inputs etc
|
|
||||||
export type COL_WIDTHS = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
|
|
|
@ -50,3 +50,10 @@ export function buildPath(...args: string[]): string {
|
||||||
|
|
||||||
return firstTrimmed === '/' ? `/${result}` : result;
|
return firstTrimmed === '/' ? `/${result}` : result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function classNames(...classes: string[]) {
|
||||||
|
return classes.filter(Boolean).join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
// column widths for inputs etc
|
||||||
|
export type COL_WIDTHS = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
|
|
@ -1165,13 +1165,20 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
regenerator-runtime "^0.13.4"
|
regenerator-runtime "^0.13.4"
|
||||||
|
|
||||||
"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.3", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
|
"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.3", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
|
||||||
version "7.16.3"
|
version "7.16.3"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.3.tgz#b86f0db02a04187a3c17caa77de69840165d42d5"
|
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.3.tgz#b86f0db02a04187a3c17caa77de69840165d42d5"
|
||||||
integrity sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==
|
integrity sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
regenerator-runtime "^0.13.4"
|
regenerator-runtime "^0.13.4"
|
||||||
|
|
||||||
|
"@babel/runtime@^7.12.0", "@babel/runtime@^7.13.10", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7":
|
||||||
|
version "7.16.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.5.tgz#7f3e34bf8bdbbadf03fbb7b1ea0d929569c9487a"
|
||||||
|
integrity sha512-TXWihFIS3Pyv5hzR7j6ihmeLkZfrXGxAr5UfSl8CHf+6q/wpiYDkUau0czckpYG8QmnCIuPpdLtuA9VmuGGyMA==
|
||||||
|
dependencies:
|
||||||
|
regenerator-runtime "^0.13.4"
|
||||||
|
|
||||||
"@babel/template@^7.10.4", "@babel/template@^7.16.0", "@babel/template@^7.3.3":
|
"@babel/template@^7.10.4", "@babel/template@^7.16.0", "@babel/template@^7.3.3":
|
||||||
version "7.16.0"
|
version "7.16.0"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.0.tgz#d16a35ebf4cd74e202083356fab21dd89363ddd6"
|
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.0.tgz#d16a35ebf4cd74e202083356fab21dd89363ddd6"
|
||||||
|
@ -1238,16 +1245,16 @@
|
||||||
resolved "https://registry.yarnpkg.com/@csstools/normalize.css/-/normalize.css-10.1.0.tgz#f0950bba18819512d42f7197e56c518aa491cf18"
|
resolved "https://registry.yarnpkg.com/@csstools/normalize.css/-/normalize.css-10.1.0.tgz#f0950bba18819512d42f7197e56c518aa491cf18"
|
||||||
integrity sha512-ij4wRiunFfaJxjB0BdrYHIH8FxBJpOwNPhhAcunlmPdXudL1WQV1qoP9un6JsEBAgQH+7UXyyjh0g7jTxXK6tg==
|
integrity sha512-ij4wRiunFfaJxjB0BdrYHIH8FxBJpOwNPhhAcunlmPdXudL1WQV1qoP9un6JsEBAgQH+7UXyyjh0g7jTxXK6tg==
|
||||||
|
|
||||||
"@emotion/cache@^11.4.0", "@emotion/cache@^11.6.0":
|
"@emotion/cache@^11.4.0", "@emotion/cache@^11.7.1":
|
||||||
version "11.6.0"
|
version "11.7.1"
|
||||||
resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.6.0.tgz#65fbdbbe4382f1991d8b20853c38e63ecccec9a1"
|
resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.7.1.tgz#08d080e396a42e0037848214e8aa7bf879065539"
|
||||||
integrity sha512-ElbsWY1KMwEowkv42vGo0UPuLgtPYfIs9BxxVrmvsaJVvktknsHYYlx5NQ5g6zLDcOTyamlDc7FkRg2TAcQDKQ==
|
integrity sha512-r65Zy4Iljb8oyjtLeCuBH8Qjiy107dOYC6SJq7g7GV5UCQWMObY4SJDPGFjiiVpPrOJ2hmJOoBiYTC7hwx9E2A==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@emotion/memoize" "^0.7.4"
|
"@emotion/memoize" "^0.7.4"
|
||||||
"@emotion/sheet" "^1.1.0"
|
"@emotion/sheet" "^1.1.0"
|
||||||
"@emotion/utils" "^1.0.0"
|
"@emotion/utils" "^1.0.0"
|
||||||
"@emotion/weak-memoize" "^0.2.5"
|
"@emotion/weak-memoize" "^0.2.5"
|
||||||
stylis "^4.0.10"
|
stylis "4.0.13"
|
||||||
|
|
||||||
"@emotion/hash@^0.8.0":
|
"@emotion/hash@^0.8.0":
|
||||||
version "0.8.0"
|
version "0.8.0"
|
||||||
|
@ -1260,12 +1267,12 @@
|
||||||
integrity sha512-igX9a37DR2ZPGYtV6suZ6whr8pTFtyHL3K/oLUotxpSVO2ASaprmAe2Dkq7tBo7CRY7MMDrAa9nuQP9/YG8FxQ==
|
integrity sha512-igX9a37DR2ZPGYtV6suZ6whr8pTFtyHL3K/oLUotxpSVO2ASaprmAe2Dkq7tBo7CRY7MMDrAa9nuQP9/YG8FxQ==
|
||||||
|
|
||||||
"@emotion/react@^11.1.1":
|
"@emotion/react@^11.1.1":
|
||||||
version "11.6.0"
|
version "11.7.1"
|
||||||
resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.6.0.tgz#61fcb95c1e01255734c2c721cb9beabcf521eb0f"
|
resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.7.1.tgz#3f800ce9b20317c13e77b8489ac4a0b922b2fe07"
|
||||||
integrity sha512-23MnRZFBN9+D1lHXC5pD6z4X9yhPxxtHr6f+iTGz6Fv6Rda0GdefPrsHL7otsEf+//7uqCdT5QtHeRxHCERzuw==
|
integrity sha512-DV2Xe3yhkF1yT4uAUoJcYL1AmrnO5SVsdfvu+fBuS7IbByDeTVx9+wFmvx9Idzv7/78+9Mgx2Hcmr7Fex3tIyw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.13.10"
|
"@babel/runtime" "^7.13.10"
|
||||||
"@emotion/cache" "^11.6.0"
|
"@emotion/cache" "^11.7.1"
|
||||||
"@emotion/serialize" "^1.0.2"
|
"@emotion/serialize" "^1.0.2"
|
||||||
"@emotion/sheet" "^1.1.0"
|
"@emotion/sheet" "^1.1.0"
|
||||||
"@emotion/utils" "^1.0.0"
|
"@emotion/utils" "^1.0.0"
|
||||||
|
@ -5318,18 +5325,6 @@ fill-range@^7.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
to-regex-range "^5.0.1"
|
to-regex-range "^5.0.1"
|
||||||
|
|
||||||
final-form-arrays@^3.0.2:
|
|
||||||
version "3.0.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/final-form-arrays/-/final-form-arrays-3.0.2.tgz#9f3bef778dec61432357744eb6f3abef7e7f3847"
|
|
||||||
integrity sha512-TfO8aZNz3RrsZCDx8GHMQcyztDNpGxSSi9w4wpSNKlmv2PfFWVVM8P7Yj5tj4n0OWax+x5YwTLhT5BnqSlCi+w==
|
|
||||||
|
|
||||||
final-form@^4.20.2:
|
|
||||||
version "4.20.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/final-form/-/final-form-4.20.4.tgz#8d59e36d3248a227265cc731d76c0564dd2606f6"
|
|
||||||
integrity sha512-hyoOVVilPLpkTvgi+FSJkFZrh0Yhy4BhE6lk/NiBwrF4aRV8/ykKEyXYvQH/pfUbRkOosvpESYouFb+FscsLrw==
|
|
||||||
dependencies:
|
|
||||||
"@babel/runtime" "^7.10.0"
|
|
||||||
|
|
||||||
finalhandler@~1.1.2:
|
finalhandler@~1.1.2:
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d"
|
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d"
|
||||||
|
@ -9681,20 +9676,6 @@ react-fast-compare@^2.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"
|
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"
|
||||||
integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==
|
integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==
|
||||||
|
|
||||||
react-final-form-arrays@^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"
|
|
||||||
integrity sha512-dzBiLfbr9l1YRExARBpJ8uA/djBenCvFrbrsXjd362joDl3vT+WhmMKKr6HDQMJffjA8T4gZ3n5+G9M59yZfuQ==
|
|
||||||
dependencies:
|
|
||||||
"@babel/runtime" "^7.12.1"
|
|
||||||
|
|
||||||
react-final-form@^6.5.3:
|
|
||||||
version "6.5.7"
|
|
||||||
resolved "https://registry.yarnpkg.com/react-final-form/-/react-final-form-6.5.7.tgz#0c1098accf0f0011adee5a46076ed1b99ed1b1ea"
|
|
||||||
integrity sha512-o7tvJXB+McGiXOILqIC8lnOcX4aLhIBiF/Xi9Qet35b7XOS8R7KL8HLRKTfnZWQJm6MCE15v1U0SFive0NcxyA==
|
|
||||||
dependencies:
|
|
||||||
"@babel/runtime" "^7.15.4"
|
|
||||||
|
|
||||||
react-hot-toast@^2.1.1:
|
react-hot-toast@^2.1.1:
|
||||||
version "2.1.1"
|
version "2.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-hot-toast/-/react-hot-toast-2.1.1.tgz#56409ab406b534e9e58274cf98d80355ba0fdda0"
|
resolved "https://registry.yarnpkg.com/react-hot-toast/-/react-hot-toast-2.1.1.tgz#56409ab406b534e9e58274cf98d80355ba0fdda0"
|
||||||
|
@ -11015,10 +10996,10 @@ stylehacks@^4.0.0:
|
||||||
postcss "^7.0.0"
|
postcss "^7.0.0"
|
||||||
postcss-selector-parser "^3.0.0"
|
postcss-selector-parser "^3.0.0"
|
||||||
|
|
||||||
stylis@^4.0.10:
|
stylis@4.0.13:
|
||||||
version "4.0.10"
|
version "4.0.13"
|
||||||
resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.0.10.tgz#446512d1097197ab3f02fb3c258358c3f7a14240"
|
resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.0.13.tgz#f5db332e376d13cc84ecfe5dace9a2a51d954c91"
|
||||||
integrity sha512-m3k+dk7QeJw660eIKRRn3xPF6uuvHs/FFzjX3HQ5ove0qYsiygoAhwn5a3IYKaZPo5LrYD0rfVmtv1gNY1uYwg==
|
integrity sha512-xGPXiFVl4YED9Jh7Euv2V220mriG9u4B2TA6Ybjc1catrstKD2PpIdU3U0RKpkVBC2EhmL/F0sPCr9vrFTNRag==
|
||||||
|
|
||||||
supports-color@^5.3.0, supports-color@^5.4.0:
|
supports-color@^5.3.0, supports-color@^5.4.0:
|
||||||
version "5.5.0"
|
version "5.5.0"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue