diff options
Diffstat (limited to 'packages/exchange-backoffice-ui/src/handlers')
17 files changed, 0 insertions, 1561 deletions
diff --git a/packages/exchange-backoffice-ui/src/handlers/Caption.tsx b/packages/exchange-backoffice-ui/src/handlers/Caption.tsx deleted file mode 100644 index fbf154d89..000000000 --- a/packages/exchange-backoffice-ui/src/handlers/Caption.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { VNode, h } from "preact"; -import { - IconAddon, - InputLine, - LabelWithTooltipMaybeRequired, - UIFormProps, -} from "./InputLine.js"; -import { TranslatedString } from "@gnu-taler/taler-util"; - -interface Props { - label: TranslatedString; - tooltip?: TranslatedString; - help?: TranslatedString; - before?: VNode; - after?: VNode; -} - -export function Caption({ before, after, label, tooltip, help }: Props): VNode { - return ( - <div class="sm:col-span-6 flex"> - {before !== undefined && ( - <span class="pointer-events-none flex items-center pr-2">{before}</span> - )} - <LabelWithTooltipMaybeRequired label={label} tooltip={tooltip} /> - {after !== undefined && ( - <span class="pointer-events-none flex items-center pl-2">{after}</span> - )} - {help && ( - <p class="mt-2 text-sm text-gray-500" id="email-description"> - {help} - </p> - )} - </div> - ); -} diff --git a/packages/exchange-backoffice-ui/src/handlers/FormProvider.tsx b/packages/exchange-backoffice-ui/src/handlers/FormProvider.tsx deleted file mode 100644 index a195c2051..000000000 --- a/packages/exchange-backoffice-ui/src/handlers/FormProvider.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { - AbsoluteTime, - AmountJson, - TranslatedString, -} from "@gnu-taler/taler-util"; -import { ComponentChildren, VNode, createContext, h } from "preact"; -import { - MutableRef, - StateUpdater, - useEffect, - useRef, - useState, -} from "preact/hooks"; - -export interface FormType<T> { - value: MutableRef<Partial<T>>; - initialValue?: Partial<T>; - onUpdate?: StateUpdater<T>; - computeFormState?: (v: T) => FormState<T>; -} - -//@ts-ignore -export const FormContext = createContext<FormType<any>>({}); - -export type FormState<T> = { - [field in keyof T]?: T[field] extends AbsoluteTime - ? Partial<InputFieldState> - : T[field] extends AmountJson - ? Partial<InputFieldState> - : T[field] extends Array<infer P> - ? Partial<InputArrayFieldState<P>> - : T[field] extends object - ? FormState<T[field]> - : Partial<InputFieldState>; -}; - -export interface InputFieldState { - /* should show the error */ - error?: TranslatedString; - /* should not allow to edit */ - readonly: boolean; - /* should show as disable */ - disabled: boolean; - /* should not show */ - hidden: boolean; -} - -export interface InputArrayFieldState<T> extends InputFieldState { - elements: FormState<T>[]; -} - -export function FormProvider<T>({ - children, - initialValue, - onUpdate: notify, - onSubmit, - computeFormState, -}: { - initialValue?: Partial<T>; - onUpdate?: (v: Partial<T>) => void; - onSubmit?: (v: Partial<T>, s: FormState<T> | undefined) => void; - computeFormState?: (v: Partial<T>) => FormState<T>; - children: ComponentChildren; -}): VNode { - // const value = useRef(initialValue ?? {}); - // useEffect(() => { - // return function onUnload() { - // value.current = initialValue ?? {}; - // }; - // }); - // const onUpdate = notify - const [state, setState] = useState<Partial<T>>(initialValue ?? {}); - const value = { current: state }; - // console.log("RENDER", initialValue, value); - const onUpdate = (v: typeof state) => { - // console.log("updated"); - setState(v); - if (notify) notify(v); - }; - return ( - <FormContext.Provider - value={{ initialValue, value, onUpdate, computeFormState }} - > - <form - onSubmit={(e) => { - e.preventDefault(); - //@ts-ignore - if (onSubmit) - onSubmit( - value.current, - !computeFormState ? undefined : computeFormState(value.current), - ); - }} - > - {children} - </form> - </FormContext.Provider> - ); -} diff --git a/packages/exchange-backoffice-ui/src/handlers/Group.tsx b/packages/exchange-backoffice-ui/src/handlers/Group.tsx deleted file mode 100644 index 0645f6d97..000000000 --- a/packages/exchange-backoffice-ui/src/handlers/Group.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { TranslatedString } from "@gnu-taler/taler-util"; -import { VNode, h } from "preact"; -import { LabelWithTooltipMaybeRequired } from "./InputLine.js"; -import { RenderAllFieldsByUiConfig, UIFormField } from "./forms.js"; - -interface Props { - before?: TranslatedString; - after?: TranslatedString; - tooltipBefore?: TranslatedString; - tooltipAfter?: TranslatedString; - fields: UIFormField[]; -} - -export function Group({ - before, - after, - tooltipAfter, - tooltipBefore, - fields, -}: Props): VNode { - return ( - <div class="sm:col-span-6 p-4 rounded-lg border-r-2 border-2 bg-gray-50"> - <div class="pb-4"> - {before && ( - <LabelWithTooltipMaybeRequired - label={before} - tooltip={tooltipBefore} - /> - )} - </div> - <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-2 sm:grid-cols-6"> - <RenderAllFieldsByUiConfig fields={fields} /> - </div> - <div class="pt-4"> - {after && ( - <LabelWithTooltipMaybeRequired label={after} tooltip={tooltipAfter} /> - )} - </div> - </div> - ); -} diff --git a/packages/exchange-backoffice-ui/src/handlers/InputAmount.tsx b/packages/exchange-backoffice-ui/src/handlers/InputAmount.tsx deleted file mode 100644 index 9be9dd4d0..000000000 --- a/packages/exchange-backoffice-ui/src/handlers/InputAmount.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { AmountJson, Amounts, TranslatedString } from "@gnu-taler/taler-util"; -import { VNode, h } from "preact"; -import { InputLine, UIFormProps } from "./InputLine.js"; -import { useField } from "./useField.js"; - -export function InputAmount<T extends object, K extends keyof T>( - props: { currency?: string } & UIFormProps<T, K>, -): VNode { - const { value } = useField<T, K>(props.name); - const currency = - !value || !(value as any).currency - ? props.currency - : (value as any).currency; - return ( - <InputLine<T, K> - type="text" - before={{ - type: "text", - text: currency as TranslatedString, - }} - converter={{ - //@ts-ignore - fromStringUI: (v): AmountJson => { - return Amounts.parseOrThrow(`${currency}:${v}`); - }, - //@ts-ignore - toStringUI: (v: AmountJson) => { - return v === undefined ? "" : Amounts.stringifyValue(v); - }, - }} - {...props} - /> - ); -} diff --git a/packages/exchange-backoffice-ui/src/handlers/InputArray.tsx b/packages/exchange-backoffice-ui/src/handlers/InputArray.tsx deleted file mode 100644 index 00379bed6..000000000 --- a/packages/exchange-backoffice-ui/src/handlers/InputArray.tsx +++ /dev/null @@ -1,183 +0,0 @@ -import { Fragment, VNode, h } from "preact"; -import { useEffect, useState } from "preact/hooks"; -import { FormProvider, InputArrayFieldState } from "./FormProvider.js"; -import { LabelWithTooltipMaybeRequired, UIFormProps } from "./InputLine.js"; -import { RenderAllFieldsByUiConfig, UIFormField } from "./forms.js"; -import { useField } from "./useField.js"; -import { TranslatedString } from "@gnu-taler/taler-util"; - -function Option({ - label, - disabled, - isFirst, - isLast, - isSelected, - onClick, -}: { - label: TranslatedString; - isFirst?: boolean; - isLast?: boolean; - isSelected?: boolean; - disabled?: boolean; - onClick: () => void; -}): VNode { - let clazz = "relative flex border p-4 focus:outline-none disabled:text-grey"; - if (isFirst) { - clazz += " rounded-tl-md rounded-tr-md "; - } - if (isLast) { - clazz += " rounded-bl-md rounded-br-md "; - } - if (isSelected) { - clazz += " z-10 border-indigo-200 bg-indigo-50 "; - } else { - clazz += " border-gray-200"; - } - if (disabled) { - clazz += - " cursor-not-allowed bg-gray-50 text-gray-500 ring-gray-200 text-gray"; - } else { - clazz += " cursor-pointer"; - } - return ( - <label class={clazz}> - <input - type="radio" - name="privacy-setting" - checked={isSelected} - disabled={disabled} - onClick={onClick} - class="mt-0.5 h-4 w-4 shrink-0 text-indigo-600 disabled:cursor-not-allowed disabled:bg-gray-50 disabled:text-gray-500 disabled:ring-gray-200 focus:ring-indigo-600" - aria-labelledby="privacy-setting-0-label" - aria-describedby="privacy-setting-0-description" - /> - <span class="ml-3 flex flex-col"> - <span - id="privacy-setting-0-label" - disabled - class="block text-sm font-medium" - > - {label} - </span> - {/* <!-- Checked: "text-indigo-700", Not Checked: "text-gray-500" --> */} - {/* <span - id="privacy-setting-0-description" - class="block text-sm" - > - This project would be available to anyone who has the link - </span> */} - </span> - </label> - ); -} - -export function InputArray<T extends object, K extends keyof T>( - props: { - fields: UIFormField[]; - labelField: string; - } & UIFormProps<T, K>, -): VNode { - const { fields, labelField, name, label, required, tooltip } = props; - const { value, onChange, state } = useField<T, K>(name); - const list = (value ?? []) as Array<Record<string, string | undefined>>; - const [selectedIndex, setSelected] = useState<number | undefined>(undefined); - const selected = - selectedIndex === undefined ? undefined : list[selectedIndex]; - - return ( - <div class="sm:col-span-6"> - <LabelWithTooltipMaybeRequired - label={label} - required={required} - tooltip={tooltip} - /> - - <div class="-space-y-px rounded-md bg-white "> - {list.map((v, idx) => { - return ( - <Option - label={v[labelField] as TranslatedString} - isSelected={selectedIndex === idx} - isLast={idx === list.length - 1} - disabled={selectedIndex !== undefined && selectedIndex !== idx} - isFirst={idx === 0} - onClick={() => { - setSelected(selectedIndex === idx ? undefined : idx); - }} - /> - ); - })} - <div class="pt-2"> - <Option - label={"Add..." as TranslatedString} - isSelected={selectedIndex === list.length} - isLast - isFirst - disabled={ - selectedIndex !== undefined && selectedIndex !== list.length - } - onClick={() => { - setSelected( - selectedIndex === list.length ? undefined : list.length, - ); - }} - /> - </div> - </div> - {selectedIndex !== undefined && ( - /** - * This form provider act as a substate of the parent form - * Consider creating an InnerFormProvider since not every feature is expected - */ - <FormProvider - initialValue={selected} - computeFormState={(v) => { - // current state is ignored - // the state is defined by the parent form - - // elements should be present in the state object since this is expected to be an array - //@ts-ignore - return state.elements[selectedIndex]; - }} - onSubmit={(v) => { - const newValue = [...list]; - newValue.splice(selectedIndex, 1, v); - onChange(newValue as T[K]); - setSelected(undefined); - }} - onUpdate={(v) => { - const newValue = [...list]; - newValue.splice(selectedIndex, 1, v); - onChange(newValue as T[K]); - }} - > - <div class="px-4 py-6"> - <div class="grid grid-cols-1 gap-y-8 "> - <RenderAllFieldsByUiConfig fields={fields} /> - </div> - </div> - </FormProvider> - )} - {selectedIndex !== undefined && ( - <div class="flex items-center pt-3"> - <div class="flex-auto"> - {selected !== undefined && ( - <button - type="button" - onClick={() => { - const newValue = [...list]; - newValue.splice(selectedIndex, 1); - onChange(newValue as T[K]); - setSelected(undefined); - }} - class="block rounded-md bg-red-600 px-3 py-2 text-center text-sm text-white shadow-sm hover:bg-red-500 " - > - Remove - </button> - )} - </div> - </div> - )} - </div> - ); -} diff --git a/packages/exchange-backoffice-ui/src/handlers/InputChoiceHorizontal.tsx b/packages/exchange-backoffice-ui/src/handlers/InputChoiceHorizontal.tsx deleted file mode 100644 index fdee35447..000000000 --- a/packages/exchange-backoffice-ui/src/handlers/InputChoiceHorizontal.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { TranslatedString } from "@gnu-taler/taler-util"; -import { Fragment, VNode, h } from "preact"; -import { LabelWithTooltipMaybeRequired, UIFormProps } from "./InputLine.js"; -import { useField } from "./useField.js"; - -export interface Choice<V> { - label: TranslatedString; - value: V; -} - -export function InputChoiceHorizontal<T extends object, K extends keyof T>( - props: { - choices: Choice<T[K]>[]; - } & UIFormProps<T, K>, -): VNode { - const { - choices, - name, - label, - tooltip, - help, - placeholder, - required, - before, - after, - converter, - } = props; - const { value, onChange, state, isDirty } = useField<T, K>(name); - if (state.hidden) { - return <Fragment />; - } - - return ( - <div class="sm:col-span-6"> - <LabelWithTooltipMaybeRequired - label={label} - required={required} - tooltip={tooltip} - /> - <fieldset class="mt-2"> - <div class="isolate inline-flex rounded-md shadow-sm"> - {choices.map((choice, idx) => { - const isFirst = idx === 0; - const isLast = idx === choices.length - 1; - let clazz = - "relative inline-flex items-center px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 focus:z-10"; - if (choice.value === value) { - clazz += - " text-white bg-indigo-600 hover:bg-indigo-500 ring-2 ring-indigo-600 hover:ring-indigo-500"; - } else { - clazz += " hover:bg-gray-100 border-gray-300"; - } - if (isFirst) { - clazz += " rounded-l-md"; - } else { - clazz += " -ml-px"; - } - if (isLast) { - clazz += " rounded-r-md"; - } - return ( - <button - type="button" - class={clazz} - onClick={(e) => { - onChange( - (value === choice.value ? undefined : choice.value) as T[K], - ); - }} - > - {(!converter - ? (choice.value as string) - : converter?.toStringUI(choice.value)) ?? ""} - </button> - ); - })} - </div> - </fieldset> - {help && ( - <p class="mt-2 text-sm text-gray-500" id="email-description"> - {help} - </p> - )} - </div> - ); -} diff --git a/packages/exchange-backoffice-ui/src/handlers/InputChoiceStacked.tsx b/packages/exchange-backoffice-ui/src/handlers/InputChoiceStacked.tsx deleted file mode 100644 index c37984368..000000000 --- a/packages/exchange-backoffice-ui/src/handlers/InputChoiceStacked.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import { TranslatedString } from "@gnu-taler/taler-util"; -import { Fragment, VNode, h } from "preact"; -import { LabelWithTooltipMaybeRequired, UIFormProps } from "./InputLine.js"; -import { useField } from "./useField.js"; - -export interface Choice<V> { - label: TranslatedString; - description?: TranslatedString; - value: V; -} - -export function InputChoiceStacked<T extends object, K extends keyof T>( - props: { - choices: Choice<T[K]>[]; - } & UIFormProps<T, K>, -): VNode { - const { - choices, - name, - label, - tooltip, - help, - placeholder, - required, - before, - after, - converter, - } = props; - const { value, onChange, state, isDirty } = useField<T, K>(name); - if (state.hidden) { - return <Fragment />; - } - - return ( - <div class="sm:col-span-6"> - <LabelWithTooltipMaybeRequired - label={label} - required={required} - tooltip={tooltip} - /> - <fieldset class="mt-2"> - <div class="space-y-4"> - {choices.map((choice) => { - // const currentValue = !converter - // ? choice.value - // : converter.fromStringUI(choice.value) ?? ""; - - let clazz = - "border relative block cursor-pointer rounded-lg bg-white px-6 py-4 shadow-sm focus:outline-none sm:flex sm:justify-between"; - if (choice.value === value) { - clazz += - " border-transparent border-indigo-600 ring-2 ring-indigo-600"; - } else { - clazz += " border-gray-300"; - } - - return ( - <label class={clazz}> - <input - type="radio" - name="server-size" - // defaultValue={choice.value} - value={ - (!converter - ? (choice.value as string) - : converter?.toStringUI(choice.value)) ?? "" - } - onClick={(e) => { - onChange( - (value === choice.value - ? undefined - : choice.value) as T[K], - ); - }} - class="sr-only" - aria-labelledby="server-size-0-label" - aria-describedby="server-size-0-description-0 server-size-0-description-1" - /> - <span class="flex items-center"> - <span class="flex flex-col text-sm"> - <span - id="server-size-0-label" - class="font-medium text-gray-900" - > - {choice.label} - </span> - {choice.description !== undefined && ( - <span - id="server-size-0-description-0" - class="text-gray-500" - > - <span class="block sm:inline"> - {choice.description} - </span> - </span> - )} - </span> - </span> - </label> - ); - })} - </div> - </fieldset> - {help && ( - <p class="mt-2 text-sm text-gray-500" id="email-description"> - {help} - </p> - )} - </div> - ); -} diff --git a/packages/exchange-backoffice-ui/src/handlers/InputDate.tsx b/packages/exchange-backoffice-ui/src/handlers/InputDate.tsx deleted file mode 100644 index 1fd81aad9..000000000 --- a/packages/exchange-backoffice-ui/src/handlers/InputDate.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { AbsoluteTime } from "@gnu-taler/taler-util"; -import { InputLine, UIFormProps } from "./InputLine.js"; -import { CalendarIcon } from "@heroicons/react/24/outline"; -import { VNode, h } from "preact"; -import { format, parse } from "date-fns"; - -export function InputDate<T extends object, K extends keyof T>( - props: { pattern?: string } & UIFormProps<T, K>, -): VNode { - const pattern = props.pattern ?? "dd/MM/yyyy"; - return ( - <InputLine<T, K> - type="text" - after={{ - type: "icon", - icon: <CalendarIcon class="h-6 w-6" />, - }} - converter={{ - //@ts-ignore - fromStringUI: (v): AbsoluteTime => { - if (!v) return AbsoluteTime.never(); - const t_ms = parse(v, pattern, Date.now()).getTime(); - return AbsoluteTime.fromMilliseconds(t_ms); - }, - //@ts-ignore - toStringUI: (v: AbsoluteTime) => { - return !v || !v.t_ms - ? "" - : v.t_ms === "never" - ? "never" - : format(v.t_ms, pattern); - }, - }} - {...props} - /> - ); -} diff --git a/packages/exchange-backoffice-ui/src/handlers/InputFile.tsx b/packages/exchange-backoffice-ui/src/handlers/InputFile.tsx deleted file mode 100644 index 0d89a98a3..000000000 --- a/packages/exchange-backoffice-ui/src/handlers/InputFile.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { Fragment, VNode, h } from "preact"; -import { LabelWithTooltipMaybeRequired, UIFormProps } from "./InputLine.js"; -import { useField } from "./useField.js"; - -export function InputFile<T extends object, K extends keyof T>( - props: { maxBites: number; accept?: string } & UIFormProps<T, K>, -): VNode { - const { - name, - label, - placeholder, - tooltip, - required, - help, - maxBites, - accept, - } = props; - const { value, onChange, state } = useField<T, K>(name); - - if (state.hidden) { - return <div />; - } - return ( - <div class="col-span-full"> - <LabelWithTooltipMaybeRequired - label={label} - tooltip={tooltip} - required={required} - /> - {!value || !(value as string).startsWith("data:image/") ? ( - <div class="mt-2 flex justify-center rounded-lg border border-dashed border-gray-900/25 py-1"> - <div class="text-center"> - <svg - class="mx-auto h-12 w-12 text-gray-300" - viewBox="0 0 24 24" - fill="currentColor" - aria-hidden="true" - > - <path - fill-rule="evenodd" - d="M1.5 6a2.25 2.25 0 012.25-2.25h16.5A2.25 2.25 0 0122.5 6v12a2.25 2.25 0 01-2.25 2.25H3.75A2.25 2.25 0 011.5 18V6zM3 16.06V18c0 .414.336.75.75.75h16.5A.75.75 0 0021 18v-1.94l-2.69-2.689a1.5 1.5 0 00-2.12 0l-.88.879.97.97a.75.75 0 11-1.06 1.06l-5.16-5.159a1.5 1.5 0 00-2.12 0L3 16.061zm10.125-7.81a1.125 1.125 0 112.25 0 1.125 1.125 0 01-2.25 0z" - clip-rule="evenodd" - /> - </svg> - <div class="my-2 flex text-sm leading-6 text-gray-600"> - <label - for="file-upload" - class="relative cursor-pointer rounded-md bg-white font-semibold text-indigo-600 focus-within:outline-none focus-within:ring-2 focus-within:ring-indigo-600 focus-within:ring-offset-2 hover:text-indigo-500" - > - <span>Upload a file</span> - <input - id="file-upload" - name="file-upload" - type="file" - class="sr-only" - accept={accept} - onChange={(e) => { - const f: FileList | null = e.currentTarget.files; - if (!f || f.length != 1) { - return onChange(undefined!); - } - if (f[0].size > maxBites) { - return onChange(undefined!); - } - return f[0].arrayBuffer().then((b) => { - const b64 = window.btoa( - new Uint8Array(b).reduce( - (data, byte) => data + String.fromCharCode(byte), - "", - ), - ); - return onChange(`data:${f[0].type};base64,${b64}` as any); - }); - }} - /> - </label> - {/* <p class="pl-1">or drag and drop</p> */} - </div> - </div> - </div> - ) : ( - <div class="mt-2 flex justify-center rounded-lg border border-dashed border-gray-900/25 relative"> - <img - src={value as string} - class=" h-24 w-full object-cover relative" - /> - - <div - class="opacity-0 hover:opacity-70 duration-300 absolute rounded-lg border inset-0 z-10 flex justify-center text-xl items-center bg-black text-white cursor-pointer " - onClick={() => { - onChange(undefined!); - }} - > - Clear - </div> - </div> - )} - {help && <p class="text-xs leading-5 text-gray-600 mt-2">{help}</p>} - </div> - ); -} diff --git a/packages/exchange-backoffice-ui/src/handlers/InputInteger.tsx b/packages/exchange-backoffice-ui/src/handlers/InputInteger.tsx deleted file mode 100644 index fb04e3852..000000000 --- a/packages/exchange-backoffice-ui/src/handlers/InputInteger.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { VNode, h } from "preact"; -import { InputLine, UIFormProps } from "./InputLine.js"; - -export function InputInteger<T extends object, K extends keyof T>( - props: UIFormProps<T, K>, -): VNode { - return ( - <InputLine - type="number" - converter={{ - //@ts-ignore - fromStringUI: (v): number => { - return !v ? 0 : Number.parseInt(v, 10); - }, - //@ts-ignore - toStringUI: (v?: number): string => { - return v === undefined ? "" : String(v); - }, - }} - {...props} - /> - ); -} diff --git a/packages/exchange-backoffice-ui/src/handlers/InputLine.tsx b/packages/exchange-backoffice-ui/src/handlers/InputLine.tsx deleted file mode 100644 index 9448ef5e4..000000000 --- a/packages/exchange-backoffice-ui/src/handlers/InputLine.tsx +++ /dev/null @@ -1,282 +0,0 @@ -import { TranslatedString } from "@gnu-taler/taler-util"; -import { ComponentChildren, Fragment, VNode, h } from "preact"; -import { useField } from "./useField.js"; - -export interface IconAddon { - type: "icon"; - icon: VNode; -} -interface ButtonAddon { - type: "button"; - onClick: () => void; - children: ComponentChildren; -} -interface TextAddon { - type: "text"; - text: TranslatedString; -} -type Addon = IconAddon | ButtonAddon | TextAddon; - -interface StringConverter<T> { - toStringUI: (v?: T) => string; - fromStringUI: (v?: string) => T; -} - -export interface UIFormProps<T extends object, K extends keyof T> { - name: K; - label: TranslatedString; - placeholder?: TranslatedString; - tooltip?: TranslatedString; - help?: TranslatedString; - before?: Addon; - after?: Addon; - required?: boolean; - converter?: StringConverter<T[K]>; -} - -export type FormErrors<T> = { - [P in keyof T]?: string | FormErrors<T[P]>; -}; - -//@ts-ignore -const TooltipIcon = ( - <svg - class="w-5 h-5" - xmlns="http://www.w3.org/2000/svg" - viewBox="0 0 20 20" - fill="currentColor" - > - <path - fill-rule="evenodd" - d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-3a1 1 0 00-.867.5 1 1 0 11-1.731-1A3 3 0 0113 8a3.001 3.001 0 01-2 2.83V11a1 1 0 11-2 0v-1a1 1 0 011-1 1 1 0 100-2zm0 8a1 1 0 100-2 1 1 0 000 2z" - clip-rule="evenodd" - /> - </svg> -); - -export function LabelWithTooltipMaybeRequired({ - label, - required, - tooltip, -}: { - label: TranslatedString; - required?: boolean; - tooltip?: TranslatedString; -}): VNode { - const Label = ( - <Fragment> - <div class="flex justify-between"> - <label - htmlFor="email" - class="block text-sm font-medium leading-6 text-gray-900" - > - {label} - </label> - </div> - </Fragment> - ); - const WithTooltip = tooltip ? ( - <div class="relative flex flex-grow items-stretch focus-within:z-10"> - {Label} - <span class="relative flex items-center group pl-2"> - {TooltipIcon} - <div class="absolute bottom-0 flex flex-col items-center hidden mb-6 group-hover:flex"> - <span class="relative z-10 p-2 text-xs leading-none text-white whitespace-no-wrap bg-black shadow-lg"> - {tooltip} - </span> - <div class="w-3 h-3 -mt-2 rotate-45 bg-black"></div> - </div> - </span> - </div> - ) : ( - Label - ); - if (required) { - return ( - <div class="flex justify-between"> - {WithTooltip} - <span class="text-sm leading-6 text-red-600">*</span> - </div> - ); - } - return WithTooltip; -} - -function InputWrapper<T extends object, K extends keyof T>({ - children, - label, - tooltip, - before, - after, - help, - error, - required, -}: { error?: string; children: ComponentChildren } & UIFormProps<T, K>): VNode { - return ( - <div class="sm:col-span-6"> - <LabelWithTooltipMaybeRequired - label={label} - required={required} - tooltip={tooltip} - /> - <div class="relative mt-2 flex rounded-md shadow-sm"> - {before && - (before.type === "text" ? ( - <span class="inline-flex items-center rounded-l-md border border-r-0 border-gray-300 px-3 text-gray-500 sm:text-sm"> - {before.text} - </span> - ) : before.type === "icon" ? ( - <div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3"> - {before.icon} - </div> - ) : before.type === "button" ? ( - <button - type="button" - onClick={before.onClick} - class="relative -ml-px inline-flex items-center gap-x-1.5 rounded-l-md px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50" - > - {before.children} - </button> - ) : undefined)} - - {children} - - {after && - (after.type === "text" ? ( - <span class="inline-flex items-center rounded-r-md border border-l-0 border-gray-300 px-3 text-gray-500 sm:text-sm"> - {after.text} - </span> - ) : after.type === "icon" ? ( - <div class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3"> - {after.icon} - </div> - ) : after.type === "button" ? ( - <button - type="button" - onClick={after.onClick} - class="relative -ml-px inline-flex items-center gap-x-1.5 rounded-r-md px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50" - > - {after.children} - </button> - ) : undefined)} - </div> - {error && ( - <p class="mt-2 text-sm text-red-600" id="email-error"> - {error} - </p> - )} - {help && ( - <p class="mt-2 text-sm text-gray-500" id="email-description"> - {help} - </p> - )} - </div> - ); -} - -function defaultToString(v: unknown) { - return v === undefined ? "" : typeof v !== "object" ? String(v) : ""; -} -function defaultFromString(v: string) { - return v; -} - -type InputType = "text" | "text-area" | "password" | "email" | "number"; - -export function InputLine<T extends object, K extends keyof T>( - props: { type: InputType } & UIFormProps<T, K>, -): VNode { - const { name, placeholder, before, after, converter, type } = props; - const { value, onChange, state, isDirty } = useField<T, K>(name); - - if (state.hidden) return <div />; - - let clazz = - "block w-full rounded-md border-0 py-1.5 shadow-sm ring-1 ring-inset focus:ring-2 focus:ring-inset sm:text-sm sm:leading-6 disabled:cursor-not-allowed disabled:bg-gray-50 disabled:text-gray-500 disabled:ring-gray-200"; - if (before) { - switch (before.type) { - case "icon": { - clazz += " pl-10"; - break; - } - case "button": { - clazz += " rounded-none rounded-r-md "; - break; - } - case "text": { - clazz += " min-w-0 flex-1 rounded-r-md rounded-none "; - break; - } - } - } - if (after) { - switch (after.type) { - case "icon": { - clazz += " pr-10"; - break; - } - case "button": { - clazz += " rounded-none rounded-l-md"; - break; - } - case "text": { - clazz += " min-w-0 flex-1 rounded-l-md rounded-none "; - break; - } - } - } - const showError = isDirty && state.error; - if (showError) { - clazz += - " text-red-900 ring-red-300 placeholder:text-red-300 focus:ring-red-500"; - } else { - clazz += - " text-gray-900 ring-gray-300 placeholder:text-gray-400 focus:ring-indigo-600"; - } - const fromString: (s: string) => any = - converter?.fromStringUI ?? defaultFromString; - const toString: (s: any) => string = converter?.toStringUI ?? defaultToString; - - if (type === "text-area") { - return ( - <InputWrapper<T, K> - {...props} - error={showError ? state.error : undefined} - > - <textarea - rows={4} - name={String(name)} - onChange={(e) => { - onChange(fromString(e.currentTarget.value)); - }} - placeholder={placeholder ? placeholder : undefined} - value={toString(value) ?? ""} - // defaultValue={toString(value)} - disabled={state.disabled} - aria-invalid={showError} - // aria-describedby="email-error" - class={clazz} - /> - </InputWrapper> - ); - } - - return ( - <InputWrapper<T, K> {...props} error={showError ? state.error : undefined}> - <input - name={String(name)} - type={type} - onChange={(e) => { - onChange(fromString(e.currentTarget.value)); - }} - placeholder={placeholder ? placeholder : undefined} - value={toString(value) ?? ""} - // defaultValue={toString(value)} - disabled={state.disabled} - aria-invalid={showError} - // aria-describedby="email-error" - class={clazz} - /> - </InputWrapper> - ); -} diff --git a/packages/exchange-backoffice-ui/src/handlers/InputSelectMultiple.tsx b/packages/exchange-backoffice-ui/src/handlers/InputSelectMultiple.tsx deleted file mode 100644 index 837744827..000000000 --- a/packages/exchange-backoffice-ui/src/handlers/InputSelectMultiple.tsx +++ /dev/null @@ -1,151 +0,0 @@ -import { Fragment, VNode, h } from "preact"; -import { Choice } from "./InputChoiceStacked.js"; -import { LabelWithTooltipMaybeRequired, UIFormProps } from "./InputLine.js"; -import { useField } from "./useField.js"; -import { useState } from "preact/hooks"; - -export function InputSelectMultiple<T extends object, K extends keyof T>( - props: { - choices: Choice<T[K]>[]; - unique?: boolean; - max?: number; - } & UIFormProps<T, K>, -): VNode { - const { name, label, choices, placeholder, tooltip, required, unique, max } = - props; - const { value, onChange } = useField<T, K>(name); - - const [filter, setFilter] = useState<string | undefined>(undefined); - const regex = new RegExp(`.*${filter}.*`, "i"); - const choiceMap = choices.reduce((prev, curr) => { - return { ...prev, [curr.value as string]: curr.label }; - }, {} as Record<string, string>); - - const list = (value ?? []) as string[]; - const filteredChoices = - filter === undefined - ? undefined - : choices.filter((v) => { - return regex.test(v.label); - }); - return ( - <div class="sm:col-span-6"> - <LabelWithTooltipMaybeRequired - label={label} - required={required} - tooltip={tooltip} - /> - {list.map((v, idx) => { - return ( - <span class="inline-flex items-center gap-x-0.5 rounded-md bg-gray-100 p-1 mr-2 text-xs font-medium text-gray-600"> - {choiceMap[v]} - <button - type="button" - onClick={() => { - const newValue = [...list]; - newValue.splice(idx, 1); - onChange(newValue as T[K]); - setFilter(undefined); - }} - class="group relative h-5 w-5 rounded-sm hover:bg-gray-500/20" - > - <span class="sr-only">Remove</span> - <svg - viewBox="0 0 14 14" - class="h-5 w-5 stroke-gray-700/50 group-hover:stroke-gray-700/75" - > - <path d="M4 4l6 6m0-6l-6 6" /> - </svg> - <span class="absolute -inset-1"></span> - </button> - </span> - ); - })} - - <div class="relative mt-2"> - <input - id="combobox" - type="text" - value={filter ?? ""} - onChange={(e) => { - setFilter(e.currentTarget.value); - }} - placeholder={placeholder} - class="w-full rounded-md border-0 bg-white py-1.5 pl-3 pr-12 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" - role="combobox" - aria-controls="options" - aria-expanded="false" - /> - <button - type="button" - onClick={() => { - setFilter(filter === undefined ? "" : undefined); - }} - class="absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none" - > - <svg - class="h-5 w-5 text-gray-400" - viewBox="0 0 20 20" - fill="currentColor" - aria-hidden="true" - > - <path - fill-rule="evenodd" - d="M10 3a.75.75 0 01.55.24l3.25 3.5a.75.75 0 11-1.1 1.02L10 4.852 7.3 7.76a.75.75 0 01-1.1-1.02l3.25-3.5A.75.75 0 0110 3zm-3.76 9.2a.75.75 0 011.06.04l2.7 2.908 2.7-2.908a.75.75 0 111.1 1.02l-3.25 3.5a.75.75 0 01-1.1 0l-3.25-3.5a.75.75 0 01.04-1.06z" - clip-rule="evenodd" - /> - </svg> - </button> - - {filteredChoices !== undefined && ( - <ul - class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm" - id="options" - role="listbox" - > - {filteredChoices.map((v, idx) => { - return ( - <li - class="relative cursor-pointer select-none py-2 pl-3 pr-9 text-gray-900 hover:text-white hover:bg-indigo-600" - id="option-0" - role="option" - onClick={() => { - setFilter(undefined); - if (unique && list.indexOf(v.value as string) !== -1) { - return; - } - if (max !== undefined && list.length >= max) { - return; - } - const newValue = [...list]; - newValue.splice(0, 0, v.value as string); - onChange(newValue as T[K]); - }} - - // tabindex="-1" - > - {/* <!-- Selected: "font-semibold" --> */} - <span class="block truncate">{v.label}</span> - - {/* <!-- - Checkmark, only display for selected option. - - Active: "text-white", Not Active: "text-indigo-600" - --> */} - </li> - ); - })} - - {/* <!-- - Combobox option, manage highlight styles based on mouseenter/mouseleave and keyboard navigation. - - Active: "text-white bg-indigo-600", Not Active: "text-gray-900" - --> */} - - {/* <!-- More items... --> */} - </ul> - )} - </div> - </div> - ); -} diff --git a/packages/exchange-backoffice-ui/src/handlers/InputSelectOne.tsx b/packages/exchange-backoffice-ui/src/handlers/InputSelectOne.tsx deleted file mode 100644 index b0e2277d3..000000000 --- a/packages/exchange-backoffice-ui/src/handlers/InputSelectOne.tsx +++ /dev/null @@ -1,134 +0,0 @@ -import { Fragment, VNode, h } from "preact"; -import { Choice } from "./InputChoiceStacked.js"; -import { LabelWithTooltipMaybeRequired, UIFormProps } from "./InputLine.js"; -import { useField } from "./useField.js"; -import { useState } from "preact/hooks"; - -export function InputSelectOne<T extends object, K extends keyof T>( - props: { - choices: Choice<T[K]>[]; - } & UIFormProps<T, K>, -): VNode { - const { name, label, choices, placeholder, tooltip, required } = props; - const { value, onChange } = useField<T, K>(name); - - const [filter, setFilter] = useState<string | undefined>(undefined); - const regex = new RegExp(`.*${filter}.*`, "i"); - const choiceMap = choices.reduce((prev, curr) => { - return { ...prev, [curr.value as string]: curr.label }; - }, {} as Record<string, string>); - - const filteredChoices = - filter === undefined - ? undefined - : choices.filter((v) => { - return regex.test(v.label); - }); - return ( - <div class="sm:col-span-6"> - <LabelWithTooltipMaybeRequired - label={label} - required={required} - tooltip={tooltip} - /> - {value ? ( - <span class="inline-flex items-center gap-x-0.5 rounded-md bg-gray-100 p-1 mr-2 font-medium text-gray-600"> - {choiceMap[value as string]} - <button - type="button" - onClick={() => { - onChange(undefined!); - }} - class="group relative h-5 w-5 rounded-sm hover:bg-gray-500/20" - > - <span class="sr-only">Remove</span> - <svg - viewBox="0 0 14 14" - class="h-5 w-5 stroke-gray-700/50 group-hover:stroke-gray-700/75" - > - <path d="M4 4l6 6m0-6l-6 6" /> - </svg> - <span class="absolute -inset-1"></span> - </button> - </span> - ) : ( - <div class="relative mt-2"> - <input - id="combobox" - type="text" - value={filter ?? ""} - onChange={(e) => { - setFilter(e.currentTarget.value); - }} - placeholder={placeholder} - class="w-full rounded-md border-0 bg-white py-1.5 pl-3 pr-12 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" - role="combobox" - aria-controls="options" - aria-expanded="false" - /> - <button - type="button" - onClick={() => { - setFilter(filter === undefined ? "" : undefined); - }} - class="absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none" - > - <svg - class="h-5 w-5 text-gray-400" - viewBox="0 0 20 20" - fill="currentColor" - aria-hidden="true" - > - <path - fill-rule="evenodd" - d="M10 3a.75.75 0 01.55.24l3.25 3.5a.75.75 0 11-1.1 1.02L10 4.852 7.3 7.76a.75.75 0 01-1.1-1.02l3.25-3.5A.75.75 0 0110 3zm-3.76 9.2a.75.75 0 011.06.04l2.7 2.908 2.7-2.908a.75.75 0 111.1 1.02l-3.25 3.5a.75.75 0 01-1.1 0l-3.25-3.5a.75.75 0 01.04-1.06z" - clip-rule="evenodd" - /> - </svg> - </button> - - {filteredChoices !== undefined && ( - <ul - class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm" - id="options" - role="listbox" - > - {filteredChoices.map((v, idx) => { - return ( - <li - class="relative cursor-pointer select-none py-2 pl-3 pr-9 text-gray-900 hover:text-white hover:bg-indigo-600" - id="option-0" - role="option" - onClick={() => { - setFilter(undefined); - onChange(v.value as T[K]); - }} - - // tabindex="-1" - > - {/* <!-- Selected: "font-semibold" --> */} - <span class="block truncate">{v.label}</span> - - {/* <!-- - Checkmark, only display for selected option. - - Active: "text-white", Not Active: "text-indigo-600" - --> */} - </li> - ); - })} - - {/* <!-- - Combobox option, manage highlight styles based on mouseenter/mouseleave and keyboard navigation. - - Active: "text-white bg-indigo-600", Not Active: "text-gray-900" - --> */} - - {/* <!-- More items... --> */} - </ul> - )} - </div> - )} - </div> - ); -} diff --git a/packages/exchange-backoffice-ui/src/handlers/InputText.tsx b/packages/exchange-backoffice-ui/src/handlers/InputText.tsx deleted file mode 100644 index 1b37ee6fb..000000000 --- a/packages/exchange-backoffice-ui/src/handlers/InputText.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { VNode, h } from "preact"; -import { InputLine, UIFormProps } from "./InputLine.js"; - -export function InputText<T extends object, K extends keyof T>( - props: UIFormProps<T, K>, -): VNode { - return <InputLine type="text" {...props} />; -} diff --git a/packages/exchange-backoffice-ui/src/handlers/InputTextArea.tsx b/packages/exchange-backoffice-ui/src/handlers/InputTextArea.tsx deleted file mode 100644 index 45229951e..000000000 --- a/packages/exchange-backoffice-ui/src/handlers/InputTextArea.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { VNode, h } from "preact"; -import { InputLine, UIFormProps } from "./InputLine.js"; - -export function InputTextArea<T extends object, K extends keyof T>( - props: UIFormProps<T, K>, -): VNode { - return <InputLine type="text-area" {...props} />; -} diff --git a/packages/exchange-backoffice-ui/src/handlers/forms.ts b/packages/exchange-backoffice-ui/src/handlers/forms.ts deleted file mode 100644 index 2c90a69ed..000000000 --- a/packages/exchange-backoffice-ui/src/handlers/forms.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { TranslatedString } from "@gnu-taler/taler-util"; -import { InputText } from "./InputText.js"; -import { InputDate } from "./InputDate.js"; -import { InputInteger } from "./InputInteger.js"; -import { h as create, Fragment, VNode } from "preact"; -import { InputChoiceStacked } from "./InputChoiceStacked.js"; -import { InputArray } from "./InputArray.js"; -import { InputSelectMultiple } from "./InputSelectMultiple.js"; -import { InputTextArea } from "./InputTextArea.js"; -import { InputFile } from "./InputFile.js"; -import { Caption } from "./Caption.js"; -import { Group } from "./Group.js"; -import { InputSelectOne } from "./InputSelectOne.js"; -import { FormProvider } from "./FormProvider.js"; -import { InputLine } from "./InputLine.js"; -import { InputAmount } from "./InputAmount.js"; -import { InputChoiceHorizontal } from "./InputChoiceHorizontal.js"; - -export type DoubleColumnForm = Array<DoubleColumnFormSection | undefined>; - -export type DoubleColumnFormSection = { - title: TranslatedString; - description?: TranslatedString; - fields: UIFormField[]; -}; - -/** - * Constrain the type with the ui props - */ -type FieldType<T extends object = any, K extends keyof T = any> = { - group: Parameters<typeof Group>[0]; - caption: Parameters<typeof Caption>[0]; - array: Parameters<typeof InputArray<T, K>>[0]; - file: Parameters<typeof InputFile<T, K>>[0]; - selectOne: Parameters<typeof InputSelectOne<T, K>>[0]; - selectMultiple: Parameters<typeof InputSelectMultiple<T, K>>[0]; - text: Parameters<typeof InputText<T, K>>[0]; - textArea: Parameters<typeof InputTextArea<T, K>>[0]; - choiceStacked: Parameters<typeof InputChoiceStacked<T, K>>[0]; - choiceHorizontal: Parameters<typeof InputChoiceHorizontal<T, K>>[0]; - date: Parameters<typeof InputDate<T, K>>[0]; - integer: Parameters<typeof InputInteger<T, K>>[0]; - amount: Parameters<typeof InputAmount<T, K>>[0]; -}; - -/** - * List all the form fields so typescript can type-check the form instance - */ -export type UIFormField = - | { type: "group"; props: FieldType["group"] } - | { type: "caption"; props: FieldType["caption"] } - | { type: "array"; props: FieldType["array"] } - | { type: "file"; props: FieldType["file"] } - | { type: "amount"; props: FieldType["amount"] } - | { type: "selectOne"; props: FieldType["selectOne"] } - | { type: "selectMultiple"; props: FieldType["selectMultiple"] } - | { type: "text"; props: FieldType["text"] } - | { type: "textArea"; props: FieldType["textArea"] } - | { type: "choiceStacked"; props: FieldType["choiceStacked"] } - | { type: "choiceHorizontal"; props: FieldType["choiceHorizontal"] } - | { type: "integer"; props: FieldType["integer"] } - | { type: "date"; props: FieldType["date"] }; - -type FieldComponentFunction<key extends keyof FieldType> = ( - props: FieldType[key], -) => VNode; - -type UIFormFieldMap = { - [key in keyof FieldType]: FieldComponentFunction<key>; -}; - -/** - * Maps input type with component implementation - */ -const UIFormConfiguration: UIFormFieldMap = { - group: Group, - caption: Caption, - //@ts-ignore - array: InputArray, - text: InputText, - //@ts-ignore - file: InputFile, - textArea: InputTextArea, - //@ts-ignore - date: InputDate, - //@ts-ignore - choiceStacked: InputChoiceStacked, - //@ts-ignore - choiceHorizontal: InputChoiceHorizontal, - integer: InputInteger, - //@ts-ignore - selectOne: InputSelectOne, - //@ts-ignore - selectMultiple: InputSelectMultiple, - //@ts-ignore - amount: InputAmount, -}; - -export function RenderAllFieldsByUiConfig({ - fields, -}: { - fields: UIFormField[]; -}): VNode { - return create( - Fragment, - {}, - fields.map((field, i) => { - const Component = UIFormConfiguration[ - field.type - ] as FieldComponentFunction<any>; - return Component(field.props); - }), - ); -} - -type FormSet<T extends object> = { - Provider: typeof FormProvider<T>; - InputLine: <K extends keyof T>() => typeof InputLine<T, K>; - InputChoiceHorizontal: <K extends keyof T>() => typeof InputChoiceHorizontal< - T, - K - >; -}; -export function createNewForm<T extends object>() { - const res: FormSet<T> = { - Provider: FormProvider, - InputLine: () => InputLine, - InputChoiceHorizontal: () => InputChoiceHorizontal, - }; - return { - Provider: res.Provider, - InputLine: res.InputLine(), - InputChoiceHorizontal: res.InputChoiceHorizontal(), - }; -} diff --git a/packages/exchange-backoffice-ui/src/handlers/useField.ts b/packages/exchange-backoffice-ui/src/handlers/useField.ts deleted file mode 100644 index bf94d2f5d..000000000 --- a/packages/exchange-backoffice-ui/src/handlers/useField.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { useContext, useState } from "preact/compat"; -import { FormContext, InputFieldState } from "./FormProvider.js"; - -export interface InputFieldHandler<Type> { - value: Type; - onChange: (s: Type) => void; - state: InputFieldState; - isDirty: boolean; -} - -export function useField<T extends object, K extends keyof T>( - name: K, -): InputFieldHandler<T[K]> { - const { - initialValue, - value: formValue, - computeFormState, - onUpdate: notifyUpdate, - } = useContext(FormContext); - - type P = typeof name; - type V = T[P]; - const formState = computeFormState ? computeFormState(formValue.current) : {}; - - const fieldValue = readField(formValue.current, String(name)) as V; - // console.log("USE FIELD", String(name), formValue.current, fieldValue); - const [currentValue, setCurrentValue] = useState<any | undefined>(fieldValue); - const fieldState = - readField<Partial<InputFieldState>>(formState, String(name)) ?? {}; - - //compute default state - const state = { - disabled: fieldState.disabled ?? false, - readonly: fieldState.readonly ?? false, - hidden: fieldState.hidden ?? false, - error: fieldState.error, - elements: "elements" in fieldState ? fieldState.elements ?? [] : [], - }; - - function onChange(value: V): void { - setCurrentValue(value); - formValue.current = setValueDeeper( - formValue.current, - String(name).split("."), - value, - ); - if (notifyUpdate) { - notifyUpdate(formValue.current); - } - } - - return { - value: fieldValue, - onChange, - isDirty: currentValue !== undefined, - state, - }; -} - -/** - * read the field of an object an support accessing it using '.' - * - * @param object - * @param name - * @returns - */ -function readField<T>( - object: any, - name: string, - debug?: boolean, -): T | undefined { - return name.split(".").reduce((prev, current) => { - if (debug) { - console.log( - "READ", - name, - prev, - current, - prev ? prev[current] : undefined, - ); - } - return prev ? prev[current] : undefined; - }, object); -} - -function setValueDeeper(object: any, names: string[], value: any): any { - if (names.length === 0) return value; - const [head, ...rest] = names; - if (object === undefined) { - return { [head]: setValueDeeper({}, rest, value) }; - } - return { ...object, [head]: setValueDeeper(object[head] ?? {}, rest, value) }; -} |