diff options
author | Sebastian <sebasjm@gmail.com> | 2023-10-24 09:41:53 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2023-10-24 09:41:53 -0300 |
commit | 21fda5751d61594ee94d995232f0ce66647533b2 (patch) | |
tree | 395b864460406087030980aec674ad10d794ba69 /packages/merchant-backoffice-ui | |
parent | 58c0b115175ffc963e394258ec888b2be7ad3d74 (diff) | |
download | wallet-core-21fda5751d61594ee94d995232f0ce66647533b2.tar.xz |
better template UI
Diffstat (limited to 'packages/merchant-backoffice-ui')
4 files changed, 250 insertions, 36 deletions
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 <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ +import { h, VNode } from "preact"; +import { InputProps, useField } from "./useField.js"; + +interface Props<T> extends InputProps<T> { + 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<T>({ + name, + readonly, + expand, + placeholder, + tooltip, + label, + help, + values, + 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 has-icons-right"> + <p class={expand ? "control is-expanded " : "control "}> + <div class="tabs is-toggle is-fullwidth is-small"> + <ul> + {values.map((v, i) => { + return ( + <li key={i} class={value === v ? "is-active" : ""} + onClick={(e) => { onChange(v) }} + > + <a style={{ cursor: "initial" }}> + <span>{toStr(v)}</span> + </a> + </li> + ); + })} + </ul> + </div> + {help} + </p> + {required && ( + <span class="icon has-text-danger is-right" style={{ height: "2.5em" }}> + <i class="mdi mdi-alert" /> + </span> + )} + {error && <p class="help is-danger">{error}</p>} + </div> + </div> + </div> + ); +} 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<LoginToken>() .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<string>] { 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<void>; @@ -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`} /> - <Input - name="template_contract.summary" - inputType="multiline" - label={i18n.str`Fixed summary`} - tooltip={i18n.str`If specified, this template will create order with the same summary`} - /> - <InputCurrency - name="template_contract.amount" - label={i18n.str`Fixed price`} - tooltip={i18n.str`If specified, this template will create order with the same price`} + <InputTab + name="type" + label={i18n.str`Type`} + help={(() => { + 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 ? + <Input + name="template_contract.summary" + inputType="multiline" + label={i18n.str`Fixed summary`} + tooltip={i18n.str`If specified, this template will create order with the same summary`} + /> + : undefined} + {state.type === Steps.BOTH_FIXED || state.type === Steps.FIXED_PRICE ? + <InputCurrency + name="template_contract.amount" + label={i18n.str`Fixed price`} + tooltip={i18n.str`If specified, this template will create order with the same price`} + /> + : undefined} <InputNumber name="template_contract.minimum_age" label={i18n.str`Minimum age`} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx index 82b74e1fa..b578d4664 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx @@ -39,6 +39,14 @@ import { InputWithAddon } from "../../../../components/form/InputWithAddon.js"; import { useBackendContext } from "../../../../context/backend.js"; import { MerchantBackend, WithId } from "../../../../declaration.js"; import { undefinedIfEmpty } from "../../../../utils/table.js"; +import { InputTab } from "../../../../components/form/InputTab.js"; + +enum Steps { + BOTH_FIXED, + FIXED_PRICE, + FIXED_SUMMARY, + NON_FIXED, +} type Entity = MerchantBackend.Template.TemplatePatchDetails & WithId; @@ -52,7 +60,16 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode { const { i18n } = useTranslationContext(); const { url: backendURL } = useBackendContext() - const [state, setState] = useState<Partial<Entity>>(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<Partial<Entity & { type: Steps }>>({ ...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`} /> - <Input - name="template_contract.summary" - inputType="multiline" - label={i18n.str`Fixed summary`} - tooltip={i18n.str`If specified, this template will create order with the same summary`} - /> - <InputCurrency - name="template_contract.amount" - label={i18n.str`Fixed price`} - tooltip={i18n.str`If specified, this template will create order with the same price`} + <InputTab + name="type" + label={i18n.str`Type`} + help={(() => { + 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 ? + <Input + name="template_contract.summary" + inputType="multiline" + label={i18n.str`Fixed summary`} + tooltip={i18n.str`If specified, this template will create order with the same summary`} + /> + : undefined} + {state.type === Steps.BOTH_FIXED || state.type === Steps.FIXED_PRICE ? + <InputCurrency + name="template_contract.amount" + label={i18n.str`Fixed price`} + tooltip={i18n.str`If specified, this template will create order with the same price`} + /> + : undefined} <InputNumber name="template_contract.minimum_age" label={i18n.str`Minimum age`} |