diff options
Diffstat (limited to 'packages/merchant-backoffice-ui/src/components')
36 files changed, 2285 insertions, 1340 deletions
diff --git a/packages/merchant-backoffice-ui/src/components/exception/AsyncButton.tsx b/packages/merchant-backoffice-ui/src/components/exception/AsyncButton.tsx index b234ce847..510bc29b8 100644 --- a/packages/merchant-backoffice-ui/src/components/exception/AsyncButton.tsx +++ b/packages/merchant-backoffice-ui/src/components/exception/AsyncButton.tsx @@ -15,9 +15,9 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ + * + * @author Sebastian Javier Marchano (sebasjm) + */ import { ComponentChildren, h } from "preact"; import { LoadingModal } from "../modal/index.js"; @@ -25,10 +25,10 @@ import { useAsync } from "../../hooks/async.js"; import { Translate } from "../../i18n/index.js"; type Props = { - children: ComponentChildren, + children: ComponentChildren; disabled: boolean; onClick?: () => Promise<void>; - [rest:string]: any, + [rest: string]: any; }; export function AsyncButton({ onClick, disabled, children, ...rest }: Props) { @@ -38,12 +38,18 @@ export function AsyncButton({ onClick, disabled, children, ...rest }: Props) { return <LoadingModal onCancel={cancel} />; } if (isLoading) { - return <button class="button"><Translate>Loading...</Translate></button>; + return ( + <button class="button"> + <Translate>Loading...</Translate> + </button> + ); } - return <span {...rest}> - <button class="button is-success" onClick={request} disabled={disabled}> - {children} - </button> - </span>; + return ( + <span {...rest}> + <button class="button is-success" onClick={request} disabled={disabled}> + {children} + </button> + </span> + ); } diff --git a/packages/merchant-backoffice-ui/src/components/exception/loading.tsx b/packages/merchant-backoffice-ui/src/components/exception/loading.tsx index 9c9b4daae..a043b81eb 100644 --- a/packages/merchant-backoffice-ui/src/components/exception/loading.tsx +++ b/packages/merchant-backoffice-ui/src/components/exception/loading.tsx @@ -15,18 +15,34 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ + * + * @author Sebastian Javier Marchano (sebasjm) + */ import { h, VNode } from "preact"; export function Loading(): VNode { - return <div class="columns is-centered is-vcentered" style={{ height: 'calc(100% - 3rem)', position: 'absolute', width: '100%' }}> - <Spinner /> - </div> + return ( + <div + class="columns is-centered is-vcentered" + style={{ + height: "calc(100% - 3rem)", + position: "absolute", + width: "100%", + }} + > + <Spinner /> + </div> + ); } export function Spinner(): VNode { - return <div class="lds-ring"><div /><div /><div /><div /></div> -}
\ No newline at end of file + return ( + <div class="lds-ring"> + <div /> + <div /> + <div /> + <div /> + </div> + ); +} diff --git a/packages/merchant-backoffice-ui/src/components/exception/login.tsx b/packages/merchant-backoffice-ui/src/components/exception/login.tsx index c2af2a83a..d1898915d 100644 --- a/packages/merchant-backoffice-ui/src/components/exception/login.tsx +++ b/packages/merchant-backoffice-ui/src/components/exception/login.tsx @@ -46,7 +46,7 @@ export function LoginModal({ onConfirm, withMessage }: Props): VNode { const { url: backendUrl, token: baseToken } = useBackendContext(); const { admin, token: instanceToken } = useInstanceContext(); const currentToken = getTokenValuePart( - !admin ? baseToken : instanceToken || "" + !admin ? baseToken : instanceToken || "", ); const [token, setToken] = useState(currentToken); diff --git a/packages/merchant-backoffice-ui/src/components/form/FormProvider.tsx b/packages/merchant-backoffice-ui/src/components/form/FormProvider.tsx index ab32b6bed..7bcebd706 100644 --- a/packages/merchant-backoffice-ui/src/components/form/FormProvider.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/FormProvider.tsx @@ -15,37 +15,59 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ + * + * @author Sebastian Javier Marchano (sebasjm) + */ import { ComponentChildren, createContext, h, VNode } from "preact"; import { useContext, useMemo } from "preact/hooks"; -type Updater<S> = (value: ((prevState: S) => S) ) => void; +type Updater<S> = (value: (prevState: S) => S) => void; export interface Props<T> { object?: Partial<T>; errors?: FormErrors<T>; name?: string; valueHandler: Updater<Partial<T>> | null; - children: ComponentChildren + children: ComponentChildren; } -const noUpdater: Updater<Partial<unknown>> = () => (s: unknown) => s +const noUpdater: Updater<Partial<unknown>> = () => (s: unknown) => s; -export function FormProvider<T>({ object = {}, errors = {}, name = '', valueHandler, children }: Props<T>): VNode { +export function FormProvider<T>({ + object = {}, + errors = {}, + name = "", + valueHandler, + children, +}: Props<T>): VNode { const initialObject = useMemo(() => object, []); - const value = useMemo<FormType<T>>(() => ({ errors, object, initialObject, valueHandler: valueHandler ? valueHandler : noUpdater, name, toStr: {}, fromStr: {} }), [errors, object, valueHandler]); - - return <FormContext.Provider value={value}> - <form class="field" onSubmit={(e) => { - e.preventDefault(); - // if (valueHandler) valueHandler(object); - }}> - {children} - </form> - </FormContext.Provider>; + const value = useMemo<FormType<T>>( + () => ({ + errors, + object, + initialObject, + valueHandler: valueHandler ? valueHandler : noUpdater, + name, + toStr: {}, + fromStr: {}, + }), + [errors, object, valueHandler], + ); + + return ( + <FormContext.Provider value={value}> + <form + class="field" + onSubmit={(e) => { + e.preventDefault(); + // if (valueHandler) valueHandler(object); + }} + > + {children} + </form> + </FormContext.Provider> + ); } export interface FormType<T> { @@ -58,24 +80,24 @@ export interface FormType<T> { valueHandler: Updater<Partial<T>>; } -const FormContext = createContext<FormType<unknown>>(null!) +const FormContext = createContext<FormType<unknown>>(null!); export function useFormContext<T>() { - return useContext<FormType<T>>(FormContext) + return useContext<FormType<T>>(FormContext); } export type FormErrors<T> = { - [P in keyof T]?: string | FormErrors<T[P]> -} + [P in keyof T]?: string | FormErrors<T[P]>; +}; export type FormtoStr<T> = { - [P in keyof T]?: ((f?: T[P]) => string) -} + [P in keyof T]?: (f?: T[P]) => string; +}; export type FormfromStr<T> = { - [P in keyof T]?: ((f: string) => T[P]) -} + [P in keyof T]?: (f: string) => T[P]; +}; export type FormUpdater<T> = { - [P in keyof T]?: (f: keyof T) => (v: T[P]) => void -} + [P in keyof T]?: (f: keyof T) => (v: T[P]) => void; +}; diff --git a/packages/merchant-backoffice-ui/src/components/form/Input.tsx b/packages/merchant-backoffice-ui/src/components/form/Input.tsx index 793477f3d..54140ba4d 100644 --- a/packages/merchant-backoffice-ui/src/components/form/Input.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/Input.tsx @@ -15,57 +15,101 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ + * + * @author Sebastian Javier Marchano (sebasjm) + */ import { ComponentChildren, h, VNode } from "preact"; import { useField, InputProps } from "./useField.js"; interface Props<T> extends InputProps<T> { - inputType?: 'text' | 'number' | 'multiline' | 'password'; + inputType?: "text" | "number" | "multiline" | "password"; expand?: boolean; toStr?: (v?: any) => string; fromStr?: (s: string) => any; - inputExtra?: any, + inputExtra?: any; side?: ComponentChildren; children?: ComponentChildren; } -const defaultToString = (f?: any): string => f || '' -const defaultFromString = (v: string): any => v as any +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} />; +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, placeholder, tooltip, label, expand, help, children, inputType, inputExtra, side, fromStr = defaultFromString, toStr = defaultToString }: Props<keyof T>): VNode { +export function Input<T>({ + name, + readonly, + placeholder, + tooltip, + label, + expand, + help, + children, + inputType, + inputExtra, + side, + fromStr = defaultFromString, + toStr = defaultToString, +}: Props<keyof T>): VNode { const { error, value, onChange, required } = useField<T>(name); - return <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label"> - {label} - {tooltip && <span class="icon has-tooltip-right" data-tooltip={tooltip}> - <i class="mdi mdi-information" /> - </span>} - </label> - </div> - <div class="field-body is-flex-grow-3"> - <div class="field"> - <p class={expand ? "control is-expanded has-icons-right" : "control has-icons-right"}> - <TextInput error={error} {...inputExtra} - inputType={inputType} - placeholder={placeholder} readonly={readonly} - name={String(name)} value={toStr(value)} - onChange={(e: h.JSX.TargetedEvent<HTMLInputElement>): void => onChange(fromStr(e.currentTarget.value))} /> - {help} - {children} - { required && <span class="icon has-text-danger is-right"> - <i class="mdi mdi-alert" /> - </span> } - </p> - {error && <p class="help is-danger">{error}</p>} + return ( + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label"> + {label} + {tooltip && ( + <span class="icon has-tooltip-right" data-tooltip={tooltip}> + <i class="mdi mdi-information" /> + </span> + )} + </label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p + class={ + expand + ? "control is-expanded has-icons-right" + : "control has-icons-right" + } + > + <TextInput + error={error} + {...inputExtra} + inputType={inputType} + placeholder={placeholder} + readonly={readonly} + name={String(name)} + value={toStr(value)} + onChange={(e: h.JSX.TargetedEvent<HTMLInputElement>): void => + onChange(fromStr(e.currentTarget.value)) + } + /> + {help} + {children} + {required && ( + <span class="icon has-text-danger is-right"> + <i class="mdi mdi-alert" /> + </span> + )} + </p> + {error && <p class="help is-danger">{error}</p>} + </div> + {side} </div> - {side} </div> - </div>; + ); } diff --git a/packages/merchant-backoffice-ui/src/components/form/InputArray.tsx b/packages/merchant-backoffice-ui/src/components/form/InputArray.tsx index f8bf6437d..b5da1117a 100644 --- a/packages/merchant-backoffice-ui/src/components/form/InputArray.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/InputArray.tsx @@ -15,9 +15,9 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ + * + * @author Sebastian Javier Marchano (sebasjm) + */ import { h, VNode } from "preact"; import { useState } from "preact/hooks"; import { Translate, useTranslator } from "../../i18n/index.js"; @@ -30,68 +30,110 @@ export interface Props<T> extends InputProps<T> { fromStr?: (s: string) => any; } -const defaultToString = (f?: any): string => f || '' -const defaultFromString = (v: string): any => v as any +const defaultToString = (f?: any): string => f || ""; +const defaultFromString = (v: string): any => v as any; -export function InputArray<T>({ name, readonly, placeholder, tooltip, label, help, addonBefore, isValid = () => true, fromStr = defaultFromString, toStr = defaultToString }: Props<keyof T>): VNode { +export function InputArray<T>({ + name, + readonly, + placeholder, + tooltip, + label, + help, + addonBefore, + isValid = () => true, + fromStr = defaultFromString, + toStr = defaultToString, +}: Props<keyof T>): VNode { const { error: formError, value, onChange, required } = useField<T>(name); - const [localError, setLocalError] = useState<string | null>(null) + const [localError, setLocalError] = useState<string | null>(null); - const error = localError || formError + const error = localError || formError; const array: any[] = (value ? value! : []) as any; - const [currentValue, setCurrentValue] = useState(''); + const [currentValue, setCurrentValue] = useState(""); const i18n = useTranslator(); - return <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label"> - {label} - {tooltip && <span class="icon has-tooltip-right" data-tooltip={tooltip}> - <i class="mdi mdi-information" /> - </span>} - </label> - </div> - <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 has-icons-right"> - <input class={error ? "input is-danger" : "input"} type="text" - placeholder={placeholder} readonly={readonly} disabled={readonly} - name={String(name)} value={currentValue} - onChange={(e): void => setCurrentValue(e.currentTarget.value)} /> - {required && <span class="icon has-text-danger is-right"> - <i class="mdi mdi-alert" /> - </span>} - </p> - <p class="control"> - <button class="button is-info has-tooltip-left" disabled={!currentValue} onClick={(): void => { - const v = fromStr(currentValue) - if (!isValid(v)) { - setLocalError(i18n`The value ${v} is invalid for a payment url`) - return; - } - setLocalError(null) - onChange([v, ...array] as any); - setCurrentValue(''); - }} data-tooltip={i18n`add element to the list`}><Translate>add</Translate></button> - </p> - </div> - {help} - {error && <p class="help is-danger"> {error} </p>} - {array.map((v, i) => <div key={i} 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)); - }} /> + return ( + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label"> + {label} + {tooltip && ( + <span class="icon has-tooltip-right" data-tooltip={tooltip}> + <i class="mdi mdi-information" /> + </span> + )} + </label> + </div> + <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 has-icons-right"> + <input + class={error ? "input is-danger" : "input"} + type="text" + placeholder={placeholder} + readonly={readonly} + disabled={readonly} + name={String(name)} + value={currentValue} + onChange={(e): void => setCurrentValue(e.currentTarget.value)} + /> + {required && ( + <span class="icon has-text-danger is-right"> + <i class="mdi mdi-alert" /> + </span> + )} + </p> + <p class="control"> + <button + class="button is-info has-tooltip-left" + disabled={!currentValue} + onClick={(): void => { + const v = fromStr(currentValue); + if (!isValid(v)) { + setLocalError( + i18n`The value ${v} is invalid for a payment url`, + ); + return; + } + setLocalError(null); + onChange([v, ...array] as any); + setCurrentValue(""); + }} + data-tooltip={i18n`add element to the list`} + > + <Translate>add</Translate> + </button> + </p> + </div> + {help} + {error && <p class="help is-danger"> {error} </p>} + {array.map((v, i) => ( + <div key={i} 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)); + }} + /> + </div> + ))} </div> - )} </div> - </div> - </div>; + ); } diff --git a/packages/merchant-backoffice-ui/src/components/form/InputBoolean.tsx b/packages/merchant-backoffice-ui/src/components/form/InputBoolean.tsx index 4c40cacf6..f79e16c07 100644 --- a/packages/merchant-backoffice-ui/src/components/form/InputBoolean.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/InputBoolean.tsx @@ -15,9 +15,9 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ + * + * @author Sebastian Javier Marchano (sebasjm) + */ import { h, VNode } from "preact"; import { InputProps, useField } from "./useField.js"; @@ -30,43 +30,62 @@ interface Props<T> extends InputProps<T> { fromBoolean?: (s: boolean | undefined) => any; } -const defaultToBoolean = (f?: any): boolean | undefined => f || '' -const defaultFromBoolean = (v: boolean | undefined): any => v as any - +const defaultToBoolean = (f?: any): boolean | undefined => f || ""; +const defaultFromBoolean = (v: boolean | undefined): any => v as any; -export function InputBoolean<T>({ name, readonly, placeholder, tooltip, label, help, threeState, expand, fromBoolean = defaultFromBoolean, toBoolean = defaultToBoolean }: Props<keyof T>): VNode { +export function InputBoolean<T>({ + name, + readonly, + placeholder, + tooltip, + label, + help, + threeState, + expand, + fromBoolean = defaultFromBoolean, + toBoolean = defaultToBoolean, +}: Props<keyof T>): VNode { const { error, value, onChange } = useField<T>(name); const onCheckboxClick = (): void => { - const c = toBoolean(value) - if (c === false && threeState) return onChange(undefined as any) - return onChange(fromBoolean(!c)) - } + const c = toBoolean(value); + if (c === false && threeState) return onChange(undefined as any); + return onChange(fromBoolean(!c)); + }; - return <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label"> - {label} - {tooltip && <span class="icon has-tooltip-right" data-tooltip={tooltip}> - <i class="mdi mdi-information" /> - </span>} - </label> - </div> - <div class="field-body is-flex-grow-3"> - <div class="field"> - <p class={expand ? "control is-expanded" : "control"}> - <label class="b-checkbox checkbox"> - <input type="checkbox" class={toBoolean(value) === undefined ? "is-indeterminate" : ""} - checked={toBoolean(value)} - placeholder={placeholder} readonly={readonly} - name={String(name)} disabled={readonly} - onChange={onCheckboxClick} /> - <span class="check" /> - </label> - {help} - </p> - {error && <p class="help is-danger">{error}</p>} + return ( + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label"> + {label} + {tooltip && ( + <span class="icon has-tooltip-right" data-tooltip={tooltip}> + <i class="mdi mdi-information" /> + </span> + )} + </label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p class={expand ? "control is-expanded" : "control"}> + <label class="b-checkbox checkbox"> + <input + type="checkbox" + class={toBoolean(value) === undefined ? "is-indeterminate" : ""} + checked={toBoolean(value)} + placeholder={placeholder} + readonly={readonly} + name={String(name)} + disabled={readonly} + onChange={onCheckboxClick} + /> + <span class="check" /> + </label> + {help} + </p> + {error && <p class="help is-danger">{error}</p>} + </div> </div> </div> - </div>; + ); } diff --git a/packages/merchant-backoffice-ui/src/components/form/InputCurrency.tsx b/packages/merchant-backoffice-ui/src/components/form/InputCurrency.tsx index 6191d7ba5..57a5163b7 100644 --- a/packages/merchant-backoffice-ui/src/components/form/InputCurrency.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/InputCurrency.tsx @@ -15,9 +15,9 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ + * + * @author Sebastian Javier Marchano (sebasjm) + */ import { ComponentChildren, h } from "preact"; import { useConfigContext } from "../../context/config.js"; import { Amount } from "../../declaration.js"; @@ -31,17 +31,36 @@ export interface Props<T> extends InputProps<T> { side?: ComponentChildren; } -export function InputCurrency<T>({ name, readonly, label, placeholder, help, tooltip, expand, addonAfter, children, side }: Props<keyof T>) { - const config = useConfigContext() - return <InputWithAddon<T> name={name} readonly={readonly} addonBefore={config.currency} - side={side} - label={label} placeholder={placeholder} help={help} tooltip={tooltip} - addonAfter={addonAfter} - inputType='number' expand={expand} - toStr={(v?: Amount) => v?.split(':')[1] || ''} - fromStr={(v: string) => !v ? '' : `${config.currency}:${v}`} - inputExtra={{ min: 0 }} - children={children} - /> +export function InputCurrency<T>({ + name, + readonly, + label, + placeholder, + help, + tooltip, + expand, + addonAfter, + children, + side, +}: Props<keyof T>) { + const config = useConfigContext(); + return ( + <InputWithAddon<T> + name={name} + readonly={readonly} + addonBefore={config.currency} + side={side} + label={label} + placeholder={placeholder} + help={help} + tooltip={tooltip} + addonAfter={addonAfter} + inputType="number" + expand={expand} + toStr={(v?: Amount) => v?.split(":")[1] || ""} + fromStr={(v: string) => (!v ? "" : `${config.currency}:${v}`)} + inputExtra={{ min: 0 }} + children={children} + /> + ); } - diff --git a/packages/merchant-backoffice-ui/src/components/form/InputDuration.tsx b/packages/merchant-backoffice-ui/src/components/form/InputDuration.tsx index dd21a4708..658cc4db7 100644 --- a/packages/merchant-backoffice-ui/src/components/form/InputDuration.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/InputDuration.tsx @@ -81,7 +81,7 @@ export function InputDuration<T>({ era: () => "e", }, }, - } + }, ); } diff --git a/packages/merchant-backoffice-ui/src/components/form/InputGroup.tsx b/packages/merchant-backoffice-ui/src/components/form/InputGroup.tsx index 26d0292d6..b5e0bd52b 100644 --- a/packages/merchant-backoffice-ui/src/components/form/InputGroup.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/InputGroup.tsx @@ -15,9 +15,9 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ + * + * @author Sebastian Javier Marchano (sebasjm) + */ import { ComponentChildren, h, VNode } from "preact"; import { useState } from "preact/hooks"; import { useGroupField } from "./useGroupField.js"; @@ -32,35 +32,55 @@ export interface Props<T> { initialActive?: boolean; } -export function InputGroup<T>({ name, label, children, tooltip, alternative, fixed, initialActive }: Props<keyof T>): VNode { +export function InputGroup<T>({ + name, + label, + children, + tooltip, + alternative, + fixed, + initialActive, +}: Props<keyof T>): VNode { const [active, setActive] = useState(initialActive || fixed); const group = useGroupField<T>(name); - return <div class="card"> - <header class="card-header"> - <p class="card-header-title"> - {label} - {tooltip && <span class="icon has-tooltip-right" data-tooltip={tooltip}> - <i class="mdi mdi-information" /> - </span>} - {group?.hasError && <span class="icon has-text-danger" data-tooltip={tooltip}> - <i class="mdi mdi-alert" /> - </span>} - </p> - { !fixed && <button class="card-header-icon" aria-label="more options" onClick={(): void => setActive(!active)}> - <span class="icon"> - {active ? - <i class="mdi mdi-arrow-up" /> : - <i class="mdi mdi-arrow-down" />} - </span> - </button> } - </header> - {active ? <div class="card-content"> - {children} - </div> : ( - alternative ? <div class="card-content"> - {alternative} - </div> : undefined - )} - </div>; + return ( + <div class="card"> + <header class="card-header"> + <p class="card-header-title"> + {label} + {tooltip && ( + <span class="icon has-tooltip-right" data-tooltip={tooltip}> + <i class="mdi mdi-information" /> + </span> + )} + {group?.hasError && ( + <span class="icon has-text-danger" data-tooltip={tooltip}> + <i class="mdi mdi-alert" /> + </span> + )} + </p> + {!fixed && ( + <button + class="card-header-icon" + aria-label="more options" + onClick={(): void => setActive(!active)} + > + <span class="icon"> + {active ? ( + <i class="mdi mdi-arrow-up" /> + ) : ( + <i class="mdi mdi-arrow-down" /> + )} + </span> + </button> + )} + </header> + {active ? ( + <div class="card-content">{children}</div> + ) : alternative ? ( + <div class="card-content">{alternative}</div> + ) : undefined} + </div> + ); } diff --git a/packages/merchant-backoffice-ui/src/components/form/InputImage.tsx b/packages/merchant-backoffice-ui/src/components/form/InputImage.tsx index 51ac23ca1..d5b2aadb6 100644 --- a/packages/merchant-backoffice-ui/src/components/form/InputImage.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/InputImage.tsx @@ -15,9 +15,9 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ + * + * @author Sebastian Javier Marchano (sebasjm) + */ import { ComponentChildren, h, VNode } from "preact"; import { useRef, useState } from "preact/hooks"; import { Translate } from "../../i18n/index.js"; @@ -30,65 +30,93 @@ export interface Props<T> extends InputProps<T> { children?: ComponentChildren; } -export function InputImage<T>({ name, readonly, placeholder, tooltip, label, help, children, expand }: Props<keyof T>): VNode { +export function InputImage<T>({ + name, + readonly, + placeholder, + tooltip, + label, + help, + children, + expand, +}: Props<keyof T>): VNode { const { error, value, onChange } = useField<T>(name); - const image = useRef<HTMLInputElement>(null) + const image = useRef<HTMLInputElement>(null); - const [sizeError, setSizeError] = useState(false) + const [sizeError, setSizeError] = useState(false); - return <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label"> - {label} - {tooltip && <span class="icon has-tooltip-right" data-tooltip={tooltip}> - <i class="mdi mdi-information" /> - </span>} - </label> - </div> - <div class="field-body is-flex-grow-3"> - <div class="field"> - <p class={expand ? "control is-expanded" : "control"}> - {value && - <img src={value} style={{ width: 200, height: 200 }} onClick={() => image.current?.click()} /> - } - <input - ref={image} style={{ display: 'none' }} - type="file" name={String(name)} - placeholder={placeholder} readonly={readonly} - onChange={e => { - const f: FileList | null = e.currentTarget.files - if (!f || f.length != 1) { - return onChange(undefined!) - } - if (f[0].size > MAX_IMAGE_UPLOAD_SIZE) { - setSizeError(true) - return onChange(undefined!) - } - setSizeError(false) - return f[0].arrayBuffer().then(b => { - const b64 = btoa( - new Uint8Array(b) - .reduce((data, byte) => data + String.fromCharCode(byte), '') - ) - return onChange(`data:${f[0].type};base64,${b64}` as any) - }) - }} /> - {help} - {children} - </p> - {error && <p class="help is-danger">{error}</p>} - {sizeError && <p class="help is-danger"> - <Translate>Image should be smaller than 1 MB</Translate> - </p>} - {!value && - <button class="button" onClick={() => image.current?.click()} ><Translate>Add</Translate></button> - } - {value && - <button class="button" onClick={() => onChange(undefined!)} ><Translate>Remove</Translate></button> - } + return ( + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label"> + {label} + {tooltip && ( + <span class="icon has-tooltip-right" data-tooltip={tooltip}> + <i class="mdi mdi-information" /> + </span> + )} + </label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p class={expand ? "control is-expanded" : "control"}> + {value && ( + <img + src={value} + style={{ width: 200, height: 200 }} + onClick={() => image.current?.click()} + /> + )} + <input + ref={image} + style={{ display: "none" }} + type="file" + name={String(name)} + placeholder={placeholder} + readonly={readonly} + onChange={(e) => { + const f: FileList | null = e.currentTarget.files; + if (!f || f.length != 1) { + return onChange(undefined!); + } + if (f[0].size > MAX_IMAGE_UPLOAD_SIZE) { + setSizeError(true); + return onChange(undefined!); + } + setSizeError(false); + return f[0].arrayBuffer().then((b) => { + const b64 = btoa( + new Uint8Array(b).reduce( + (data, byte) => data + String.fromCharCode(byte), + "", + ), + ); + return onChange(`data:${f[0].type};base64,${b64}` as any); + }); + }} + /> + {help} + {children} + </p> + {error && <p class="help is-danger">{error}</p>} + {sizeError && ( + <p class="help is-danger"> + <Translate>Image should be smaller than 1 MB</Translate> + </p> + )} + {!value && ( + <button class="button" onClick={() => image.current?.click()}> + <Translate>Add</Translate> + </button> + )} + {value && ( + <button class="button" onClick={() => onChange(undefined!)}> + <Translate>Remove</Translate> + </button> + )} + </div> </div> </div> - </div> + ); } - diff --git a/packages/merchant-backoffice-ui/src/components/form/InputLocation.tsx b/packages/merchant-backoffice-ui/src/components/form/InputLocation.tsx index c97fe928b..613b2f1e6 100644 --- a/packages/merchant-backoffice-ui/src/components/form/InputLocation.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/InputLocation.tsx @@ -15,29 +15,36 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ + * + * @author Sebastian Javier Marchano (sebasjm) + */ import { Fragment, h } from "preact"; import { useTranslator } from "../../i18n/index.js"; import { Input } from "./Input.js"; -export function InputLocation({name}:{name:string}) { - const i18n = useTranslator() - return <> - <Input name={`${name}.country`} label={i18n`Country`} /> - <Input name={`${name}.address_lines`} inputType="multiline" - label={i18n`Address`} - toStr={(v: string[] | undefined) => !v ? '' : v.join('\n')} - fromStr={(v: string) => v.split('\n')} - /> - <Input name={`${name}.building_number`} label={i18n`Building number`} /> - <Input name={`${name}.building_name`} label={i18n`Building name`} /> - <Input name={`${name}.street`} label={i18n`Street`} /> - <Input name={`${name}.post_code`} label={i18n`Post code`} /> - <Input name={`${name}.town_location`} label={i18n`Town location`} /> - <Input name={`${name}.town`} label={i18n`Town`} /> - <Input name={`${name}.district`} label={i18n`District`} /> - <Input name={`${name}.country_subdivision`} label={i18n`Country subdivision`} /> - </> -}
\ No newline at end of file +export function InputLocation({ name }: { name: string }) { + const i18n = useTranslator(); + return ( + <> + <Input name={`${name}.country`} label={i18n`Country`} /> + <Input + name={`${name}.address_lines`} + inputType="multiline" + label={i18n`Address`} + toStr={(v: string[] | undefined) => (!v ? "" : v.join("\n"))} + fromStr={(v: string) => v.split("\n")} + /> + <Input name={`${name}.building_number`} label={i18n`Building number`} /> + <Input name={`${name}.building_name`} label={i18n`Building name`} /> + <Input name={`${name}.street`} label={i18n`Street`} /> + <Input name={`${name}.post_code`} label={i18n`Post code`} /> + <Input name={`${name}.town_location`} label={i18n`Town location`} /> + <Input name={`${name}.town`} label={i18n`Town`} /> + <Input name={`${name}.district`} label={i18n`District`} /> + <Input + name={`${name}.country_subdivision`} + label={i18n`Country subdivision`} + /> + </> + ); +} diff --git a/packages/merchant-backoffice-ui/src/components/form/InputNumber.tsx b/packages/merchant-backoffice-ui/src/components/form/InputNumber.tsx index 9f0b28ff2..3b5df1474 100644 --- a/packages/merchant-backoffice-ui/src/components/form/InputNumber.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/InputNumber.tsx @@ -15,9 +15,9 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ + * + * @author Sebastian Javier Marchano (sebasjm) + */ import { ComponentChildren, h } from "preact"; import { InputWithAddon } from "./InputWithAddon.js"; import { InputProps } from "./useField.js"; @@ -29,14 +29,32 @@ export interface Props<T> extends InputProps<T> { children?: ComponentChildren; } -export function InputNumber<T>({ name, readonly, placeholder, tooltip, label, help, expand, children, side }: Props<keyof T>) { - return <InputWithAddon<T> name={name} readonly={readonly} - fromStr={(v) => !v ? undefined : parseInt(v, 10) } toStr={(v) => `${v}`} - inputType='number' expand={expand} - label={label} placeholder={placeholder} help={help} tooltip={tooltip} - inputExtra={{ min: 0 }} - children={children} - side={side} - /> +export function InputNumber<T>({ + name, + readonly, + placeholder, + tooltip, + label, + help, + expand, + children, + side, +}: Props<keyof T>) { + return ( + <InputWithAddon<T> + name={name} + readonly={readonly} + fromStr={(v) => (!v ? undefined : parseInt(v, 10))} + toStr={(v) => `${v}`} + inputType="number" + expand={expand} + label={label} + placeholder={placeholder} + help={help} + tooltip={tooltip} + inputExtra={{ min: 0 }} + children={children} + side={side} + /> + ); } - diff --git a/packages/merchant-backoffice-ui/src/components/form/InputPayto.tsx b/packages/merchant-backoffice-ui/src/components/form/InputPayto.tsx index 021616e3f..6e88e8f2c 100644 --- a/packages/merchant-backoffice-ui/src/components/form/InputPayto.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/InputPayto.tsx @@ -15,9 +15,9 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ + * + * @author Sebastian Javier Marchano (sebasjm) + */ import { h, VNode } from "preact"; import { InputArray } from "./InputArray.js"; import { PAYTO_REGEX } from "../../utils/constants.js"; @@ -25,15 +25,28 @@ import { InputProps } from "./useField.js"; export type Props<T> = InputProps<T>; -const PAYTO_START_REGEX = /^payto:\/\// - -export function InputPayto<T>({ name, readonly, placeholder, tooltip, label, help }: Props<keyof T>): VNode { - return <InputArray<T> name={name} readonly={readonly} - addonBefore="payto://" - label={label} placeholder={placeholder} help={help} tooltip={tooltip} - isValid={(v) => v && PAYTO_REGEX.test(v) } - toStr={(v?: string) => !v ? '': v.replace(PAYTO_START_REGEX, '')} - fromStr={(v: string) => `payto://${v}` } - /> +const PAYTO_START_REGEX = /^payto:\/\//; + +export function InputPayto<T>({ + name, + readonly, + placeholder, + tooltip, + label, + help, +}: Props<keyof T>): VNode { + return ( + <InputArray<T> + name={name} + readonly={readonly} + addonBefore="payto://" + label={label} + placeholder={placeholder} + help={help} + tooltip={tooltip} + 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/merchant-backoffice-ui/src/components/form/InputSearchProduct.tsx b/packages/merchant-backoffice-ui/src/components/form/InputSearchProduct.tsx index 0c91cc5a1..fceee9d56 100644 --- a/packages/merchant-backoffice-ui/src/components/form/InputSearchProduct.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/InputSearchProduct.tsx @@ -15,9 +15,9 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ + * + * @author Sebastian Javier Marchano (sebasjm) + */ import { h, VNode } from "preact"; import { useState } from "preact/hooks"; import emptyImage from "../../assets/empty.png"; @@ -26,74 +26,97 @@ import { Translate, useTranslator } from "../../i18n/index.js"; import { FormErrors, FormProvider } from "./FormProvider.js"; import { InputWithAddon } from "./InputWithAddon.js"; -type Entity = MerchantBackend.Products.ProductDetail & WithId +type Entity = MerchantBackend.Products.ProductDetail & WithId; export interface Props { selected?: Entity; onChange: (p?: Entity) => void; - products: (MerchantBackend.Products.ProductDetail & WithId)[], + products: (MerchantBackend.Products.ProductDetail & WithId)[]; } interface ProductSearch { name: string; } -export function InputSearchProduct({ selected, onChange, products }: Props): VNode { - const [prodForm, setProdName] = useState<Partial<ProductSearch>>({ name: '' }) +export function InputSearchProduct({ + selected, + onChange, + products, +}: Props): VNode { + const [prodForm, setProdName] = useState<Partial<ProductSearch>>({ + name: "", + }); const errors: FormErrors<ProductSearch> = { - name: undefined - } - const i18n = useTranslator() - + name: undefined, + }; + const i18n = useTranslator(); if (selected) { - return <article class="media"> - <figure class="media-left"> - <p class="image is-128x128"> - <img src={selected.image ? selected.image : emptyImage} /> - </p> - </figure> - <div class="media-content"> - <div class="content"> - <p class="media-meta"><Translate>Product id</Translate>: <b>{selected.id}</b></p> - <p><Translate>Description</Translate>: {selected.description}</p> - <div class="buttons is-right mt-5"> - <button class="button is-info" onClick={() => onChange(undefined)}>clear</button> + return ( + <article class="media"> + <figure class="media-left"> + <p class="image is-128x128"> + <img src={selected.image ? selected.image : emptyImage} /> + </p> + </figure> + <div class="media-content"> + <div class="content"> + <p class="media-meta"> + <Translate>Product id</Translate>: <b>{selected.id}</b> + </p> + <p> + <Translate>Description</Translate>: {selected.description} + </p> + <div class="buttons is-right mt-5"> + <button + class="button is-info" + onClick={() => onChange(undefined)} + > + clear + </button> + </div> </div> </div> - </div> - </article> + </article> + ); } - return <FormProvider<ProductSearch> errors={errors} object={prodForm} valueHandler={setProdName} > - - <InputWithAddon<ProductSearch> - name="name" - label={i18n`Product`} - tooltip={i18n`search products by it's description or id`} - addonAfter={<span class="icon" ><i class="mdi mdi-magnify" /></span>} + return ( + <FormProvider<ProductSearch> + errors={errors} + object={prodForm} + valueHandler={setProdName} > - <div> - <ProductList - name={prodForm.name} - list={products} - onSelect={(p) => { - setProdName({ name: '' }) - onChange(p) - }} - /> - </div> - </InputWithAddon> - - </FormProvider> - + <InputWithAddon<ProductSearch> + name="name" + label={i18n`Product`} + tooltip={i18n`search products by it's description or id`} + addonAfter={ + <span class="icon"> + <i class="mdi mdi-magnify" /> + </span> + } + > + <div> + <ProductList + name={prodForm.name} + list={products} + onSelect={(p) => { + setProdName({ name: "" }); + onChange(p); + }} + /> + </div> + </InputWithAddon> + </FormProvider> + ); } interface ProductListProps { name?: string; onSelect: (p: MerchantBackend.Products.ProductDetail & WithId) => void; - list: (MerchantBackend.Products.ProductDetail & WithId)[] + list: (MerchantBackend.Products.ProductDetail & WithId)[]; } function ProductList({ name, onSelect, list }: ProductListProps) { @@ -102,37 +125,61 @@ function ProductList({ name, onSelect, list }: ProductListProps) { this BR is added to occupy the space that will be added when the dropdown appears */ - return <div ><br /></div> + return ( + <div> + <br /> + </div> + ); } - const filtered = list.filter(p => p.id.includes(name) || p.description.includes(name)) - - return <div class="dropdown is-active"> - <div class="dropdown-menu" id="dropdown-menu" role="menu" style={{ minWidth: '20rem' }}> - <div class="dropdown-content"> - {!filtered.length ? - <div class="dropdown-item" > - <Translate>no products found with that description</Translate> - </div> : - filtered.map(p => ( - <div key={p.id} class="dropdown-item" onClick={() => onSelect(p)} style={{ cursor: 'pointer' }}> - <article class="media"> - <div class="media-left"> - <div class="image" style={{ minWidth: 64 }}><img src={p.image ? p.image : emptyImage} style={{ width: 64, height: 64 }} /></div> - </div> - <div class="media-content"> - <div class="content"> - <p> - <strong>{p.id}</strong> <small>{p.price}</small> - <br /> - {p.description} - </p> - </div> - </div> - </article> + const filtered = list.filter( + (p) => p.id.includes(name) || p.description.includes(name), + ); + + return ( + <div class="dropdown is-active"> + <div + class="dropdown-menu" + id="dropdown-menu" + role="menu" + style={{ minWidth: "20rem" }} + > + <div class="dropdown-content"> + {!filtered.length ? ( + <div class="dropdown-item"> + <Translate>no products found with that description</Translate> </div> - )) - } + ) : ( + filtered.map((p) => ( + <div + key={p.id} + class="dropdown-item" + onClick={() => onSelect(p)} + style={{ cursor: "pointer" }} + > + <article class="media"> + <div class="media-left"> + <div class="image" style={{ minWidth: 64 }}> + <img + src={p.image ? p.image : emptyImage} + style={{ width: 64, height: 64 }} + /> + </div> + </div> + <div class="media-content"> + <div class="content"> + <p> + <strong>{p.id}</strong> <small>{p.price}</small> + <br /> + {p.description} + </p> + </div> + </div> + </article> + </div> + )) + )} + </div> </div> </div> - </div> + ); } diff --git a/packages/merchant-backoffice-ui/src/components/form/InputSecured.stories.tsx b/packages/merchant-backoffice-ui/src/components/form/InputSecured.stories.tsx index 061525d9e..12ce6c6aa 100644 --- a/packages/merchant-backoffice-ui/src/components/form/InputSecured.stories.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/InputSecured.stories.tsx @@ -15,41 +15,47 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ + * + * @author Sebastian Javier Marchano (sebasjm) + */ -import { h, VNode } from 'preact'; -import { useState } from 'preact/hooks'; +import { h, VNode } from "preact"; +import { useState } from "preact/hooks"; import { FormProvider } from "./FormProvider.js"; -import { InputSecured } from './InputSecured.js'; +import { InputSecured } from "./InputSecured.js"; export default { - title: 'Components/Form/InputSecured', + title: "Components/Form/InputSecured", component: InputSecured, }; -type T = { auth_token: string | null } +type T = { auth_token: string | null }; export const InitialValueEmpty = (): VNode => { - const [state, setState] = useState<Partial<T>>({ auth_token: '' }) - return <FormProvider<T> object={state} errors={{}} valueHandler={setState}> - Initial value: '' - <InputSecured<T> name="auth_token" label="Access token" /> - </FormProvider> -} + const [state, setState] = useState<Partial<T>>({ auth_token: "" }); + return ( + <FormProvider<T> object={state} errors={{}} valueHandler={setState}> + Initial value: '' + <InputSecured<T> name="auth_token" label="Access token" /> + </FormProvider> + ); +}; export const InitialValueToken = (): VNode => { - const [state, setState] = useState<Partial<T>>({ auth_token: 'token' }) - return <FormProvider<T> object={state} errors={{}} valueHandler={setState}> - <InputSecured<T> name="auth_token" label="Access token" /> - </FormProvider> -} + const [state, setState] = useState<Partial<T>>({ auth_token: "token" }); + return ( + <FormProvider<T> object={state} errors={{}} valueHandler={setState}> + <InputSecured<T> name="auth_token" label="Access token" /> + </FormProvider> + ); +}; export const InitialValueNull = (): VNode => { - const [state, setState] = useState<Partial<T>>({ auth_token: null }) - return <FormProvider<T> object={state} errors={{}} valueHandler={setState}> - Initial value: '' - <InputSecured<T> name="auth_token" label="Access token" /> - </FormProvider> -} + const [state, setState] = useState<Partial<T>>({ auth_token: null }); + return ( + <FormProvider<T> object={state} errors={{}} valueHandler={setState}> + Initial value: '' + <InputSecured<T> name="auth_token" label="Access token" /> + </FormProvider> + ); +}; diff --git a/packages/merchant-backoffice-ui/src/components/form/InputSecured.tsx b/packages/merchant-backoffice-ui/src/components/form/InputSecured.tsx index b0168e505..799978683 100644 --- a/packages/merchant-backoffice-ui/src/components/form/InputSecured.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/InputSecured.tsx @@ -15,9 +15,9 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ + * + * @author Sebastian Javier Marchano (sebasjm) + */ import { Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; import { Translate, useTranslator } from "../../i18n/index.js"; @@ -26,94 +26,160 @@ import { InputProps, useField } from "./useField.js"; export type Props<T> = InputProps<T>; const TokenStatus = ({ prev, post }: any) => { - if ((prev === undefined || prev === null) && (post === undefined || post === null)) - return null - return (prev === post) ? null : ( - post === null ? - <span class="tag is-danger is-align-self-center ml-2"><Translate>Deleting</Translate></span> : - <span class="tag is-warning is-align-self-center ml-2"><Translate>Changing</Translate></span> + if ( + (prev === undefined || prev === null) && + (post === undefined || post === null) ) -} + return null; + return prev === post ? null : post === null ? ( + <span class="tag is-danger is-align-self-center ml-2"> + <Translate>Deleting</Translate> + </span> + ) : ( + <span class="tag is-warning is-align-self-center ml-2"> + <Translate>Changing</Translate> + </span> + ); +}; -export function InputSecured<T>({ name, readonly, placeholder, tooltip, label, help }: Props<keyof T>): VNode { +export function InputSecured<T>({ + name, + readonly, + placeholder, + tooltip, + label, + help, +}: Props<keyof T>): VNode { const { error, value, initial, onChange, toStr, fromStr } = useField<T>(name); const [active, setActive] = useState(false); - const [newValue, setNuewValue] = useState("") + const [newValue, setNuewValue] = useState(""); - const i18n = useTranslator() + const i18n = useTranslator(); - return <Fragment> - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label"> - {label} - {tooltip && <span class="icon has-tooltip-right" 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><Translate>Manage access token</Translate></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 is-expanded"> - <input class="input" type="text" - placeholder={placeholder} readonly={readonly || !active} - disabled={readonly || !active} - name={String(name)} value={newValue} - onInput={(e): void => { - setNuewValue(e.currentTarget.value) - }} /> - {help} - </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><Translate>Update</Translate></span> - </button> - </div> - </div> - </Fragment> - } - {error ? <p class="help is-danger">{error}</p> : null} - </div> - </div> - {active && + return ( + <Fragment> <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label"> + {label} + {tooltip && ( + <span class="icon has-tooltip-right" data-tooltip={tooltip}> + <i class="mdi mdi-information" /> + </span> + )} + </label> + </div> <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><Translate>Remove</Translate></span> + {!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> + <Translate>Manage access token</Translate> + </span> </button> + <TokenStatus prev={initial} post={value} /> </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><Translate>Cancel</Translate></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="text" + placeholder={placeholder} + readonly={readonly || !active} + disabled={readonly || !active} + name={String(name)} + value={newValue} + onInput={(e): void => { + setNuewValue(e.currentTarget.value); + }} + /> + {help} + </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> + <Translate>Update</Translate> + </span> + </button> + </div> + </div> + </Fragment> + )} + {error ? <p class="help is-danger">{error}</p> : null} + </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> + <Translate>Remove</Translate> + </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> + <Translate>Cancel</Translate> + </span> + </button> + </div> </div> </div> - </div> </div> - </div> - } - </Fragment >; + )} + </Fragment> + ); } diff --git a/packages/merchant-backoffice-ui/src/components/form/InputStock.tsx b/packages/merchant-backoffice-ui/src/components/form/InputStock.tsx index 74806734c..57aa5968d 100644 --- a/packages/merchant-backoffice-ui/src/components/form/InputStock.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/InputStock.tsx @@ -15,9 +15,9 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ + * + * @author Sebastian Javier Marchano (sebasjm) + */ import { Fragment, h } from "preact"; import { MerchantBackend, Timestamp } from "../../declaration.js"; import { InputProps, useField } from "./useField.js"; @@ -34,8 +34,7 @@ export interface Props<T> extends InputProps<T> { alreadyExist?: boolean; } - -type Entity = Stock +type Entity = Stock; export interface Stock { current: number; @@ -50,66 +49,96 @@ interface StockDelta { lost: number; } - -export function InputStock<T>({ name, tooltip, label, alreadyExist }: Props<keyof T>) { +export function InputStock<T>({ + name, + tooltip, + label, + alreadyExist, +}: Props<keyof T>) { const { error, value, onChange } = useField<T>(name); - const [errors, setErrors] = useState<FormErrors<Entity>>({}) - - const [formValue, valueHandler] = useState<Partial<Entity>>(value) - const [addedStock, setAddedStock] = useState<StockDelta>({ incoming: 0, lost: 0 }) - const i18n = useTranslator() + const [errors, setErrors] = useState<FormErrors<Entity>>({}); + const [formValue, valueHandler] = useState<Partial<Entity>>(value); + const [addedStock, setAddedStock] = useState<StockDelta>({ + incoming: 0, + lost: 0, + }); + const i18n = useTranslator(); useLayoutEffect(() => { if (!formValue) { - onChange(undefined as any) + onChange(undefined as any); } else { onChange({ ...formValue, current: (formValue?.current || 0) + addedStock.incoming, - lost: (formValue?.lost || 0) + addedStock.lost - } as any) + lost: (formValue?.lost || 0) + addedStock.lost, + } as any); } - }, [formValue, addedStock]) + }, [formValue, addedStock]); if (!formValue) { - return <Fragment> - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label"> - {label} - {tooltip && <span class="icon has-tooltip-right" data-tooltip={tooltip}> - <i class="mdi mdi-information" /> - </span>} - </label> - </div> - <div class="field-body is-flex-grow-3"> - <div class="field has-addons"> - {!alreadyExist ? - <button class="button" - data-tooltip={i18n`click here to configure the stock of the product, leave it as is and the backend will not control stock`} - onClick={(): void => { valueHandler({ current: 0, lost: 0, sold: 0 } as Stock as any); }} > - <span><Translate>Manage stock</Translate></span> - </button> : <button class="button" - data-tooltip={i18n`this product has been configured without stock control`} - disabled > - <span><Translate>Infinite</Translate></span> - </button> - } + return ( + <Fragment> + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label"> + {label} + {tooltip && ( + <span class="icon has-tooltip-right" data-tooltip={tooltip}> + <i class="mdi mdi-information" /> + </span> + )} + </label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field has-addons"> + {!alreadyExist ? ( + <button + class="button" + data-tooltip={i18n`click here to configure the stock of the product, leave it as is and the backend will not control stock`} + onClick={(): void => { + valueHandler({ + current: 0, + lost: 0, + sold: 0, + } as Stock as any); + }} + > + <span> + <Translate>Manage stock</Translate> + </span> + </button> + ) : ( + <button + class="button" + data-tooltip={i18n`this product has been configured without stock control`} + disabled + > + <span> + <Translate>Infinite</Translate> + </span> + </button> + )} + </div> </div> </div> - </div> - </Fragment > + </Fragment> + ); } - const currentStock = (formValue.current || 0) - (formValue.lost || 0) - (formValue.sold || 0) + const currentStock = + (formValue.current || 0) - (formValue.lost || 0) - (formValue.sold || 0); const stockAddedErrors: FormErrors<typeof addedStock> = { - lost: currentStock + addedStock.incoming < addedStock.lost ? - i18n`lost cannot be greater than current and incoming (max ${currentStock + addedStock.incoming})` - : undefined - } + lost: + currentStock + addedStock.incoming < addedStock.lost + ? i18n`lost cannot be greater than current and incoming (max ${ + currentStock + addedStock.incoming + })` + : undefined, + }; // const stockUpdateDescription = stockAddedErrors.lost ? '' : ( // !!addedStock.incoming || !!addedStock.lost ? @@ -117,26 +146,39 @@ export function InputStock<T>({ name, tooltip, label, alreadyExist }: Props<keyo // i18n`current stock will stay at ${currentStock}` // ) - return <Fragment> - <div class="card"> - <header class="card-header"> - <p class="card-header-title"> - {label} - {tooltip && <span class="icon" data-tooltip={tooltip}> - <i class="mdi mdi-information" /> - </span>} - </p> - </header> - <div class="card-content"> - <FormProvider<Entity> name="stock" errors={errors} object={formValue} valueHandler={valueHandler}> - {alreadyExist ? <Fragment> - - <FormProvider name="added" errors={stockAddedErrors} object={addedStock} valueHandler={setAddedStock as any}> - <InputNumber name="incoming" label={i18n`Incoming`} /> - <InputNumber name="lost" label={i18n`Lost`} /> - </FormProvider> - - {/* <div class="field is-horizontal"> + return ( + <Fragment> + <div class="card"> + <header class="card-header"> + <p class="card-header-title"> + {label} + {tooltip && ( + <span class="icon" data-tooltip={tooltip}> + <i class="mdi mdi-information" /> + </span> + )} + </p> + </header> + <div class="card-content"> + <FormProvider<Entity> + name="stock" + errors={errors} + object={formValue} + valueHandler={valueHandler} + > + {alreadyExist ? ( + <Fragment> + <FormProvider + name="added" + errors={stockAddedErrors} + object={addedStock} + valueHandler={setAddedStock as any} + > + <InputNumber name="incoming" label={i18n`Incoming`} /> + <InputNumber name="lost" label={i18n`Lost`} /> + </FormProvider> + + {/* <div class="field is-horizontal"> <div class="field-label is-normal" /> <div class="field-body is-flex-grow-3"> <div class="field"> @@ -144,28 +186,40 @@ export function InputStock<T>({ name, tooltip, label, alreadyExist }: Props<keyo </div> </div> </div> */} - - </Fragment> : <InputNumber<Entity> name="current" - label={i18n`Current`} - side={ - <button class="button is-danger" - data-tooltip={i18n`remove stock control for this product`} - onClick={(): void => { valueHandler(undefined as any) }} > - <span><Translate>without stock</Translate></span> - </button> - } - />} - - <InputDate<Entity> name="nextRestock" label={i18n`Next restock`} withTimestampSupport /> - - <InputGroup<Entity> name="address" label={i18n`Delivery address`}> - <InputLocation name="address" /> - </InputGroup> - </FormProvider> + </Fragment> + ) : ( + <InputNumber<Entity> + name="current" + label={i18n`Current`} + side={ + <button + class="button is-danger" + data-tooltip={i18n`remove stock control for this product`} + onClick={(): void => { + valueHandler(undefined as any); + }} + > + <span> + <Translate>without stock</Translate> + </span> + </button> + } + /> + )} + + <InputDate<Entity> + name="nextRestock" + label={i18n`Next restock`} + withTimestampSupport + /> + + <InputGroup<Entity> name="address" label={i18n`Delivery address`}> + <InputLocation name="address" /> + </InputGroup> + </FormProvider> + </div> </div> - </div> - </Fragment> + </Fragment> + ); } - // ( - - +// ( diff --git a/packages/merchant-backoffice-ui/src/components/form/InputTaxes.tsx b/packages/merchant-backoffice-ui/src/components/form/InputTaxes.tsx index 84f9234e9..d95463790 100644 --- a/packages/merchant-backoffice-ui/src/components/form/InputTaxes.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/InputTaxes.tsx @@ -15,12 +15,12 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ + * + * @author Sebastian Javier Marchano (sebasjm) + */ import { h, VNode } from "preact"; import { useCallback, useState } from "preact/hooks"; -import * as yup from 'yup'; +import * as yup from "yup"; import { MerchantBackend } from "../../declaration.js"; import { Translate, useTranslator } from "../../i18n/index.js"; import { TaxSchema as schema } from "../../schemas/index.js"; @@ -33,65 +33,114 @@ export interface Props<T> extends InputProps<T> { isValid?: (e: any) => boolean; } -type Entity = MerchantBackend.Tax -export function InputTaxes<T>({ name, readonly, label }: Props<keyof T>): VNode { - const { value: taxes, onChange, } = useField<T>(name); +type Entity = MerchantBackend.Tax; +export function InputTaxes<T>({ + name, + readonly, + label, +}: Props<keyof T>): VNode { + const { value: taxes, onChange } = useField<T>(name); - const [value, valueHandler] = useState<Partial<Entity>>({}) + const [value, valueHandler] = useState<Partial<Entity>>({}); // const [errors, setErrors] = useState<FormErrors<Entity>>({}) - let errors: FormErrors<Entity> = {} + let errors: FormErrors<Entity> = {}; try { - schema.validateSync(value, { abortEarly: false }) + schema.validateSync(value, { abortEarly: false }); } catch (err) { if (err instanceof yup.ValidationError) { - const yupErrors = err.inner as yup.ValidationError[] - errors = yupErrors.reduce((prev, cur) => !cur.path ? prev : ({ ...prev, [cur.path]: cur.message }), {}) + const yupErrors = err.inner as yup.ValidationError[]; + errors = yupErrors.reduce( + (prev, cur) => + !cur.path ? prev : { ...prev, [cur.path]: cur.message }, + {}, + ); } } - const hasErrors = Object.keys(errors).some(k => (errors as any)[k] !== undefined) + const hasErrors = Object.keys(errors).some( + (k) => (errors as any)[k] !== undefined, + ); const submit = useCallback((): void => { - onChange([value as any, ...taxes] as any) - valueHandler({}) - }, [value]) + onChange([value as any, ...taxes] as any); + valueHandler({}); + }, [value]); - const i18n = useTranslator() + const i18n = useTranslator(); //FIXME: translating plural singular return ( - <InputGroup name="tax" label={label} alternative={taxes.length > 0 && <p>This product has {taxes.length} applicable taxes configured.</p>}> - <FormProvider<Entity> name="tax" errors={errors} object={value} valueHandler={valueHandler} > - + <InputGroup + name="tax" + label={label} + alternative={ + taxes.length > 0 && ( + <p>This product has {taxes.length} applicable taxes configured.</p> + ) + } + > + <FormProvider<Entity> + name="tax" + errors={errors} + object={value} + valueHandler={valueHandler} + > <div class="field is-horizontal"> <div class="field-label is-normal" /> - <div class="field-body" style={{ display: 'block' }}> - {taxes.map((v: any, i: number) => <div key={i} class="tags has-addons mt-3 mb-0 mr-3" style={{ flexWrap: 'nowrap' }}> - <span class="tag is-medium is-info mb-0" style={{ maxWidth: '90%' }}><b>{v.tax}</b>: {v.name}</span> - <a class="tag is-medium is-danger is-delete mb-0" onClick={() => { - onChange(taxes.filter((f: any) => f !== v) as any); - valueHandler(v); - }} /> - </div> - )} + <div class="field-body" style={{ display: "block" }}> + {taxes.map((v: any, i: number) => ( + <div + key={i} + class="tags has-addons mt-3 mb-0 mr-3" + style={{ flexWrap: "nowrap" }} + > + <span + class="tag is-medium is-info mb-0" + style={{ maxWidth: "90%" }} + > + <b>{v.tax}</b>: {v.name} + </span> + <a + class="tag is-medium is-danger is-delete mb-0" + onClick={() => { + onChange(taxes.filter((f: any) => f !== v) as any); + valueHandler(v); + }} + /> + </div> + ))} {!taxes.length && i18n`No taxes configured for this product.`} </div> </div> - <Input<Entity> name="tax" label={i18n`Amount`} tooltip={i18n`Taxes can be in currencies that differ from the main currency used by the merchant.`}> - <Translate>Enter currency and value separated with a colon, e.g. "USD:2.3".</Translate> + <Input<Entity> + name="tax" + label={i18n`Amount`} + tooltip={i18n`Taxes can be in currencies that differ from the main currency used by the merchant.`} + > + <Translate> + Enter currency and value separated with a colon, e.g. "USD:2.3". + </Translate> </Input> - <Input<Entity> name="name" label={i18n`Description`} tooltip={i18n`Legal name of the tax, e.g. VAT or import duties.`} /> + <Input<Entity> + name="name" + label={i18n`Description`} + tooltip={i18n`Legal name of the tax, e.g. VAT or import duties.`} + /> <div class="buttons is-right mt-5"> - <button class="button is-info" + <button + class="button is-info" data-tooltip={i18n`add tax to the tax list`} disabled={hasErrors} - onClick={submit}><Translate>Add</Translate></button> + onClick={submit} + > + <Translate>Add</Translate> + </button> </div> </FormProvider> </InputGroup> - ) + ); } diff --git a/packages/merchant-backoffice-ui/src/components/form/InputWithAddon.tsx b/packages/merchant-backoffice-ui/src/components/form/InputWithAddon.tsx index bdc9eaa0c..620922584 100644 --- a/packages/merchant-backoffice-ui/src/components/form/InputWithAddon.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/InputWithAddon.tsx @@ -15,63 +15,99 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ + * + * @author Sebastian Javier Marchano (sebasjm) + */ import { ComponentChildren, h, VNode } from "preact"; import { InputProps, useField } from "./useField.js"; export interface Props<T> extends InputProps<T> { expand?: boolean; - inputType?: 'text' | 'number'; + inputType?: "text" | "number"; addonBefore?: ComponentChildren; addonAfter?: ComponentChildren; toStr?: (v?: any) => string; fromStr?: (s: string) => any; - inputExtra?: any, - children?: ComponentChildren, + inputExtra?: any; + children?: ComponentChildren; side?: ComponentChildren; } -const defaultToString = (f?: any): string => f || '' -const defaultFromString = (v: string): any => v as any +const defaultToString = (f?: any): string => f || ""; +const defaultFromString = (v: string): any => v as any; -export function InputWithAddon<T>({ name, readonly, addonBefore, children, expand, label, placeholder, help, tooltip, inputType, inputExtra, side, addonAfter, toStr = defaultToString, fromStr = defaultFromString }: Props<keyof T>): VNode { +export function InputWithAddon<T>({ + name, + readonly, + addonBefore, + children, + expand, + label, + placeholder, + help, + tooltip, + inputType, + inputExtra, + side, + addonAfter, + toStr = defaultToString, + fromStr = defaultFromString, +}: Props<keyof T>): VNode { const { error, value, onChange, required } = useField<T>(name); - return <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label"> - {label} - {tooltip && <span class="icon has-tooltip-right" data-tooltip={tooltip}> - <i class="mdi mdi-information" /> - </span>} - </label> - </div> - <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${expand ? " is-expanded" :""}${required ? " has-icons-right" : ''}`}> - <input {...(inputExtra || {})} class={error ? "input is-danger" : "input"} type={inputType} - placeholder={placeholder} readonly={readonly} - name={String(name)} value={toStr(value)} - onChange={(e): void => onChange(fromStr(e.currentTarget.value))} /> - {required && <span class="icon has-text-danger is-right"> - <i class="mdi mdi-alert" /> - </span>} - {help} - {children} - </p> - {addonAfter && <div class="control"> - <a class="button is-static">{addonAfter}</a> - </div>} + return ( + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label"> + {label} + {tooltip && ( + <span class="icon has-tooltip-right" data-tooltip={tooltip}> + <i class="mdi mdi-information" /> + </span> + )} + </label> + </div> + <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${expand ? " is-expanded" : ""}${ + required ? " has-icons-right" : "" + }`} + > + <input + {...(inputExtra || {})} + class={error ? "input is-danger" : "input"} + type={inputType} + placeholder={placeholder} + readonly={readonly} + name={String(name)} + value={toStr(value)} + onChange={(e): void => onChange(fromStr(e.currentTarget.value))} + /> + {required && ( + <span class="icon has-text-danger is-right"> + <i class="mdi mdi-alert" /> + </span> + )} + {help} + {children} + </p> + {addonAfter && ( + <div class="control"> + <a class="button is-static">{addonAfter}</a> + </div> + )} + </div> + {error && <p class="help is-danger">{error}</p>} </div> - {error && <p class="help is-danger">{error}</p>} + {side} </div> - {side} </div> - </div>; + ); } diff --git a/packages/merchant-backoffice-ui/src/components/form/TextField.tsx b/packages/merchant-backoffice-ui/src/components/form/TextField.tsx index 2cda71599..03f36dcbb 100644 --- a/packages/merchant-backoffice-ui/src/components/form/TextField.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/TextField.tsx @@ -15,39 +15,57 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ + * + * @author Sebastian Javier Marchano (sebasjm) + */ import { ComponentChildren, h, VNode } from "preact"; import { useField, InputProps } from "./useField.js"; interface Props<T> extends InputProps<T> { - inputType?: 'text' | 'number' | 'multiline' | 'password'; + inputType?: "text" | "number" | "multiline" | "password"; expand?: boolean; side?: ComponentChildren; children: ComponentChildren; } -export function TextField<T>({ name, tooltip, label, expand, help, children, side}: Props<keyof T>): VNode { +export function TextField<T>({ + name, + tooltip, + label, + expand, + help, + children, + side, +}: Props<keyof T>): VNode { const { error } = useField<T>(name); - return <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label"> - {label} - {tooltip && <span class="icon has-tooltip-right" data-tooltip={tooltip}> - <i class="mdi mdi-information" /> - </span>} - </label> - </div> - <div class="field-body is-flex-grow-3"> - <div class="field"> - <p class={expand ? "control is-expanded has-icons-right" : "control has-icons-right"}> - {children} - {help} - </p> - {error && <p class="help is-danger">{error}</p>} + return ( + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label"> + {label} + {tooltip && ( + <span class="icon has-tooltip-right" data-tooltip={tooltip}> + <i class="mdi mdi-information" /> + </span> + )} + </label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p + class={ + expand + ? "control is-expanded has-icons-right" + : "control has-icons-right" + } + > + {children} + {help} + </p> + {error && <p class="help is-danger">{error}</p>} + </div> + {side} </div> - {side} </div> - </div>; + ); } diff --git a/packages/merchant-backoffice-ui/src/components/form/useField.tsx b/packages/merchant-backoffice-ui/src/components/form/useField.tsx index 6b685d722..dffb0cc66 100644 --- a/packages/merchant-backoffice-ui/src/components/form/useField.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/useField.tsx @@ -15,9 +15,9 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ + * + * @author Sebastian Javier Marchano (sebasjm) + */ import { ComponentChildren, VNode } from "preact"; import { useFormContext } from "./FormProvider.js"; @@ -29,26 +29,29 @@ interface Use<V> { initial: any; onChange: (v: V) => void; toStr: (f: V | undefined) => string; - fromStr: (v: string) => V + fromStr: (v: string) => V; } export function useField<T>(name: keyof T): Use<T[typeof name]> { - const { errors, object, initialObject, toStr, fromStr, valueHandler } = useFormContext<T>() - type P = typeof name - type V = T[P] + const { errors, object, initialObject, toStr, fromStr, valueHandler } = + useFormContext<T>(); + type P = typeof name; + type V = T[P]; - const updateField = (field: P) => (value: V): void => { - return valueHandler((prev) => { - return setValueDeeper(prev, String(field).split('.'), value) - }) - } + const updateField = + (field: P) => + (value: V): void => { + return valueHandler((prev) => { + return setValueDeeper(prev, String(field).split("."), value); + }); + }; - const defaultToString = ((f?: V): string => String(!f ? '' : f)) - const defaultFromString = ((v: string): V => v as any) - const value = readField(object, String(name)) - const initial = readField(initialObject, String(name)) - const isDirty = value !== initial - const hasError = readField(errors, String(name)) + const defaultToString = (f?: V): string => String(!f ? "" : f); + const defaultFromString = (v: string): V => v as any; + const value = readField(object, String(name)); + const initial = readField(initialObject, String(name)); + const isDirty = value !== initial; + const hasError = readField(errors, String(name)); return { error: isDirty ? hasError : undefined, required: !isDirty && hasError, @@ -57,24 +60,26 @@ export function useField<T>(name: keyof T): Use<T[typeof name]> { onChange: updateField(name) as any, toStr: toStr[name] ? toStr[name]! : defaultToString, fromStr: fromStr[name] ? fromStr[name]! : defaultFromString, - } + }; } /** * read the field of an object an support accessing it using '.' - * - * @param object - * @param name - * @returns + * + * @param object + * @param name + * @returns */ const readField = (object: any, name: string) => { - return name.split('.').reduce((prev, current) => prev && prev[current], object) -} + return name + .split(".") + .reduce((prev, current) => prev && 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) } -} + if (names.length === 0) return value; + const [head, ...rest] = names; + return { ...object, [head]: setValueDeeper(object[head] || {}, rest, value) }; +}; export interface InputProps<T> { name: T; @@ -83,4 +88,4 @@ export interface InputProps<T> { tooltip?: ComponentChildren; readonly?: boolean; help?: ComponentChildren; -}
\ No newline at end of file +} diff --git a/packages/merchant-backoffice-ui/src/components/form/useGroupField.tsx b/packages/merchant-backoffice-ui/src/components/form/useGroupField.tsx index e6365e3ad..9a445eb32 100644 --- a/packages/merchant-backoffice-ui/src/components/form/useGroupField.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/useGroupField.tsx @@ -15,9 +15,9 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ + * + * @author Sebastian Javier Marchano (sebasjm) + */ import { useFormContext } from "./FormProvider.js"; @@ -27,14 +27,15 @@ interface Use { export function useGroupField<T>(name: keyof T): Use { const f = useFormContext<T>(); - if (!f) - return {}; + if (!f) return {}; return { - hasError: readField(f.errors, String(name)) + hasError: readField(f.errors, String(name)), }; } const readField = (object: any, name: string) => { - return name.split('.').reduce((prev, current) => prev && prev[current], object) -} + return name + .split(".") + .reduce((prev, current) => prev && prev[current], object); +}; diff --git a/packages/merchant-backoffice-ui/src/components/menu/LangSelector.tsx b/packages/merchant-backoffice-ui/src/components/menu/LangSelector.tsx index e5c6b6914..d618d6480 100644 --- a/packages/merchant-backoffice-ui/src/components/menu/LangSelector.tsx +++ b/packages/merchant-backoffice-ui/src/components/menu/LangSelector.tsx @@ -15,59 +15,78 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ + * + * @author Sebastian Javier Marchano (sebasjm) + */ import { h, VNode } from "preact"; import { useState } from "preact/hooks"; -import langIcon from '../../assets/icons/languageicon.svg'; +import langIcon from "../../assets/icons/languageicon.svg"; import { useTranslationContext } from "../../context/translation.js"; -import { strings as messages } from '../../i18n/strings' +import { strings as messages } from "../../i18n/strings"; type LangsNames = { - [P in keyof typeof messages]: string -} + [P in keyof typeof messages]: string; +}; const names: LangsNames = { - es: 'Español [es]', - en: 'English [en]', - fr: 'Français [fr]', - de: 'Deutsch [de]', - sv: 'Svenska [sv]', - it: 'Italiano [it]', -} + es: "Español [es]", + en: "English [en]", + fr: "Français [fr]", + de: "Deutsch [de]", + sv: "Svenska [sv]", + it: "Italiano [it]", +}; function getLangName(s: keyof LangsNames | string) { - if (names[s]) return names[s] - return s + if (names[s]) return names[s]; + return s; } export function LangSelector(): VNode { - const [updatingLang, setUpdatingLang] = useState(false) - const { lang, changeLanguage } = useTranslationContext() + const [updatingLang, setUpdatingLang] = useState(false); + const { lang, changeLanguage } = useTranslationContext(); - return <div class="dropdown is-active "> - <div class="dropdown-trigger"> - <button class="button has-tooltip-left" - data-tooltip="change language selection" - aria-haspopup="true" - aria-controls="dropdown-menu" onClick={() => setUpdatingLang(!updatingLang)}> - <div class="icon is-small is-left"> - <img src={langIcon} /> - </div> - <span>{getLangName(lang)}</span> - <div class="icon is-right"> - <i class="mdi mdi-chevron-down" /> + return ( + <div class="dropdown is-active "> + <div class="dropdown-trigger"> + <button + class="button has-tooltip-left" + data-tooltip="change language selection" + aria-haspopup="true" + aria-controls="dropdown-menu" + onClick={() => setUpdatingLang(!updatingLang)} + > + <div class="icon is-small is-left"> + <img src={langIcon} /> + </div> + <span>{getLangName(lang)}</span> + <div class="icon is-right"> + <i class="mdi mdi-chevron-down" /> + </div> + </button> + </div> + {updatingLang && ( + <div class="dropdown-menu" id="dropdown-menu" role="menu"> + <div class="dropdown-content"> + {Object.keys(messages) + .filter((l) => l !== lang) + .map((l) => ( + <a + key={l} + class="dropdown-item" + value={l} + onClick={() => { + changeLanguage(l); + setUpdatingLang(false); + }} + > + {getLangName(l)} + </a> + ))} + </div> </div> - </button> + )} </div> - {updatingLang && <div class="dropdown-menu" id="dropdown-menu" role="menu"> - <div class="dropdown-content"> - {Object.keys(messages) - .filter((l) => l !== lang) - .map(l => <a key={l} class="dropdown-item" value={l} onClick={() => { changeLanguage(l); setUpdatingLang(false) }}>{getLangName(l)}</a>)} - </div> - </div>} - </div> -}
\ No newline at end of file + ); +} diff --git a/packages/merchant-backoffice-ui/src/components/menu/NavigationBar.tsx b/packages/merchant-backoffice-ui/src/components/menu/NavigationBar.tsx index 39d1b0e35..46c13adf0 100644 --- a/packages/merchant-backoffice-ui/src/components/menu/NavigationBar.tsx +++ b/packages/merchant-backoffice-ui/src/components/menu/NavigationBar.tsx @@ -15,12 +15,12 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ + * + * @author Sebastian Javier Marchano (sebasjm) + */ -import { h, VNode } from 'preact'; -import logo from '../../assets/logo.jpeg'; +import { h, VNode } from "preact"; +import logo from "../../assets/logo.jpeg"; import { LangSelector } from "./LangSelector.js"; interface Props { @@ -29,30 +29,46 @@ interface Props { } export function NavigationBar({ onMobileMenu, title }: Props): VNode { - return (<nav class="navbar is-fixed-top" role="navigation" aria-label="main navigation"> - <div class="navbar-brand"> - <span class="navbar-item" style={{ fontSize: 24, fontWeight: 900 }}>{title}</span> - - <a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" onClick={(e) => { - onMobileMenu() - e.stopPropagation() - }}> - <span aria-hidden="true" /> - <span aria-hidden="true" /> - <span aria-hidden="true" /> - </a> - </div> - - <div class="navbar-menu "> - <a class="navbar-start is-justify-content-center is-flex-grow-1" href="https://taler.net"> - <img src={logo} style={{ height: 50, maxHeight: 50 }} /> - </a> - <div class="navbar-end"> - <div class="navbar-item" style={{ paddingTop: 4, paddingBottom: 4 }}> - <LangSelector /> + return ( + <nav + class="navbar is-fixed-top" + role="navigation" + aria-label="main navigation" + > + <div class="navbar-brand"> + <span class="navbar-item" style={{ fontSize: 24, fontWeight: 900 }}> + {title} + </span> + + <a + role="button" + class="navbar-burger" + aria-label="menu" + aria-expanded="false" + onClick={(e) => { + onMobileMenu(); + e.stopPropagation(); + }} + > + <span aria-hidden="true" /> + <span aria-hidden="true" /> + <span aria-hidden="true" /> + </a> + </div> + + <div class="navbar-menu "> + <a + class="navbar-start is-justify-content-center is-flex-grow-1" + href="https://taler.net" + > + <img src={logo} style={{ height: 50, maxHeight: 50 }} /> + </a> + <div class="navbar-end"> + <div class="navbar-item" style={{ paddingTop: 4, paddingBottom: 4 }}> + <LangSelector /> + </div> </div> </div> - </div> - </nav> + </nav> ); -}
\ No newline at end of file +} diff --git a/packages/merchant-backoffice-ui/src/components/modal/index.tsx b/packages/merchant-backoffice-ui/src/components/modal/index.tsx index 7c7e19316..6e5575f63 100644 --- a/packages/merchant-backoffice-ui/src/components/modal/index.tsx +++ b/packages/merchant-backoffice-ui/src/components/modal/index.tsx @@ -15,10 +15,9 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ - + * + * @author Sebastian Javier Marchano (sebasjm) + */ import { ComponentChildren, h, VNode } from "preact"; import { useState } from "preact/hooks"; @@ -40,104 +39,220 @@ interface Props { disabled?: boolean; } -export function ConfirmModal({ active, description, onCancel, onConfirm, children, danger, disabled, label = 'Confirm' }: Props): VNode { - return <div class={active ? "modal is-active" : "modal"}> - <div class="modal-background " onClick={onCancel} /> - <div class="modal-card" style={{maxWidth: 700}}> - <header class="modal-card-head"> - {!description ? null : <p class="modal-card-title"><b>{description}</b></p>} - <button class="delete " aria-label="close" onClick={onCancel} /> - </header> - <section class="modal-card-body"> - {children} - </section> - <footer class="modal-card-foot"> - <div class="buttons is-right" style={{ width: '100%' }}> - <button class="button " onClick={onCancel} ><Translate>Cancel</Translate></button> - <button class={danger ? "button is-danger " : "button is-info "} disabled={disabled} onClick={onConfirm} ><Translate>{label}</Translate></button> - </div> - </footer> +export function ConfirmModal({ + active, + description, + onCancel, + onConfirm, + children, + danger, + disabled, + label = "Confirm", +}: Props): VNode { + return ( + <div class={active ? "modal is-active" : "modal"}> + <div class="modal-background " onClick={onCancel} /> + <div class="modal-card" style={{ maxWidth: 700 }}> + <header class="modal-card-head"> + {!description ? null : ( + <p class="modal-card-title"> + <b>{description}</b> + </p> + )} + <button class="delete " aria-label="close" onClick={onCancel} /> + </header> + <section class="modal-card-body">{children}</section> + <footer class="modal-card-foot"> + <div class="buttons is-right" style={{ width: "100%" }}> + <button class="button " onClick={onCancel}> + <Translate>Cancel</Translate> + </button> + <button + class={danger ? "button is-danger " : "button is-info "} + disabled={disabled} + onClick={onConfirm} + > + <Translate>{label}</Translate> + </button> + </div> + </footer> + </div> + <button + class="modal-close is-large " + aria-label="close" + onClick={onCancel} + /> </div> - <button class="modal-close is-large " aria-label="close" onClick={onCancel} /> - </div> + ); } -export function ContinueModal({ active, description, onCancel, onConfirm, children, disabled }: Props): VNode { - return <div class={active ? "modal is-active" : "modal"}> - <div class="modal-background " onClick={onCancel} /> - <div class="modal-card"> - <header class="modal-card-head has-background-success"> - {!description ? null : <p class="modal-card-title">{description}</p>} - <button class="delete " aria-label="close" onClick={onCancel} /> - </header> - <section class="modal-card-body"> - {children} - </section> - <footer class="modal-card-foot"> - <div class="buttons is-right" style={{ width: '100%' }}> - <button class="button is-success " disabled={disabled} onClick={onConfirm} ><Translate>Continue</Translate></button> - </div> - </footer> +export function ContinueModal({ + active, + description, + onCancel, + onConfirm, + children, + disabled, +}: Props): VNode { + return ( + <div class={active ? "modal is-active" : "modal"}> + <div class="modal-background " onClick={onCancel} /> + <div class="modal-card"> + <header class="modal-card-head has-background-success"> + {!description ? null : <p class="modal-card-title">{description}</p>} + <button class="delete " aria-label="close" onClick={onCancel} /> + </header> + <section class="modal-card-body">{children}</section> + <footer class="modal-card-foot"> + <div class="buttons is-right" style={{ width: "100%" }}> + <button + class="button is-success " + disabled={disabled} + onClick={onConfirm} + > + <Translate>Continue</Translate> + </button> + </div> + </footer> + </div> + <button + class="modal-close is-large " + aria-label="close" + onClick={onCancel} + /> </div> - <button class="modal-close is-large " aria-label="close" onClick={onCancel} /> - </div> + ); } export function SimpleModal({ onCancel, children }: any): VNode { - return <div class="modal is-active"> - <div class="modal-background " onClick={onCancel} /> - <div class="modal-card"> - <section class="modal-card-body is-main-section"> - {children} - </section> + return ( + <div class="modal is-active"> + <div class="modal-background " onClick={onCancel} /> + <div class="modal-card"> + <section class="modal-card-body is-main-section">{children}</section> + </div> + <button + class="modal-close is-large " + aria-label="close" + onClick={onCancel} + /> </div> - <button class="modal-close is-large " aria-label="close" onClick={onCancel} /> - </div> + ); } -export function ClearConfirmModal({ description, onCancel, onClear, onConfirm, children }: Props & { onClear?: () => void }): VNode { - return <div class="modal is-active"> - <div class="modal-background " onClick={onCancel} /> - <div class="modal-card"> - <header class="modal-card-head"> - {!description ? null : <p class="modal-card-title">{description}</p>} - <button class="delete " aria-label="close" onClick={onCancel} /> - </header> - <section class="modal-card-body is-main-section"> - {children} - </section> - <footer class="modal-card-foot"> - {onClear && <button class="button is-danger" onClick={onClear} disabled={onClear === undefined} ><Translate>Clear</Translate></button>} - <div class="buttons is-right" style={{ width: '100%' }}> - <button class="button " onClick={onCancel} ><Translate>Cancel</Translate></button> - <button class="button is-info" onClick={onConfirm} disabled={onConfirm === undefined} ><Translate>Confirm</Translate></button> - </div> - </footer> +export function ClearConfirmModal({ + description, + onCancel, + onClear, + onConfirm, + children, +}: Props & { onClear?: () => void }): VNode { + return ( + <div class="modal is-active"> + <div class="modal-background " onClick={onCancel} /> + <div class="modal-card"> + <header class="modal-card-head"> + {!description ? null : <p class="modal-card-title">{description}</p>} + <button class="delete " aria-label="close" onClick={onCancel} /> + </header> + <section class="modal-card-body is-main-section">{children}</section> + <footer class="modal-card-foot"> + {onClear && ( + <button + class="button is-danger" + onClick={onClear} + disabled={onClear === undefined} + > + <Translate>Clear</Translate> + </button> + )} + <div class="buttons is-right" style={{ width: "100%" }}> + <button class="button " onClick={onCancel}> + <Translate>Cancel</Translate> + </button> + <button + class="button is-info" + onClick={onConfirm} + disabled={onConfirm === undefined} + > + <Translate>Confirm</Translate> + </button> + </div> + </footer> + </div> + <button + class="modal-close is-large " + aria-label="close" + onClick={onCancel} + /> </div> - <button class="modal-close is-large " aria-label="close" onClick={onCancel} /> - </div> + ); } interface DeleteModalProps { - element: { id: string, name: string }; + element: { id: string; name: string }; onCancel: () => void; onConfirm: (id: string) => void; } -export function DeleteModal({ element, onCancel, onConfirm }: DeleteModalProps): VNode { - return <ConfirmModal label={`Delete instance`} description={`Delete the instance "${element.name}"`} danger active onCancel={onCancel} onConfirm={() => onConfirm(element.id)}> - <p>If you delete the instance named <b>"{element.name}"</b> (ID: <b>{element.id}</b>), the merchant will no longer be able to process orders or refunds</p> - <p>This action deletes the instance private key, but preserves all transaction data. You can still access that data after deleting the instance.</p> - <p class="warning">Deleting an instance <b>cannot be undone</b>.</p> - </ConfirmModal> +export function DeleteModal({ + element, + onCancel, + onConfirm, +}: DeleteModalProps): VNode { + return ( + <ConfirmModal + label={`Delete instance`} + description={`Delete the instance "${element.name}"`} + danger + active + onCancel={onCancel} + onConfirm={() => onConfirm(element.id)} + > + <p> + If you delete the instance named <b>"{element.name}"</b> (ID:{" "} + <b>{element.id}</b>), the merchant will no longer be able to process + orders or refunds + </p> + <p> + This action deletes the instance private key, but preserves all + transaction data. You can still access that data after deleting the + instance. + </p> + <p class="warning"> + Deleting an instance <b>cannot be undone</b>. + </p> + </ConfirmModal> + ); } -export function PurgeModal({ element, onCancel, onConfirm }: DeleteModalProps): VNode { - return <ConfirmModal label={`Purge the instance`} description={`Purge the instance "${element.name}"`} danger active onCancel={onCancel} onConfirm={() => onConfirm(element.id)}> - <p>If you purge the instance named <b>"{element.name}"</b> (ID: <b>{element.id}</b>), you will also delete all it's transaction data.</p> - <p>The instance will disappear from your list, and you will no longer be able to access it's data.</p> - <p class="warning">Purging an instance <b>cannot be undone</b>.</p> - </ConfirmModal> +export function PurgeModal({ + element, + onCancel, + onConfirm, +}: DeleteModalProps): VNode { + return ( + <ConfirmModal + label={`Purge the instance`} + description={`Purge the instance "${element.name}"`} + danger + active + onCancel={onCancel} + onConfirm={() => onConfirm(element.id)} + > + <p> + If you purge the instance named <b>"{element.name}"</b> (ID:{" "} + <b>{element.id}</b>), you will also delete all it's transaction data. + </p> + <p> + The instance will disappear from your list, and you will no longer be + able to access it's data. + </p> + <p class="warning"> + Purging an instance <b>cannot be undone</b>. + </p> + </ConfirmModal> + ); } interface UpdateTokenModalProps { @@ -148,115 +263,217 @@ interface UpdateTokenModalProps { } //FIXME: merge UpdateTokenModal with SetTokenNewInstanceModal -export function UpdateTokenModal({ onCancel, onClear, onConfirm, oldToken }: UpdateTokenModalProps): VNode { - type State = { old_token: string, new_token: string, repeat_token: string } +export function UpdateTokenModal({ + onCancel, + onClear, + onConfirm, + oldToken, +}: UpdateTokenModalProps): VNode { + type State = { old_token: string; new_token: string; repeat_token: string }; const [form, setValue] = useState<Partial<State>>({ - old_token: '', new_token: '', repeat_token: '', - }) - const i18n = useTranslator() + old_token: "", + new_token: "", + repeat_token: "", + }); + const i18n = useTranslator(); - const hasInputTheCorrectOldToken = oldToken && oldToken !== form.old_token + const hasInputTheCorrectOldToken = oldToken && oldToken !== form.old_token; const errors = { - old_token: hasInputTheCorrectOldToken ? i18n`is not the same as the current access token` : undefined, - new_token: !form.new_token ? i18n`cannot be empty` : (form.new_token === form.old_token ? i18n`cannot be the same as the old token` : undefined), - repeat_token: form.new_token !== form.repeat_token ? i18n`is not the same` : undefined - } + old_token: hasInputTheCorrectOldToken + ? i18n`is not the same as the current access token` + : undefined, + new_token: !form.new_token + ? i18n`cannot be empty` + : form.new_token === form.old_token + ? i18n`cannot be the same as the old token` + : undefined, + repeat_token: + form.new_token !== form.repeat_token ? i18n`is not the same` : undefined, + }; - const hasErrors = Object.keys(errors).some(k => (errors as any)[k] !== undefined) + const hasErrors = Object.keys(errors).some( + (k) => (errors as any)[k] !== undefined, + ); - const instance = useInstanceContext() + const instance = useInstanceContext(); - const text = i18n`You are updating the access token from instance with id ${instance.id}` + const text = i18n`You are updating the access token from instance with id ${instance.id}`; - return <ClearConfirmModal description={text} - onCancel={onCancel} - onConfirm={!hasErrors ? () => onConfirm(form.new_token!) : undefined} - onClear={!hasInputTheCorrectOldToken && oldToken ? onClear : undefined} - > - <div class="columns"> - <div class="column" /> - <div class="column is-four-fifths" > - <FormProvider errors={errors} object={form} valueHandler={setValue}> - {oldToken && <Input<State> name="old_token" label={i18n`Old access token`} tooltip={i18n`access token currently in use`} inputType="password" />} - <Input<State> name="new_token" label={i18n`New access token`} tooltip={i18n`next access token to be used`} inputType="password" /> - <Input<State> name="repeat_token" label={i18n`Repeat access token`} tooltip={i18n`confirm the same access token`} inputType="password" /> - </FormProvider> - <p><Translate>Clearing the access token will mean public access to the instance</Translate></p> + return ( + <ClearConfirmModal + description={text} + onCancel={onCancel} + onConfirm={!hasErrors ? () => onConfirm(form.new_token!) : undefined} + onClear={!hasInputTheCorrectOldToken && oldToken ? onClear : undefined} + > + <div class="columns"> + <div class="column" /> + <div class="column is-four-fifths"> + <FormProvider errors={errors} object={form} valueHandler={setValue}> + {oldToken && ( + <Input<State> + name="old_token" + label={i18n`Old access token`} + tooltip={i18n`access token currently in use`} + inputType="password" + /> + )} + <Input<State> + name="new_token" + label={i18n`New access token`} + tooltip={i18n`next access token to be used`} + inputType="password" + /> + <Input<State> + name="repeat_token" + label={i18n`Repeat access token`} + tooltip={i18n`confirm the same access token`} + inputType="password" + /> + </FormProvider> + <p> + <Translate> + Clearing the access token will mean public access to the instance + </Translate> + </p> + </div> + <div class="column" /> </div> - <div class="column" /> - </div> - </ClearConfirmModal> + </ClearConfirmModal> + ); } -export function SetTokenNewInstanceModal({ onCancel, onClear, onConfirm }: UpdateTokenModalProps): VNode { - type State = { old_token: string, new_token: string, repeat_token: string } +export function SetTokenNewInstanceModal({ + onCancel, + onClear, + onConfirm, +}: UpdateTokenModalProps): VNode { + type State = { old_token: string; new_token: string; repeat_token: string }; const [form, setValue] = useState<Partial<State>>({ - new_token: '', repeat_token: '', - }) - const i18n = useTranslator() + new_token: "", + repeat_token: "", + }); + const i18n = useTranslator(); const errors = { - new_token: !form.new_token ? i18n`cannot be empty` : (form.new_token === form.old_token ? i18n`cannot be the same as the old access token` : undefined), - repeat_token: form.new_token !== form.repeat_token ? i18n`is not the same` : undefined - } - - const hasErrors = Object.keys(errors).some(k => (errors as any)[k] !== undefined) + new_token: !form.new_token + ? i18n`cannot be empty` + : form.new_token === form.old_token + ? i18n`cannot be the same as the old access token` + : undefined, + repeat_token: + form.new_token !== form.repeat_token ? i18n`is not the same` : undefined, + }; + const hasErrors = Object.keys(errors).some( + (k) => (errors as any)[k] !== undefined, + ); - return <div class="modal is-active"> - <div class="modal-background " onClick={onCancel} /> - <div class="modal-card"> - <header class="modal-card-head"> - <p class="modal-card-title">{i18n`You are setting the access token for the new instance`}</p> - <button class="delete " aria-label="close" onClick={onCancel} /> - </header> - <section class="modal-card-body is-main-section"> - <div class="columns"> - <div class="column" /> - <div class="column is-four-fifths" > - <FormProvider errors={errors} object={form} valueHandler={setValue}> - <Input<State> name="new_token" label={i18n`New access token`} tooltip={i18n`next access token to be used`} inputType="password" /> - <Input<State> name="repeat_token" label={i18n`Repeat access token`} tooltip={i18n`confirm the same access token`} inputType="password" /> - </FormProvider> - <p><Translate>With external authorization method no check will be done by the merchant backend</Translate></p> + return ( + <div class="modal is-active"> + <div class="modal-background " onClick={onCancel} /> + <div class="modal-card"> + <header class="modal-card-head"> + <p class="modal-card-title">{i18n`You are setting the access token for the new instance`}</p> + <button class="delete " aria-label="close" onClick={onCancel} /> + </header> + <section class="modal-card-body is-main-section"> + <div class="columns"> + <div class="column" /> + <div class="column is-four-fifths"> + <FormProvider + errors={errors} + object={form} + valueHandler={setValue} + > + <Input<State> + name="new_token" + label={i18n`New access token`} + tooltip={i18n`next access token to be used`} + inputType="password" + /> + <Input<State> + name="repeat_token" + label={i18n`Repeat access token`} + tooltip={i18n`confirm the same access token`} + inputType="password" + /> + </FormProvider> + <p> + <Translate> + With external authorization method no check will be done by + the merchant backend + </Translate> + </p> + </div> + <div class="column" /> </div> - <div class="column" /> - </div> - </section> - <footer class="modal-card-foot"> - {onClear && <button class="button is-danger" onClick={onClear} disabled={onClear === undefined} ><Translate>Set external authorization</Translate></button>} - <div class="buttons is-right" style={{ width: '100%' }}> - <button class="button " onClick={onCancel} ><Translate>Cancel</Translate></button> - <button class="button is-info" onClick={() => onConfirm(form.new_token!)} disabled={hasErrors} ><Translate>Set access token</Translate></button> - </div> - </footer> + </section> + <footer class="modal-card-foot"> + {onClear && ( + <button + class="button is-danger" + onClick={onClear} + disabled={onClear === undefined} + > + <Translate>Set external authorization</Translate> + </button> + )} + <div class="buttons is-right" style={{ width: "100%" }}> + <button class="button " onClick={onCancel}> + <Translate>Cancel</Translate> + </button> + <button + class="button is-info" + onClick={() => onConfirm(form.new_token!)} + disabled={hasErrors} + > + <Translate>Set access token</Translate> + </button> + </div> + </footer> + </div> + <button + class="modal-close is-large " + aria-label="close" + onClick={onCancel} + /> </div> - <button class="modal-close is-large " aria-label="close" onClick={onCancel} /> - </div> + ); } export function LoadingModal({ onCancel }: { onCancel: () => void }): VNode { - const i18n = useTranslator() - return <div class="modal is-active"> - <div class="modal-background " onClick={onCancel} /> - <div class="modal-card"> - <header class="modal-card-head"> - <p class="modal-card-title"><Translate>Operation in progress...</Translate></p> - </header> - <section class="modal-card-body"> - <div class="columns"> - <div class="column" /> - <Spinner /> - <div class="column" /> - </div> - <p>{i18n`The operation will be automatically canceled after ${DEFAULT_REQUEST_TIMEOUT} seconds`}</p> - </section> - <footer class="modal-card-foot"> - <div class="buttons is-right" style={{ width: '100%' }}> - <button class="button " onClick={onCancel} ><Translate>Cancel</Translate></button> - </div> - </footer> + const i18n = useTranslator(); + return ( + <div class="modal is-active"> + <div class="modal-background " onClick={onCancel} /> + <div class="modal-card"> + <header class="modal-card-head"> + <p class="modal-card-title"> + <Translate>Operation in progress...</Translate> + </p> + </header> + <section class="modal-card-body"> + <div class="columns"> + <div class="column" /> + <Spinner /> + <div class="column" /> + </div> + <p>{i18n`The operation will be automatically canceled after ${DEFAULT_REQUEST_TIMEOUT} seconds`}</p> + </section> + <footer class="modal-card-foot"> + <div class="buttons is-right" style={{ width: "100%" }}> + <button class="button " onClick={onCancel}> + <Translate>Cancel</Translate> + </button> + </div> + </footer> + </div> + <button + class="modal-close is-large " + aria-label="close" + onClick={onCancel} + /> </div> - <button class="modal-close is-large " aria-label="close" onClick={onCancel} /> - </div> + ); } diff --git a/packages/merchant-backoffice-ui/src/components/notifications/CreatedSuccessfully.tsx b/packages/merchant-backoffice-ui/src/components/notifications/CreatedSuccessfully.tsx index 4089f2222..073382fb1 100644 --- a/packages/merchant-backoffice-ui/src/components/notifications/CreatedSuccessfully.tsx +++ b/packages/merchant-backoffice-ui/src/components/notifications/CreatedSuccessfully.tsx @@ -14,9 +14,9 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ + * + * @author Sebastian Javier Marchano (sebasjm) + */ import { ComponentChildren, h, VNode } from "preact"; interface Props { @@ -25,25 +25,33 @@ interface Props { children: ComponentChildren; } -export function CreatedSuccessfully({ children, onConfirm, onCreateAnother }: Props): VNode { - return <div class="columns is-fullwidth is-vcentered mt-3"> - <div class="column" /> - <div class="column is-four-fifths"> - <div class="card"> - <header class="card-header has-background-success"> - <p class="card-header-title has-text-white-ter"> - Success. - </p> - </header> - <div class="card-content"> - {children} +export function CreatedSuccessfully({ + children, + onConfirm, + onCreateAnother, +}: Props): VNode { + return ( + <div class="columns is-fullwidth is-vcentered mt-3"> + <div class="column" /> + <div class="column is-four-fifths"> + <div class="card"> + <header class="card-header has-background-success"> + <p class="card-header-title has-text-white-ter">Success.</p> + </header> + <div class="card-content">{children}</div> </div> - </div> <div class="buttons is-right"> - {onCreateAnother && <button class="button is-info" onClick={onCreateAnother}>Create another</button>} - <button class="button is-info" onClick={onConfirm}>Continue</button> + {onCreateAnother && ( + <button class="button is-info" onClick={onCreateAnother}> + Create another + </button> + )} + <button class="button is-info" onClick={onConfirm}> + Continue + </button> </div> + </div> + <div class="column" /> </div> - <div class="column" /> - </div> + ); } diff --git a/packages/merchant-backoffice-ui/src/components/notifications/Notifications.stories.tsx b/packages/merchant-backoffice-ui/src/components/notifications/Notifications.stories.tsx index 8bc6818b7..af594de0f 100644 --- a/packages/merchant-backoffice-ui/src/components/notifications/Notifications.stories.tsx +++ b/packages/merchant-backoffice-ui/src/components/notifications/Notifications.stories.tsx @@ -15,43 +15,48 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ + * + * @author Sebastian Javier Marchano (sebasjm) + */ -import { h } from 'preact'; +import { h } from "preact"; import { Notifications } from "./index.js"; - export default { - title: 'Components/Notification', + title: "Components/Notification", component: Notifications, argTypes: { - removeNotification: { action: 'removeNotification' }, + removeNotification: { action: "removeNotification" }, }, }; export const Info = (a: any) => <Notifications {...a} />; Info.args = { - notifications: [{ - message: 'Title', - description: 'Some large description', - type: 'INFO', - }] -} + notifications: [ + { + message: "Title", + description: "Some large description", + type: "INFO", + }, + ], +}; export const Warn = (a: any) => <Notifications {...a} />; Warn.args = { - notifications: [{ - message: 'Title', - description: 'Some large description', - type: 'WARN', - }] -} + notifications: [ + { + message: "Title", + description: "Some large description", + type: "WARN", + }, + ], +}; export const Error = (a: any) => <Notifications {...a} />; Error.args = { - notifications: [{ - message: 'Title', - description: 'Some large description', - type: 'ERROR', - }] -} + notifications: [ + { + message: "Title", + description: "Some large description", + type: "ERROR", + }, + ], +}; diff --git a/packages/merchant-backoffice-ui/src/components/notifications/index.tsx b/packages/merchant-backoffice-ui/src/components/notifications/index.tsx index 7c4ab7e2d..235c75577 100644 --- a/packages/merchant-backoffice-ui/src/components/notifications/index.tsx +++ b/packages/merchant-backoffice-ui/src/components/notifications/index.tsx @@ -15,9 +15,9 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ + * + * @author Sebastian Javier Marchano (sebasjm) + */ import { h, VNode } from "preact"; import { MessageType, Notification } from "../../utils/types.js"; @@ -29,24 +29,37 @@ interface Props { function messageStyle(type: MessageType): string { switch (type) { - case "INFO": return "message is-info"; - case "WARN": return "message is-warning"; - case "ERROR": return "message is-danger"; - case "SUCCESS": return "message is-success"; - default: return "message" + case "INFO": + return "message is-info"; + case "WARN": + return "message is-warning"; + case "ERROR": + return "message is-danger"; + case "SUCCESS": + return "message is-success"; + default: + return "message"; } } -export function Notifications({ notifications, removeNotification }: Props): VNode { - return <div class="toast"> - {notifications.map((n,i) => <article key={i} class={messageStyle(n.type)}> - <div class="message-header"> - <p>{n.message}</p> - <button class="delete" onClick={() => removeNotification && removeNotification(n)} /> - </div> - {n.description && <div class="message-body"> - {n.description} - </div>} - </article>)} - </div> -}
\ No newline at end of file +export function Notifications({ + notifications, + removeNotification, +}: Props): VNode { + return ( + <div class="toast"> + {notifications.map((n, i) => ( + <article key={i} class={messageStyle(n.type)}> + <div class="message-header"> + <p>{n.message}</p> + <button + class="delete" + onClick={() => removeNotification && removeNotification(n)} + /> + </div> + {n.description && <div class="message-body">{n.description}</div>} + </article> + ))} + </div> + ); +} diff --git a/packages/merchant-backoffice-ui/src/components/picker/DatePicker.tsx b/packages/merchant-backoffice-ui/src/components/picker/DatePicker.tsx index 79b4fa5b1..0bc629d46 100644 --- a/packages/merchant-backoffice-ui/src/components/picker/DatePicker.tsx +++ b/packages/merchant-backoffice-ui/src/components/picker/DatePicker.tsx @@ -15,9 +15,9 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ + * + * @author Sebastian Javier Marchano (sebasjm) + */ import { h, Component } from "preact"; @@ -35,36 +35,33 @@ interface State { // inspired by https://codepen.io/m4r1vs/pen/MOOxyE export class DatePicker extends Component<Props, State> { - closeDatePicker() { this.props.closeFunction && this.props.closeFunction(); // Function gets passed by parent } /** - * Gets fired when a day gets clicked. - * @param {object} e The event thrown by the <span /> element clicked - */ + * Gets fired when a day gets clicked. + * @param {object} e The event thrown by the <span /> element clicked + */ dayClicked(e: any) { - const element = e.target; // the actual element clicked - if (element.innerHTML === '') return false; // don't continue if <span /> empty + if (element.innerHTML === "") return false; // don't continue if <span /> empty // get date from clicked element (gets attached when rendered) - const date = new Date(element.getAttribute('data-value')); + const date = new Date(element.getAttribute("data-value")); // update the state this.setState({ currentDate: date }); - this.passDateToParent(date) + this.passDateToParent(date); } /** - * returns days in month as array - * @param {number} month the month to display - * @param {number} year the year to display - */ + * returns days in month as array + * @param {number} month the month to display + * @param {number} year the year to display + */ getDaysByMonth(month: number, year: number) { - const calendar = []; const date = new Date(year, month, 1); // month to display @@ -76,15 +73,17 @@ export class DatePicker extends Component<Props, State> { // the calendar is 7*6 fields big, so 42 loops for (let i = 0; i < 42; i++) { - if (i >= firstDay && day !== null) day = day + 1; if (day !== null && day > lastDate) day = null; // append the calendar Array calendar.push({ - day: (day === 0 || day === null) ? null : day, // null or number - date: (day === 0 || day === null) ? null : new Date(year, month, day), // null or Date() - today: (day === now.getDate() && month === now.getMonth() && year === now.getFullYear()) // boolean + day: day === 0 || day === null ? null : day, // null or number + date: day === 0 || day === null ? null : new Date(year, month, day), // null or Date() + today: + day === now.getDate() && + month === now.getMonth() && + year === now.getFullYear(), // boolean }); } @@ -92,51 +91,48 @@ export class DatePicker extends Component<Props, State> { } /** - * Display previous month by updating state - */ + * Display previous month by updating state + */ displayPrevMonth() { if (this.state.displayedMonth <= 0) { this.setState({ displayedMonth: 11, - displayedYear: this.state.displayedYear - 1 + displayedYear: this.state.displayedYear - 1, }); - } - else { + } else { this.setState({ - displayedMonth: this.state.displayedMonth - 1 + displayedMonth: this.state.displayedMonth - 1, }); } } /** - * Display next month by updating state - */ + * Display next month by updating state + */ displayNextMonth() { if (this.state.displayedMonth >= 11) { this.setState({ displayedMonth: 0, - displayedYear: this.state.displayedYear + 1 + displayedYear: this.state.displayedYear + 1, }); - } - else { + } else { this.setState({ - displayedMonth: this.state.displayedMonth + 1 + displayedMonth: this.state.displayedMonth + 1, }); } } /** - * Display the selected month (gets fired when clicking on the date string) - */ + * Display the selected month (gets fired when clicking on the date string) + */ displaySelectedMonth() { if (this.state.selectYearMode) { this.toggleYearSelector(); - } - else { + } else { if (!this.state.currentDate) return false; this.setState({ displayedMonth: this.state.currentDate.getMonth(), - displayedYear: this.state.currentDate.getFullYear() + displayedYear: this.state.currentDate.getFullYear(), }); } } @@ -148,23 +144,27 @@ export class DatePicker extends Component<Props, State> { changeDisplayedYear(e: any) { const element = e.target; this.toggleYearSelector(); - this.setState({ displayedYear: parseInt(element.innerHTML, 10), displayedMonth: 0 }); + this.setState({ + displayedYear: parseInt(element.innerHTML, 10), + displayedMonth: 0, + }); } /** - * Pass the selected date to parent when 'OK' is clicked - */ + * Pass the selected date to parent when 'OK' is clicked + */ passSavedDateDateToParent() { - this.passDateToParent(this.state.currentDate) + this.passDateToParent(this.state.currentDate); } passDateToParent(date: Date) { - if (typeof this.props.dateReceiver === 'function') this.props.dateReceiver(date); + if (typeof this.props.dateReceiver === "function") + this.props.dateReceiver(date); this.closeDatePicker(); } componentDidUpdate() { if (this.state.selectYearMode) { - document.getElementsByClassName('selected')[0].scrollIntoView(); // works in every browser incl. IE, replace with scrollIntoViewIfNeeded when browsers support it + document.getElementsByClassName("selected")[0].scrollIntoView(); // works in every browser incl. IE, replace with scrollIntoViewIfNeeded when browsers support it } } @@ -181,143 +181,168 @@ export class DatePicker extends Component<Props, State> { this.toggleYearSelector = this.toggleYearSelector.bind(this); this.displaySelectedMonth = this.displaySelectedMonth.bind(this); - this.state = { currentDate: now, displayedMonth: now.getMonth(), displayedYear: now.getFullYear(), - selectYearMode: false - } + selectYearMode: false, + }; } render() { - - const { currentDate, displayedMonth, displayedYear, selectYearMode } = this.state; + const { currentDate, displayedMonth, displayedYear, selectYearMode } = + this.state; return ( <div> - <div class={`datePicker ${ this.props.opened && "datePicker--opened"}`} > - + <div class={`datePicker ${this.props.opened && "datePicker--opened"}`}> <div class="datePicker--titles"> - <h3 style={{ - color: selectYearMode ? 'rgba(255,255,255,.87)' : 'rgba(255,255,255,.57)' - }} onClick={this.toggleYearSelector}>{currentDate.getFullYear()}</h3> - <h2 style={{ - color: !selectYearMode ? 'rgba(255,255,255,.87)' : 'rgba(255,255,255,.57)' - }} onClick={this.displaySelectedMonth}> - {dayArr[currentDate.getDay()]}, {monthArrShort[currentDate.getMonth()]} {currentDate.getDate()} + <h3 + style={{ + color: selectYearMode + ? "rgba(255,255,255,.87)" + : "rgba(255,255,255,.57)", + }} + onClick={this.toggleYearSelector} + > + {currentDate.getFullYear()} + </h3> + <h2 + style={{ + color: !selectYearMode + ? "rgba(255,255,255,.87)" + : "rgba(255,255,255,.57)", + }} + onClick={this.displaySelectedMonth} + > + {dayArr[currentDate.getDay()]},{" "} + {monthArrShort[currentDate.getMonth()]} {currentDate.getDate()} </h2> </div> - {!selectYearMode && <nav> - <span onClick={this.displayPrevMonth} class="icon"><i style={{ transform: 'rotate(180deg)' }} class="mdi mdi-forward" /></span> - <h4>{monthArrShortFull[displayedMonth]} {displayedYear}</h4> - <span onClick={this.displayNextMonth} class="icon"><i class="mdi mdi-forward" /></span> - </nav>} + {!selectYearMode && ( + <nav> + <span onClick={this.displayPrevMonth} class="icon"> + <i + style={{ transform: "rotate(180deg)" }} + class="mdi mdi-forward" + /> + </span> + <h4> + {monthArrShortFull[displayedMonth]} {displayedYear} + </h4> + <span onClick={this.displayNextMonth} class="icon"> + <i class="mdi mdi-forward" /> + </span> + </nav> + )} <div class="datePicker--scroll"> - - {!selectYearMode && <div class="datePicker--calendar" > - - <div class="datePicker--dayNames"> - {['S', 'M', 'T', 'W', 'T', 'F', 'S'].map((day,i) => <span key={i}>{day}</span>)} - </div> - - <div onClick={this.dayClicked} class="datePicker--days"> - - {/* + {!selectYearMode && ( + <div class="datePicker--calendar"> + <div class="datePicker--dayNames"> + {["S", "M", "T", "W", "T", "F", "S"].map((day, i) => ( + <span key={i}>{day}</span> + ))} + </div> + + <div onClick={this.dayClicked} class="datePicker--days"> + {/* Loop through the calendar object returned by getDaysByMonth(). */} - {this.getDaysByMonth(this.state.displayedMonth, this.state.displayedYear) - .map( - day => { - let selected = false; - - if (currentDate && day.date) selected = (currentDate.toLocaleDateString() === day.date.toLocaleDateString()); - - return (<span key={day.day} - class={(day.today ? 'datePicker--today ' : '') + (selected ? 'datePicker--selected' : '')} + {this.getDaysByMonth( + this.state.displayedMonth, + this.state.displayedYear, + ).map((day) => { + let selected = false; + + if (currentDate && day.date) + selected = + currentDate.toLocaleDateString() === + day.date.toLocaleDateString(); + + return ( + <span + key={day.day} + class={ + (day.today ? "datePicker--today " : "") + + (selected ? "datePicker--selected" : "") + } disabled={!day.date} data-value={day.date} > {day.day} - </span>) - } - ) - } - + </span> + ); + })} + </div> </div> - - </div>} - - {selectYearMode && <div class="datePicker--selectYear"> - - {yearArr.map(year => ( - <span key={year} class={(year === displayedYear) ? 'selected' : ''} onClick={this.changeDisplayedYear}> - {year} - </span> - ))} - - </div>} - + )} + + {selectYearMode && ( + <div class="datePicker--selectYear"> + {yearArr.map((year) => ( + <span + key={year} + class={year === displayedYear ? "selected" : ""} + onClick={this.changeDisplayedYear} + > + {year} + </span> + ))} + </div> + )} </div> </div> - <div class="datePicker--background" onClick={this.closeDatePicker} style={{ - display: this.props.opened ? 'block' : 'none' - }} + <div + class="datePicker--background" + onClick={this.closeDatePicker} + style={{ + display: this.props.opened ? "block" : "none", + }} /> - </div> - ) + ); } } - const monthArrShortFull = [ - 'January', - 'February', - 'March', - 'April', - 'May', - 'June', - 'July', - 'August', - 'September', - 'October', - 'November', - 'December' -] + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", +]; const monthArrShort = [ - 'Jan', - 'Feb', - 'Mar', - 'Apr', - 'May', - 'Jun', - 'Jul', - 'Aug', - 'Sep', - 'Oct', - 'Nov', - 'Dec' -] - -const dayArr = [ - 'Sun', - 'Mon', - 'Tue', - 'Wed', - 'Thu', - 'Fri', - 'Sat' -] - -const now = new Date() - -const yearArr: number[] = [] + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", +]; + +const dayArr = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + +const now = new Date(); + +const yearArr: number[] = []; for (let i = 2010; i <= now.getFullYear() + 10; i++) { yearArr.push(i); diff --git a/packages/merchant-backoffice-ui/src/components/picker/DurationPicker.stories.tsx b/packages/merchant-backoffice-ui/src/components/picker/DurationPicker.stories.tsx index 888bf17b0..8f74d55ac 100644 --- a/packages/merchant-backoffice-ui/src/components/picker/DurationPicker.stories.tsx +++ b/packages/merchant-backoffice-ui/src/components/picker/DurationPicker.stories.tsx @@ -15,36 +15,41 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ + * + * @author Sebastian Javier Marchano (sebasjm) + */ -import { h, FunctionalComponent } from 'preact'; -import { useState } from 'preact/hooks'; +import { h, FunctionalComponent } from "preact"; +import { useState } from "preact/hooks"; import { DurationPicker as TestedComponent } from "./DurationPicker.js"; - export default { - title: 'Components/Picker/Duration', + title: "Components/Picker/Duration", component: TestedComponent, argTypes: { - onCreate: { action: 'onCreate' }, - goBack: { action: 'goBack' }, - } + onCreate: { action: "onCreate" }, + goBack: { action: "goBack" }, + }, }; -function createExample<Props>(Component: FunctionalComponent<Props>, props: Partial<Props>) { - const r = (args: any) => <Component {...args} /> - r.args = props - return r +function createExample<Props>( + Component: FunctionalComponent<Props>, + props: Partial<Props>, +) { + const r = (args: any) => <Component {...args} />; + r.args = props; + return r; } export const Example = createExample(TestedComponent, { - days: true, minutes: true, hours: true, seconds: true, - value: 10000000 + days: true, + minutes: true, + hours: true, + seconds: true, + value: 10000000, }); export const WithState = () => { - const [v,s] = useState<number>(1000000) - return <TestedComponent value={v} onChange={s} days minutes hours seconds /> -} + const [v, s] = useState<number>(1000000); + return <TestedComponent value={v} onChange={s} days minutes hours seconds />; +}; diff --git a/packages/merchant-backoffice-ui/src/components/product/InventoryProductForm.stories.tsx b/packages/merchant-backoffice-ui/src/components/product/InventoryProductForm.stories.tsx index 68bf7e438..2d5a54cde 100644 --- a/packages/merchant-backoffice-ui/src/components/product/InventoryProductForm.stories.tsx +++ b/packages/merchant-backoffice-ui/src/components/product/InventoryProductForm.stories.tsx @@ -15,44 +15,48 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ + * + * @author Sebastian Javier Marchano (sebasjm) + */ -import { h, VNode, FunctionalComponent } from 'preact'; +import { h, VNode, FunctionalComponent } from "preact"; import { InventoryProductForm as TestedComponent } from "./InventoryProductForm.js"; - export default { - title: 'Components/Product/Add', + title: "Components/Product/Add", component: TestedComponent, argTypes: { - onAddProduct: { action: 'onAddProduct' }, + onAddProduct: { action: "onAddProduct" }, }, }; -function createExample<Props>(Component: FunctionalComponent<Props>, props: Partial<Props>) { - const r = (args: any) => <Component {...args} /> - r.args = props - return r +function createExample<Props>( + Component: FunctionalComponent<Props>, + props: Partial<Props>, +) { + const r = (args: any) => <Component {...args} />; + r.args = props; + return r; } export const WithASimpleList = createExample(TestedComponent, { - inventory:[{ - id: 'this id', - description: 'this is the description', - } as any] + inventory: [ + { + id: "this id", + description: "this is the description", + } as any, + ], }); export const WithAProductSelected = createExample(TestedComponent, { - inventory:[], + inventory: [], currentProducts: { thisid: { quantity: 1, product: { - id: 'asd', - description: 'asdsadsad', - } as any - } - } + id: "asd", + description: "asdsadsad", + } as any, + }, + }, }); diff --git a/packages/merchant-backoffice-ui/src/components/product/InventoryProductForm.tsx b/packages/merchant-backoffice-ui/src/components/product/InventoryProductForm.tsx index e44044372..da47f1be3 100644 --- a/packages/merchant-backoffice-ui/src/components/product/InventoryProductForm.tsx +++ b/packages/merchant-backoffice-ui/src/components/product/InventoryProductForm.tsx @@ -23,24 +23,32 @@ import { Translate, useTranslator } from "../../i18n/index.js"; import { ProductMap } from "../../paths/instance/orders/create/CreatePage.js"; type Form = { - product: MerchantBackend.Products.ProductDetail & WithId, + product: MerchantBackend.Products.ProductDetail & WithId; quantity: number; -} +}; interface Props { - currentProducts: ProductMap, - onAddProduct: (product: MerchantBackend.Products.ProductDetail & WithId, quantity: number) => void, - inventory: (MerchantBackend.Products.ProductDetail & WithId)[], + currentProducts: ProductMap; + onAddProduct: ( + product: MerchantBackend.Products.ProductDetail & WithId, + quantity: number, + ) => void; + inventory: (MerchantBackend.Products.ProductDetail & WithId)[]; } -export function InventoryProductForm({ currentProducts, onAddProduct, inventory }: Props): VNode { - const initialState = { quantity: 1 } - const [state, setState] = useState<Partial<Form>>(initialState) - const [errors, setErrors] = useState<FormErrors<Form>>({}) +export function InventoryProductForm({ + currentProducts, + onAddProduct, + inventory, +}: Props): VNode { + const initialState = { quantity: 1 }; + const [state, setState] = useState<Partial<Form>>(initialState); + const [errors, setErrors] = useState<FormErrors<Form>>({}); - const i18n = useTranslator() + const i18n = useTranslator(); - const productWithInfiniteStock = state.product && state.product.total_stock === -1 + const productWithInfiniteStock = + state.product && state.product.total_stock === -1; const submit = (): void => { if (!state.product) { @@ -48,48 +56,68 @@ export function InventoryProductForm({ currentProducts, onAddProduct, inventory return; } if (productWithInfiniteStock) { - onAddProduct(state.product, 1) + onAddProduct(state.product, 1); } else { if (!state.quantity || state.quantity <= 0) { setErrors({ quantity: i18n`Quantity must be greater than 0!` }); return; } - const currentStock = state.product.total_stock - state.product.total_lost - state.product.total_sold - const p = currentProducts[state.product.id] + const currentStock = + state.product.total_stock - + state.product.total_lost - + state.product.total_sold; + const p = currentProducts[state.product.id]; if (p) { if (state.quantity + p.quantity > currentStock) { const left = currentStock - p.quantity; - setErrors({ quantity: i18n`This quantity exceeds remaining stock. Currently, only ${left} units remain unreserved in stock.` }); + setErrors({ + quantity: i18n`This quantity exceeds remaining stock. Currently, only ${left} units remain unreserved in stock.`, + }); return; } - onAddProduct(state.product, state.quantity + p.quantity) + onAddProduct(state.product, state.quantity + p.quantity); } else { if (state.quantity > currentStock) { const left = currentStock; - setErrors({ quantity: i18n`This quantity exceeds remaining stock. Currently, only ${left} units remain unreserved in stock.` }); + setErrors({ + quantity: i18n`This quantity exceeds remaining stock. Currently, only ${left} units remain unreserved in stock.`, + }); return; } - onAddProduct(state.product, state.quantity) + onAddProduct(state.product, state.quantity); } } - setState(initialState) - } + setState(initialState); + }; - return <FormProvider<Form> errors={errors} object={state} valueHandler={setState}> - <InputSearchProduct selected={state.product} onChange={(p) => setState(v => ({ ...v, product: p }))} products={inventory} /> - { state.product && <div class="columns mt-5"> - <div class="column is-two-thirds"> - {!productWithInfiniteStock && - <InputNumber<Form> name="quantity" label={i18n`Quantity`} tooltip={i18n`how many products will be added`} /> - } - </div> - <div class="column"> - <div class="buttons is-right"> - <button class="button is-success" onClick={submit}><Translate>Add from inventory</Translate></button> + return ( + <FormProvider<Form> errors={errors} object={state} valueHandler={setState}> + <InputSearchProduct + selected={state.product} + onChange={(p) => setState((v) => ({ ...v, product: p }))} + products={inventory} + /> + {state.product && ( + <div class="columns mt-5"> + <div class="column is-two-thirds"> + {!productWithInfiniteStock && ( + <InputNumber<Form> + name="quantity" + label={i18n`Quantity`} + tooltip={i18n`how many products will be added`} + /> + )} + </div> + <div class="column"> + <div class="buttons is-right"> + <button class="button is-success" onClick={submit}> + <Translate>Add from inventory</Translate> + </button> + </div> + </div> </div> - </div> - </div> } - - </FormProvider> + )} + </FormProvider> + ); } diff --git a/packages/merchant-backoffice-ui/src/components/product/NonInventoryProductForm.tsx b/packages/merchant-backoffice-ui/src/components/product/NonInventoryProductForm.tsx index b468a4e86..fe9692c02 100644 --- a/packages/merchant-backoffice-ui/src/components/product/NonInventoryProductForm.tsx +++ b/packages/merchant-backoffice-ui/src/components/product/NonInventoryProductForm.tsx @@ -15,7 +15,7 @@ */ import { Fragment, h, VNode } from "preact"; import { useCallback, useEffect, useState } from "preact/hooks"; -import * as yup from 'yup'; +import * as yup from "yup"; import { FormErrors, FormProvider } from "../form/FormProvider.js"; import { Input } from "../form/Input.js"; import { InputCurrency } from "../form/InputCurrency.js"; @@ -25,67 +25,104 @@ import { InputTaxes } from "../form/InputTaxes.js"; import { MerchantBackend } from "../../declaration.js"; import { useListener } from "../../hooks/listener.js"; import { Translate, useTranslator } from "../../i18n/index.js"; -import { - NonInventoryProductSchema as schema -} from "../../schemas/index.js"; +import { NonInventoryProductSchema as schema } from "../../schemas/index.js"; - -type Entity = MerchantBackend.Product +type Entity = MerchantBackend.Product; interface Props { onAddProduct: (p: Entity) => Promise<void>; productToEdit?: Entity; } -export function NonInventoryProductFrom({ productToEdit, onAddProduct }: Props): VNode { - const [showCreateProduct, setShowCreateProduct] = useState(false) +export function NonInventoryProductFrom({ + productToEdit, + onAddProduct, +}: Props): VNode { + const [showCreateProduct, setShowCreateProduct] = useState(false); - const isEditing = !!productToEdit + const isEditing = !!productToEdit; useEffect(() => { - setShowCreateProduct(isEditing) - }, [isEditing]) + setShowCreateProduct(isEditing); + }, [isEditing]); - const [submitForm, addFormSubmitter] = useListener<Partial<MerchantBackend.Product> | undefined>((result) => { + const [submitForm, addFormSubmitter] = useListener< + Partial<MerchantBackend.Product> | undefined + >((result) => { if (result) { - setShowCreateProduct(false) + setShowCreateProduct(false); return onAddProduct({ quantity: result.quantity || 0, taxes: result.taxes || [], - description: result.description || '', - image: result.image || '', - price: result.price || '', - unit: result.unit || '' - }) + description: result.description || "", + image: result.image || "", + price: result.price || "", + unit: result.unit || "", + }); } - return Promise.resolve() - }) - - const i18n = useTranslator() - - return <Fragment> - <div class="buttons"> - <button class="button is-success" data-tooltip={i18n`describe and add a product that is not in the inventory list`} onClick={() => setShowCreateProduct(true)} ><Translate>Add custom product</Translate></button> - </div> - {showCreateProduct && <div class="modal is-active"> - <div class="modal-background " onClick={() => setShowCreateProduct(false)} /> - <div class="modal-card"> - <header class="modal-card-head"> - <p class="modal-card-title">{i18n`Complete information of the product`}</p> - <button class="delete " aria-label="close" onClick={() => setShowCreateProduct(false)} /> - </header> - <section class="modal-card-body"> - <ProductForm initial={productToEdit} onSubscribe={addFormSubmitter} /> - </section> - <footer class="modal-card-foot"> - <div class="buttons is-right" style={{ width: '100%' }}> - <button class="button " onClick={() => setShowCreateProduct(false)} ><Translate>Cancel</Translate></button> - <button class="button is-info " disabled={!submitForm} onClick={submitForm} ><Translate>Confirm</Translate></button> - </div> - </footer> + return Promise.resolve(); + }); + + const i18n = useTranslator(); + + return ( + <Fragment> + <div class="buttons"> + <button + class="button is-success" + data-tooltip={i18n`describe and add a product that is not in the inventory list`} + onClick={() => setShowCreateProduct(true)} + > + <Translate>Add custom product</Translate> + </button> </div> - <button class="modal-close is-large " aria-label="close" onClick={() => setShowCreateProduct(false)} /> - </div>} - </Fragment> + {showCreateProduct && ( + <div class="modal is-active"> + <div + class="modal-background " + onClick={() => setShowCreateProduct(false)} + /> + <div class="modal-card"> + <header class="modal-card-head"> + <p class="modal-card-title">{i18n`Complete information of the product`}</p> + <button + class="delete " + aria-label="close" + onClick={() => setShowCreateProduct(false)} + /> + </header> + <section class="modal-card-body"> + <ProductForm + initial={productToEdit} + onSubscribe={addFormSubmitter} + /> + </section> + <footer class="modal-card-foot"> + <div class="buttons is-right" style={{ width: "100%" }}> + <button + class="button " + onClick={() => setShowCreateProduct(false)} + > + <Translate>Cancel</Translate> + </button> + <button + class="button is-info " + disabled={!submitForm} + onClick={submitForm} + > + <Translate>Confirm</Translate> + </button> + </div> + </footer> + </div> + <button + class="modal-close is-large " + aria-label="close" + onClick={() => setShowCreateProduct(false)} + /> + </div> + )} + </Fragment> + ); } interface ProductProps { @@ -106,41 +143,73 @@ export function ProductForm({ onSubscribe, initial }: ProductProps): VNode { const [value, valueHandler] = useState<Partial<NonInventoryProduct>>({ taxes: [], ...initial, - }) - let errors: FormErrors<Entity> = {} + }); + let errors: FormErrors<Entity> = {}; try { - schema.validateSync(value, { abortEarly: false }) + schema.validateSync(value, { abortEarly: false }); } catch (err) { if (err instanceof yup.ValidationError) { - const yupErrors = err.inner as yup.ValidationError[] - errors = yupErrors.reduce((prev, cur) => !cur.path ? prev : ({ ...prev, [cur.path]: cur.message }), {}) + const yupErrors = err.inner as yup.ValidationError[]; + errors = yupErrors.reduce( + (prev, cur) => + !cur.path ? prev : { ...prev, [cur.path]: cur.message }, + {}, + ); } } const submit = useCallback((): Entity | undefined => { - return value as MerchantBackend.Product - }, [value]) + return value as MerchantBackend.Product; + }, [value]); - const hasErrors = Object.keys(errors).some(k => (errors as any)[k] !== undefined) + const hasErrors = Object.keys(errors).some( + (k) => (errors as any)[k] !== undefined, + ); useEffect(() => { - onSubscribe(hasErrors ? undefined : submit) - }, [submit, hasErrors]) - - const i18n = useTranslator() - - return <div> - <FormProvider<NonInventoryProduct> name="product" errors={errors} object={value} valueHandler={valueHandler} > - - <InputImage<NonInventoryProduct> name="image" label={i18n`Image`} tooltip={i18n`photo of the product`} /> - <Input<NonInventoryProduct> name="description" inputType="multiline" label={i18n`Description`} tooltip={i18n`full product description`} /> - <Input<NonInventoryProduct> name="unit" label={i18n`Unit`} tooltip={i18n`name of the product unit`} /> - <InputCurrency<NonInventoryProduct> name="price" label={i18n`Price`} tooltip={i18n`amount in the current currency`} /> - - <InputNumber<NonInventoryProduct> name="quantity" label={i18n`Quantity`} tooltip={i18n`how many products will be added`} /> - - <InputTaxes<NonInventoryProduct> name="taxes" label={i18n`Taxes`} /> - - </FormProvider> - </div> + onSubscribe(hasErrors ? undefined : submit); + }, [submit, hasErrors]); + + const i18n = useTranslator(); + + return ( + <div> + <FormProvider<NonInventoryProduct> + name="product" + errors={errors} + object={value} + valueHandler={valueHandler} + > + <InputImage<NonInventoryProduct> + name="image" + label={i18n`Image`} + tooltip={i18n`photo of the product`} + /> + <Input<NonInventoryProduct> + name="description" + inputType="multiline" + label={i18n`Description`} + tooltip={i18n`full product description`} + /> + <Input<NonInventoryProduct> + name="unit" + label={i18n`Unit`} + tooltip={i18n`name of the product unit`} + /> + <InputCurrency<NonInventoryProduct> + name="price" + label={i18n`Price`} + tooltip={i18n`amount in the current currency`} + /> + + <InputNumber<NonInventoryProduct> + name="quantity" + label={i18n`Quantity`} + tooltip={i18n`how many products will be added`} + /> + + <InputTaxes<NonInventoryProduct> name="taxes" label={i18n`Taxes`} /> + </FormProvider> + </div> + ); } diff --git a/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx b/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx index c078e7cee..a6bb090a5 100644 --- a/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx +++ b/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx @@ -77,12 +77,12 @@ export function ProductForm({ onSubscribe, initial, alreadyExist }: Props) { errors = yupErrors.reduce( (prev, cur) => !cur.path ? prev : { ...prev, [cur.path]: cur.message }, - {} + {}, ); } } const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined + (k) => (errors as any)[k] !== undefined, ); const submit = useCallback((): Entity | undefined => { diff --git a/packages/merchant-backoffice-ui/src/components/product/ProductList.tsx b/packages/merchant-backoffice-ui/src/components/product/ProductList.tsx index d8b0104ea..774da8975 100644 --- a/packages/merchant-backoffice-ui/src/components/product/ProductList.tsx +++ b/packages/merchant-backoffice-ui/src/components/product/ProductList.tsx @@ -59,8 +59,8 @@ export function ProductList({ list, actions = [] }: Props): VNode { : Amounts.stringify( Amounts.mult( Amounts.parseOrThrow(entry.price), - entry.quantity - ).amount + entry.quantity, + ).amount, ); return ( |