taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit 567a9617cee2dbb5001040f06a5e40db222d4666
parent 3fd9cb0598913097b159876f1948da00eaa21e1a
Author: Sebastian <sebasjm@gmail.com>
Date:   Thu, 14 Nov 2024 13:37:29 -0300

fix array input

Diffstat:
Apackages/web-util/dev.mjs | 41+++++++++++++++++++++++++++++++++++++++++
Mpackages/web-util/src/forms/InputAbsoluteTime.stories.tsx | 2+-
Mpackages/web-util/src/forms/InputAmount.stories.tsx | 2+-
Mpackages/web-util/src/forms/InputArray.stories.tsx | 19++++++++++++++++---
Mpackages/web-util/src/forms/InputArray.tsx | 184++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Mpackages/web-util/src/forms/InputChoiceHorizontal.stories.tsx | 2+-
Mpackages/web-util/src/forms/InputChoiceStacked.stories.tsx | 2+-
Mpackages/web-util/src/forms/InputFile.stories.tsx | 2+-
Mpackages/web-util/src/forms/InputInteger.stories.tsx | 2+-
Mpackages/web-util/src/forms/InputLine.stories.tsx | 2+-
Mpackages/web-util/src/forms/InputSelectMultiple.stories.tsx | 2+-
Mpackages/web-util/src/forms/InputSelectOne.stories.tsx | 2+-
Mpackages/web-util/src/forms/InputText.stories.tsx | 2+-
Mpackages/web-util/src/forms/InputTextArea.stories.tsx | 2+-
Mpackages/web-util/src/forms/InputToggle.stories.tsx | 2+-
Mpackages/web-util/src/index.browser.ts | 2+-
Mpackages/web-util/src/index.build.ts | 23+++++++++++++++++++++--
Apackages/web-util/src/index.html | 41+++++++++++++++++++++++++++++++++++++++++
Mpackages/web-util/src/serve.ts | 8++++----
Cpackages/web-util/src/stories.tsx -> packages/web-util/src/stories-utils.tsx | 0
Mpackages/web-util/src/stories.tsx | 567+++----------------------------------------------------------------------------
21 files changed, 249 insertions(+), 660 deletions(-)

diff --git a/packages/web-util/dev.mjs b/packages/web-util/dev.mjs @@ -0,0 +1,41 @@ +#!/usr/bin/env node +/* + 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 {initializeDevOnWebUtils} from "./lib/index.build.js"; +import {serve} from "./lib/index.node.cjs"; + +const devEntryPoints = ["src/stories.tsx"]; + +const build = initializeDevOnWebUtils({ + type: "development", + source: { + js: devEntryPoints, + assets: [{ base: "src", files: ["src/index.html"] }], + }, + destination: "./dist/dev", + public: "/app", + css: "postcss", +}); + +await build(); + +serve({ + folder: "./dist/dev", + port: 8080, + source: "./src", + onSourceUpdate: build, +}); diff --git a/packages/web-util/src/forms/InputAbsoluteTime.stories.tsx b/packages/web-util/src/forms/InputAbsoluteTime.stories.tsx @@ -20,7 +20,7 @@ */ 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, diff --git a/packages/web-util/src/forms/InputAmount.stories.tsx b/packages/web-util/src/forms/InputAmount.stories.tsx @@ -20,7 +20,7 @@ */ 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, diff --git a/packages/web-util/src/forms/InputArray.stories.tsx b/packages/web-util/src/forms/InputArray.stories.tsx @@ -20,7 +20,7 @@ */ 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, @@ -56,13 +56,26 @@ const form: FlexibleForm_Deprecated<TargetObject> = { 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: [], + 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: "ame" as UIHandlerId, + labelFieldId: "name" as UIHandlerId, }, ], }, diff --git a/packages/web-util/src/forms/InputArray.tsx b/packages/web-util/src/forms/InputArray.tsx @@ -96,7 +96,9 @@ 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]; @@ -108,98 +110,108 @@ 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 new..." 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> ); } diff --git a/packages/web-util/src/forms/InputChoiceHorizontal.stories.tsx b/packages/web-util/src/forms/InputChoiceHorizontal.stories.tsx @@ -20,7 +20,7 @@ */ 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, diff --git a/packages/web-util/src/forms/InputChoiceStacked.stories.tsx b/packages/web-util/src/forms/InputChoiceStacked.stories.tsx @@ -20,7 +20,7 @@ */ 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, diff --git a/packages/web-util/src/forms/InputFile.stories.tsx b/packages/web-util/src/forms/InputFile.stories.tsx @@ -20,7 +20,7 @@ */ 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, diff --git a/packages/web-util/src/forms/InputInteger.stories.tsx b/packages/web-util/src/forms/InputInteger.stories.tsx @@ -20,7 +20,7 @@ */ 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, diff --git a/packages/web-util/src/forms/InputLine.stories.tsx b/packages/web-util/src/forms/InputLine.stories.tsx @@ -20,7 +20,7 @@ */ 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, diff --git a/packages/web-util/src/forms/InputSelectMultiple.stories.tsx b/packages/web-util/src/forms/InputSelectMultiple.stories.tsx @@ -20,7 +20,7 @@ */ 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, diff --git a/packages/web-util/src/forms/InputSelectOne.stories.tsx b/packages/web-util/src/forms/InputSelectOne.stories.tsx @@ -20,7 +20,7 @@ */ 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, diff --git a/packages/web-util/src/forms/InputText.stories.tsx b/packages/web-util/src/forms/InputText.stories.tsx @@ -20,7 +20,7 @@ */ 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, diff --git a/packages/web-util/src/forms/InputTextArea.stories.tsx b/packages/web-util/src/forms/InputTextArea.stories.tsx @@ -20,7 +20,7 @@ */ 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, diff --git a/packages/web-util/src/forms/InputToggle.stories.tsx b/packages/web-util/src/forms/InputToggle.stories.tsx @@ -20,7 +20,7 @@ */ 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, diff --git a/packages/web-util/src/index.browser.ts b/packages/web-util/src/index.browser.ts @@ -8,4 +8,4 @@ export * from "./context/index.js"; export * from "./components/index.js"; export * from "./forms/index.js"; export { encodeCrockForURI, decodeCrockFromURI } from "./utils/base64.js"; -export { renderStories, parseGroupImport } from "./stories.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 @@ -312,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 @@ -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/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"); diff --git a/packages/web-util/src/stories.tsx b/packages/web-util/src/stories-utils.tsx diff --git 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(); }