From 21fda5751d61594ee94d995232f0ce66647533b2 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 24 Oct 2023 09:41:53 -0300 Subject: better template UI --- packages/demobank-ui/src/hooks/backend.ts | 2 +- packages/demobank-ui/src/hooks/settings.ts | 2 +- .../src/components/form/InputTab.tsx | 90 +++++++++++++++++++ packages/merchant-backoffice-ui/src/hooks/index.ts | 4 +- .../paths/instance/templates/create/CreatePage.tsx | 92 +++++++++++++++---- .../paths/instance/templates/update/UpdatePage.tsx | 100 +++++++++++++++++---- 6 files changed, 252 insertions(+), 38 deletions(-) create mode 100644 packages/merchant-backoffice-ui/src/components/form/InputTab.tsx (limited to 'packages') diff --git a/packages/demobank-ui/src/hooks/backend.ts b/packages/demobank-ui/src/hooks/backend.ts index 589d7fab0..93d647e73 100644 --- a/packages/demobank-ui/src/hooks/backend.ts +++ b/packages/demobank-ui/src/hooks/backend.ts @@ -92,7 +92,7 @@ export interface BackendStateHandler { } const BACKEND_STATE_KEY = buildStorageKey( - "backend-state", + "bank-state", codecForBackendState(), ); diff --git a/packages/demobank-ui/src/hooks/settings.ts b/packages/demobank-ui/src/hooks/settings.ts index ca2d131f2..1e656b3ba 100644 --- a/packages/demobank-ui/src/hooks/settings.ts +++ b/packages/demobank-ui/src/hooks/settings.ts @@ -74,7 +74,7 @@ const defaultSettings: Settings = { }; const DEMOBANK_SETTINGS_KEY = buildStorageKey( - "demobank-settings", + "bank-settings", codecForSettings(), ); diff --git a/packages/merchant-backoffice-ui/src/components/form/InputTab.tsx b/packages/merchant-backoffice-ui/src/components/form/InputTab.tsx new file mode 100644 index 000000000..2701768aa --- /dev/null +++ b/packages/merchant-backoffice-ui/src/components/form/InputTab.tsx @@ -0,0 +1,90 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ +import { h, VNode } from "preact"; +import { InputProps, useField } from "./useField.js"; + +interface Props extends InputProps { + readonly?: boolean; + expand?: boolean; + values: any[]; + toStr?: (v?: any) => string; + fromStr?: (s: string) => any; +} + +const defaultToString = (f?: any): string => f || ""; +const defaultFromString = (v: string): any => v as any; + +export function InputTab({ + name, + readonly, + expand, + placeholder, + tooltip, + label, + help, + values, + fromStr = defaultFromString, + toStr = defaultToString, +}: Props): VNode { + const { error, value, onChange, required } = useField(name); + return ( +
+
+ +
+
+
+

+

+
    + {values.map((v, i) => { + return ( +
  • { onChange(v) }} + > + + {toStr(v)} + +
  • + ); + })} +
+
+ {help} +

+ {required && ( + + + + )} + {error &&

{error}

} +
+
+
+ ); +} diff --git a/packages/merchant-backoffice-ui/src/hooks/index.ts b/packages/merchant-backoffice-ui/src/hooks/index.ts index 3c8ef15ed..f0cd1bfb9 100644 --- a/packages/merchant-backoffice-ui/src/hooks/index.ts +++ b/packages/merchant-backoffice-ui/src/hooks/index.ts @@ -45,14 +45,14 @@ const loginTokenCodec = buildCodecForObject() .property("token", codecForString()) .property("expiration", codecForTimestamp) .build("loginToken") -const TOKENS_KEY = buildStorageKey("backend-token", codecForMap(loginTokenCodec)); +const TOKENS_KEY = buildStorageKey("merchant-token", codecForMap(loginTokenCodec)); export function useBackendURL( url?: string, ): [string, StateUpdater] { const [value, setter] = useSimpleLocalStorage( - "backend-url", + "merchant-base-url", url || calculateRootPath(), ); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx index 78ea07477..947f3572c 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx @@ -41,8 +41,16 @@ import { useBackendContext } from "../../../../context/backend.js"; import { MerchantBackend } from "../../../../declaration.js"; import { useInstanceOtpDevices } from "../../../../hooks/otp.js"; import { undefinedIfEmpty } from "../../../../utils/table.js"; +import { InputTab } from "../../../../components/form/InputTab.js"; -type Entity = MerchantBackend.Template.TemplateAddDetails; +enum Steps { + BOTH_FIXED, + FIXED_PRICE, + FIXED_SUMMARY, + NON_FIXED, +} + +type Entity = MerchantBackend.Template.TemplateAddDetails & { type: Steps }; interface Props { onCreate: (d: Entity) => Promise; @@ -61,6 +69,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { d_us: 1000 * 1000 * 60 * 30, //30 min }, }, + type: Steps.NON_FIXED, }); const parsedPrice = !state.template_contract?.amount @@ -79,13 +88,20 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { template_contract: !state.template_contract ? undefined : undefinedIfEmpty({ - amount: !state.template_contract?.amount + amount: !(state.type === Steps.FIXED_PRICE || state.type === Steps.BOTH_FIXED) ? undefined - : !parsedPrice - ? i18n.str`not valid` - : Amounts.isZero(parsedPrice) - ? i18n.str`must be greater than 0` - : undefined, + : !state.template_contract?.amount + ? i18n.str`required` + : !parsedPrice + ? i18n.str`not valid` + : Amounts.isZero(parsedPrice) + ? i18n.str`must be greater than 0` + : undefined, + summary: !(state.type === Steps.FIXED_SUMMARY || state.type === Steps.BOTH_FIXED) + ? undefined + : !state.template_contract?.summary + ? i18n.str`required` + : undefined, minimum_age: state.template_contract.minimum_age < 0 ? i18n.str`should be greater that 0` @@ -106,6 +122,17 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { const submitForm = () => { if (hasErrors) return Promise.reject(); + if (state.template_contract) { + if (state.type === Steps.NON_FIXED) { + delete state.template_contract.amount; + delete state.template_contract.summary; + } else if (state.type === Steps.FIXED_SUMMARY) { + delete state.template_contract.amount; + } else if (state.type === Steps.FIXED_PRICE) { + delete state.template_contract.summary; + } + } + delete state.type return onCreate(state as any); }; @@ -134,17 +161,48 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { help="" tooltip={i18n.str`Describe what this template stands for`} /> - - { + switch (state.type) { + case Steps.NON_FIXED: return i18n.str`User will be able to input price and summary before payment.` + case Steps.FIXED_PRICE: return i18n.str`User will be able to add a summary before payment.` + case Steps.FIXED_SUMMARY: return i18n.str`User will be able to set the price before payment.` + case Steps.BOTH_FIXED: return i18n.str`User will not be able to change the price or the summary.` + } + })()} + tooltip={i18n.str`Define what the user be allowed to modify`} + values={[ + Steps.NON_FIXED, + Steps.FIXED_PRICE, + Steps.FIXED_SUMMARY, + Steps.BOTH_FIXED, + ]} + toStr={(v: Steps): string => { + switch (v) { + case Steps.NON_FIXED: return i18n.str`Simple` + case Steps.FIXED_PRICE: return i18n.str`With price` + case Steps.FIXED_SUMMARY: return i18n.str`With summary` + case Steps.BOTH_FIXED: return i18n.str`With price and summary` + } + }} /> + {state.type === Steps.BOTH_FIXED || state.type === Steps.FIXED_SUMMARY ? + + : undefined} + {state.type === Steps.BOTH_FIXED || state.type === Steps.FIXED_PRICE ? + + : undefined} >(template); + const intialStep = + template.template_contract?.amount === undefined && template.template_contract?.summary === undefined + ? Steps.NON_FIXED + : template.template_contract?.summary === undefined + ? Steps.FIXED_PRICE + : template.template_contract?.amount === undefined + ? Steps.FIXED_SUMMARY + : Steps.BOTH_FIXED; + + const [state, setState] = useState>({ ...template, type: intialStep }); const parsedPrice = !state.template_contract?.amount ? undefined @@ -65,13 +82,20 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode { template_contract: !state.template_contract ? undefined : undefinedIfEmpty({ - amount: !state.template_contract?.amount + amount: !(state.type === Steps.FIXED_PRICE || state.type === Steps.BOTH_FIXED) ? undefined - : !parsedPrice - ? i18n.str`not valid` - : Amounts.isZero(parsedPrice) - ? i18n.str`must be greater than 0` - : undefined, + : !state.template_contract?.amount + ? i18n.str`required` + : !parsedPrice + ? i18n.str`not valid` + : Amounts.isZero(parsedPrice) + ? i18n.str`must be greater than 0` + : undefined, + summary: !(state.type === Steps.FIXED_SUMMARY || state.type === Steps.BOTH_FIXED) + ? undefined + : !state.template_contract?.summary + ? i18n.str`required` + : undefined, minimum_age: state.template_contract.minimum_age < 0 ? i18n.str`should be greater that 0` @@ -92,6 +116,17 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode { const submitForm = () => { if (hasErrors) return Promise.reject(); + if (state.template_contract) { + if (state.type === Steps.NON_FIXED) { + delete state.template_contract.amount; + delete state.template_contract.summary; + } else if (state.type === Steps.FIXED_SUMMARY) { + delete state.template_contract.amount; + } else if (state.type === Steps.FIXED_PRICE) { + delete state.template_contract.summary; + } + } + delete state.type return onUpdate(state as any); }; @@ -136,17 +171,48 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode { help="" tooltip={i18n.str`Describe what this template stands for`} /> - - { + switch (state.type) { + case Steps.NON_FIXED: return i18n.str`User will be able to input price and summary before payment.` + case Steps.FIXED_PRICE: return i18n.str`User will be able to add a summary before payment.` + case Steps.FIXED_SUMMARY: return i18n.str`User will be able to set the price before payment.` + case Steps.BOTH_FIXED: return i18n.str`User will not be able to change the price or the summary.` + } + })()} + tooltip={i18n.str`Define what the user be allowed to modify`} + values={[ + Steps.NON_FIXED, + Steps.FIXED_PRICE, + Steps.FIXED_SUMMARY, + Steps.BOTH_FIXED, + ]} + toStr={(v: Steps): string => { + switch (v) { + case Steps.NON_FIXED: return i18n.str`Simple` + case Steps.FIXED_PRICE: return i18n.str`With price` + case Steps.FIXED_SUMMARY: return i18n.str`With summary` + case Steps.BOTH_FIXED: return i18n.str`With price and summary` + } + }} /> + {state.type === Steps.BOTH_FIXED || state.type === Steps.FIXED_SUMMARY ? + + : undefined} + {state.type === Steps.BOTH_FIXED || state.type === Steps.FIXED_PRICE ? + + : undefined}