mirror of
https://github.com/idanoo/autobrr
synced 2025-07-25 17:59:14 +00:00
Refactor(web): Replace final-form with Formik and cleanup (#46)
* refactor: begin to replace final-form * refactor: replace final-form with formik n cleanup
This commit is contained in:
parent
c4d580eb03
commit
5e29564f03
66 changed files with 1523 additions and 3409 deletions
|
@ -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 React from "react";
|
||||
|
||||
interface props {
|
||||
title: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
function AlertWarning({ title, text }: props) {
|
||||
export function AlertWarning({ title, text }: props) {
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div className="rounded-md bg-yellow-50 p-4">
|
||||
|
@ -28,5 +28,3 @@ function AlertWarning({ title, text }: props) {
|
|||
</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") {
|
||||
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 {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
}
|
||||
|
||||
const TitleSubtitle: React.FC<Props> = ({ title, subtitle }) => (
|
||||
export const TitleSubtitle: FC<Props> = ({ title, subtitle }) => (
|
||||
<div>
|
||||
<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>
|
||||
</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 { default as TextFieldWide } from "./TextFieldWide";
|
||||
export { default as PasswordField } from "./PasswordField";
|
||||
export { default as TextAreaWide } from "./TextAreaWide";
|
||||
export { default as MultiSelectField } from "./MultiSelectField";
|
||||
export { default as RadioFieldset } from "./RadioFieldset";
|
||||
export { default as SwitchGroup } from "./SwitchGroup";
|
||||
export { ErrorField } from "./common";
|
||||
export { TextField, NumberField, PasswordField } from "./input";
|
||||
export { NumberFieldWide, PasswordFieldWide, SwitchGroupWide, TextFieldWide } from "./input_wide";
|
||||
export { RadioFieldsetWide } from "./radio";
|
||||
export { DownloadClientSelect, MultiSelect, Select} from "./select";
|
||||
export { SwitchGroup } from "./switch";
|
||||
|
||||
|
|
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 }
|
145
web/src/components/inputs/switch.tsx
Normal file
145
web/src/components/inputs/switch.tsx
Normal file
|
@ -0,0 +1,145 @@
|
|||
import React, { InputHTMLAttributes } from 'react'
|
||||
import { Switch as HeadlessSwitch } from '@headlessui/react'
|
||||
import { FieldInputProps, FieldMetaProps, FieldProps, FormikProps, FormikValues, Field } from 'formik'
|
||||
import { classNames } from "../../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} />
|
||||
|
||||
interface SwitchGroupProps {
|
||||
name: string;
|
||||
label?: string;
|
||||
description?: string;
|
||||
defaultValue?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const SwitchGroup: React.FC<SwitchGroupProps> = ({ name, label, description, defaultValue }) => (
|
||||
<ul className="mt-2 divide-y divide-gray-200">
|
||||
<HeadlessSwitch.Group as="li" className="py-4 flex items-center justify-between">
|
||||
{label && <div className="flex flex-col">
|
||||
<HeadlessSwitch.Label as="p" className="text-sm font-medium text-gray-900 dark:text-gray-100"
|
||||
passive>
|
||||
{label}
|
||||
</HeadlessSwitch.Label>
|
||||
{description && (
|
||||
<HeadlessSwitch.Description className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{description}
|
||||
</HeadlessSwitch.Description>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
|
||||
<Field name={name} type="checkbox">
|
||||
{({
|
||||
field,
|
||||
form: { setFieldValue },
|
||||
}: any) => (
|
||||
<Switch
|
||||
{...field}
|
||||
type="button"
|
||||
value={field.value}
|
||||
checked={field.checked}
|
||||
onChange={value => {
|
||||
setFieldValue(field?.name ?? '', value)
|
||||
}}
|
||||
className={classNames(
|
||||
field.value ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200',
|
||||
'ml-4 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500'
|
||||
)}
|
||||
>
|
||||
{/* <span className="sr-only">{label}</span> */}
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={classNames(
|
||||
field.value ? 'translate-x-5' : 'translate-x-0',
|
||||
'inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200'
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
|
||||
)}
|
||||
</Field>
|
||||
|
||||
{/* <Field
|
||||
name={name}
|
||||
defaultValue={defaultValue as any}
|
||||
render={({input: {onChange, checked, value}}) => (
|
||||
<Switch
|
||||
value={value}
|
||||
checked={value}
|
||||
onChange={onChange}
|
||||
className={classNames(
|
||||
value ? 'bg-teal-500' : 'bg-gray-200',
|
||||
'ml-4 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500'
|
||||
)}
|
||||
>
|
||||
<span className="sr-only">Use setting</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={classNames(
|
||||
value ? 'translate-x-5' : 'translate-x-0',
|
||||
'inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200'
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
)}
|
||||
/> */}
|
||||
</HeadlessSwitch.Group>
|
||||
</ul>
|
||||
)
|
||||
|
||||
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 { ExclamationIcon } from "@heroicons/react/solid";
|
||||
|
||||
interface props {
|
||||
interface DeleteModalProps {
|
||||
isOpen: boolean;
|
||||
buttonRef: any;
|
||||
toggle: any;
|
||||
|
@ -11,7 +11,7 @@ interface props {
|
|||
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}>
|
||||
<Dialog
|
||||
as="div"
|
||||
|
@ -86,6 +86,4 @@ const DeleteModal = ({ isOpen, buttonRef, toggle, deleteAction, title, text }: p
|
|||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
)
|
||||
|
||||
export default DeleteModal;
|
||||
)
|
|
@ -1,7 +1,7 @@
|
|||
import { FC } from 'react'
|
||||
import { XIcon, CheckCircleIcon, ExclamationIcon, ExclamationCircleIcon } from '@heroicons/react/solid'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import { classNames } from '../../styles/utils'
|
||||
import { classNames } from '../../utils'
|
||||
|
||||
type Props = {
|
||||
type: 'error' | 'success' | 'warning'
|
||||
|
@ -9,45 +9,39 @@ type Props = {
|
|||
t?: any;
|
||||
}
|
||||
|
||||
const Toast: FC<Props> = ({
|
||||
type,
|
||||
body,
|
||||
t
|
||||
}) => {
|
||||
return (
|
||||
<div className={classNames(
|
||||
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")}>
|
||||
<div className="p-4">
|
||||
<div className="flex items-start">
|
||||
<div className="flex-shrink-0">
|
||||
{type === 'success' && <CheckCircleIcon className="h-6 w-6 text-green-400" aria-hidden="true" />}
|
||||
{type === 'error' && <ExclamationCircleIcon className="h-6 w-6 text-red-400" aria-hidden="true" />}
|
||||
{type === 'warning' && <ExclamationIcon className="h-6 w-6 text-yellow-400" aria-hidden="true" />}
|
||||
</div>
|
||||
<div className="ml-3 w-0 flex-1 pt-0.5">
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-gray-200">
|
||||
{type === 'success' && "Success"}
|
||||
{type === 'error' && "Error"}
|
||||
{type === 'warning' && "Warning"}
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">{body}</p>
|
||||
</div>
|
||||
<div className="ml-4 flex-shrink-0 flex">
|
||||
<button
|
||||
className="bg-white dark:bg-gray-700 rounded-md inline-flex text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
||||
onClick={() => {
|
||||
toast.dismiss(t.id)
|
||||
}}
|
||||
>
|
||||
<span className="sr-only">Close</span>
|
||||
<XIcon className="h-5 w-5" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
const Toast: FC<Props> = ({ type, body, t }) => (
|
||||
<div className={classNames(
|
||||
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")}>
|
||||
<div className="p-4">
|
||||
<div className="flex items-start">
|
||||
<div className="flex-shrink-0">
|
||||
{type === 'success' && <CheckCircleIcon className="h-6 w-6 text-green-400" aria-hidden="true" />}
|
||||
{type === 'error' && <ExclamationCircleIcon className="h-6 w-6 text-red-400" aria-hidden="true" />}
|
||||
{type === 'warning' && <ExclamationIcon className="h-6 w-6 text-yellow-400" aria-hidden="true" />}
|
||||
</div>
|
||||
<div className="ml-3 w-0 flex-1 pt-0.5">
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-gray-200">
|
||||
{type === 'success' && "Success"}
|
||||
{type === 'error' && "Error"}
|
||||
{type === 'warning' && "Warning"}
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">{body}</p>
|
||||
</div>
|
||||
<div className="ml-4 flex-shrink-0 flex">
|
||||
<button
|
||||
className="bg-white dark:bg-gray-700 rounded-md inline-flex text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
||||
onClick={() => {
|
||||
toast.dismiss(t.id)
|
||||
}}
|
||||
>
|
||||
<span className="sr-only">Close</span>
|
||||
<XIcon className="h-5 w-5" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
|
||||
export default Toast;
|
|
@ -1 +0,0 @@
|
|||
export { default as SlideOver } from "./SlideOver";
|
|
@ -1,16 +1,15 @@
|
|||
import { Fragment, useRef } from "react";
|
||||
import { XIcon } from "@heroicons/react/solid";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { Form } from "react-final-form";
|
||||
import DEBUG from "../../components/debug";
|
||||
import { Form, Formik } from "formik";
|
||||
import DEBUG from "../debug";
|
||||
import { useToggle } from "../../hooks/hooks";
|
||||
import { DeleteModal } from "../../components/modals";
|
||||
import { classNames } from "../../styles/utils";
|
||||
import { DeleteModal } from "../modals";
|
||||
import { classNames } from "../../utils";
|
||||
|
||||
interface props {
|
||||
interface SlideOverProps {
|
||||
title: string;
|
||||
initialValues: any;
|
||||
mutators?: any;
|
||||
validate?: any;
|
||||
onSubmit: any;
|
||||
isOpen: boolean;
|
||||
|
@ -20,7 +19,7 @@ interface props {
|
|||
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 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">
|
||||
|
||||
<Form
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
mutators={mutators}
|
||||
onSubmit={onSubmit}
|
||||
validate={validate}
|
||||
>
|
||||
{({ handleSubmit, values }) => {
|
||||
return (
|
||||
<form className="h-full flex flex-col bg-white dark:bg-gray-800 shadow-xl overflow-y-scroll"
|
||||
{({ handleSubmit, values }) => (
|
||||
<Form className="h-full flex flex-col bg-white dark:bg-gray-800 shadow-xl overflow-y-scroll"
|
||||
onSubmit={handleSubmit}>
|
||||
|
||||
<div className="flex-1">
|
||||
|
@ -90,7 +87,6 @@ function SlideOver({ title, initialValues, mutators, validate, onSubmit, deleteA
|
|||
{children !== undefined && children(values)}
|
||||
</div>
|
||||
|
||||
|
||||
<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" && (
|
||||
|
@ -122,10 +118,9 @@ function SlideOver({ title, initialValues, mutators, validate, onSubmit, deleteA
|
|||
</div>
|
||||
|
||||
<DEBUG values={values} />
|
||||
</form>
|
||||
)
|
||||
}}
|
||||
</Form>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
|
||||
</div>
|
||||
|
||||
|
@ -137,4 +132,4 @@ function SlideOver({ title, initialValues, mutators, validate, onSubmit, deleteA
|
|||
)
|
||||
}
|
||||
|
||||
export default SlideOver;
|
||||
export { SlideOver };
|
Loading…
Add table
Add a link
Reference in a new issue