merchant-backoffice

ZZZ: Inactive/Deprecated
Log | Files | Refs | Submodules | README

commit 224cbe6846fbf3ea7652bc2413cdd8fc73535b20
parent e76acb3878c13104210631e429ca9cbbb3a6af8d
Author: Sebastian <sebasjm@gmail.com>
Date:   Wed, 10 Mar 2021 15:45:36 -0300

updated form for deep object

Diffstat:
Mpackages/frontend/src/components/form/Field.tsx | 23++++++++++++++++++++---
Mpackages/frontend/src/components/form/Input.tsx | 24++++++++++++++++++------
Mpackages/frontend/src/components/form/InputArray.tsx | 20++++++++++----------
Mpackages/frontend/src/components/form/InputCurrency.tsx | 5+++--
Mpackages/frontend/src/components/form/InputDuration.tsx | 6++++--
Mpackages/frontend/src/components/form/InputPayto.tsx | 2+-
Mpackages/frontend/src/components/form/InputSecured.tsx | 125+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Mpackages/frontend/src/components/form/InputWithAddon.tsx | 10++++++----
Mpackages/frontend/src/routes/admin/list/Table.tsx | 7+++++--
Mpackages/frontend/src/routes/instance/update/UpdatePage.tsx | 43++++++++++++++++++++++++++++++++++---------
Mpackages/frontend/src/routes/instance/update/index.tsx | 14++------------
11 files changed, 171 insertions(+), 108 deletions(-)

diff --git a/packages/frontend/src/components/form/Field.tsx b/packages/frontend/src/components/form/Field.tsx @@ -74,14 +74,31 @@ export function FormProvider<T>({ object = {}, errors = {}, valueHandler, childr </FormContext.Provider> } +/** + * read the field of an object an support accesing it using '.' + * + * @param object + * @param name + * @returns + */ +const readField = (object: any, name: string) => { + return name.split('.').reduce((prev, current) => prev[current], object) +} + +const setValueDeeper = (object: any, names: string[], value: any): any => { + if (names.length === 0) return value + const [head, ...rest] = names + return {...object, [head]: setValueDeeper(object[head], rest, value) } +} + export function useField<T>(name: keyof T) { const { errors, object, initial, toStr, fromStr, valueHandler } = useContext<FormType<T>>(FormContext) type P = typeof name type V = T[P] - const updateField = (f: P) => (v: V): void => { + const updateField = (field: P) => (value: V): void => { return valueHandler((prev) => { - return ({ ...prev, [f]: v }) + return setValueDeeper(prev, String(field).split('.'), value) }) } @@ -90,7 +107,7 @@ export function useField<T>(name: keyof T) { return { error: errors[name], - value: object[name], + value: readField(object, String(name)), initial: initial[name], onChange: updateField(name), toStr: toStr[name] ? toStr[name]! : defaultToString, diff --git a/packages/frontend/src/components/form/Input.tsx b/packages/frontend/src/components/form/Input.tsx @@ -25,10 +25,21 @@ import { useField } from "./Field"; interface Props<T> { name: T; readonly?: boolean; + inputType?: 'text' | 'number' | 'multiline'; + expand?: boolean; + toStr?: (v?: any) => string; + fromStr?: (s: string) => any; } -export function Input<T>({ name, readonly }: Props<keyof T>): VNode { - const { error, value, onChange, toStr, fromStr } = useField<T>(name); +const defaultToString = (f?: any): string => f || '' +const defaultFromString = (v: string): any => v as any + +const TextInput = ({inputType, error, ...rest}:any) => inputType === 'multiline' ? + <textarea {...rest} class={error ? "textarea is-danger" : "textarea"} rows="3" /> : + <input {...rest} class={error ? "input is-danger" : "input"} type={inputType} />; + +export function Input<T>({ name, readonly, expand, inputType, fromStr = defaultFromString, toStr = defaultToString }: Props<keyof T>): VNode { + const { error, value, onChange } = useField<T>(name); const placeholder = useMessage(`fields.instance.${name}.placeholder`); const tooltip = useMessage(`fields.instance.${name}.tooltip`); @@ -42,13 +53,14 @@ export function Input<T>({ name, readonly }: Props<keyof T>): VNode { </span>} </label> </div> - <div class="field-body"> + <div class="field-body is-flex-grow-3"> <div class="field"> - <p class="control"> - <input class={error ? "input is-danger" : "input"} type="text" + <p class={ expand ? "control is-expanded" : "control" }> + <TextInput error={error} + inputType={inputType} placeholder={placeholder} readonly={readonly} name={String(name)} value={toStr(value)} disabled={readonly} - onChange={(e): void => onChange(fromStr(e.currentTarget.value))} /> + onChange={(e:h.JSX.TargetedEvent<HTMLInputElement>): void => onChange(fromStr(e.currentTarget.value))} /> <Message id={`fields.instance.${name}.help`}> </Message> </p> {error ? <p class="help is-danger"> diff --git a/packages/frontend/src/components/form/InputArray.tsx b/packages/frontend/src/components/form/InputArray.tsx @@ -26,7 +26,7 @@ import { FormErrors, useField, ValidationError } from "./Field"; export interface Props<T> { name: T; readonly?: boolean; - isValid: (e: any) => boolean; + isValid?: (e: any) => boolean; addonBefore?: string; toStr?: (v?: any) => string; fromStr?: (s: string) => any; @@ -35,7 +35,7 @@ export interface Props<T> { const defaultToString = (f?: any): string => f || '' const defaultFromString = (v: string): any => v as any -export function InputArray<T>({ name, readonly, addonBefore, isValid, fromStr = defaultFromString, toStr = defaultToString }: Props<keyof T>): VNode { +export function InputArray<T>({ name, readonly, addonBefore, isValid = () => true, fromStr = defaultFromString, toStr = defaultToString }: Props<keyof T>): VNode { const { error: formError, value, onChange } = useField<T>(name); const [localError, setLocalError] = useState<ValidationError | null>(null) const placeholder = useMessage(`fields.instance.${name}.placeholder`); @@ -56,13 +56,13 @@ export function InputArray<T>({ name, readonly, addonBefore, isValid, fromStr = </span>} </label> </div> - <div class="field-body"> + <div class="field-body is-flex-grow-3"> <div class="field"> <div class="field has-addons"> {addonBefore && <div class="control"> <a class="button is-static">{addonBefore}</a> </div>} - <p class="control"> + <p class="control is-expanded"> <input class={error ? "input is-danger" : "input"} type="text" placeholder={placeholder} readonly={readonly} disabled={readonly} name={String(name)} value={currentValue} @@ -75,9 +75,9 @@ export function InputArray<T>({ name, readonly, addonBefore, isValid, fromStr = if (!isValid(v)) { setLocalError({ message: i18n`The value ${v} is invalid for a payment url` }) return; - } - setLocalError(null) - + } + setLocalError(null) + onChange([v, ...array] as any); setCurrentValue(''); }}>add</button> @@ -86,9 +86,9 @@ export function InputArray<T>({ name, readonly, addonBefore, isValid, fromStr = {error ? <p class="help is-danger"> <Message id={`validation.${error.type}`} fields={error.params}>{error.message}</Message> </p> : null} - {array.map(v => <div class="tags has-addons"> - <span class="tag is-medium is-info" style={{maxWidth:'90%'}}>{v}</span> - <a class="tag is-medium is-danger is-delete" onClick={() => { + {array.map(v => <div class="tags has-addons mt-3 mb-0"> + <span class="tag is-medium is-info mb-0" style={{ maxWidth: '90%' }}>{v}</span> + <a class="tag is-medium is-danger is-delete mb-0" onClick={() => { onChange(array.filter(f => f !== v) as any); setCurrentValue(toStr(v)); }} /> diff --git a/packages/frontend/src/components/form/InputCurrency.tsx b/packages/frontend/src/components/form/InputCurrency.tsx @@ -21,16 +21,17 @@ import { h } from "preact"; import { Amount } from "../../declaration"; import { InputWithAddon } from "./InputWithAddon"; -import { useField } from "./Field"; export interface Props<T> { name: keyof T; readonly?: boolean; + expand?: boolean; currency: string; } -export function InputCurrency<T>({ name, readonly, currency }: Props<T>) { +export function InputCurrency<T>({ name, readonly, expand, currency }: Props<T>) { return <InputWithAddon<T> name={name} readonly={readonly} addonBefore={currency} + inputType='number' expand={expand} toStr={(v?: Amount) => v?.split(':')[1] || ''} fromStr={(v: string) => `${currency}:${v}`} /> diff --git a/packages/frontend/src/components/form/InputDuration.tsx b/packages/frontend/src/components/form/InputDuration.tsx @@ -26,12 +26,14 @@ import { useField } from "./Field"; export interface Props<T> { name: keyof T; + expand?: boolean; readonly?: boolean; } -export function InputDuration<T>({ name, readonly }: Props<T>) { +export function InputDuration<T>({ name, expand, readonly }: Props<T>) { const { value } = useField<T>(name); - return <InputWithAddon<T> name={name} readonly={readonly} addonAfter={readableDuration( value as any )} + return <InputWithAddon<T> name={name} readonly={readonly} addonAfter={readableDuration(value as any)} + expand={expand} toStr={(v?: RelativeTime) => `${(v && v.d_ms !== "forever" && v.d_ms ? v.d_ms / 1000 : '')}`} fromStr={(v: string) => ({ d_ms: (parseInt(v, 10) * 1000) || undefined })} /> diff --git a/packages/frontend/src/components/form/InputPayto.tsx b/packages/frontend/src/components/form/InputPayto.tsx @@ -31,7 +31,7 @@ const PAYTO_START_REGEX = /^payto:\/\// export function InputPayto<T>({ name, readonly }: Props<T>) { return <InputArray<T> name={name} readonly={readonly} - addonBefore="payto://" + addonBefore="payto://" isValid={(v) => v && PAYTO_REGEX.test(v) } toStr={(v?: string) => !v ? '': v.replace(PAYTO_START_REGEX, '')} fromStr={(v: string) => `payto://${v}` } diff --git a/packages/frontend/src/components/form/InputSecured.tsx b/packages/frontend/src/components/form/InputSecured.tsx @@ -47,65 +47,76 @@ export function InputSecured<T>({ name, readonly }: Props<T>): VNode { const [active, setActive] = useState(false); const [newValue, setNuewValue] = useState("") - return <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label"> - <Message id={`fields.instance.${name}.label`} /> - {tooltip && <span class="icon" data-tooltip={tooltip}> - <i class="mdi mdi-information" /> - </span>} - </label> - </div> - <div class="field-body"> - {!active ? - <Fragment> - <div class="field has-addons"> - <button class="button" onClick={(): void => { setActive(!active); }} > - <div class="icon is-left"><i class="mdi mdi-lock-reset" /></div> - <span>Manage token</span> - </button> - <TokenStatus prev={initial} post={value} /> - </div> - </Fragment> : - <Fragment> - <div class="field has-addons"> - <div class="control"> - <a class="button is-static">secret-token:</a> - </div> - <div class="control"> - <input class="input" type="password" - placeholder={placeholder} readonly={readonly || !active} - disabled={readonly || !active} - name={String(name)} value={newValue} - onInput={(e): void => { - setNuewValue(e.currentTarget.value) - }} /> - <Message id={`fields.instance.${name}.help`}> </Message> - </div> - <div class="control"> - <button class="button is-info" disabled={fromStr(newValue) === value} onClick={(): void => { onChange(fromStr(newValue)); setActive(!active); setNuewValue(""); }} > - <div class="icon is-left"><i class="mdi mdi-lock-outline" /></div> - <span>Update</span> + return <Fragment> + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label"> + <Message id={`fields.instance.${name}.label`} /> + {tooltip && <span class="icon" data-tooltip={tooltip}> + <i class="mdi mdi-information" /> + </span>} + </label> + </div> + <div class="field-body is-flex-grow-3"> + {!active ? + <Fragment> + <div class="field has-addons"> + <button class="button" onClick={(): void => { setActive(!active); }} > + <div class="icon is-left"><i class="mdi mdi-lock-reset" /></div> + <span>Manage token</span> </button> + <TokenStatus prev={initial} post={value} /> </div> - </div> - <div class="control"> - <button class="button is-danger" disabled={null === value || undefined === value} onClick={(): void => { onChange(null!); setActive(!active); setNuewValue("");}} > - <div class="icon is-left"><i class="mdi mdi-lock-open-variant" /></div> - <span>Remove</span> - </button> - </div> - <div class="field ml-4"> - <div class="control"> - <button class="button " onClick={(): void => { onChange(initial!); setActive(!active); setNuewValue(""); }} > - <div class="icon is-left"><i class="mdi mdi-lock-open-variant" /></div> - <span>Cancel update</span> - </button> + </Fragment> : + <Fragment> + <div class="field has-addons"> + <div class="control"> + <a class="button is-static">secret-token:</a> + </div> + <div class="control is-expanded"> + <input class="input" type="password" + placeholder={placeholder} readonly={readonly || !active} + disabled={readonly || !active} + name={String(name)} value={newValue} + onInput={(e): void => { + setNuewValue(e.currentTarget.value) + }} /> + <Message id={`fields.instance.${name}.help`}> </Message> + </div> + <div class="control"> + <button class="button is-info" disabled={fromStr(newValue) === value} onClick={(): void => { onChange(fromStr(newValue)); setActive(!active); setNuewValue(""); }} > + <div class="icon is-left"><i class="mdi mdi-lock-outline" /></div> + <span>Update</span> + </button> + </div> </div> - </div> - </Fragment> - } - {error ? <p class="help is-danger"><Message id={`validation.${error.type}`} fields={error.params}>{error.message}</Message></p> : null} + </Fragment> + } + {error ? <p class="help is-danger"><Message id={`validation.${error.type}`} fields={error.params}>{error.message}</Message></p> : null} + </div> </div> - </div>; + {active && + <div class="field is-horizontal"> + <div class="field-body is-flex-grow-3"> + <div class="level" style={{width:'100%'}}> + <div class="level-right is-flex-grow-1"> + <div class="level-item"> + <button class="button is-danger" disabled={null === value || undefined === value} onClick={(): void => { onChange(null!); setActive(!active); setNuewValue(""); }} > + <div class="icon is-left"><i class="mdi mdi-lock-open-variant" /></div> + <span>Remove</span> + </button> + </div> + <div class="level-item"> + <button class="button " onClick={(): void => { onChange(initial!); setActive(!active); setNuewValue(""); }} > + <div class="icon is-left"><i class="mdi mdi-lock-open-variant" /></div> + <span>Cancel</span> + </button> + </div> + </div> + + </div> + </div> + </div> + } + </Fragment >; } diff --git a/packages/frontend/src/components/form/InputWithAddon.tsx b/packages/frontend/src/components/form/InputWithAddon.tsx @@ -25,6 +25,8 @@ import { useField } from "./Field"; export interface Props<T> { name: keyof T; readonly?: boolean; + expand?: boolean; + inputType?: 'text' | 'number'; addonBefore?: string; addonAfter?: string; toStr?: (v?: any) => string; @@ -34,7 +36,7 @@ export interface Props<T> { const defaultToString = (f?: any):string => f || '' const defaultFromString = (v: string):any => v as any -export function InputWithAddon<T>({ name, readonly, addonBefore, addonAfter, toStr = defaultToString, fromStr = defaultFromString }: Props<T>): VNode { +export function InputWithAddon<T>({ name, readonly, addonBefore, expand, inputType, addonAfter, toStr = defaultToString, fromStr = defaultFromString }: Props<T>): VNode { const { error, value, onChange } = useField<T>(name); const placeholder = useMessage(`fields.instance.${name}.placeholder`); @@ -49,14 +51,14 @@ export function InputWithAddon<T>({ name, readonly, addonBefore, addonAfter, toS </span>} </label> </div> - <div class="field-body"> + <div class="field-body is-flex-grow-3"> <div class="field"> <div class="field has-addons"> {addonBefore && <div class="control"> <a class="button is-static">{addonBefore}</a> </div>} - <p class="control is-expanded"> - <input class={error ? "input is-danger" : "input"} type="text" + <p class={ expand ? "control is-expanded" : "control" }> + <input class={error ? "input is-danger" : "input"} type={inputType} placeholder={placeholder} readonly={readonly} disabled={readonly} name={String(name)} value={toStr(value)} onChange={(e): void => onChange(fromStr(e.currentTarget.value))} /> diff --git a/packages/frontend/src/routes/admin/list/Table.tsx b/packages/frontend/src/routes/admin/list/Table.tsx @@ -53,7 +53,7 @@ export function CardTable({ instances, onCreate, onUpdate, onDelete, selected }: return <div class="card has-table"> <header class="card-header"> - <p class="card-header-title"><span class="icon"><i class="mdi mdi-account-multiple" /></span><Message id="Instances" /></p> + <p class="card-header-title"><span class="icon"><i class="mdi mdi-desktop-mac" /></span><Message id="Instances" /></p> <div class="card-header-icon" aria-label="more options"> @@ -118,10 +118,13 @@ function Table({ rowSelection, rowSelectionHandler, instances, onUpdate, onDelet <span class="check" /> </label> </td> - <td><a onClick={(): void => onUpdate(i.id)} style={{cursor: 'pointer'}} >{i.id}</a></td> + <td><a onClick={(): void => onUpdate(i.id)} style={{ cursor: 'pointer' }} >{i.id}</a></td> <td >{i.name}</td> <td class="is-actions-cell"> <div class="buttons is-right"> + <button class="button is-small is-success jb-modal" type="button" onClick={(): void => navigator.clipboard.writeText(i.id) as any}> + <span class="icon"><i class="mdi mdi-content-copy" /></span> + </button> <button class="button is-small is-success jb-modal" type="button" onClick={(): void => onUpdate(i.id)}> <span class="icon"><i class="mdi mdi-pen" /></span> </button> diff --git a/packages/frontend/src/routes/instance/update/UpdatePage.tsx b/packages/frontend/src/routes/instance/update/UpdatePage.tsx @@ -34,8 +34,9 @@ import { useConfigContext, useInstanceContext } from "../../../context/backend"; import { InputDuration } from "../../../components/form/InputDuration"; import { InputCurrency } from "../../../components/form/InputCurrency"; import { InputPayto } from "../../../components/form/InputPayto"; +import { InputArray } from "../../../components/form/InputArray"; -type Entity = MerchantBackend.Instances.InstanceReconfigurationMessage & {auth_token?: string} +type Entity = MerchantBackend.Instances.InstanceReconfigurationMessage & { auth_token?: string } interface Props { onUpdate: (d: Entity, auth?: MerchantBackend.Instances.InstanceAuthConfigurationMessage) => void; @@ -73,11 +74,11 @@ export function UpdatePage({ onUpdate, isLoading, selected, onBack }: Props): VN // use conversion instead of this const newToken = value.auth_token; value.auth_token = undefined; - const auth: MerchantBackend.Instances.InstanceAuthConfigurationMessage | undefined = - newToken === currentTokenValue ? undefined : (newToken === null ? - { method: "external" } : + const auth: MerchantBackend.Instances.InstanceAuthConfigurationMessage | undefined = + newToken === currentTokenValue ? undefined : (newToken === null ? + { method: "external" } : { method: "token", token: `secret-token:${newToken}` }); - + // remove above use conversion schema.validateSync(value, { abortEarly: false }) onUpdate(schema.cast(value), auth); @@ -94,7 +95,7 @@ export function UpdatePage({ onUpdate, isLoading, selected, onBack }: Props): VN <section class="section is-main-section"> <div class="columns"> <div class="column" /> - <div class="column is-two-thirds"> + <div class="column is-four-fifths"> <FormProvider<Entity> errors={errors} object={value} valueHandler={valueHandler} > <Input<Entity> name="name" /> @@ -107,14 +108,38 @@ export function UpdatePage({ onUpdate, isLoading, selected, onBack }: Props): VN <InputCurrency<Entity> name="default_max_wire_fee" currency={config.currency} /> - <Input<Entity> name="default_wire_fee_amortization" /> + <Input<Entity> name="default_wire_fee_amortization" inputType="number" /> <InputGroup name="address"> - <Input<Entity> name="name" /> + <Input name="address.country" /> + <Input name="address.address_lines" inputType="multiline" + toStr={(v: string[] | undefined) => !v ? '' : v.join('\n')} + fromStr={(v: string) => v.split('\n')} + /> + <Input name="address.building_number" /> + <Input name="address.building_name" /> + <Input name="address.street" /> + <Input name="address.post_code" /> + <Input name="address.town_location" /> + <Input name="address.town" /> + <Input name="address.district" /> + <Input name="address.country_subdivision" /> </InputGroup> <InputGroup name="jurisdiction"> - <Input<Entity> name="name" /> + <Input name="jurisdiction.country" /> + <Input name="jurisdiction.address_lines" inputType="multiline" + toStr={(v: string[] | undefined) => !v ? '' : v.join('\n')} + fromStr={(v: string) => v.split('\n')} + /> + <Input name="jurisdiction.building_number" /> + <Input name="jurisdiction.building_name" /> + <Input name="jurisdiction.street" /> + <Input name="jurisdiction.post_code" /> + <Input name="jurisdiction.town_location" /> + <Input name="jurisdiction.town" /> + <Input name="jurisdiction.district" /> + <Input name="jurisdiction.country_subdivision" /> </InputGroup> <InputDuration<Entity> name="default_pay_delay" /> diff --git a/packages/frontend/src/routes/instance/update/index.tsx b/packages/frontend/src/routes/instance/update/index.tsx @@ -33,10 +33,8 @@ interface Props { } export default function Update({ onBack, onConfirm, onLoadError, onUpdateError, onUnauthorized }: Props): VNode { - const { updateInstance, setNewToken, clearToken } = useInstanceMutateAPI(); - const [updatingToken, setUpdatingToken] = useState<boolean>(false) + const { updateInstance } = useInstanceMutateAPI(); const details = useInstanceDetails() - const { id, token } = useInstanceContext() if (!details.data) { if (details.unauthorized) return onUnauthorized() @@ -52,15 +50,7 @@ export default function Update({ onBack, onConfirm, onLoadError, onUpdateError, isLoading={false} selected={details.data} onUpdate={(d: MerchantBackend.Instances.InstanceReconfigurationMessage, t?: MerchantBackend.Instances.InstanceAuthConfigurationMessage): Promise<void> => { - return updateInstance(d,t).then(onConfirm).catch(onUpdateError) + return updateInstance(d, t).then(onConfirm).catch(onUpdateError) }} /> - <button class="button" onClick={() => setUpdatingToken(true)}>auth</button> - {updatingToken && <UpdateTokenModal - oldToken={token} - element={{ id, name: details.data.name }} - onCancel={() => setUpdatingToken(false)} - onClear={() => clearToken()} - onConfirm={(newToken) => setNewToken(newToken)} - />} </Fragment> } \ No newline at end of file