aboutsummaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2023-10-24 09:41:53 -0300
committerSebastian <sebasjm@gmail.com>2023-10-24 09:41:53 -0300
commit21fda5751d61594ee94d995232f0ce66647533b2 (patch)
tree395b864460406087030980aec674ad10d794ba69 /packages
parent58c0b115175ffc963e394258ec888b2be7ad3d74 (diff)
downloadwallet-core-21fda5751d61594ee94d995232f0ce66647533b2.tar.xz
better template UI
Diffstat (limited to 'packages')
-rw-r--r--packages/demobank-ui/src/hooks/backend.ts2
-rw-r--r--packages/demobank-ui/src/hooks/settings.ts2
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputTab.tsx90
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/index.ts4
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx92
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx100
6 files changed, 252 insertions, 38 deletions
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 <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`}