diff options
Diffstat (limited to 'packages/exchange-backoffice-ui')
12 files changed, 123 insertions, 74 deletions
diff --git a/packages/exchange-backoffice-ui/src/handlers/InputArray.tsx b/packages/exchange-backoffice-ui/src/handlers/InputArray.tsx index c09806e58..00379bed6 100644 --- a/packages/exchange-backoffice-ui/src/handlers/InputArray.tsx +++ b/packages/exchange-backoffice-ui/src/handlers/InputArray.tsx @@ -71,17 +71,15 @@ function Option({ ); } -export function InputArray( +export function InputArray<T extends object, K extends keyof T>( props: { fields: UIFormField[]; labelField: string; - } & UIFormProps<Array<{}>>, + } & UIFormProps<T, K>, ): VNode { const { fields, labelField, name, label, required, tooltip } = props; - const { value, onChange, state } = useField<{ [s: string]: Array<any> }>( - name, - ); - const list = value ?? []; + 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]; @@ -98,7 +96,7 @@ export function InputArray( {list.map((v, idx) => { return ( <Option - label={v[labelField]} + label={v[labelField] as TranslatedString} isSelected={selectedIndex === idx} isLast={idx === list.length - 1} disabled={selectedIndex !== undefined && selectedIndex !== idx} @@ -141,10 +139,16 @@ export function InputArray( //@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); + onChange(newValue as T[K]); }} > <div class="px-4 py-6"> @@ -163,7 +167,7 @@ export function InputArray( onClick={() => { const newValue = [...list]; newValue.splice(selectedIndex, 1); - onChange(newValue); + 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 " diff --git a/packages/exchange-backoffice-ui/src/handlers/InputChoiceStacked.tsx b/packages/exchange-backoffice-ui/src/handlers/InputChoiceStacked.tsx index 361eb39a3..3bce0123f 100644 --- a/packages/exchange-backoffice-ui/src/handlers/InputChoiceStacked.tsx +++ b/packages/exchange-backoffice-ui/src/handlers/InputChoiceStacked.tsx @@ -9,10 +9,10 @@ export interface Choice { value: string; } -export function InputChoiceStacked( +export function InputChoiceStacked<T extends object, K extends keyof T>( props: { choices: Choice[]; - } & UIFormProps<string>, + } & UIFormProps<T, K>, ): VNode { const { choices, @@ -26,9 +26,7 @@ export function InputChoiceStacked( after, converter, } = props; - const { value, onChange, state, isDirty } = useField<{ - [s: string]: undefined | string; - }>(name); + const { value, onChange, state, isDirty } = useField<T, K>(name); if (state.hidden) { return <Fragment />; } @@ -58,7 +56,11 @@ export function InputChoiceStacked( name="server-size" defaultValue={choice.value} onClick={(e) => { - onChange(value === choice.value ? undefined : choice.value); + onChange( + (value === choice.value + ? undefined + : choice.value) as T[K], + ); }} class="sr-only" aria-labelledby="server-size-0-label" diff --git a/packages/exchange-backoffice-ui/src/handlers/InputDate.tsx b/packages/exchange-backoffice-ui/src/handlers/InputDate.tsx index 00dd59996..e834b6cdb 100644 --- a/packages/exchange-backoffice-ui/src/handlers/InputDate.tsx +++ b/packages/exchange-backoffice-ui/src/handlers/InputDate.tsx @@ -4,24 +4,26 @@ import { CalendarIcon } from "@heroicons/react/24/outline"; import { VNode, h } from "preact"; import { format, parse } from "date-fns"; -export function InputDate( - props: { pattern?: string } & UIFormProps<AbsoluteTime>, +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<AbsoluteTime> + <InputLine<T, K> type="text" after={{ type: "icon", icon: <CalendarIcon class="h-6 w-6" />, }} converter={{ - fromStringUI: (v) => { + //@ts-ignore + fromStringUI: (v): AbsoluteTime => { if (!v) return { t_ms: "never" }; const t_ms = parse(v, pattern, Date.now()).getTime(); return { t_ms }; }, - toStringUI: (v) => { + //@ts-ignore + toStringUI: (v: AbsoluteTime) => { return !v || !v.t_ms ? "" : v.t_ms === "never" diff --git a/packages/exchange-backoffice-ui/src/handlers/InputFile.tsx b/packages/exchange-backoffice-ui/src/handlers/InputFile.tsx index 37216c982..0d89a98a3 100644 --- a/packages/exchange-backoffice-ui/src/handlers/InputFile.tsx +++ b/packages/exchange-backoffice-ui/src/handlers/InputFile.tsx @@ -1,13 +1,9 @@ import { Fragment, VNode, h } from "preact"; -import { - InputLine, - LabelWithTooltipMaybeRequired, - UIFormProps, -} from "./InputLine.js"; +import { LabelWithTooltipMaybeRequired, UIFormProps } from "./InputLine.js"; import { useField } from "./useField.js"; -export function InputFile( - props: { maxBites: number; accept?: string } & UIFormProps<string>, +export function InputFile<T extends object, K extends keyof T>( + props: { maxBites: number; accept?: string } & UIFormProps<T, K>, ): VNode { const { name, @@ -19,7 +15,7 @@ export function InputFile( maxBites, accept, } = props; - const { value, onChange, state } = useField<{ [s: string]: string }>(name); + const { value, onChange, state } = useField<T, K>(name); if (state.hidden) { return <div />; @@ -31,7 +27,7 @@ export function InputFile( tooltip={tooltip} required={required} /> - {!value || !value.startsWith("data:image/") ? ( + {!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 @@ -84,7 +80,10 @@ export function InputFile( </div> ) : ( <div class="mt-2 flex justify-center rounded-lg border border-dashed border-gray-900/25 relative"> - <img src={value} class=" h-24 w-full object-cover 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 " diff --git a/packages/exchange-backoffice-ui/src/handlers/InputInteger.tsx b/packages/exchange-backoffice-ui/src/handlers/InputInteger.tsx index 49e6973fc..fb04e3852 100644 --- a/packages/exchange-backoffice-ui/src/handlers/InputInteger.tsx +++ b/packages/exchange-backoffice-ui/src/handlers/InputInteger.tsx @@ -1,15 +1,19 @@ import { VNode, h } from "preact"; import { InputLine, UIFormProps } from "./InputLine.js"; -export function InputInteger(props: UIFormProps<number>): VNode { +export function InputInteger<T extends object, K extends keyof T>( + props: UIFormProps<T, K>, +): VNode { return ( <InputLine type="number" converter={{ - fromStringUI: (v) => { + //@ts-ignore + fromStringUI: (v): number => { return !v ? 0 : Number.parseInt(v, 10); }, - toStringUI: (v?: number) => { + //@ts-ignore + toStringUI: (v?: number): string => { return v === undefined ? "" : String(v); }, }} diff --git a/packages/exchange-backoffice-ui/src/handlers/InputLine.tsx b/packages/exchange-backoffice-ui/src/handlers/InputLine.tsx index 32b16313d..8e847a273 100644 --- a/packages/exchange-backoffice-ui/src/handlers/InputLine.tsx +++ b/packages/exchange-backoffice-ui/src/handlers/InputLine.tsx @@ -22,8 +22,8 @@ interface StringConverter<T> { fromStringUI: (v?: string) => T; } -export interface UIFormProps<T> { - name: keyof T; +export interface UIFormProps<T extends object, K extends keyof T> { + name: K; label: TranslatedString; placeholder?: TranslatedString; tooltip?: TranslatedString; @@ -31,7 +31,7 @@ export interface UIFormProps<T> { before?: Addon; after?: Addon; required?: boolean; - converter?: StringConverter<T>; + converter?: StringConverter<T[K]>; } export type FormErrors<T> = { @@ -102,7 +102,7 @@ export function LabelWithTooltipMaybeRequired({ return WithTooltip; } -function InputWrapper<T>({ +function InputWrapper<T extends object, K extends keyof T>({ children, label, tooltip, @@ -111,7 +111,7 @@ function InputWrapper<T>({ help, error, required, -}: { error?: string; children: ComponentChildren } & UIFormProps<T>): VNode { +}: { error?: string; children: ComponentChildren } & UIFormProps<T, K>): VNode { return ( <div class="sm:col-span-6"> <LabelWithTooltipMaybeRequired @@ -181,13 +181,13 @@ function defaultFromString(v: string) { return v; } -type InputType = "text" | "text-area" | "password" | "email"; +type InputType = "text" | "text-area" | "password" | "email" | "number"; -export function InputLine<T>( - props: { type: InputType } & UIFormProps<T>, +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(name); + const { value, onChange, state, isDirty } = useField<T, K>(name); if (state.hidden) return <div />; @@ -239,7 +239,10 @@ export function InputLine<T>( if (type === "text-area") { return ( - <InputWrapper<T> {...props} error={showError ? state.error : undefined}> + <InputWrapper<T, K> + {...props} + error={showError ? state.error : undefined} + > <textarea rows={4} name={String(name)} @@ -258,7 +261,7 @@ export function InputLine<T>( } return ( - <InputWrapper<T> {...props} error={showError ? state.error : undefined}> + <InputWrapper<T, K> {...props} error={showError ? state.error : undefined}> <input name={String(name)} type={type} diff --git a/packages/exchange-backoffice-ui/src/handlers/InputSelectMultiple.tsx b/packages/exchange-backoffice-ui/src/handlers/InputSelectMultiple.tsx index dd85453e5..8369e509d 100644 --- a/packages/exchange-backoffice-ui/src/handlers/InputSelectMultiple.tsx +++ b/packages/exchange-backoffice-ui/src/handlers/InputSelectMultiple.tsx @@ -4,16 +4,16 @@ import { LabelWithTooltipMaybeRequired, UIFormProps } from "./InputLine.js"; import { useField } from "./useField.js"; import { useState } from "preact/hooks"; -export function InputSelectMultiple( +export function InputSelectMultiple<T extends object, K extends keyof T>( props: { choices: Choice[]; unique?: boolean; max?: number; - } & UIFormProps<Array<string>>, + } & UIFormProps<T, K>, ): VNode { const { name, label, choices, placeholder, tooltip, required, unique, max } = props; - const { value, onChange } = useField<{ [s: string]: Array<string> }>(name); + const { value, onChange } = useField<T, K>(name); const [filter, setFilter] = useState<string | undefined>(undefined); const regex = new RegExp(`.*${filter}.*`, "i"); @@ -21,7 +21,7 @@ export function InputSelectMultiple( return { ...prev, [curr.value]: curr.label }; }, {} as Record<string, string>); - const list = value ?? []; + const list = (value ?? []) as string[]; const filteredChoices = filter === undefined ? undefined @@ -44,7 +44,7 @@ export function InputSelectMultiple( onClick={() => { const newValue = [...list]; newValue.splice(idx, 1); - onChange(newValue); + onChange(newValue as T[K]); setFilter(undefined); }} class="group relative h-5 w-5 rounded-sm hover:bg-gray-500/20" @@ -119,7 +119,7 @@ export function InputSelectMultiple( } const newValue = [...list]; newValue.splice(0, 0, v.value); - onChange(newValue); + onChange(newValue as T[K]); }} // tabindex="-1" diff --git a/packages/exchange-backoffice-ui/src/handlers/InputSelectOne.tsx b/packages/exchange-backoffice-ui/src/handlers/InputSelectOne.tsx index a4ed1ba1c..9af446e6c 100644 --- a/packages/exchange-backoffice-ui/src/handlers/InputSelectOne.tsx +++ b/packages/exchange-backoffice-ui/src/handlers/InputSelectOne.tsx @@ -4,15 +4,13 @@ import { LabelWithTooltipMaybeRequired, UIFormProps } from "./InputLine.js"; import { useField } from "./useField.js"; import { useState } from "preact/hooks"; -export function InputSelectOne( +export function InputSelectOne<T extends object, K extends keyof T>( props: { choices: Choice[]; - } & UIFormProps<Array<string>>, + } & UIFormProps<T, K>, ): VNode { const { name, label, choices, placeholder, tooltip, required } = props; - const { value, onChange } = useField<{ [s: string]: string | undefined }>( - name, - ); + const { value, onChange } = useField<T, K>(name); const [filter, setFilter] = useState<string | undefined>(undefined); const regex = new RegExp(`.*${filter}.*`, "i"); @@ -35,11 +33,11 @@ export function InputSelectOne( /> {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]} + {choiceMap[value as string]} <button type="button" onClick={() => { - onChange(undefined); + onChange(undefined!); }} class="group relative h-5 w-5 rounded-sm hover:bg-gray-500/20" > @@ -103,7 +101,7 @@ export function InputSelectOne( role="option" onClick={() => { setFilter(undefined); - onChange(v.value); + onChange(v.value as T[K]); }} // tabindex="-1" diff --git a/packages/exchange-backoffice-ui/src/handlers/InputText.tsx b/packages/exchange-backoffice-ui/src/handlers/InputText.tsx index 014730d92..1b37ee6fb 100644 --- a/packages/exchange-backoffice-ui/src/handlers/InputText.tsx +++ b/packages/exchange-backoffice-ui/src/handlers/InputText.tsx @@ -1,6 +1,8 @@ import { VNode, h } from "preact"; import { InputLine, UIFormProps } from "./InputLine.js"; -export function InputText<T>(props: UIFormProps<T>): VNode { +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 index b819a5549..45229951e 100644 --- a/packages/exchange-backoffice-ui/src/handlers/InputTextArea.tsx +++ b/packages/exchange-backoffice-ui/src/handlers/InputTextArea.tsx @@ -1,6 +1,8 @@ import { VNode, h } from "preact"; import { InputLine, UIFormProps } from "./InputLine.js"; -export function InputTextArea(props: UIFormProps<string>): VNode { +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 index a97b8561d..115127cc3 100644 --- a/packages/exchange-backoffice-ui/src/handlers/forms.ts +++ b/packages/exchange-backoffice-ui/src/handlers/forms.ts @@ -25,18 +25,18 @@ type DoubleColumnFormSection = { /** * Constrain the type with the ui props */ -type FieldType = { +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>[0]; - file: Parameters<typeof InputFile>[0]; - selectOne: Parameters<typeof InputSelectOne>[0]; - selectMultiple: Parameters<typeof InputSelectMultiple>[0]; - text: Parameters<typeof InputText>[0]; - textArea: Parameters<typeof InputTextArea>[0]; - choiceStacked: Parameters<typeof InputChoiceStacked>[0]; - date: Parameters<typeof InputDate>[0]; - integer: Parameters<typeof InputInteger>[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]; + date: Parameters<typeof InputDate<T, K>>[0]; + integer: Parameters<typeof InputInteger<T, K>>[0]; }; /** @@ -69,14 +69,20 @@ type UIFormFieldMap = { 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, integer: InputInteger, + //@ts-ignore selectOne: InputSelectOne, + //@ts-ignore selectMultiple: InputSelectMultiple, }; @@ -97,11 +103,11 @@ export function RenderAllFieldsByUiConfig({ ); } -type FormSet<T> = { +type FormSet<T extends object, K extends keyof T = any> = { Provider: typeof FormProvider<T>; - InputLine: typeof InputLine<T>; + InputLine: typeof InputLine<T, K>; }; -export function createNewForm<T>(): FormSet<T> { +export function createNewForm<T extends object>(): FormSet<T> { return { Provider: FormProvider, InputLine: InputLine, diff --git a/packages/exchange-backoffice-ui/src/handlers/useField.ts b/packages/exchange-backoffice-ui/src/handlers/useField.ts index 3be397314..94635646f 100644 --- a/packages/exchange-backoffice-ui/src/handlers/useField.ts +++ b/packages/exchange-backoffice-ui/src/handlers/useField.ts @@ -12,7 +12,9 @@ export interface InputFieldHandler<Type> { isDirty: boolean; } -export function useField<T>(name: keyof T): InputFieldHandler<T[keyof T]> { +export function useField<T extends object, K extends keyof T>( + name: K, +): InputFieldHandler<T[K]> { const { initialValue, value: formValue, @@ -78,3 +80,28 @@ function setValueDeeper(object: any, names: string[], value: any): any { } return { ...object, [head]: setValueDeeper(object[head] ?? {}, rest, value) }; } + +type TTT<T extends object, K extends keyof T, R> = K extends keyof T + ? R extends T[K] + ? number + : never + : never; + +function impl<T extends object, K extends keyof T, R extends T[K]>( + obj: T, + name: K, +): T[K] { + return obj[name]; +} + +interface Pepe { + name: string; + when: Date; + size: number; +} +const p: Pepe = { + name: "n", + when: new Date(), + size: 1, +}; +const a = impl(p, "size"); |