aboutsummaryrefslogtreecommitdiff
path: root/packages/web-util/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/web-util/src')
-rw-r--r--packages/web-util/src/components/Time.tsx80
-rw-r--r--packages/web-util/src/components/index.ts1
-rw-r--r--packages/web-util/src/context/api.ts15
-rw-r--r--packages/web-util/src/context/bank-api.ts18
-rw-r--r--packages/web-util/src/context/challenger-api.ts6
-rw-r--r--packages/web-util/src/context/exchange-api.ts6
-rw-r--r--packages/web-util/src/context/merchant-api.ts18
-rw-r--r--packages/web-util/src/context/navigation.ts3
-rw-r--r--packages/web-util/src/forms/Caption.tsx2
-rw-r--r--packages/web-util/src/forms/DefaultForm.tsx41
-rw-r--r--packages/web-util/src/forms/FormProvider.tsx2
-rw-r--r--packages/web-util/src/forms/InputAbsoluteTime.stories.tsx40
-rw-r--r--packages/web-util/src/forms/InputAmount.stories.tsx39
-rw-r--r--packages/web-util/src/forms/InputArray.stories.tsx75
-rw-r--r--packages/web-util/src/forms/InputArray.tsx192
-rw-r--r--packages/web-util/src/forms/InputChoiceHorizontal.stories.tsx62
-rw-r--r--packages/web-util/src/forms/InputChoiceStacked.stories.tsx62
-rw-r--r--packages/web-util/src/forms/InputFile.stories.tsx48
-rw-r--r--packages/web-util/src/forms/InputInteger.stories.tsx41
-rw-r--r--packages/web-util/src/forms/InputLine.stories.tsx38
-rw-r--r--packages/web-util/src/forms/InputLine.tsx24
-rw-r--r--packages/web-util/src/forms/InputSelectMultiple.stories.tsx101
-rw-r--r--packages/web-util/src/forms/InputSelectOne.stories.tsx64
-rw-r--r--packages/web-util/src/forms/InputText.stories.tsx38
-rw-r--r--packages/web-util/src/forms/InputTextArea.stories.tsx38
-rw-r--r--packages/web-util/src/forms/InputToggle.stories.tsx38
-rw-r--r--packages/web-util/src/forms/forms.ts58
-rw-r--r--packages/web-util/src/forms/ui-form.ts11
-rw-r--r--packages/web-util/src/hooks/useNotifications.ts2
-rw-r--r--packages/web-util/src/index.browser.ts3
-rw-r--r--packages/web-util/src/index.build.ts24
-rw-r--r--packages/web-util/src/index.html41
-rw-r--r--packages/web-util/src/live-reload.ts22
-rw-r--r--packages/web-util/src/serve.ts32
-rw-r--r--packages/web-util/src/stories-utils.tsx578
-rw-r--r--packages/web-util/src/stories.tsx567
-rw-r--r--packages/web-util/src/utils/base64.ts75
-rw-r--r--packages/web-util/src/utils/http-impl.sw.ts137
-rw-r--r--packages/web-util/src/utils/request.ts19
-rw-r--r--packages/web-util/src/utils/route.ts33
40 files changed, 1586 insertions, 1108 deletions
diff --git a/packages/web-util/src/components/Time.tsx b/packages/web-util/src/components/Time.tsx
new file mode 100644
index 000000000..9057b34f0
--- /dev/null
+++ b/packages/web-util/src/components/Time.tsx
@@ -0,0 +1,80 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 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/>
+ */
+
+import { AbsoluteTime, Duration } from "@gnu-taler/taler-util";
+import {
+ formatISO,
+ format,
+ formatDuration,
+ intervalToDuration,
+} from "date-fns";
+import { Fragment, h, VNode } from "preact";
+import { useTranslationContext } from "../index.browser.js";
+
+/**
+ *
+ * @param timestamp time to be formatted
+ * @param relative duration threshold, if the difference is lower
+ * the timestamp will be formatted as relative time from "now"
+ *
+ * @returns
+ */
+export function Time({
+ timestamp,
+ relative,
+ format: formatString,
+}: {
+ timestamp: AbsoluteTime | undefined;
+ relative?: Duration;
+ format: string;
+}): VNode {
+ const { i18n, dateLocale } = useTranslationContext();
+ if (!timestamp) return <Fragment />;
+
+ if (timestamp.t_ms === "never") {
+ return <time>{i18n.str`never`}</time>;
+ }
+
+ const now = AbsoluteTime.now();
+ const diff = AbsoluteTime.difference(now, timestamp);
+ if (relative && now.t_ms !== "never" && Duration.cmp(diff, relative) === -1) {
+ const d = intervalToDuration({
+ start: now.t_ms,
+ end: timestamp.t_ms,
+ });
+ d.seconds = 0;
+ const duration = formatDuration(d, { locale: dateLocale });
+ const isFuture = AbsoluteTime.cmp(now, timestamp) < 0;
+ if (isFuture) {
+ return (
+ <time dateTime={formatISO(timestamp.t_ms)}>
+ <i18n.Translate>in {duration}</i18n.Translate>
+ </time>
+ );
+ } else {
+ return (
+ <time dateTime={formatISO(timestamp.t_ms)}>
+ <i18n.Translate>{duration} ago</i18n.Translate>
+ </time>
+ );
+ }
+ }
+ return (
+ <time dateTime={formatISO(timestamp.t_ms)}>
+ {format(timestamp.t_ms, formatString, { locale: dateLocale })}
+ </time>
+ );
+}
diff --git a/packages/web-util/src/components/index.ts b/packages/web-util/src/components/index.ts
index d7ea41874..63231f8a2 100644
--- a/packages/web-util/src/components/index.ts
+++ b/packages/web-util/src/components/index.ts
@@ -10,3 +10,4 @@ export * from "./Button.js";
export * from "./ShowInputErrorLabel.js";
export * from "./NotificationBanner.js";
export * from "./ToastBanner.js";
+export * from "./Time.js";
diff --git a/packages/web-util/src/context/api.ts b/packages/web-util/src/context/api.ts
index c1eaa37f8..89561e239 100644
--- a/packages/web-util/src/context/api.ts
+++ b/packages/web-util/src/context/api.ts
@@ -19,7 +19,12 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { TalerBankIntegrationHttpClient, TalerCoreBankHttpClient, TalerRevenueHttpClient, TalerWireGatewayHttpClient } from "@gnu-taler/taler-util";
+import {
+ TalerBankIntegrationHttpClient,
+ TalerCoreBankHttpClient,
+ TalerRevenueHttpClient,
+ TalerWireGatewayHttpClient,
+} from "@gnu-taler/taler-util";
import { ComponentChildren, createContext, h, VNode } from "preact";
import { useContext } from "preact/hooks";
import { defaultRequestHandler } from "../utils/request.js";
@@ -29,10 +34,10 @@ interface Type {
* @deprecated this show not be used
*/
request: typeof defaultRequestHandler;
- bankCore: TalerCoreBankHttpClient,
- bankIntegration: TalerBankIntegrationHttpClient,
- bankWire: TalerWireGatewayHttpClient,
- bankRevenue: TalerRevenueHttpClient,
+ bankCore: TalerCoreBankHttpClient;
+ bankIntegration: TalerBankIntegrationHttpClient;
+ bankWire: TalerWireGatewayHttpClient;
+ bankRevenue: TalerRevenueHttpClient;
}
const Context = createContext<Type>({ request: defaultRequestHandler } as any);
diff --git a/packages/web-util/src/context/bank-api.ts b/packages/web-util/src/context/bank-api.ts
index 3f6a32f4b..e610b49e0 100644
--- a/packages/web-util/src/context/bank-api.ts
+++ b/packages/web-util/src/context/bank-api.ts
@@ -16,6 +16,7 @@
import {
CacheEvictor,
+ TalerCorebankConfigResponse,
LibtoolVersion,
ObservabilityEvent,
ObservableHttpClientLibrary,
@@ -24,7 +25,6 @@ import {
TalerBankConversionHttpClient,
TalerCoreBankCacheEviction,
TalerCoreBankHttpClient,
- TalerCorebankApi,
TalerError,
} from "@gnu-taler/taler-util";
import {
@@ -35,9 +35,9 @@ import {
h,
} from "preact";
import { useContext, useEffect, useState } from "preact/hooks";
+import { BrowserFetchHttpLib, ErrorLoading } from "../index.browser.js";
import { APIClient, ActiviyTracker, BankLib, Subscriber } from "./activity.js";
import { useTranslationContext } from "./translation.js";
-import { BrowserFetchHttpLib, ErrorLoading } from "../index.browser.js";
/**
*
@@ -46,7 +46,7 @@ import { BrowserFetchHttpLib, ErrorLoading } from "../index.browser.js";
export type BankContextType = {
url: URL;
- config: TalerCorebankApi.Config;
+ config: TalerCorebankConfigResponse;
lib: BankLib;
hints: VersionHint[];
onActivity: Subscriber<ObservabilityEvent>;
@@ -88,7 +88,7 @@ export const BankApiProvider = ({
frameOnError: FunctionComponent<{ children: ComponentChildren }>;
}): VNode => {
const [checked, setChecked] =
- useState<ConfigResult<TalerCorebankApi.Config>>();
+ useState<ConfigResult<TalerCorebankConfigResponse>>();
const { i18n } = useTranslationContext();
const { getRemoteConfig, VERSION, lib, cancelRequest, onActivity } =
@@ -165,7 +165,7 @@ export const BankApiProvider = ({
function buildBankApiClient(
url: URL,
evictors: Evictors,
-): APIClient<BankLib, TalerCorebankApi.Config> {
+): APIClient<BankLib, TalerCorebankConfigResponse> {
const httpFetch = new BrowserFetchHttpLib({
enableThrottling: true,
requireTls: false,
@@ -189,10 +189,14 @@ function buildBankApiClient(
httpLib,
);
- async function getRemoteConfig(): Promise<TalerCorebankApi.Config> {
+ async function getRemoteConfig(): Promise<TalerCorebankConfigResponse> {
const resp = await bank.getConfig();
if (resp.type === "fail") {
- throw TalerError.fromUncheckedDetail(resp.detail);
+ if (resp.detail) {
+ throw TalerError.fromUncheckedDetail(resp.detail);
+ } else {
+ throw TalerError.fromException(new Error("failed to get bank remote config"))
+ }
}
return resp.body;
}
diff --git a/packages/web-util/src/context/challenger-api.ts b/packages/web-util/src/context/challenger-api.ts
index 8748f5f69..e2a6e05c3 100644
--- a/packages/web-util/src/context/challenger-api.ts
+++ b/packages/web-util/src/context/challenger-api.ts
@@ -183,7 +183,11 @@ function buildChallengerApiClient(
async function getRemoteConfig(): Promise<ChallengerApi.ChallengerTermsOfServiceResponse> {
const resp = await challenger.getConfig();
if (resp.type === "fail") {
- throw TalerError.fromUncheckedDetail(resp.detail);
+ if (resp.detail) {
+ throw TalerError.fromUncheckedDetail(resp.detail);
+ } else {
+ throw TalerError.fromException(new Error("failed to get challenger remote config"))
+ }
}
return resp.body;
}
diff --git a/packages/web-util/src/context/exchange-api.ts b/packages/web-util/src/context/exchange-api.ts
index 39f889ba9..967b042f9 100644
--- a/packages/web-util/src/context/exchange-api.ts
+++ b/packages/web-util/src/context/exchange-api.ts
@@ -187,7 +187,11 @@ function buildExchangeApiClient(
async function getRemoteConfig(): Promise<TalerExchangeApi.ExchangeVersionResponse> {
const resp = await ex.getConfig();
if (resp.type === "fail") {
- throw TalerError.fromUncheckedDetail(resp.detail);
+ if (resp.detail) {
+ throw TalerError.fromUncheckedDetail(resp.detail);
+ } else {
+ throw TalerError.fromException(new Error("failed to get exchange remote config"))
+ }
}
return resp.body;
}
diff --git a/packages/web-util/src/context/merchant-api.ts b/packages/web-util/src/context/merchant-api.ts
index 03c95d48e..8d929ae12 100644
--- a/packages/web-util/src/context/merchant-api.ts
+++ b/packages/web-util/src/context/merchant-api.ts
@@ -49,7 +49,7 @@ import {
export type MerchantContextType = {
url: URL;
- config: TalerMerchantApi.VersionResponse;
+ config: TalerMerchantApi.TalerMerchantConfigResponse;
lib: MerchantLib;
hints: VersionHint[];
onActivity: Subscriber<ObservabilityEvent>;
@@ -95,11 +95,13 @@ export const MerchantApiProvider = ({
evictors?: Evictors;
children: ComponentChildren;
frameOnError: FunctionComponent<{
- state: ConfigResultFail<TalerMerchantApi.VersionResponse> | undefined;
+ state:
+ | ConfigResultFail<TalerMerchantApi.TalerMerchantConfigResponse>
+ | undefined;
}>;
}): VNode => {
const [checked, setChecked] =
- useState<ConfigResult<TalerMerchantApi.VersionResponse>>();
+ useState<ConfigResult<TalerMerchantApi.TalerMerchantConfigResponse>>();
const [merchantEndpoint, changeMerchantEndpoint] = useState(baseUrl);
@@ -162,7 +164,7 @@ export const MerchantApiProvider = ({
function buildMerchantApiClient(
url: URL,
evictors: Evictors,
-): APIClient<MerchantLib, TalerMerchantApi.VersionResponse> {
+): APIClient<MerchantLib, TalerMerchantApi.TalerMerchantConfigResponse> {
const httpFetch = new BrowserFetchHttpLib({
enableThrottling: true,
requireTls: false,
@@ -193,10 +195,14 @@ function buildMerchantApiClient(
return api.lib;
}
- async function getRemoteConfig(): Promise<TalerMerchantApi.VersionResponse> {
+ async function getRemoteConfig(): Promise<TalerMerchantApi.TalerMerchantConfigResponse> {
const resp = await instance.getConfig();
if (resp.type === "fail") {
- throw TalerError.fromUncheckedDetail(resp.detail);
+ if (resp.detail) {
+ throw TalerError.fromUncheckedDetail(resp.detail);
+ } else {
+ throw TalerError.fromException(new Error("failed to get merchant remote config"))
+ }
}
return resp.body;
}
diff --git a/packages/web-util/src/context/navigation.ts b/packages/web-util/src/context/navigation.ts
index c2f2bbbc1..bd756318b 100644
--- a/packages/web-util/src/context/navigation.ts
+++ b/packages/web-util/src/context/navigation.ts
@@ -22,6 +22,7 @@ import {
Location,
findMatch,
RouteDefinition,
+ LocationNotFound,
} from "../utils/route.js";
/**
@@ -44,7 +45,7 @@ export const useNavigationContext = (): Type => useContext(Context);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function useCurrentLocation<T extends ObjectOf<RouteDefinition<any>>>(
pagesMap: T,
-): Location<T> | undefined {
+): Location<T> | LocationNotFound<T> {
const pageList = Object.keys(pagesMap as object) as Array<keyof T>;
const { path, params } = useNavigationContext();
diff --git a/packages/web-util/src/forms/Caption.tsx b/packages/web-util/src/forms/Caption.tsx
index be4725ffa..0a4de5a4c 100644
--- a/packages/web-util/src/forms/Caption.tsx
+++ b/packages/web-util/src/forms/Caption.tsx
@@ -13,7 +13,7 @@ interface Props {
export function Caption({ before, after, label, tooltip, help }: Props): VNode {
return (
- <div class="sm:col-span-6 flex">
+ <div class="sm:col-span-6">
{before !== undefined && <RenderAddon addon={before} />}
<LabelWithTooltipMaybeRequired label={label} tooltip={tooltip} />
{after !== undefined && <RenderAddon addon={after} />}
diff --git a/packages/web-util/src/forms/DefaultForm.tsx b/packages/web-util/src/forms/DefaultForm.tsx
index 338460170..239577e24 100644
--- a/packages/web-util/src/forms/DefaultForm.tsx
+++ b/packages/web-util/src/forms/DefaultForm.tsx
@@ -1,12 +1,21 @@
+import { TranslatedString } from "@gnu-taler/taler-util";
import { Fragment, VNode, h } from "preact";
+import {
+ UIFormElementConfig,
+ getConverterById,
+ useTranslationContext,
+} from "../index.browser.js";
import { FormProvider, FormProviderProps, FormState } from "./FormProvider.js";
-import { RenderAllFieldsByUiConfig, UIFormField } from "./forms.js";
-import { TranslatedString } from "@gnu-taler/taler-util";
+import {
+ RenderAllFieldsByUiConfig,
+ UIFormField,
+ convertUiField,
+} from "./forms.js";
// import { FlexibleForm } from "./ui-form.js";
/**
* Flexible form uses a DoubleColumForm for design
- * and may have a dynamic properties defined by
+ * and may have a dynamic properties defined by
* behavior function.
*/
export interface FlexibleForm_Deprecated<T extends object> {
@@ -16,17 +25,19 @@ export interface FlexibleForm_Deprecated<T extends object> {
/**
* Double column form
- *
+ *
* Form with sections, every sections have a title and may
* have a description.
* Every sections contain a set of fields.
*/
-export type DoubleColumnForm_Deprecated = Array<DoubleColumnFormSection_Deprecated | undefined>;
+export type DoubleColumnForm_Deprecated = Array<
+ DoubleColumnFormSection_Deprecated | undefined
+>;
export type DoubleColumnFormSection_Deprecated = {
title: TranslatedString;
description?: TranslatedString;
- fields: UIFormField[];
+ fields: UIFormElementConfig[];
};
/**
@@ -40,20 +51,25 @@ export function DefaultForm<T extends object>({
onSubmit,
children,
readOnly,
-}: Omit<FormProviderProps<T>, "computeFormState"> & { form: FlexibleForm_Deprecated<T> }): VNode {
+}: Omit<FormProviderProps<T>, "computeFormState"> & {
+ form: FlexibleForm_Deprecated<T>;
+}): VNode {
+ const { i18n } = useTranslationContext();
return (
<FormProvider
initial={initial}
onUpdate={onUpdate}
onSubmit={onSubmit}
readOnly={readOnly}
- // computeFormState={form.behavior}
>
<div class="space-y-10 divide-y -mt-5 divide-gray-900/10">
{form.design.map((section, i) => {
if (!section) return <Fragment />;
return (
- <div key={i} class="grid grid-cols-1 gap-x-8 gap-y-8 pt-5 md:grid-cols-3">
+ <div
+ key={i}
+ class="grid grid-cols-1 gap-x-8 gap-y-8 pt-5 md:grid-cols-3"
+ >
<div class="px-4 sm:px-0">
<h2 class="text-base font-semibold leading-7 text-gray-900">
{section.title}
@@ -69,7 +85,12 @@ export function DefaultForm<T extends object>({
<div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
<RenderAllFieldsByUiConfig
key={i}
- fields={section.fields}
+ fields={convertUiField(
+ i18n,
+ section.fields,
+ form,
+ getConverterById,
+ )}
/>
</div>
</div>
diff --git a/packages/web-util/src/forms/FormProvider.tsx b/packages/web-util/src/forms/FormProvider.tsx
index 5e08efb32..fe886030a 100644
--- a/packages/web-util/src/forms/FormProvider.tsx
+++ b/packages/web-util/src/forms/FormProvider.tsx
@@ -14,7 +14,7 @@ export interface FormType<T extends object> {
computeFormState?: (v: Partial<T>) => FormState<T>;
}
-export const FormContext = createContext<FormType<any>| undefined>(undefined);
+export const FormContext = createContext<FormType<any> | undefined>(undefined);
/**
* Map of {[field]:FieldUIOptions}
diff --git a/packages/web-util/src/forms/InputAbsoluteTime.stories.tsx b/packages/web-util/src/forms/InputAbsoluteTime.stories.tsx
index 6b792bfee..858349a00 100644
--- a/packages/web-util/src/forms/InputAbsoluteTime.stories.tsx
+++ b/packages/web-util/src/forms/InputAbsoluteTime.stories.tsx
@@ -20,11 +20,12 @@
*/
import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util";
-import * as tests from "@gnu-taler/web-util/testing";
+import * as tests from "../tests/hook.js";
import {
FlexibleForm_Deprecated,
DefaultForm as TestedComponent,
} from "./DefaultForm.js";
+import { UIHandlerId } from "./ui-form.js";
export default {
title: "Input Absolute Time",
@@ -38,23 +39,28 @@ export namespace Simplest {
type TargetObject = {
today: AbsoluteTime;
-}
+};
const initial: TargetObject = {
- today: AbsoluteTime.now()
-}
+ today: AbsoluteTime.now(),
+};
const form: FlexibleForm_Deprecated<TargetObject> = {
- design: [{
- title: "this is a simple form" as TranslatedString,
- fields: [{
- type: "absoluteTimeText",
- properties: {
- label: "label of the field" as TranslatedString,
- name: "today",
- pattern: "dd/MM/yyyy HH:mm"
- },
- }]
- }]
-}
+ design: [
+ {
+ title: "this is a simple form" as TranslatedString,
+ fields: [
+ {
+ type: "absoluteTimeText",
+ label: "label of the field" as TranslatedString,
+ id: "today" as UIHandlerId,
+ pattern: "dd/MM/yyyy HH:mm",
+ },
+ ],
+ },
+ ],
+};
-export const SimpleComment = tests.createExample(TestedComponent, { initial, form });
+export const SimpleComment = tests.createExample(TestedComponent, {
+ initial,
+ form,
+});
diff --git a/packages/web-util/src/forms/InputAmount.stories.tsx b/packages/web-util/src/forms/InputAmount.stories.tsx
index f05887515..4351a9655 100644
--- a/packages/web-util/src/forms/InputAmount.stories.tsx
+++ b/packages/web-util/src/forms/InputAmount.stories.tsx
@@ -20,11 +20,12 @@
*/
import { AmountJson, Amounts, TranslatedString } from "@gnu-taler/taler-util";
-import * as tests from "@gnu-taler/web-util/testing";
+import * as tests from "../tests/hook.js";
import {
FlexibleForm_Deprecated,
DefaultForm as TestedComponent,
} from "./DefaultForm.js";
+import { UIHandlerId } from "./ui-form.js";
export default {
title: "Input Amount",
@@ -38,22 +39,28 @@ export namespace Simplest {
type TargetObject = {
amount: AmountJson;
-}
+};
const initial: TargetObject = {
- amount: Amounts.parseOrThrow("USD:10")
-}
+ amount: Amounts.parseOrThrow("USD:10"),
+};
const form: FlexibleForm_Deprecated<TargetObject> = {
- design: [{
- title: "this is a simple form" as TranslatedString,
- fields: [{
- type: "amount",
- properties: {
- label: "label of the field" as TranslatedString,
- name: "amount",
- },
- }]
- }]
-}
+ design: [
+ {
+ title: "this is a simple form" as TranslatedString,
+ fields: [
+ {
+ type: "amount",
+ label: "label of the field" as TranslatedString,
+ id: "amount" as UIHandlerId,
+ currency: "ARS",
+ },
+ ],
+ },
+ ],
+};
-export const SimpleComment = tests.createExample(TestedComponent, { initial, form });
+export const SimpleComment = tests.createExample(TestedComponent, {
+ initial,
+ form,
+});
diff --git a/packages/web-util/src/forms/InputArray.stories.tsx b/packages/web-util/src/forms/InputArray.stories.tsx
index 143e73f02..fc6889189 100644
--- a/packages/web-util/src/forms/InputArray.stories.tsx
+++ b/packages/web-util/src/forms/InputArray.stories.tsx
@@ -20,11 +20,12 @@
*/
import { TranslatedString } from "@gnu-taler/taler-util";
-import * as tests from "@gnu-taler/web-util/testing";
+import * as tests from "../tests/hook.js";
import {
FlexibleForm_Deprecated,
DefaultForm as TestedComponent,
} from "./DefaultForm.js";
+import { UIHandlerId } from "./ui-form.js";
export default {
title: "Input Array",
@@ -41,39 +42,47 @@ type TargetObject = {
name: string;
age: number;
}[];
-}
+};
const initial: TargetObject = {
- people: [{
- name: "me",
- age: 17,
- }]
-}
+ people: [
+ {
+ name: "me",
+ age: 17,
+ },
+ ],
+};
const form: FlexibleForm_Deprecated<TargetObject> = {
- design: [{
- title: "this is a simple form" as TranslatedString,
- fields: [{
- type: "array",
- properties: {
- label: "People" as TranslatedString,
- name: "comment",
- fields: [{
- type: "text",
- properties: {
- label: "the name" as TranslatedString,
- name: "name",
- }
- }, {
- type: "integer",
- properties: {
- label: "the age" as TranslatedString,
- name: "age",
- }
- }],
- labelField: "name"
- },
- }]
- }]
-}
+ design: [
+ {
+ title: "this is a simple form" as TranslatedString,
+ description: "to test how arrays are used" as TranslatedString,
+ fields: [
+ {
+ type: "array",
+ label: "People" as TranslatedString,
+ fields: [ {
+ id: "name" as UIHandlerId,
+ type: "text",
+ required: true,
+ label: "Name" as TranslatedString,
+ },
+ {
+ id: "age" as UIHandlerId,
+ type: "integer",
+ required: true,
+ label: "Age" as TranslatedString,
+ },
+ ],
+ id: "name" as UIHandlerId,
+ labelFieldId: "name" as UIHandlerId,
+ },
+ ],
+ },
+ ],
+};
-export const SimpleComment = tests.createExample(TestedComponent, { initial, form });
+export const SimpleComment = tests.createExample(TestedComponent, {
+ initial,
+ form,
+});
diff --git a/packages/web-util/src/forms/InputArray.tsx b/packages/web-util/src/forms/InputArray.tsx
index d90028508..6b14a65b7 100644
--- a/packages/web-util/src/forms/InputArray.tsx
+++ b/packages/web-util/src/forms/InputArray.tsx
@@ -96,10 +96,12 @@ export function InputArray<T extends object, K extends keyof T>(
props.handler ?? fieldCtx ?? noHandlerPropsAndNoContextForField(props.name);
const list = (value ?? []) as Array<Record<string, string | undefined>>;
- const [selectedIndex, setSelected] = useState<number | undefined>(undefined);
+ const [selectedIndex, setSelectedIndex] = useState<number | undefined>(
+ undefined,
+ );
const selected =
selectedIndex === undefined ? undefined : list[selectedIndex];
-
+
return (
<div class="sm:col-span-6">
<LabelWithTooltipMaybeRequired
@@ -108,104 +110,112 @@ export function InputArray<T extends object, K extends keyof T>(
tooltip={tooltip}
/>
- <div class="-space-y-px rounded-md bg-white ">
- {list.map((v, idx) => {
- const label = getValueDeeper(v, labelField.split("."))
- return (
- <Option
- label={label as TranslatedString}
- key={idx}
- isSelected={selectedIndex === idx}
- isLast={idx === list.length - 1}
- disabled={selectedIndex !== undefined && selectedIndex !== idx}
- isFirst={idx === 0}
+ <div class="overflow-hidden ring-1 ring-gray-900/5 rounded-xl p-4">
+ <div class="-space-y-px rounded-md bg-white ">
+ {list.map((v, idx) => {
+ const label =
+ getValueDeeper(v, labelField.split(".")) ?? "<<incomplete>>";
+ return (
+ <Option
+ label={label as TranslatedString}
+ key={idx}
+ isSelected={selectedIndex === idx}
+ isLast={idx === list.length - 1}
+ disabled={selectedIndex !== undefined && selectedIndex !== idx}
+ isFirst={idx === 0}
+ onClick={() => {
+ setSelectedIndex(selectedIndex === idx ? undefined : idx);
+ }}
+ />
+ );
+ })}
+ {!state.disabled && (
+ <div class="pt-2">
+ <Option
+ label={"Add new..." as TranslatedString}
+ isSelected={selectedIndex === list.length}
+ isLast
+ isFirst
+ disabled={
+ selectedIndex !== undefined && selectedIndex !== list.length
+ }
+ onClick={() => {
+ setSelectedIndex(
+ selectedIndex === list.length ? undefined : list.length,
+ );
+ }}
+ />
+ </div>
+ )}
+ </div>
+ {selectedIndex !== undefined && (
+ /**
+ * This form provider act as a substate of the parent form
+ * Consider creating an InnerFormProvider since not every feature is expected
+ */
+ <FormProvider
+ initial={selected}
+ readOnly={state.disabled}
+ computeFormState={(v) => {
+ // current state is ignored
+ // the state is defined by the parent form
+
+ // elements should be present in the state object since this is expected to be an array
+ //@ts-ignore
+ // return state.elements[selectedIndex];
+ return {};
+ }}
+ onSubmit={(v) => {
+ const newValue = [...list];
+ newValue.splice(selectedIndex, 1, v);
+ onChange(newValue as any);
+ setSelectedIndex(undefined);
+ }}
+ onUpdate={(v) => {
+ const newValue = [...list];
+ newValue.splice(selectedIndex, 1, v);
+ onChange(newValue as any);
+ }}
+ >
+ <div class="px-4 py-6">
+ <div class="grid grid-cols-1 gap-y-8 ">
+ <RenderAllFieldsByUiConfig fields={fields} />
+ </div>
+ </div>
+ </FormProvider>
+ )}
+ {selectedIndex !== undefined && (
+ <div class="flex items-center justify-end gap-x-6">
+ <button
+ type="button"
onClick={() => {
- setSelected(selectedIndex === idx ? undefined : idx);
+ setSelectedIndex(undefined);
}}
- />
- );
- })}
- {!state.disabled && (
- <div class="pt-2">
- <Option
- label={"Add..." as TranslatedString}
- isSelected={selectedIndex === list.length}
- isLast
- isFirst
- disabled={
- selectedIndex !== undefined && selectedIndex !== list.length
- }
+ class="block px-3 py-2 text-sm font-semibold leading-6 text-gray-900"
+ >
+ Close
+ </button>
+
+ <button
+ type="button"
+ disabled={selected !== undefined}
onClick={() => {
- setSelected(
- selectedIndex === list.length ? undefined : list.length,
- );
+ const newValue = [...list];
+ newValue.splice(selectedIndex, 1);
+ onChange(newValue as any);
+ setSelectedIndex(undefined);
}}
- />
+ class="block rounded-md bg-red-600 px-3 py-2 text-center text-sm text-white shadow-sm hover:bg-red-500 "
+ >
+ Remove
+ </button>
</div>
)}
</div>
- {selectedIndex !== undefined && (
- /**
- * This form provider act as a substate of the parent form
- * Consider creating an InnerFormProvider since not every feature is expected
- */
- <FormProvider
- initial={selected}
- readOnly={state.disabled}
- computeFormState={(v) => {
- // current state is ignored
- // the state is defined by the parent form
-
- // elements should be present in the state object since this is expected to be an array
- //@ts-ignore
- // return state.elements[selectedIndex];
- return {};
- }}
- onSubmit={(v) => {
- const newValue = [...list];
- newValue.splice(selectedIndex, 1, v);
- onChange(newValue as any);
- setSelected(undefined);
- }}
- onUpdate={(v) => {
- const newValue = [...list];
- newValue.splice(selectedIndex, 1, v);
- onChange(newValue as any);
- }}
- >
- <div class="px-4 py-6">
- <div class="grid grid-cols-1 gap-y-8 ">
- <RenderAllFieldsByUiConfig fields={fields} />
- </div>
- </div>
- </FormProvider>
- )}
- {selectedIndex !== undefined && (
- <div class="flex items-center pt-3">
- <div class="flex-auto">
- {selected !== undefined && (
- <button
- type="button"
- onClick={() => {
- const newValue = [...list];
- newValue.splice(selectedIndex, 1);
- onChange(newValue as any);
- setSelected(undefined);
- }}
- class="block rounded-md bg-red-600 px-3 py-2 text-center text-sm text-white shadow-sm hover:bg-red-500 "
- >
- Remove
- </button>
- )}
- </div>
- </div>
- )}
</div>
);
}
-
-
export function getValueDeeper(
object: Record<string, any>,
names: string[],
@@ -218,9 +228,7 @@ export function getValueDeeper(
return getValueDeeper(object, rest);
}
if (object === undefined) {
- return ""
+ return "";
}
return getValueDeeper(object[head], rest);
}
-
-
diff --git a/packages/web-util/src/forms/InputChoiceHorizontal.stories.tsx b/packages/web-util/src/forms/InputChoiceHorizontal.stories.tsx
index 786dfe5bc..a00bcd6a1 100644
--- a/packages/web-util/src/forms/InputChoiceHorizontal.stories.tsx
+++ b/packages/web-util/src/forms/InputChoiceHorizontal.stories.tsx
@@ -20,11 +20,12 @@
*/
import { TranslatedString } from "@gnu-taler/taler-util";
-import * as tests from "@gnu-taler/web-util/testing";
+import * as tests from "../tests/hook.js";
import {
FlexibleForm_Deprecated,
DefaultForm as TestedComponent,
} from "./DefaultForm.js";
+import { UIHandlerId } from "./ui-form.js";
export default {
title: "Input Choice Horizontal",
@@ -38,32 +39,41 @@ export namespace Simplest {
type TargetObject = {
comment: string;
-}
+};
const initial: TargetObject = {
- comment: "0"
-}
+ comment: "0",
+};
const form: FlexibleForm_Deprecated<TargetObject> = {
- design: [{
- title: "this is a simple form" as TranslatedString,
- fields: [{
- type: "choiceHorizontal",
- properties: {
- label: "label of the field" as TranslatedString,
- name: "comment",
- choices: [{
- label: "first choice" as TranslatedString,
- value: "1"
- }, {
- label: "second choice" as TranslatedString,
- value: "2"
- }, {
- label: "third choice" as TranslatedString,
- value: "3"
- },],
- },
- }]
- }]
-}
+ design: [
+ {
+ title: "this is a simple form" as TranslatedString,
+ fields: [
+ {
+ type: "choiceHorizontal",
+ label: "label of the field" as TranslatedString,
+ id: "comment" as UIHandlerId,
+ choices: [
+ {
+ label: "first choice" as TranslatedString,
+ value: "1",
+ },
+ {
+ label: "second choice" as TranslatedString,
+ value: "2",
+ },
+ {
+ label: "third choice" as TranslatedString,
+ value: "3",
+ },
+ ],
+ },
+ ],
+ },
+ ],
+};
-export const SimpleComment = tests.createExample(TestedComponent, { initial, form });
+export const SimpleComment = tests.createExample(TestedComponent, {
+ initial,
+ form,
+});
diff --git a/packages/web-util/src/forms/InputChoiceStacked.stories.tsx b/packages/web-util/src/forms/InputChoiceStacked.stories.tsx
index 9a634d05c..6e6a1a126 100644
--- a/packages/web-util/src/forms/InputChoiceStacked.stories.tsx
+++ b/packages/web-util/src/forms/InputChoiceStacked.stories.tsx
@@ -20,11 +20,12 @@
*/
import { TranslatedString } from "@gnu-taler/taler-util";
-import * as tests from "@gnu-taler/web-util/testing";
+import * as tests from "../tests/hook.js";
import {
FlexibleForm_Deprecated,
DefaultForm as TestedComponent,
} from "./DefaultForm.js";
+import { UIHandlerId } from "./ui-form.js";
export default {
title: "Input Choice Stacked",
@@ -38,32 +39,41 @@ export namespace Simplest {
type TargetObject = {
comment: string;
-}
+};
const initial: TargetObject = {
- comment: "some initial comment"
-}
+ comment: "some initial comment",
+};
const form: FlexibleForm_Deprecated<TargetObject> = {
- design: [{
- title: "this is a simple form" as TranslatedString,
- fields: [{
- type: "choiceStacked",
- properties: {
- label: "label of the field" as TranslatedString,
- name: "comment",
- choices: [{
- label: "first choice" as TranslatedString,
- value: "1"
- }, {
- label: "second choice" as TranslatedString,
- value: "2"
- }, {
- label: "third choice" as TranslatedString,
- value: "3"
- },],
- },
- }]
- }]
-}
+ design: [
+ {
+ title: "this is a simple form" as TranslatedString,
+ fields: [
+ {
+ type: "choiceStacked",
+ label: "label of the field" as TranslatedString,
+ id: "comment" as UIHandlerId,
+ choices: [
+ {
+ label: "first choice" as TranslatedString,
+ value: "1",
+ },
+ {
+ label: "second choice" as TranslatedString,
+ value: "2",
+ },
+ {
+ label: "third choice" as TranslatedString,
+ value: "3",
+ },
+ ],
+ },
+ ],
+ },
+ ],
+};
-export const SimpleComment = tests.createExample(TestedComponent, { initial, form });
+export const SimpleComment = tests.createExample(TestedComponent, {
+ initial,
+ form,
+});
diff --git a/packages/web-util/src/forms/InputFile.stories.tsx b/packages/web-util/src/forms/InputFile.stories.tsx
index eff18d071..75b1fb918 100644
--- a/packages/web-util/src/forms/InputFile.stories.tsx
+++ b/packages/web-util/src/forms/InputFile.stories.tsx
@@ -20,11 +20,12 @@
*/
import { TranslatedString } from "@gnu-taler/taler-util";
-import * as tests from "@gnu-taler/web-util/testing";
+import * as tests from "../tests/hook.js";
import {
FlexibleForm_Deprecated,
DefaultForm as TestedComponent,
} from "./DefaultForm.js";
+import { UIHandlerId } from "./ui-form.js";
export default {
title: "Input File",
@@ -38,27 +39,32 @@ export namespace Simplest {
type TargetObject = {
comment: string;
-}
+};
const initial: TargetObject = {
- comment: "some initial comment"
-}
+ comment: "some initial comment",
+};
const form: FlexibleForm_Deprecated<TargetObject> = {
- design: [{
- title: "this is a simple form" as TranslatedString,
- fields: [{
- type: "file",
- properties: {
- label: "label of the field" as TranslatedString,
- name: "comment",
- required: true,
- maxBites: 2 * 1024 * 1024,
- accept: ".png",
- tooltip: "this is a very long tooltip that explain what the field does without being short" as TranslatedString,
- help: "Max size of 2 mega bytes" as TranslatedString,
- },
- }]
- }]
-}
+ design: [
+ {
+ title: "this is a simple form" as TranslatedString,
+ fields: [
+ {
+ type: "file",
+ label: "label of the field" as TranslatedString,
+ required: true,
+ id: "comment" as UIHandlerId,
+ accept: ".png",
+ tooltip:
+ "this is a very long tooltip that explain what the field does without being short" as TranslatedString,
+ help: "Max size of 2 mega bytes" as TranslatedString,
+ },
+ ],
+ },
+ ],
+};
-export const SimpleComment = tests.createExample(TestedComponent, { initial, form });
+export const SimpleComment = tests.createExample(TestedComponent, {
+ initial,
+ form,
+});
diff --git a/packages/web-util/src/forms/InputInteger.stories.tsx b/packages/web-util/src/forms/InputInteger.stories.tsx
index 378736a24..76d9e8668 100644
--- a/packages/web-util/src/forms/InputInteger.stories.tsx
+++ b/packages/web-util/src/forms/InputInteger.stories.tsx
@@ -20,36 +20,41 @@
*/
import { TranslatedString } from "@gnu-taler/taler-util";
-import * as tests from "@gnu-taler/web-util/testing";
+import * as tests from "../tests/hook.js";
import {
FlexibleForm_Deprecated,
DefaultForm as TestedComponent,
} from "./DefaultForm.js";
+import { UIHandlerId } from "./ui-form.js";
export default {
title: "Input Integer",
};
-
type TargetObject = {
age: number;
-}
+};
const initial: TargetObject = {
age: 5,
-}
+};
const form: FlexibleForm_Deprecated<TargetObject> = {
- design: [{
- title: "this is a simple form" as TranslatedString,
- fields: [{
- type: "integer",
- properties: {
- label: "label of the field" as TranslatedString,
- name: "age",
- tooltip: "just numbers" as TranslatedString,
- },
- }]
- }]
-}
-
-export const SimpleComment = tests.createExample(TestedComponent, { initial, form });
+ design: [
+ {
+ title: "this is a simple form" as TranslatedString,
+ fields: [
+ {
+ type: "integer",
+ label: "label of the field" as TranslatedString,
+ id: "comment" as UIHandlerId,
+ tooltip: "just numbers" as TranslatedString,
+ },
+ ],
+ },
+ ],
+};
+
+export const SimpleComment = tests.createExample(TestedComponent, {
+ initial,
+ form,
+});
diff --git a/packages/web-util/src/forms/InputLine.stories.tsx b/packages/web-util/src/forms/InputLine.stories.tsx
index dea5c142a..e5209f4d4 100644
--- a/packages/web-util/src/forms/InputLine.stories.tsx
+++ b/packages/web-util/src/forms/InputLine.stories.tsx
@@ -20,11 +20,12 @@
*/
import { TranslatedString } from "@gnu-taler/taler-util";
-import * as tests from "@gnu-taler/web-util/testing";
+import * as tests from "../tests/hook.js";
import {
FlexibleForm_Deprecated,
DefaultForm as TestedComponent,
} from "./DefaultForm.js";
+import { UIHandlerId } from "./ui-form.js";
export default {
title: "Input Line",
@@ -38,22 +39,27 @@ export namespace Simplest {
type TargetObject = {
comment: string;
-}
+};
const initial: TargetObject = {
- comment: "some initial comment"
-}
+ comment: "some initial comment",
+};
const form: FlexibleForm_Deprecated<TargetObject> = {
- design: [{
- title: "this is a simple form" as TranslatedString,
- fields: [{
- type: "text",
- properties: {
- label: "label of the field" as TranslatedString,
- name: "comment",
- },
- }]
- }]
-}
+ design: [
+ {
+ title: "this is a simple form" as TranslatedString,
+ fields: [
+ {
+ type: "text",
+ label: "label of the field" as TranslatedString,
+ id: "comment" as UIHandlerId,
+ },
+ ],
+ },
+ ],
+};
-export const SimpleComment = tests.createExample(TestedComponent, { initial, form });
+export const SimpleComment = tests.createExample(TestedComponent, {
+ initial,
+ form,
+});
diff --git a/packages/web-util/src/forms/InputLine.tsx b/packages/web-util/src/forms/InputLine.tsx
index eb3238ef9..4c0176195 100644
--- a/packages/web-util/src/forms/InputLine.tsx
+++ b/packages/web-util/src/forms/InputLine.tsx
@@ -59,16 +59,22 @@ export function LabelWithTooltipMaybeRequired({
);
if (required) {
return (
- <div class="flex justify-between">
+ <div class="flex justify-between w-fit">
{WithTooltip}
- <span class="text-sm leading-6 text-red-600">*</span>
+ <span class="text-sm leading-6 text-red-600 pl-2">*</span>
</div>
);
}
return WithTooltip;
}
-export function RenderAddon({ disabled, addon }: { disabled?: boolean, addon: Addon }): VNode {
+export function RenderAddon({
+ disabled,
+ addon,
+}: {
+ disabled?: boolean;
+ addon: Addon;
+}): VNode {
switch (addon.type) {
case "text": {
return (
@@ -115,7 +121,7 @@ function InputWrapper<T extends object, K extends keyof T>({
children: ComponentChildren;
} & UIFormProps<T, K>): VNode {
return (
- <div class="sm:col-span-6">
+ <div class="sm:col-span-6 ">
<LabelWithTooltipMaybeRequired
label={label}
required={required}
@@ -154,7 +160,7 @@ type InputType = "text" | "text-area" | "password" | "email" | "number";
export function InputLine<T extends object, K extends keyof T>(
props: { type: InputType } & UIFormProps<T, K>,
): VNode {
- const { name, placeholder, before, after, converter, type } = props;
+ const { name, placeholder, before, after, converter, type, disabled } = props;
//FIXME: remove deprecated
const fieldCtx = useField<T, K>(props.name);
const { value, onChange, state, error } =
@@ -222,7 +228,7 @@ export function InputLine<T extends object, K extends keyof T>(
<InputWrapper<T, K>
{...props}
help={props.help ?? state.help}
- disabled={state.disabled ?? false}
+ disabled={disabled ?? false}
error={showError ? error : undefined}
>
<textarea
@@ -234,7 +240,7 @@ export function InputLine<T extends object, K extends keyof T>(
placeholder={placeholder ? placeholder : undefined}
value={toString(value) ?? ""}
// defaultValue={toString(value)}
- disabled={state.disabled}
+ disabled={disabled ?? false}
aria-invalid={showError}
// aria-describedby="email-error"
class={clazz}
@@ -247,7 +253,7 @@ export function InputLine<T extends object, K extends keyof T>(
<InputWrapper<T, K>
{...props}
help={props.help ?? state.help}
- disabled={state.disabled ?? false}
+ disabled={disabled ?? false}
error={showError ? error : undefined}
>
<input
@@ -262,7 +268,7 @@ export function InputLine<T extends object, K extends keyof T>(
// onChange(fromString(value as any));
// }}
// defaultValue={toString(value)}
- disabled={state.disabled}
+ disabled={disabled ?? false}
aria-invalid={showError}
// aria-describedby="email-error"
class={clazz}
diff --git a/packages/web-util/src/forms/InputSelectMultiple.stories.tsx b/packages/web-util/src/forms/InputSelectMultiple.stories.tsx
index ab17545f5..9cb997490 100644
--- a/packages/web-util/src/forms/InputSelectMultiple.stories.tsx
+++ b/packages/web-util/src/forms/InputSelectMultiple.stories.tsx
@@ -20,11 +20,12 @@
*/
import { TranslatedString } from "@gnu-taler/taler-util";
-import * as tests from "@gnu-taler/web-util/testing";
+import * as tests from "../tests/hook.js";
import {
FlexibleForm_Deprecated,
DefaultForm as TestedComponent,
} from "./DefaultForm.js";
+import { UIHandlerId } from "./ui-form.js";
export default {
title: "Input Select Multiple",
@@ -39,52 +40,64 @@ export namespace Simplest {
type TargetObject = {
pets: string[];
things: string[];
-}
+};
const initial: TargetObject = {
pets: [],
things: [],
-}
+};
const form: FlexibleForm_Deprecated<TargetObject> = {
- design: [{
- title: "this is a simple form" as TranslatedString,
- fields: [{
- type: "selectMultiple",
- properties: {
- label: "allow diplicates" as TranslatedString,
- name: "pets",
- placeholder: "search..." as TranslatedString,
- choices: [{
- label: "one label" as TranslatedString,
- value: "one"
- }, {
- label: "two label" as TranslatedString,
- value: "two"
- }, {
- label: "five label" as TranslatedString,
- value: "five"
- }]
- },
- }, {
- type: "selectMultiple",
- properties: {
- label: "unique values" as TranslatedString,
- name: "things",
- unique: true,
- placeholder: "search..." as TranslatedString,
- choices: [{
- label: "one label" as TranslatedString,
- value: "one"
- }, {
- label: "two label" as TranslatedString,
- value: "two"
- }, {
- label: "five label" as TranslatedString,
- value: "five"
- }]
- },
- }]
- }]
-}
+ design: [
+ {
+ title: "this is a simple form" as TranslatedString,
+ fields: [
+ {
+ type: "selectMultiple",
+ label: "allow diplicates" as TranslatedString,
+ id: "pets" as UIHandlerId,
+ placeholder: "search..." as TranslatedString,
+ choices: [
+ {
+ label: "one label" as TranslatedString,
+ value: "one",
+ },
+ {
+ label: "two label" as TranslatedString,
+ value: "two",
+ },
+ {
+ label: "five label" as TranslatedString,
+ value: "five",
+ },
+ ],
+ },
+ {
+ type: "selectMultiple",
+ label: "unique values" as TranslatedString,
+ id: "things" as UIHandlerId,
+ unique: true,
+ placeholder: "search..." as TranslatedString,
+ choices: [
+ {
+ label: "one label" as TranslatedString,
+ value: "one",
+ },
+ {
+ label: "two label" as TranslatedString,
+ value: "two",
+ },
+ {
+ label: "five label" as TranslatedString,
+ value: "five",
+ },
+ ],
+ },
+ ],
+ },
+ ],
+};
-export const SimpleComment = tests.createExample(TestedComponent, { initial, form });
+export const SimpleComment = tests.createExample(TestedComponent, {
+ initial,
+ form,
+});
diff --git a/packages/web-util/src/forms/InputSelectOne.stories.tsx b/packages/web-util/src/forms/InputSelectOne.stories.tsx
index 2ebde3096..25b96f0c0 100644
--- a/packages/web-util/src/forms/InputSelectOne.stories.tsx
+++ b/packages/web-util/src/forms/InputSelectOne.stories.tsx
@@ -20,11 +20,12 @@
*/
import { TranslatedString } from "@gnu-taler/taler-util";
-import * as tests from "@gnu-taler/web-util/testing";
+import * as tests from "../tests/hook.js";
import {
FlexibleForm_Deprecated,
DefaultForm as TestedComponent,
} from "./DefaultForm.js";
+import { UIHandlerId } from "./ui-form.js";
export default {
title: "Input Select One",
@@ -38,33 +39,42 @@ export namespace Simplest {
type TargetObject = {
things: string;
-}
+};
const initial: TargetObject = {
- things: "one"
-}
+ things: "one",
+};
const form: FlexibleForm_Deprecated<TargetObject> = {
- design: [{
- title: "this is a simple form" as TranslatedString,
- fields: [{
- type: "selectOne",
- properties: {
- label: "label of the field" as TranslatedString,
- name: "things",
- placeholder: "search..." as TranslatedString,
- choices: [{
- label: "one label" as TranslatedString,
- value: "one"
- }, {
- label: "two label" as TranslatedString,
- value: "two"
- }, {
- label: "five label" as TranslatedString,
- value: "five"
- }]
- },
- }]
- }]
-}
+ design: [
+ {
+ title: "this is a simple form" as TranslatedString,
+ fields: [
+ {
+ type: "selectOne",
+ label: "label of the field" as TranslatedString,
+ id: "things" as UIHandlerId,
+ placeholder: "search..." as TranslatedString,
+ choices: [
+ {
+ label: "one label" as TranslatedString,
+ value: "one",
+ },
+ {
+ label: "two label" as TranslatedString,
+ value: "two",
+ },
+ {
+ label: "five label" as TranslatedString,
+ value: "five",
+ },
+ ],
+ },
+ ],
+ },
+ ],
+};
-export const SimpleComment = tests.createExample(TestedComponent, { initial, form });
+export const SimpleComment = tests.createExample(TestedComponent, {
+ initial,
+ form,
+});
diff --git a/packages/web-util/src/forms/InputText.stories.tsx b/packages/web-util/src/forms/InputText.stories.tsx
index 60b6ca224..6d0db938b 100644
--- a/packages/web-util/src/forms/InputText.stories.tsx
+++ b/packages/web-util/src/forms/InputText.stories.tsx
@@ -20,11 +20,12 @@
*/
import { TranslatedString } from "@gnu-taler/taler-util";
-import * as tests from "@gnu-taler/web-util/testing";
+import * as tests from "../tests/hook.js";
import {
FlexibleForm_Deprecated,
DefaultForm as TestedComponent,
} from "./DefaultForm.js";
+import { UIHandlerId } from "./ui-form.js";
export default {
title: "Input Text",
@@ -38,22 +39,27 @@ export namespace Simplest {
type TargetObject = {
comment: string;
-}
+};
const initial: TargetObject = {
- comment: "some initial comment"
-}
+ comment: "some initial comment",
+};
const form: FlexibleForm_Deprecated<TargetObject> = {
- design: [{
- title: "this is a simple form" as TranslatedString,
- fields: [{
- type: "text",
- properties: {
- label: "label of the field" as TranslatedString,
- name: "comment",
- },
- }]
- }]
-}
+ design: [
+ {
+ title: "this is a simple form" as TranslatedString,
+ fields: [
+ {
+ type: "text",
+ label: "label of the field" as TranslatedString,
+ id: "comment" as UIHandlerId,
+ },
+ ],
+ },
+ ],
+};
-export const SimpleComment = tests.createExample(TestedComponent, { initial, form });
+export const SimpleComment = tests.createExample(TestedComponent, {
+ initial,
+ form,
+});
diff --git a/packages/web-util/src/forms/InputTextArea.stories.tsx b/packages/web-util/src/forms/InputTextArea.stories.tsx
index ab1a695f5..a3b135c36 100644
--- a/packages/web-util/src/forms/InputTextArea.stories.tsx
+++ b/packages/web-util/src/forms/InputTextArea.stories.tsx
@@ -20,11 +20,12 @@
*/
import { TranslatedString } from "@gnu-taler/taler-util";
-import * as tests from "@gnu-taler/web-util/testing";
+import * as tests from "../tests/hook.js";
import {
DefaultForm as TestedComponent,
FlexibleForm_Deprecated,
} from "./DefaultForm.js";
+import { UIHandlerId } from "./ui-form.js";
export default {
title: "Input Text Area",
@@ -38,22 +39,27 @@ export namespace Simplest {
type TargetObject = {
comment: string;
-}
+};
const initial: TargetObject = {
- comment: "some initial comment"
-}
+ comment: "some initial comment",
+};
const form: FlexibleForm_Deprecated<TargetObject> = {
- design: [{
- title: "this is a simple form" as TranslatedString,
- fields: [{
- type: "text",
- properties: {
- label: "label of the field" as TranslatedString,
- name: "comment",
- },
- }]
- }]
-}
+ design: [
+ {
+ title: "this is a simple form" as TranslatedString,
+ fields: [
+ {
+ type: "text",
+ label: "label of the field" as TranslatedString,
+ id: "comment" as UIHandlerId,
+ },
+ ],
+ },
+ ],
+};
-export const SimpleComment = tests.createExample(TestedComponent, { initial, form });
+export const SimpleComment = tests.createExample(TestedComponent, {
+ initial,
+ form,
+});
diff --git a/packages/web-util/src/forms/InputToggle.stories.tsx b/packages/web-util/src/forms/InputToggle.stories.tsx
index fcc57ffe2..5b5794308 100644
--- a/packages/web-util/src/forms/InputToggle.stories.tsx
+++ b/packages/web-util/src/forms/InputToggle.stories.tsx
@@ -20,11 +20,12 @@
*/
import { TranslatedString } from "@gnu-taler/taler-util";
-import * as tests from "@gnu-taler/web-util/testing";
+import * as tests from "../tests/hook.js";
import {
FlexibleForm_Deprecated,
DefaultForm as TestedComponent,
} from "./DefaultForm.js";
+import { UIHandlerId } from "./ui-form.js";
export default {
title: "Input Toggle",
@@ -38,22 +39,27 @@ export namespace Simplest {
type TargetObject = {
comment: string;
-}
+};
const initial: TargetObject = {
- comment: "some initial comment"
-}
+ comment: "some initial comment",
+};
const form: FlexibleForm_Deprecated<TargetObject> = {
- design: [{
- title: "this is a simple form" as TranslatedString,
- fields: [{
- type: "toggle",
- properties: {
- label: "label of the field" as TranslatedString,
- name: "comment",
- },
- }]
- }]
-}
+ design: [
+ {
+ title: "this is a simple form" as TranslatedString,
+ fields: [
+ {
+ type: "toggle",
+ label: "label of the field" as TranslatedString,
+ id: "comment" as UIHandlerId,
+ },
+ ],
+ },
+ ],
+};
-export const SimpleComment = tests.createExample(TestedComponent, { initial, form });
+export const SimpleComment = tests.createExample(TestedComponent, {
+ initial,
+ form,
+});
diff --git a/packages/web-util/src/forms/forms.ts b/packages/web-util/src/forms/forms.ts
index 4c5050830..2c789b9a3 100644
--- a/packages/web-util/src/forms/forms.ts
+++ b/packages/web-util/src/forms/forms.ts
@@ -14,9 +14,12 @@ import { InputText } from "./InputText.js";
import { InputTextArea } from "./InputTextArea.js";
import { InputToggle } from "./InputToggle.js";
import { Addon, StringConverter, UIFieldHandler } from "./FormProvider.js";
-import { InternationalizationAPI, UIFieldElementDescription } from "../index.browser.js";
+import {
+ InternationalizationAPI,
+ UIFieldElementDescription,
+} from "../index.browser.js";
import { assertUnreachable, TranslatedString } from "@gnu-taler/taler-util";
-import {UIFormFieldBaseConfig, UIFormElementConfig} from "./ui-form.js";
+import { UIFormFieldBaseConfig, UIFormElementConfig } from "./ui-form.js";
/**
* Constrain the type with the ui props
*/
@@ -148,11 +151,11 @@ export function RenderAllFieldsByUiConfig({
/**
* convert field configuration to render function
- *
- * @param i18n_
- * @param fieldConfig
- * @param form
- * @returns
+ *
+ * @param i18n_
+ * @param fieldConfig
+ * @param form
+ * @returns
*/
export function convertUiField(
i18n_: InternationalizationAPI,
@@ -175,7 +178,12 @@ export function convertUiField(
type: config.type,
properties: {
...converBaseFieldsProps(i18n_, config),
- fields: convertUiField(i18n_, config.fields, form, getConverterById),
+ fields: convertUiField(
+ i18n_,
+ config.fields,
+ form,
+ getConverterById,
+ ),
},
};
return resp;
@@ -190,7 +198,12 @@ export function convertUiField(
...converBaseFieldsProps(i18n_, config),
...converInputFieldsProps(form, config, getConverterById),
labelField: config.labelFieldId,
- fields: convertUiField(i18n_, config.fields, form, getConverterById),
+ fields: convertUiField(
+ i18n_,
+ config.fields,
+ form,
+ getConverterById,
+ ),
},
} as UIFormField;
}
@@ -208,8 +221,8 @@ export function convertUiField(
type: "amount",
properties: {
...converBaseFieldsProps(i18n_, config),
- ...converInputFieldsProps(form, config, getConverterById),
- currency: config.currency,
+ ...converInputFieldsProps(form, config, getConverterById),
+ currency: config.currency,
},
} as UIFormField;
}
@@ -230,11 +243,10 @@ export function convertUiField(
...converBaseFieldsProps(i18n_, config),
...converInputFieldsProps(form, config, getConverterById),
choices: config.choices,
-
},
- }as UIFormField;
+ } as UIFormField;
}
- case "file":{
+ case "file": {
return {
type: "file",
properties: {
@@ -245,7 +257,7 @@ export function convertUiField(
},
} as UIFormField;
}
- case "integer":{
+ case "integer": {
return {
type: "integer",
properties: {
@@ -254,7 +266,7 @@ export function convertUiField(
},
} as UIFormField;
}
- case "selectMultiple":{
+ case "selectMultiple": {
return {
type: "selectMultiple",
properties: {
@@ -285,7 +297,7 @@ export function convertUiField(
}
case "textArea": {
return {
- type: "text",
+ type: "textArea",
properties: {
...converBaseFieldsProps(i18n_, config),
...converInputFieldsProps(form, config, getConverterById),
@@ -308,30 +320,27 @@ export function convertUiField(
});
}
-
-
function getAddonById(_id: string | undefined): Addon {
return undefined!;
}
-
type GetConverterById = (
id: string | undefined,
config: unknown,
) => StringConverter<unknown>;
-
function converInputFieldsProps(
form: object,
p: UIFormFieldBaseConfig,
getConverterById: GetConverterById,
) {
+ const names = p.id.split(".");
return {
converter: getConverterById(p.converterId, p),
- handler: getValueDeeper2(form, p.id.split(".")),
- name: p.name,
+ handler: getValueDeeper2(form, names),
required: p.required,
disabled: p.disabled,
+ name: names[names.length - 1],
help: p.help,
placeholder: p.placeholder,
tooltip: p.tooltip,
@@ -347,7 +356,6 @@ function converBaseFieldsProps(
after: getAddonById(p.addonAfterId),
before: getAddonById(p.addonBeforeId),
hidden: p.hidden,
- name: p.name,
help: i18n_.str`${p.help}`,
label: i18n_.str`${p.label}`,
tooltip: i18n_.str`${p.tooltip}`,
@@ -368,5 +376,3 @@ export function getValueDeeper2(
}
return getValueDeeper2(object[head], rest);
}
-
-
diff --git a/packages/web-util/src/forms/ui-form.ts b/packages/web-util/src/forms/ui-form.ts
index 012499d6d..f26e08f3b 100644
--- a/packages/web-util/src/forms/ui-form.ts
+++ b/packages/web-util/src/forms/ui-form.ts
@@ -12,7 +12,9 @@ import {
codecOptional,
Integer,
TalerProtocolTimestamp,
+ TranslatedString,
} from "@gnu-taler/taler-util";
+import { InternationalizationAPI } from "../index.browser.js";
export type FormConfiguration = DoubleColumnForm;
@@ -134,9 +136,6 @@ export type UIFieldElementDescription = {
/* short text to be shown close to the field, usually below and dimmer*/
help?: string;
- /* name of the field, useful for a11y */
- name: string;
-
/* if the field should be initially hidden */
hidden?: boolean;
@@ -162,6 +161,11 @@ export type UIFormFieldBaseConfig = UIFieldElementDescription & {
*/
converterId?: string;
+ /* return an error message if the value is not valid, returns un undefined
+ if there is no error
+ */
+ validator?: (value: string) => TranslatedString | undefined;
+
/* property id of the form */
id: UIHandlerId;
};
@@ -181,7 +185,6 @@ const codecForUIFormFieldBaseDescriptionTemplate = <
.property("hidden", codecOptional(codecForBoolean()))
.property("help", codecOptional(codecForString()))
.property("label", codecForString())
- .property("name", codecForString())
.property("tooltip", codecOptional(codecForString()));
const codecForUIFormFieldBaseConfigTemplate = <
diff --git a/packages/web-util/src/hooks/useNotifications.ts b/packages/web-util/src/hooks/useNotifications.ts
index 103b88c86..929c54a58 100644
--- a/packages/web-util/src/hooks/useNotifications.ts
+++ b/packages/web-util/src/hooks/useNotifications.ts
@@ -155,7 +155,7 @@ function errorMap<T extends OperationFail<unknown>>(
notify({
type: "error",
title: map(resp.case),
- description: resp.detail.hint as TranslatedString,
+ description: (resp.detail?.hint as TranslatedString) ?? "",
debug: resp.detail,
when: AbsoluteTime.now(),
});
diff --git a/packages/web-util/src/index.browser.ts b/packages/web-util/src/index.browser.ts
index 2f3b57b8d..7d413a17a 100644
--- a/packages/web-util/src/index.browser.ts
+++ b/packages/web-util/src/index.browser.ts
@@ -7,4 +7,5 @@ export * from "./utils/route.js";
export * from "./context/index.js";
export * from "./components/index.js";
export * from "./forms/index.js";
-export { renderStories, parseGroupImport } from "./stories.js";
+export { encodeCrockForURI, decodeCrockFromURI } from "./utils/base64.js";
+export { renderStories, parseGroupImport } from "./stories-utils.js";
diff --git a/packages/web-util/src/index.build.ts b/packages/web-util/src/index.build.ts
index 2260ecb9a..9095db892 100644
--- a/packages/web-util/src/index.build.ts
+++ b/packages/web-util/src/index.build.ts
@@ -292,6 +292,7 @@ export function computeConfig(params: BuildParams): esbuild.BuildOptions {
entryPoints: params.source.js,
publicPath: params.public,
outdir: params.destination,
+ treeShaking: true,
minify: false, //params.type === "production",
sourcemap: true, //params.type !== "production",
define: {
@@ -311,11 +312,30 @@ export async function build(config: BuildParams) {
return res;
}
-const LIVE_RELOAD_SCRIPT =
- "./node_modules/@gnu-taler/web-util/lib/live-reload.mjs";
+const LIVE_RELOAD_SCRIPT = "./node_modules/@gnu-taler/web-util/lib/live-reload.mjs";
+const LIVE_RELOAD_SCRIPT_LOCALLY = "./lib/live-reload.mjs";
/**
* Do startup for development environment
+ *
+ * To be used from web-utils project
+ */
+export function initializeDevOnWebUtils(
+ config: BuildParams,
+): () => Promise<esbuild.BuildResult> {
+ function buildDevelopment() {
+ const result = computeConfig(config);
+ result.inject = [LIVE_RELOAD_SCRIPT_LOCALLY];
+ return esbuild.build(result);
+ }
+ return buildDevelopment;
+}
+
+
+/**
+ * Do startup for development environment
+ *
+ * To be used when web-utils is a library
*/
export function initializeDev(
config: BuildParams,
diff --git a/packages/web-util/src/index.html b/packages/web-util/src/index.html
new file mode 100644
index 000000000..a51fe776a
--- /dev/null
+++ b/packages/web-util/src/index.html
@@ -0,0 +1,41 @@
+<!--
+ This file is part of GNU Taler
+ (C) 2021--2022 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
+-->
+<!doctype html>
+<html lang="en" class="h-full bg-gray-100">
+ <head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
+ <meta name="taler-support" content="uri,api" />
+ <meta name="mobile-web-app-capable" content="yes" />
+ <meta name="apple-mobile-web-app-capable" content="yes" />
+ <link
+ rel="icon"
+ href="data:;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///////////////////////////////////////////////////////////////////////////////////////////////////7//v38//78/P/+/fz//vz7///+/v/+/f3//vz7///+/v/+/fz//v38///////////////////////+/v3///7+/////////////////////////////////////////////////////////v3//v79///////+/v3///////r28v/ct5//06SG/9Gffv/Xqo7/7N/V/9e2nf/bsJb/6uDW/9Sskf/euKH/+/j2///////+/v3//////+3azv+/eE3/2rWd/9Kkhv/Vr5T/48i2/8J+VP/Qn3//3ryn/795Tf/WrpP/2LCW/8B6T//w4Nb///////Pn4P+/d0v/9u3n/+7d0v/EhV7//v///+HDr//fxLD/zph2/+TJt//8/Pv/woBX//Lm3f/y5dz/v3hN//bu6f/JjGn/4sW0///////Df1j/8OLZ//v6+P+/elH/+vj1//jy7f+/elL//////+zYzP/Eg13//////967p//MlHT/wn5X///////v4Nb/yY1s///////jw7H/06KG////////////z5t9/+fNvf//////x4pn//Pp4v/8+vn/w39X/8WEX///////5s/A/9CbfP//////27Oc/9y2n////////////9itlf/gu6f//////86Vdf/r2Mz//////8SCXP/Df1j//////+7d0v/KkG7//////+HBrf/VpYr////////////RnoH/5sq6///////Ii2n/8ubf//39/P/Cf1j/xohk/+bNvv//////wn5W//Tq4//58/D/wHxV//7+/f/59fH/v3xU//39/P/w4Nf/xIFb///////hw7H/yo9t/+/f1f/AeU3/+/n2/+nSxP/FhmD//////9qzm//Upon/4MSx/96+qf//////xINc/+3bz//48e3/v3hN//Pn3///////6M+//752S//gw6//06aK/8J+VP/kzLr/zZd1/8OCWv/q18r/17KZ/9Ooi//fv6r/v3dK/+vWyP///////v39///////27un/1aeK/9Opjv/m1cf/1KCC/9a0nP/n08T/0Jx8/82YdP/QnHz/16yR//jx7P///////v39///////+/f3///7+///////+//7//v7+///////+/v7//v/+/////////////////////////v7//v79///////////////////+/v/+/Pv//v39///+/v/+/Pv///7+//7+/f/+/Pv//v39//79/P/+/Pv///7+////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
+ />
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
+ <title>Web Util</title>
+ <!-- Entry point for the bank SPA. -->
+ <script type="module" src="index.js"></script>
+ <link rel="stylesheet" href="index.css" />
+ </head>
+
+ <body class="h-full">
+ <div id="app"></div>
+ </body>
+</html>
diff --git a/packages/web-util/src/live-reload.ts b/packages/web-util/src/live-reload.ts
index cd3a7540d..c89d09383 100644
--- a/packages/web-util/src/live-reload.ts
+++ b/packages/web-util/src/live-reload.ts
@@ -1,10 +1,12 @@
/* eslint-disable no-undef */
function setupLiveReload(): void {
- const stopWs = localStorage.getItem("stop-ws")
+ const stopWs = localStorage.getItem("stop-ws");
if (!!stopWs) return;
const protocol = window.location.protocol === "http:" ? "ws:" : "wss:";
- const ws = new WebSocket(`${protocol}//${window.location.hostname}:${window.location.port}/ws`);
+ const ws = new WebSocket(
+ `${protocol}//${window.location.hostname}:${window.location.port}/ws`,
+ );
ws.addEventListener("message", (message) => {
try {
@@ -60,18 +62,22 @@ setupLiveReload();
function showReloadOverlay(): void {
const d = document.createElement("div");
d.id = "overlay";
- d.style.position = "absolute";
- d.style.width = "100%";
- d.style.height = "100%";
+ d.style.position = "fixed";
+ d.style.left = "0px";
+ d.style.top = "0px";
+ d.style.width = "100vw";
+ d.style.height = "100vh";
+ d.style.display = "flex";
+ d.style.alignItems = "center";
+ d.style.justifyContent = "center";
+ d.style.fontFamily = `system-ui, -apple-system, BlinkMacSystemFont, Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif`;
d.style.color = "white";
d.style.backgroundColor = "rgba(0,0,0,0.5)";
- d.style.display = "flex";
d.style.zIndex = String(Number.MAX_SAFE_INTEGER);
- d.style.justifyContent = "center";
const h = document.createElement("h1");
h.id = "overlay-text";
h.style.margin = "auto";
- h.innerHTML = "reloading...";
+ h.innerHTML = "Reloading...";
d.appendChild(h);
if (document.body.firstChild) {
document.body.insertBefore(d, document.body.firstChild);
diff --git a/packages/web-util/src/serve.ts b/packages/web-util/src/serve.ts
index 1daea15bf..5d2b770c9 100644
--- a/packages/web-util/src/serve.ts
+++ b/packages/web-util/src/serve.ts
@@ -6,15 +6,15 @@ import http from "http";
import { parse } from "url";
import WebSocket from "ws";
-import locahostCrt from "./keys/localhost.crt";
-import locahostKey from "./keys/localhost.key";
+// import locahostCrt from "./keys/localhost.crt";
+// import locahostKey from "./keys/localhost.key";
import storiesHtml from "./stories.html";
import path from "path";
const httpServerOptions = {
- key: locahostKey,
- cert: locahostCrt,
+ // key: locahostKey,
+ // cert: locahostCrt,
};
const logger = new Logger("serve.ts");
@@ -22,6 +22,7 @@ const logger = new Logger("serve.ts");
const PATHS = {
WS: "/ws",
EXAMPLE: "/examples",
+ ROOT: "/",
APP: "/app",
};
@@ -46,7 +47,7 @@ export async function serve(opts: {
if (opts.tls) {
httpsServer = https.createServer(httpServerOptions, app);
httpsPort = opts.port + 1;
- servers.push(httpsServer)
+ servers.push(httpsServer);
}
logger.info(`Dev server. Endpoints:`);
@@ -124,6 +125,27 @@ export async function serve(opts: {
);
});
+ app.get(PATHS.ROOT, function (req: any, res: any) {
+ res.set("Content-Type", "text/html");
+ res.send(`<hmtl>
+ <head><title>Development Server</title></head>
+ <body>
+ it will connect to this server using websocket and reload automatically when the code changes
+ <h1>Endpoints</h1>
+ <dl>
+ <dt><a href="./app">app</a></dt>
+ <dd>Where you can find the application. Reloads on update.</dd>
+
+ <dt><a href="./examples">ui examples</a></dt>
+ <dd>Where you can browse static UI examples. Reloads on update.</dd>
+
+ <dt><a href="./ws">websocket</a></dt>
+ <dd>Announce when the code changes</dd>
+ </dl>
+ </body>
+ </html>`);
+ });
+
logger.info(`Serving ${opts.folder} on ${httpPort}: plain HTTP`);
httpServer.listen(httpPort);
if (httpsServer !== undefined) {
diff --git a/packages/web-util/src/stories-utils.tsx b/packages/web-util/src/stories-utils.tsx
new file mode 100644
index 000000000..d9c2406eb
--- /dev/null
+++ b/packages/web-util/src/stories-utils.tsx
@@ -0,0 +1,578 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 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 { setupI18n } from "@gnu-taler/taler-util";
+import {
+ ComponentChild,
+ ComponentChildren,
+ Fragment,
+ FunctionalComponent,
+ FunctionComponent,
+ h,
+ JSX,
+ render,
+ VNode,
+} from "preact";
+import { useEffect, useErrorBoundary, useState } from "preact/hooks";
+import { ExampleItemSetup } from "./tests/hook.js";
+
+const Page: FunctionalComponent = ({ children }): VNode => {
+ return (
+ <div
+ style={{
+ fontFamily: "Arial, Helvetica, sans-serif",
+ width: "100%",
+ display: "flex",
+ flexDirection: "row",
+ }}
+ >
+ {children}
+ </div>
+ );
+};
+
+const SideBar: FunctionalComponent<{ width: number }> = ({
+ width,
+ children,
+}): VNode => {
+ return (
+ <div
+ style={{
+ minWidth: width,
+ height: "calc(100vh - 20px)",
+ overflowX: "hidden",
+ overflowY: "visible",
+ scrollBehavior: "smooth",
+ }}
+ >
+ {children}
+ </div>
+ );
+};
+
+const ResizeHandleDiv: FunctionalComponent<
+ JSX.HTMLAttributes<HTMLDivElement>
+> = ({ children, ...props }): VNode => {
+ return (
+ <div
+ {...props}
+ style={{
+ width: 10,
+ backgroundColor: "#ddd",
+ cursor: "ew-resize",
+ }}
+ >
+ {children}
+ </div>
+ );
+};
+
+const Content: FunctionalComponent = ({ children }): VNode => {
+ return (
+ <div
+ style={{
+ width: "100%",
+ padding: 20,
+ }}
+ >
+ {children}
+ </div>
+ );
+};
+
+function findByGroupComponentName(
+ allExamples: Group[],
+ group: string,
+ component: string,
+ name: string,
+): ExampleItem | undefined {
+ const gl = allExamples.filter((e) => e.title === group);
+ if (gl.length === 0) {
+ return undefined;
+ }
+ const cl = gl[0].list.filter((l) => l.name === component);
+ if (cl.length === 0) {
+ return undefined;
+ }
+ const el = cl[0].examples.filter((c) => c.name === name);
+ if (el.length === 0) {
+ return undefined;
+ }
+ return el[0];
+}
+
+function getContentForExample(
+ item: ExampleItem | undefined,
+ allExamples: Group[],
+): FunctionalComponent {
+ if (!item)
+ return function SelectExampleMessage() {
+ return <div>select example from the list on the left</div>;
+ };
+ const example = findByGroupComponentName(
+ allExamples,
+ item.group,
+ item.component,
+ item.name,
+ );
+ if (!example) {
+ return function ExampleNotFoundMessage() {
+ return <div>example not found</div>;
+ };
+ }
+ return () => example.render.component(example.render.props);
+}
+
+function ExampleList({
+ name,
+ list,
+ selected,
+ onSelectStory,
+}: {
+ name: string;
+ list: {
+ name: string;
+ examples: ExampleItem[];
+ }[];
+ selected: ExampleItem | undefined;
+ onSelectStory: (i: ExampleItem, id: string) => void;
+}): VNode {
+ const [isOpen, setOpen] = useState(selected && selected.group === name);
+ return (
+ <ol style={{ padding: 4, margin: 0 }}>
+ <div
+ style={{ backgroundColor: "lightcoral", cursor: "pointer" }}
+ onClick={() => setOpen(!isOpen)}
+ >
+ {name}
+ </div>
+ <div style={{ display: isOpen ? undefined : "none" }}>
+ {list.map((k) => (
+ <li key={k.name}>
+ <dl style={{ margin: 0 }}>
+ <dt>{k.name}</dt>
+ {k.examples.map((r, i) => {
+ const e = encodeURIComponent;
+ const eId = `${e(r.group)}-${e(r.component)}-${e(r.name)}`;
+ const isSelected =
+ selected &&
+ selected.component === r.component &&
+ selected.group === r.group &&
+ selected.name === r.name;
+ return (
+ <dd
+ id={eId}
+ key={r.name}
+ style={{
+ backgroundColor: isSelected
+ ? "green"
+ : i % 2
+ ? "lightgray"
+ : "lightblue",
+ marginLeft: "1em",
+ padding: 4,
+ cursor: "pointer",
+ borderRadius: 4,
+ marginBottom: 4,
+ }}
+ >
+ <a
+ href={`#${eId}`}
+ style={{ color: "black" }}
+ onClick={(e) => {
+ e.preventDefault();
+ location.hash = `#${eId}`;
+ onSelectStory(r, eId);
+ history.pushState({}, "", `#${eId}`);
+ }}
+ >
+ {r.name}
+ </a>
+ </dd>
+ );
+ })}
+ </dl>
+ </li>
+ ))}
+ </div>
+ </ol>
+ );
+}
+
+/**
+ * Prevents the UI from redirecting and inform the dev
+ * where the <a /> should have redirected
+ * @returns
+ */
+function PreventLinkNavigation({
+ children,
+}: {
+ children: ComponentChildren;
+}): VNode {
+ return (
+ <div
+ onClick={(e) => {
+ let t: any = e.target;
+ do {
+ if (t.localName === "a" && t.getAttribute("href")) {
+ alert(`should navigate to: ${t.attributes.href.value}`);
+ e.stopImmediatePropagation();
+ e.stopPropagation();
+ e.preventDefault();
+ return false;
+ }
+ } while ((t = t.parentNode));
+ return true;
+ }}
+ >
+ {children}
+ </div>
+ );
+}
+
+function ErrorReport({
+ children,
+ selected,
+}: {
+ children: ComponentChild;
+ selected: ExampleItem | undefined;
+}): VNode {
+ const [error, resetError] = useErrorBoundary();
+ //if there is an error, reset when unloading this component
+ useEffect(() => (error ? resetError : undefined));
+ if (error) {
+ return (
+ <div>
+ <p>Error was thrown trying to render</p>
+ {selected && (
+ <ul>
+ <li>
+ <b>group</b>: {selected.group}
+ </li>
+ <li>
+ <b>component</b>: {selected.component}
+ </li>
+ <li>
+ <b>example</b>: {selected.name}
+ </li>
+ <li>
+ <b>args</b>:{" "}
+ <pre>{JSON.stringify(selected.render.props, undefined, 2)}</pre>
+ </li>
+ </ul>
+ )}
+ <p>{error.message}</p>
+ <pre>{error.stack}</pre>
+ </div>
+ );
+ }
+ return <Fragment>{children}</Fragment>;
+}
+
+function getSelectionFromLocationHash(
+ hash: string,
+ allExamples: Group[],
+): ExampleItem | undefined {
+ if (!hash) return undefined;
+ const parts = hash.substring(1).split("-");
+ if (parts.length < 3) return undefined;
+ return findByGroupComponentName(
+ allExamples,
+ decodeURIComponent(parts[0]),
+ decodeURIComponent(parts[1]),
+ decodeURIComponent(parts[2]),
+ );
+}
+
+function parseExampleImport(
+ group: string,
+ componentName: string,
+ im: MaybeComponent,
+): ComponentItem {
+ const examples: ExampleItem[] = Object.entries(im)
+ .filter(([k]) => k !== "default")
+ .map(([exampleName, exampleValue]): ExampleItem => {
+ if (!exampleValue) {
+ throw Error(
+ `example "${exampleName}" from component "${componentName}" in group "${group}" is undefined`,
+ );
+ }
+
+ if (typeof exampleValue === "function") {
+ return {
+ group,
+ component: componentName,
+ name: exampleName,
+ render: {
+ component: exampleValue as FunctionComponent,
+ props: {},
+ contextProps: {},
+ },
+ };
+ }
+ const v: any = exampleValue;
+ if (
+ "component" in v &&
+ typeof v.component === "function" &&
+ "props" in v
+ ) {
+ return {
+ group,
+ component: componentName,
+ name: exampleName,
+ render: v,
+ };
+ }
+ throw Error(
+ `example "${exampleName}" from component "${componentName}" in group "${group}" doesn't follow one of the two ways of example`,
+ );
+ });
+ return {
+ name: componentName,
+ examples,
+ };
+}
+
+export function parseGroupImport(
+ groups: Record<string, ComponentOrFolder>,
+): Group[] {
+ return Object.entries(groups).map(([groupName, value]) => {
+ return {
+ title: groupName,
+ list: Object.entries(value).flatMap(([key, value]) =>
+ folder(groupName, value),
+ ),
+ };
+ });
+}
+
+export interface Group {
+ title: string;
+ list: ComponentItem[];
+}
+
+export interface ComponentItem<Props extends object = {}> {
+ name: string;
+ examples: ExampleItem<Props>[];
+}
+
+export interface ExampleItem<Props extends object = {}> {
+ group: string;
+ component: string;
+ name: string;
+ render: ExampleItemSetup<Props>;
+}
+
+type ComponentOrFolder = MaybeComponent | MaybeFolder;
+interface MaybeFolder {
+ default?: { title: string };
+ // [exampleName: string]: FunctionalComponent;
+}
+interface MaybeComponent {
+ // default?: undefined;
+ [exampleName: string]: undefined | object;
+}
+
+function folder(groupName: string, value: ComponentOrFolder): ComponentItem[] {
+ let title: string | undefined = undefined;
+ try {
+ title =
+ typeof value === "object" &&
+ typeof value.default === "object" &&
+ value.default !== undefined &&
+ "title" in value.default &&
+ typeof value.default.title === "string"
+ ? value.default.title
+ : undefined;
+ } catch (e) {
+ throw Error(
+ `Could not defined if it is component or folder ${groupName}: ${JSON.stringify(
+ value,
+ undefined,
+ 2,
+ )}`,
+ );
+ }
+ if (title) {
+ const c = parseExampleImport(groupName, title, value as MaybeComponent);
+ return [c];
+ }
+ return Object.entries(value).flatMap(([subkey, value]) =>
+ folder(groupName, value),
+ );
+}
+
+interface Props {
+ getWrapperForGroup: (name: string) => FunctionComponent;
+ examplesInGroups: Group[];
+ langs: Record<string, object>;
+}
+
+function Application({
+ langs,
+ examplesInGroups,
+ getWrapperForGroup,
+}: Props): VNode {
+ const url = new URL(window.location.href);
+ const initialSelection = getSelectionFromLocationHash(
+ url.hash,
+ examplesInGroups,
+ );
+
+ const currentLang = url.searchParams.get("lang") || "en";
+
+ if (!langs["en"]) {
+ langs["en"] = {};
+ }
+ setupI18n(currentLang, langs);
+
+ const [selected, updateSelected] = useState<ExampleItem | undefined>(
+ initialSelection,
+ );
+ const [sidebarWidth, setSidebarWidth] = useState(200);
+ useEffect(() => {
+ if (url.hash) {
+ const hash = url.hash.substring(1);
+ const found = document.getElementById(hash);
+ if (found) {
+ setTimeout(() => {
+ found.scrollIntoView({
+ block: "center",
+ });
+ }, 50);
+ }
+ }
+ }, []);
+
+ const GroupWrapper = getWrapperForGroup(selected?.group || "default");
+ const ExampleContent = getContentForExample(selected, examplesInGroups);
+
+ //style={{ "--with-size": `${sidebarWidth}px` }}
+ return (
+ <Page>
+ {/* <LiveReload /> */}
+ <SideBar width={sidebarWidth}>
+ <div>
+ Language:
+ <select
+ value={currentLang}
+ onChange={(e) => {
+ const url = new URL(window.location.href);
+ url.searchParams.set("lang", e.currentTarget.value);
+ window.location.href = url.href;
+ }}
+ >
+ {Object.keys(langs).map((l) => (
+ <option key={l}>{l}</option>
+ ))}
+ </select>
+ </div>
+ {examplesInGroups.map((group) => (
+ <ExampleList
+ key={group.title}
+ name={group.title}
+ list={group.list}
+ selected={selected}
+ onSelectStory={(item, htmlId) => {
+ document.getElementById(htmlId)?.scrollIntoView({
+ block: "center",
+ });
+ updateSelected(item);
+ }}
+ />
+ ))}
+ <hr />
+ </SideBar>
+ {/* <ResizeHandle
+ onUpdate={(x) => {
+ setSidebarWidth((s) => s + x);
+ }}
+ /> */}
+ <Content>
+ <ErrorReport selected={selected}>
+ <PreventLinkNavigation>
+ <GroupWrapper>
+ <ExampleContent />
+ </GroupWrapper>
+ </PreventLinkNavigation>
+ </ErrorReport>
+ </Content>
+ </Page>
+ );
+}
+
+export interface Options {
+ id?: string;
+ strings?: any;
+ getWrapperForGroup?: (name: string) => FunctionComponent;
+}
+
+export function renderStories(
+ groups: Record<string, ComponentOrFolder>,
+ options: Options = {},
+): void {
+ const examples = parseGroupImport(groups);
+
+ try {
+ const cid = options.id ?? "container";
+ const container = document.getElementById(cid);
+ if (!container) {
+ throw Error(
+ `container with id ${cid} not found, can't mount page contents`,
+ );
+ }
+ render(
+ <Application
+ examplesInGroups={examples}
+ getWrapperForGroup={options.getWrapperForGroup ?? (() => Fragment)}
+ langs={options.strings ?? { en: {} }}
+ />,
+ container,
+ );
+ } catch (e) {
+ console.error("got error", e);
+ if (e instanceof Error) {
+ document.body.innerText = `Fatal error: "${e.message}". Please report this bug at https://bugs.gnunet.org/.`;
+ }
+ }
+}
+
+function ResizeHandle({ onUpdate }: { onUpdate: (x: number) => void }): VNode {
+ const [start, setStart] = useState<number | undefined>(undefined);
+ return (
+ <ResizeHandleDiv
+ onMouseDown={(e: any) => {
+ setStart(e.pageX);
+ console.log("active", e.pageX);
+ return false;
+ }}
+ onMouseMove={(e: any) => {
+ if (start !== undefined) {
+ onUpdate(e.pageX - start);
+ }
+ return false;
+ }}
+ onMouseUp={() => {
+ setStart(undefined);
+ return false;
+ }}
+ />
+ );
+}
diff --git a/packages/web-util/src/stories.tsx b/packages/web-util/src/stories.tsx
index d9c2406eb..00e1fb7f9 100644
--- a/packages/web-util/src/stories.tsx
+++ b/packages/web-util/src/stories.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
+ (C) 2022-2024 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
@@ -18,561 +18,24 @@
*
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { setupI18n } from "@gnu-taler/taler-util";
-import {
- ComponentChild,
- ComponentChildren,
- Fragment,
- FunctionalComponent,
- FunctionComponent,
- h,
- JSX,
- render,
- VNode,
-} from "preact";
-import { useEffect, useErrorBoundary, useState } from "preact/hooks";
-import { ExampleItemSetup } from "./tests/hook.js";
+// import { strings } from "./i18n/strings.js";
-const Page: FunctionalComponent = ({ children }): VNode => {
- return (
- <div
- style={{
- fontFamily: "Arial, Helvetica, sans-serif",
- width: "100%",
- display: "flex",
- flexDirection: "row",
- }}
- >
- {children}
- </div>
- );
-};
-
-const SideBar: FunctionalComponent<{ width: number }> = ({
- width,
- children,
-}): VNode => {
- return (
- <div
- style={{
- minWidth: width,
- height: "calc(100vh - 20px)",
- overflowX: "hidden",
- overflowY: "visible",
- scrollBehavior: "smooth",
- }}
- >
- {children}
- </div>
- );
-};
-
-const ResizeHandleDiv: FunctionalComponent<
- JSX.HTMLAttributes<HTMLDivElement>
-> = ({ children, ...props }): VNode => {
- return (
- <div
- {...props}
- style={{
- width: 10,
- backgroundColor: "#ddd",
- cursor: "ew-resize",
- }}
- >
- {children}
- </div>
- );
-};
-
-const Content: FunctionalComponent = ({ children }): VNode => {
- return (
- <div
- style={{
- width: "100%",
- padding: 20,
- }}
- >
- {children}
- </div>
- );
-};
-
-function findByGroupComponentName(
- allExamples: Group[],
- group: string,
- component: string,
- name: string,
-): ExampleItem | undefined {
- const gl = allExamples.filter((e) => e.title === group);
- if (gl.length === 0) {
- return undefined;
- }
- const cl = gl[0].list.filter((l) => l.name === component);
- if (cl.length === 0) {
- return undefined;
- }
- const el = cl[0].examples.filter((c) => c.name === name);
- if (el.length === 0) {
- return undefined;
- }
- return el[0];
-}
-
-function getContentForExample(
- item: ExampleItem | undefined,
- allExamples: Group[],
-): FunctionalComponent {
- if (!item)
- return function SelectExampleMessage() {
- return <div>select example from the list on the left</div>;
- };
- const example = findByGroupComponentName(
- allExamples,
- item.group,
- item.component,
- item.name,
- );
- if (!example) {
- return function ExampleNotFoundMessage() {
- return <div>example not found</div>;
- };
- }
- return () => example.render.component(example.render.props);
-}
-
-function ExampleList({
- name,
- list,
- selected,
- onSelectStory,
-}: {
- name: string;
- list: {
- name: string;
- examples: ExampleItem[];
- }[];
- selected: ExampleItem | undefined;
- onSelectStory: (i: ExampleItem, id: string) => void;
-}): VNode {
- const [isOpen, setOpen] = useState(selected && selected.group === name);
- return (
- <ol style={{ padding: 4, margin: 0 }}>
- <div
- style={{ backgroundColor: "lightcoral", cursor: "pointer" }}
- onClick={() => setOpen(!isOpen)}
- >
- {name}
- </div>
- <div style={{ display: isOpen ? undefined : "none" }}>
- {list.map((k) => (
- <li key={k.name}>
- <dl style={{ margin: 0 }}>
- <dt>{k.name}</dt>
- {k.examples.map((r, i) => {
- const e = encodeURIComponent;
- const eId = `${e(r.group)}-${e(r.component)}-${e(r.name)}`;
- const isSelected =
- selected &&
- selected.component === r.component &&
- selected.group === r.group &&
- selected.name === r.name;
- return (
- <dd
- id={eId}
- key={r.name}
- style={{
- backgroundColor: isSelected
- ? "green"
- : i % 2
- ? "lightgray"
- : "lightblue",
- marginLeft: "1em",
- padding: 4,
- cursor: "pointer",
- borderRadius: 4,
- marginBottom: 4,
- }}
- >
- <a
- href={`#${eId}`}
- style={{ color: "black" }}
- onClick={(e) => {
- e.preventDefault();
- location.hash = `#${eId}`;
- onSelectStory(r, eId);
- history.pushState({}, "", `#${eId}`);
- }}
- >
- {r.name}
- </a>
- </dd>
- );
- })}
- </dl>
- </li>
- ))}
- </div>
- </ol>
- );
-}
-
-/**
- * Prevents the UI from redirecting and inform the dev
- * where the <a /> should have redirected
- * @returns
- */
-function PreventLinkNavigation({
- children,
-}: {
- children: ComponentChildren;
-}): VNode {
- return (
- <div
- onClick={(e) => {
- let t: any = e.target;
- do {
- if (t.localName === "a" && t.getAttribute("href")) {
- alert(`should navigate to: ${t.attributes.href.value}`);
- e.stopImmediatePropagation();
- e.stopPropagation();
- e.preventDefault();
- return false;
- }
- } while ((t = t.parentNode));
- return true;
- }}
- >
- {children}
- </div>
- );
-}
-
-function ErrorReport({
- children,
- selected,
-}: {
- children: ComponentChild;
- selected: ExampleItem | undefined;
-}): VNode {
- const [error, resetError] = useErrorBoundary();
- //if there is an error, reset when unloading this component
- useEffect(() => (error ? resetError : undefined));
- if (error) {
- return (
- <div>
- <p>Error was thrown trying to render</p>
- {selected && (
- <ul>
- <li>
- <b>group</b>: {selected.group}
- </li>
- <li>
- <b>component</b>: {selected.component}
- </li>
- <li>
- <b>example</b>: {selected.name}
- </li>
- <li>
- <b>args</b>:{" "}
- <pre>{JSON.stringify(selected.render.props, undefined, 2)}</pre>
- </li>
- </ul>
- )}
- <p>{error.message}</p>
- <pre>{error.stack}</pre>
- </div>
- );
- }
- return <Fragment>{children}</Fragment>;
-}
-
-function getSelectionFromLocationHash(
- hash: string,
- allExamples: Group[],
-): ExampleItem | undefined {
- if (!hash) return undefined;
- const parts = hash.substring(1).split("-");
- if (parts.length < 3) return undefined;
- return findByGroupComponentName(
- allExamples,
- decodeURIComponent(parts[0]),
- decodeURIComponent(parts[1]),
- decodeURIComponent(parts[2]),
- );
-}
-
-function parseExampleImport(
- group: string,
- componentName: string,
- im: MaybeComponent,
-): ComponentItem {
- const examples: ExampleItem[] = Object.entries(im)
- .filter(([k]) => k !== "default")
- .map(([exampleName, exampleValue]): ExampleItem => {
- if (!exampleValue) {
- throw Error(
- `example "${exampleName}" from component "${componentName}" in group "${group}" is undefined`,
- );
- }
-
- if (typeof exampleValue === "function") {
- return {
- group,
- component: componentName,
- name: exampleName,
- render: {
- component: exampleValue as FunctionComponent,
- props: {},
- contextProps: {},
- },
- };
- }
- const v: any = exampleValue;
- if (
- "component" in v &&
- typeof v.component === "function" &&
- "props" in v
- ) {
- return {
- group,
- component: componentName,
- name: exampleName,
- render: v,
- };
- }
- throw Error(
- `example "${exampleName}" from component "${componentName}" in group "${group}" doesn't follow one of the two ways of example`,
- );
- });
- return {
- name: componentName,
- examples,
- };
-}
+import * as forms from "./forms/index.stories.js";
+import { renderStories } from "./stories-utils.js";
-export function parseGroupImport(
- groups: Record<string, ComponentOrFolder>,
-): Group[] {
- return Object.entries(groups).map(([groupName, value]) => {
- return {
- title: groupName,
- list: Object.entries(value).flatMap(([key, value]) =>
- folder(groupName, value),
- ),
- };
- });
-}
-
-export interface Group {
- title: string;
- list: ComponentItem[];
-}
-
-export interface ComponentItem<Props extends object = {}> {
- name: string;
- examples: ExampleItem<Props>[];
-}
-
-export interface ExampleItem<Props extends object = {}> {
- group: string;
- component: string;
- name: string;
- render: ExampleItemSetup<Props>;
-}
-
-type ComponentOrFolder = MaybeComponent | MaybeFolder;
-interface MaybeFolder {
- default?: { title: string };
- // [exampleName: string]: FunctionalComponent;
-}
-interface MaybeComponent {
- // default?: undefined;
- [exampleName: string]: undefined | object;
-}
-
-function folder(groupName: string, value: ComponentOrFolder): ComponentItem[] {
- let title: string | undefined = undefined;
- try {
- title =
- typeof value === "object" &&
- typeof value.default === "object" &&
- value.default !== undefined &&
- "title" in value.default &&
- typeof value.default.title === "string"
- ? value.default.title
- : undefined;
- } catch (e) {
- throw Error(
- `Could not defined if it is component or folder ${groupName}: ${JSON.stringify(
- value,
- undefined,
- 2,
- )}`,
- );
- }
- if (title) {
- const c = parseExampleImport(groupName, title, value as MaybeComponent);
- return [c];
- }
- return Object.entries(value).flatMap(([subkey, value]) =>
- folder(groupName, value),
- );
-}
-
-interface Props {
- getWrapperForGroup: (name: string) => FunctionComponent;
- examplesInGroups: Group[];
- langs: Record<string, object>;
-}
-
-function Application({
- langs,
- examplesInGroups,
- getWrapperForGroup,
-}: Props): VNode {
- const url = new URL(window.location.href);
- const initialSelection = getSelectionFromLocationHash(
- url.hash,
- examplesInGroups,
- );
-
- const currentLang = url.searchParams.get("lang") || "en";
-
- if (!langs["en"]) {
- langs["en"] = {};
- }
- setupI18n(currentLang, langs);
-
- const [selected, updateSelected] = useState<ExampleItem | undefined>(
- initialSelection,
- );
- const [sidebarWidth, setSidebarWidth] = useState(200);
- useEffect(() => {
- if (url.hash) {
- const hash = url.hash.substring(1);
- const found = document.getElementById(hash);
- if (found) {
- setTimeout(() => {
- found.scrollIntoView({
- block: "center",
- });
- }, 50);
- }
- }
- }, []);
+const TALER_SCREEN_ID = 101;
- const GroupWrapper = getWrapperForGroup(selected?.group || "default");
- const ExampleContent = getContentForExample(selected, examplesInGroups);
-
- //style={{ "--with-size": `${sidebarWidth}px` }}
- return (
- <Page>
- {/* <LiveReload /> */}
- <SideBar width={sidebarWidth}>
- <div>
- Language:
- <select
- value={currentLang}
- onChange={(e) => {
- const url = new URL(window.location.href);
- url.searchParams.set("lang", e.currentTarget.value);
- window.location.href = url.href;
- }}
- >
- {Object.keys(langs).map((l) => (
- <option key={l}>{l}</option>
- ))}
- </select>
- </div>
- {examplesInGroups.map((group) => (
- <ExampleList
- key={group.title}
- name={group.title}
- list={group.list}
- selected={selected}
- onSelectStory={(item, htmlId) => {
- document.getElementById(htmlId)?.scrollIntoView({
- block: "center",
- });
- updateSelected(item);
- }}
- />
- ))}
- <hr />
- </SideBar>
- {/* <ResizeHandle
- onUpdate={(x) => {
- setSidebarWidth((s) => s + x);
- }}
- /> */}
- <Content>
- <ErrorReport selected={selected}>
- <PreventLinkNavigation>
- <GroupWrapper>
- <ExampleContent />
- </GroupWrapper>
- </PreventLinkNavigation>
- </ErrorReport>
- </Content>
- </Page>
+function main(): void {
+ renderStories(
+ { forms },
+ {
+ strings: {},
+ },
);
}
-export interface Options {
- id?: string;
- strings?: any;
- getWrapperForGroup?: (name: string) => FunctionComponent;
-}
-
-export function renderStories(
- groups: Record<string, ComponentOrFolder>,
- options: Options = {},
-): void {
- const examples = parseGroupImport(groups);
-
- try {
- const cid = options.id ?? "container";
- const container = document.getElementById(cid);
- if (!container) {
- throw Error(
- `container with id ${cid} not found, can't mount page contents`,
- );
- }
- render(
- <Application
- examplesInGroups={examples}
- getWrapperForGroup={options.getWrapperForGroup ?? (() => Fragment)}
- langs={options.strings ?? { en: {} }}
- />,
- container,
- );
- } catch (e) {
- console.error("got error", e);
- if (e instanceof Error) {
- document.body.innerText = `Fatal error: "${e.message}". Please report this bug at https://bugs.gnunet.org/.`;
- }
- }
-}
-
-function ResizeHandle({ onUpdate }: { onUpdate: (x: number) => void }): VNode {
- const [start, setStart] = useState<number | undefined>(undefined);
- return (
- <ResizeHandleDiv
- onMouseDown={(e: any) => {
- setStart(e.pageX);
- console.log("active", e.pageX);
- return false;
- }}
- onMouseMove={(e: any) => {
- if (start !== undefined) {
- onUpdate(e.pageX - start);
- }
- return false;
- }}
- onMouseUp={() => {
- setStart(undefined);
- return false;
- }}
- />
- );
+if (document.readyState === "loading") {
+ document.addEventListener("DOMContentLoaded", main);
+} else {
+ main();
}
diff --git a/packages/web-util/src/utils/base64.ts b/packages/web-util/src/utils/base64.ts
index 0e075880f..e51591df6 100644
--- a/packages/web-util/src/utils/base64.ts
+++ b/packages/web-util/src/utils/base64.ts
@@ -14,13 +14,25 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
+import { decodeCrock, encodeCrock } from "@gnu-taler/taler-util";
+
+const utf8Encoder = new TextEncoder();
+const utf8Decoder = new TextDecoder("utf-8", { ignoreBOM: true });
+
+export function encodeCrockForURI(string: string): string {
+ return encodeCrock(utf8Encoder.encode(string));
+}
+
+export function decodeCrockFromURI(enc: string): string {
+ return utf8Decoder.decode(decodeCrock(enc));
+}
export function base64encode(str: string): string {
- return base64EncArr(strToUTF8Arr(str))
+ return base64EncArr(strToUTF8Arr(str));
}
export function base64decode(str: string): string {
- return UTF8ArrToStr(base64DecToArr(str))
+ return UTF8ArrToStr(base64DecToArr(str));
}
// from https://developer.mozilla.org/en-US/docs/Glossary/Base64
@@ -103,7 +115,7 @@ function base64EncArr(aBytes: Uint8Array): string {
uint6ToB64((nUint24 >>> 18) & 63),
uint6ToB64((nUint24 >>> 12) & 63),
uint6ToB64((nUint24 >>> 6) & 63),
- uint6ToB64(nUint24 & 63)
+ uint6ToB64(nUint24 & 63),
);
nUint24 = 0;
}
@@ -114,8 +126,13 @@ function base64EncArr(aBytes: Uint8Array): string {
);
}
-/* UTF-8 array to JS string and vice versa */
-
+/**
+ * UTF-8 array to JS string and vice versa
+ *
+ * @param aBytes
+ * @deprecated use textEncoder
+ * @returns
+ */
function UTF8ArrToStr(aBytes: Uint8Array): string {
let sView = "";
let nPart;
@@ -125,40 +142,46 @@ function UTF8ArrToStr(aBytes: Uint8Array): string {
sView += String.fromCodePoint(
nPart > 251 && nPart < 254 && nIdx + 5 < nLen /* six bytes */
? /* (nPart - 252 << 30) may be not so safe in ECMAScript! So…: */
- (nPart - 252) * 1073741824 +
- ((aBytes[++nIdx] - 128) << 24) +
- ((aBytes[++nIdx] - 128) << 18) +
- ((aBytes[++nIdx] - 128) << 12) +
- ((aBytes[++nIdx] - 128) << 6) +
- aBytes[++nIdx] -
- 128
+ (nPart - 252) * 1073741824 +
+ ((aBytes[++nIdx] - 128) << 24) +
+ ((aBytes[++nIdx] - 128) << 18) +
+ ((aBytes[++nIdx] - 128) << 12) +
+ ((aBytes[++nIdx] - 128) << 6) +
+ aBytes[++nIdx] -
+ 128
: nPart > 247 && nPart < 252 && nIdx + 4 < nLen /* five bytes */
? ((nPart - 248) << 24) +
- ((aBytes[++nIdx] - 128) << 18) +
- ((aBytes[++nIdx] - 128) << 12) +
- ((aBytes[++nIdx] - 128) << 6) +
- aBytes[++nIdx] -
- 128
- : nPart > 239 && nPart < 248 && nIdx + 3 < nLen /* four bytes */
- ? ((nPart - 240) << 18) +
+ ((aBytes[++nIdx] - 128) << 18) +
((aBytes[++nIdx] - 128) << 12) +
((aBytes[++nIdx] - 128) << 6) +
aBytes[++nIdx] -
128
- : nPart > 223 && nPart < 240 && nIdx + 2 < nLen /* three bytes */
- ? ((nPart - 224) << 12) +
+ : nPart > 239 && nPart < 248 && nIdx + 3 < nLen /* four bytes */
+ ? ((nPart - 240) << 18) +
+ ((aBytes[++nIdx] - 128) << 12) +
((aBytes[++nIdx] - 128) << 6) +
aBytes[++nIdx] -
128
+ : nPart > 223 && nPart < 240 && nIdx + 2 < nLen /* three bytes */
+ ? ((nPart - 224) << 12) +
+ ((aBytes[++nIdx] - 128) << 6) +
+ aBytes[++nIdx] -
+ 128
: nPart > 191 && nPart < 224 && nIdx + 1 < nLen /* two bytes */
? ((nPart - 192) << 6) + aBytes[++nIdx] - 128
: /* nPart < 127 ? */ /* one byte */
- nPart
+ nPart,
);
}
return sView;
}
+/**
+ *
+ * @param sDOMStr
+ * @deprecated use textEncoder
+ * @returns
+ */
function strToUTF8Arr(sDOMStr: string): Uint8Array {
let nChr;
const nStrLen = sDOMStr.length;
@@ -168,7 +191,9 @@ function strToUTF8Arr(sDOMStr: string): Uint8Array {
for (let nMapIdx = 0; nMapIdx < nStrLen; nMapIdx++) {
nChr = sDOMStr.codePointAt(nMapIdx);
if (nChr === undefined) {
- throw Error(`No char at ${nMapIdx} on string with length: ${sDOMStr.length}`)
+ throw Error(
+ `No char at ${nMapIdx} on string with length: ${sDOMStr.length}`,
+ );
}
if (nChr >= 0x10000) {
@@ -197,7 +222,9 @@ function strToUTF8Arr(sDOMStr: string): Uint8Array {
while (nIdx < nArrLen) {
nChr = sDOMStr.codePointAt(nChrIdx);
if (nChr === undefined) {
- throw Error(`No char at ${nChrIdx} on string with length: ${sDOMStr.length}`)
+ throw Error(
+ `No char at ${nChrIdx} on string with length: ${sDOMStr.length}`,
+ );
}
if (nChr < 128) {
/* one byte */
diff --git a/packages/web-util/src/utils/http-impl.sw.ts b/packages/web-util/src/utils/http-impl.sw.ts
index 9c820bb4b..2f7f24fd6 100644
--- a/packages/web-util/src/utils/http-impl.sw.ts
+++ b/packages/web-util/src/utils/http-impl.sw.ts
@@ -21,7 +21,7 @@ import {
Duration,
RequestThrottler,
TalerError,
- TalerErrorCode
+ TalerErrorCode,
} from "@gnu-taler/taler-util";
import {
@@ -85,7 +85,9 @@ export class BrowserFetchHttpLib implements HttpRequestLibrary {
}
const myBody: ArrayBuffer | undefined =
- requestMethod === "POST" || requestMethod === "PUT" || requestMethod === "PATCH"
+ requestMethod === "POST" ||
+ requestMethod === "PUT" ||
+ requestMethod === "PATCH"
? encodeBody(requestBody)
: undefined;
@@ -93,8 +95,19 @@ export class BrowserFetchHttpLib implements HttpRequestLibrary {
if (requestHeader) {
Object.entries(requestHeader).forEach(([key, value]) => {
if (value === undefined) return;
- requestHeadersMap[key] = value
- })
+ requestHeadersMap[key] = value;
+ });
+ }
+
+ /**
+ * default header assume everything is json
+ * in case of formData the content-type will be
+ * auto generated
+ */
+ if (requestBody instanceof FormData) {
+ delete requestHeadersMap["Content-Type"]
+ } else if (requestBody instanceof URLSearchParams) {
+ requestHeadersMap["Content-Type"] = "application/x-www-form-urlencoded"
}
const controller = new AbortController();
@@ -106,7 +119,7 @@ export class BrowserFetchHttpLib implements HttpRequestLibrary {
}
if (requestCancel) {
requestCancel.onCancelled(() => {
- controller.abort(TalerErrorCode.GENERIC_CLIENT_INTERNAL_ERROR)
+ controller.abort(TalerErrorCode.GENERIC_CLIENT_INTERNAL_ERROR);
});
}
@@ -116,7 +129,7 @@ export class BrowserFetchHttpLib implements HttpRequestLibrary {
body: myBody,
method: requestMethod,
signal: controller.signal,
- redirect: requestRedirect
+ redirect: requestRedirect,
});
if (timeoutId) {
@@ -127,13 +140,15 @@ export class BrowserFetchHttpLib implements HttpRequestLibrary {
response.headers.forEach((value, key) => {
headerMap.set(key, value);
});
+ const text = makeTextHandler(response, requestUrl, requestMethod);
+ const json = makeJsonHandler(response, requestUrl, requestMethod, text);
return {
headers: headerMap,
status: response.status,
requestMethod,
requestUrl,
- json: makeJsonHandler(response, requestUrl, requestMethod),
- text: makeTextHandler(response, requestUrl, requestMethod),
+ json,
+ text,
bytes: async () => (await response.blob()).arrayBuffer(),
};
} catch (e) {
@@ -143,7 +158,8 @@ export class BrowserFetchHttpLib implements HttpRequestLibrary {
{
requestUrl,
requestMethod,
- timeoutMs: requestTimeout.d_ms === "forever" ? 0 : requestTimeout.d_ms
+ timeoutMs:
+ requestTimeout.d_ms === "forever" ? 0 : requestTimeout.d_ms,
},
`HTTP request failed.`,
);
@@ -151,7 +167,6 @@ export class BrowserFetchHttpLib implements HttpRequestLibrary {
throw e;
}
}
-
}
function makeTextHandler(
@@ -159,20 +174,29 @@ function makeTextHandler(
requestUrl: string,
requestMethod: string,
) {
- return async function getTextFromResponse(): Promise<any> {
- let respText;
- try {
- respText = await response.text();
- } catch (e) {
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
- {
- requestUrl,
- requestMethod,
- httpStatusCode: response.status,
- },
- "Invalid text from HTTP response",
- );
+ let firstTime = true;
+ let respText: string;
+ let error: TalerError | undefined;
+ return async function getTextFromResponse(): Promise<string> {
+ if (firstTime) {
+ firstTime = false;
+ try {
+ respText = await response.text();
+ } catch (e) {
+ error = TalerError.fromDetail(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ {
+ requestUrl,
+ requestMethod,
+ httpStatusCode: response.status,
+ validationError: e instanceof Error ? e.message : String(e),
+ },
+ "Invalid text from HTTP response",
+ );
+ }
+ }
+ if (error !== undefined) {
+ throw error;
}
return respText;
};
@@ -182,35 +206,70 @@ function makeJsonHandler(
response: Response,
requestUrl: string,
requestMethod: string,
+ readTextHandler: () => Promise<string>,
) {
- let responseJson: unknown = undefined;
+ let firstTime = true;
+ let responseJson: string | undefined = undefined;
+ let error: TalerError | undefined;
return async function getJsonFromResponse(): Promise<any> {
- if (responseJson === undefined) {
+ if (firstTime) {
+ let responseText: string;
try {
- responseJson = await response.json();
+ responseText = await readTextHandler();
} catch (e) {
- const message = e instanceof Error ? `Invalid JSON from HTTP response: ${e.message}` : "Invalid JSON from HTTP response"
- throw TalerError.fromDetail(
+ const message =
+ e instanceof Error
+ ? `Couldn't read HTTP response: ${e.message}`
+ : "Couldn't read HTTP response";
+ error = TalerError.fromDetail(
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
{
requestUrl,
requestMethod,
httpStatusCode: response.status,
+ validationError: e instanceof Error ? e.message : String(e),
},
message,
);
}
+ if (!error) {
+ try {
+ // @ts-expect-error no error then text is initialized
+ responseJson = JSON.parse(responseText);
+ } catch (e) {
+ const message =
+ e instanceof Error
+ ? `Invalid JSON from HTTP response: ${e.message}`
+ : "Invalid JSON from HTTP response";
+ error = TalerError.fromDetail(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ {
+ requestUrl,
+ requestMethod,
+ // @ts-expect-error no error then text is initialized
+ response: responseText,
+ httpStatusCode: response.status,
+ validationError: e instanceof Error ? e.message : String(e),
+ },
+ message,
+ );
+ }
+ if (responseJson === null || typeof responseJson !== "object") {
+ error = TalerError.fromDetail(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ {
+ requestUrl,
+ requestMethod,
+ response: JSON.stringify(responseJson),
+ httpStatusCode: response.status,
+ },
+ "Invalid JSON from HTTP response: null or not object",
+ );
+ }
+ }
}
- if (responseJson === null || typeof responseJson !== "object") {
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
- {
- requestUrl,
- requestMethod,
- httpStatusCode: response.status,
- },
- "Invalid JSON from HTTP response: null or not object",
- );
+ if (error !== undefined) {
+ throw error;
}
return responseJson;
};
diff --git a/packages/web-util/src/utils/request.ts b/packages/web-util/src/utils/request.ts
index 944e65945..0c11c8c8a 100644
--- a/packages/web-util/src/utils/request.ts
+++ b/packages/web-util/src/utils/request.ts
@@ -28,8 +28,6 @@ export enum ErrorType {
UNEXPECTED,
}
-
-
/**
*
* @param baseUrl URL where the service is located
@@ -53,7 +51,9 @@ export async function defaultRequestHandler<T>(
}
requestHeaders["Content-Type"] =
- !options.contentType || options.contentType === "json" ? "application/json" : "text/plain";
+ !options.contentType || options.contentType === "json"
+ ? "application/json"
+ : "text/plain";
if (options.talerAmlOfficerSignature) {
requestHeaders["Taler-AML-Officer-Signature"] =
@@ -83,7 +83,7 @@ export async function defaultRequestHandler<T>(
loading: false,
message: `invalid URL: "${baseUrl}${endpoint}"`,
};
- throw new RequestError(error)
+ throw new RequestError(error);
}
Object.entries(requestParams).forEach(([key, value]) => {
@@ -114,7 +114,7 @@ export async function defaultRequestHandler<T>(
loading: false,
message: `unsupported request body type: "${typeof requestBody}"`,
};
- throw new RequestError(error)
+ throw new RequestError(error);
}
}
@@ -159,7 +159,7 @@ export async function defaultRequestHandler<T>(
type: ErrorType.UNEXPECTED,
exception: ex,
loading: false,
- message: (ex instanceof Error ? ex.message : ""),
+ message: ex instanceof Error ? ex.message : "",
};
throw new RequestError(error);
}
@@ -470,9 +470,8 @@ export function buildRequestFailed<ErrorDetail>(
*/
function validateURL(baseUrl: string, endpoint: string): URL | undefined {
try {
- return new URL(`${baseUrl}${endpoint}`)
+ return new URL(`${baseUrl}${endpoint}`);
} catch (ex) {
- return undefined
+ return undefined;
}
-
-} \ No newline at end of file
+}
diff --git a/packages/web-util/src/utils/route.ts b/packages/web-util/src/utils/route.ts
index 494a61efa..fbbbfebd1 100644
--- a/packages/web-util/src/utils/route.ts
+++ b/packages/web-util/src/utils/route.ts
@@ -25,7 +25,13 @@ export type AppLocation = string & {
};
export type EmptyObject = Record<string, never>;
-
+/**
+ * FIXME: receive parameters
+ * maybe return URL for reverse function instead of string
+ * @param pattern
+ * @param reverse
+ * @returns
+ */
export function urlPattern<
T extends Record<string, string | undefined> = EmptyObject,
>(pattern: RegExp, reverse: (p: T) => string): RouteDefinition<T> {
@@ -75,7 +81,7 @@ export function findMatch<T extends ObjectOf<RouteDefinition>>(
pageList: Array<keyof T>,
path: string,
params: Record<string, string[]>,
-): Location<T> | undefined {
+): Location<T> | LocationNotFound<T> {
for (let idx = 0; idx < pageList.length; idx++) {
const name = pageList[idx];
const found = pagesMap[name].pattern.exec(path);
@@ -92,7 +98,8 @@ export function findMatch<T extends ObjectOf<RouteDefinition>>(
return { name, parent: pagesMap, values, params };
}
}
- return undefined;
+ // @ts-expect-error values is a map string which is equivalent to the RouteParamsType
+ return { name: undefined, parent: pagesMap, values: {}, params };
}
/**
@@ -109,13 +116,13 @@ type RouteParamsType<
*/
type MapKeyValue<Type> = {
[Key in keyof Type]: Key extends string
- ? {
- parent: Type;
- name: Key;
- values: RouteParamsType<Type, Key>;
- params: Record<string, string[]>;
- }
- : never;
+ ? {
+ parent: Type;
+ name: Key;
+ values: RouteParamsType<Type, Key>;
+ params: Record<string, string[]>;
+ }
+ : never;
};
/**
@@ -124,3 +131,9 @@ type MapKeyValue<Type> = {
type EnumerationOf<T> = T[keyof T];
export type Location<T> = EnumerationOf<MapKeyValue<T>>;
+export type LocationNotFound<Type> = {
+ parent: Type;
+ name: undefined;
+ values: RouteParamsType<Type, keyof Type>;
+ params: Record<string, string[]>;
+};