taler-typescript-core

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

commit 9f401ff1601181f9476f10b6a42daf1fc372115c
parent a68991fa243dd18b572dd46d702094bcc60d9bcb
Author: Sebastian <sebasjm@gmail.com>
Date:   Mon,  9 Dec 2024 10:50:02 -0300

working on array input

Diffstat:
Mpackages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx | 1-
Mpackages/kyc-ui/src/forms.json | 376++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mpackages/kyc-ui/src/hooks/form.ts | 1+
Mpackages/kyc-ui/src/pages/FillForm.tsx | 88+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Mpackages/kyc-ui/src/pages/TriggerKyc.tsx | 188+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
Mpackages/web-util/src/forms/InputArray.stories.tsx | 81++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------
Mpackages/web-util/src/forms/InputArray.tsx | 83+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
Mpackages/web-util/src/forms/InputLine.tsx | 2+-
Mpackages/web-util/src/forms/forms.ts | 5+++--
9 files changed, 683 insertions(+), 142 deletions(-)

diff --git a/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx b/packages/aml-backoffice-ui/src/pages/ShowConsolidated.tsx @@ -69,7 +69,6 @@ export function ShowConsolidated({ const formConfig: FormConfiguration = { type: "double-column", design: Object.entries(fixed).length > 0 ? [ - { title: i18n.str`KYC collected info`, fields: Object.entries(fixed).map(([key, field]) => { diff --git a/packages/kyc-ui/src/forms.json b/packages/kyc-ui/src/forms.json @@ -12,34 +12,34 @@ "description": "Establishment of the beneficial owner of the assets and/or controlling person", "fields": [ { - "type": "choiceStacked", "id": "LEGAL_ENTITY_TYPE", "label": "The customer is", "required": true, + "type": "choiceStacked", "choices": [ { "label": "a natural person and there are no doubts that this person is the sole beneficial owner of the assets", - "value": "natural" + "value": "NATURAL" }, { "label": "a foundation (or a similar construct; incl. underlying companies)", - "value": "foundation" + "value": "FOUNDATION" }, { "label": "an operation legal entity or partnership", - "value": "legal-entity" + "value": "OPERATIONAL" }, { "label": "a trust (incl. underlying companies)", - "value": "trust" + "value": "TRUST" }, { "label": "a life insurance policy with separately managed accounts/securities accounts", - "value": "insurance" + "value": "LIFEINSURANCE" }, { "label": "all other cases", - "value": "other" + "value": "OTHER" } ] } @@ -66,7 +66,7 @@ "choices": [ { "label": "natural", - "value": "natural" + "value": "NATURAL" } ] } @@ -77,7 +77,7 @@ }, { "label": "VQF legal entity form", - "id": "vqf-legal-entity", + "id": "vqf-operational", "version": 1, "config": { "type": "double-column", @@ -93,7 +93,7 @@ "choices": [ { "label": "legal entity", - "value": "natural" + "value": "NATURAL" } ] } @@ -120,7 +120,7 @@ "choices": [ { "label": "foundation", - "value": "natural" + "value": "NATURAL" } ] } @@ -147,7 +147,7 @@ "choices": [ { "label": "trust", - "value": "natural" + "value": "NATURAL" } ] } @@ -174,7 +174,7 @@ "choices": [ { "label": "insurance", - "value": "natural" + "value": "NATURAL" } ] } @@ -201,9 +201,357 @@ "choices": [ { "label": "other", - "value": "natural" + "value": "NATURAL" + } + ] + } + ] + } + ] + } + }, + { + "label": "GLS Onboarding form", + "id": "gls-onboarding", + "version": 1, + "config": { + "type": "double-column", + "design": [ + { + "title": "Personal information", + "fields": [ + { + "type": "text", + "id": "PERSON_FULL_NAME", + "label": "Full name", + "required": true + }, + { + "type": "text", + "id": "PERSON_LAST_NAME", + "label": "Last name", + "required": true + }, + { + "type": "text", + "id": "CONTACT_PHONE", + "label": "Phone", + "required": true + }, + { + "type": "text", + "id": "CONTACT_EMAIL", + "label": "E-Mail", + "required": true + }, + { + "type": "toggle", + "id": "ACCEPTED_TERMS_OF_SERVICE", + "label": "I accept terms of service", + "required": true + } + ] + }, + { + "title": "Business information", + "fields": [ + { + "type": "text", + "id": "BUSINESS_DISPLAY_NAME", + "label": "Name", + "required": true + }, + { + "type": "selectOne", + "id": "BUSINESS_TYPE", + "label": "The company type is", + "required": true, + "choices": [ + { + "label": "GmbH", + "value": "GMBH" + }, + { + "label": "ug", + "value": "UG" + }, + { + "label": "an operation legal entity or partnership", + "value": "OPERATIONAL" + }, + { + "label": "ohg", + "value": "OHG" + }, + { + "label": "kg", + "value": "KG" + }, + { + "label": "eg", + "value": "EG" + }, + { + "label": "eV", + "value": "EV" + }, + { + "label": "PartG", + "value": "PARTG" + }, + { + "label": "ek", + "value": "EK" + }, + { + "label": "AG (nicht börsennotiert)", + "value": "AGNB" + }, + { + "label": "AG (börsennotiert)", + "value": "AGB" + }, + { + "label": "GbR", + "value": "GBR" + }, + { + "label": "n.e.V.", + "value": "NEV" + }, + { + "label": "Partei", + "value": "PARTEI" + }, + { + "label": "GmbH i.G.", + "value": "GMBHIG" + }, + { + "label": "eG. i.G.", + "value": "EGIG" + }, + { + "label": "e.V. i.G.", + "value": "EVIG" + }, + { + "label": "Other", + "value": "OTHER" } ] + }, + { + "type": "text", + "id": "BUSINESS_REGISTRATION_ID", + "label": "Registration ID", + "required": true + }, + { + "type": "text", + "id": "BUSINESS_LEGAL_JURISDICTION", + "label": "Legal jurisdiction", + "required": true + }, + { + "type": "text", + "id": "BUSINESS_REGISTRATION_DATE", + "label": "Founding date", + "required": true + }, + { + "type": "toggle", + "id": "BUSINESS_IS_NON_PROFIT", + "label": "I this company a non-profit?", + "required": true + } + ] + }, + { + "title": "Business industry", + "fields": [ + { + "type": "selectOne", + "id": "BUSINESS_INDUSTRY", + "label": "Industry", + "choices": [{ + "label": "Car", + "value": "CAR" + },{ + "label": "Food", + "value": "FOOD" + },{ + "description": "preventing... not doing it", + "label": "Money laundry", + "value": "MONEY" + }], + "required": true + } + ] + }, + { + "title": "Adress", + "fields": [ + { + "type": "text", + "id": "ADDRESS_STREET_NAME", + "label": "Street name", + "required": true + }, + { + "type": "text", + "id": "ADDRESS_STREET_NUMBER", + "label": "Street number", + "required": true + }, + { + "type": "text", + "id": "ADDRESS_BUILDING_NAME", + "label": "Building name", + "required": true + }, + { + "type": "text", + "id": "ADDRESS_BUILDING_NUMBER", + "label": "Building number", + "required": true + }, + { + "type": "textArea", + "id": "ADDRESS_LINES", + "label": "Additional address reference", + "required": true + }, + { + "type": "text", + "id": "ADDRESS_TOWN_LOCATION", + "label": "Building number", + "required": true + }, + { + "type": "text", + "id": "ADDRESS_TOWN_DISTRICT", + "label": "Building number", + "required": true + }, + { + "type": "text", + "id": "ADDRESS_COUNTRY_SUBDIVISION", + "label": "Building number", + "required": true + }, + { + "type": "text", + "id": "ADDRESS_COUNTRY_CC", + "label": "Building number", + "required": true + } + ] + }, + { + "title": "Tax information", + "fields": [ + { + "type": "text", + "id": "TAX_COUNTRY_CC", + "label": "Country tax", + "required": true + }, + { + "type": "text", + "id": "TAX_ID", + "label": "Tax identifier", + "required": true + }, + { + "type": "toggle", + "id": "TAX_IS_USA_LAW", + "label": "Is business founded or under USA law?", + "required": true + }, + { + "type": "toggle", + "id": "TAX_IS_ACTIVE", + "label": "Is the business economically active?", + "required": true + }, + { + "type": "toggle", + "id": "TAX_IS_DEDUCTED", + "label": "Is the business economically active", + "required": true + } + ] + }, + { + "title": "Representatives", + "fields": [ + { + "type": "array", + "id": "BUSINESS_LEGAL_REPRESENTATIVES", + "labelFieldId": "CONTACT_PHONE", + "label": "List of natural persons that are legal representatives or shareholders", + "fields": [ + { + "type": "text", + "id": "PERSON_FULL_NAME", + "label": "Name", + "required": true + }, + { + "type": "text", + "id": "PERSON_DATE_OF_BIRTH", + "label": "Date of birth", + "required": true + }, + { + "type": "text", + "id": "CONTACT_PHONE", + "label": "Phone", + "required": true + }, + { + "type": "text", + "id": "CONTACT_EMAIL", + "label": "E-Mail", + "required": true + }, + { + "type": "text", + "id": "PERSON_NATIONALITY_CC", + "label": "Nationality", + "required": true + }, + { + "id": "PERSON_BUSINESS_REPRESENTATIVE_TYPE", + "label": "Full name", + "required": true, + "type": "choiceStacked", + "choices": [ + { + "label": "Individual", + "value": "INDIVIDUAL" + }, + { + "label": "Authorized to represent", + "value": "AUTHORIZED" + }, + { + "label": "Majority", + "value": "MAJORITY" + }, + { + "label": "In pair", + "value": "IN_PAIR" + }, + { + "label": "Other", + "value": "OTHERS" + } + ] + } + ], + "required": true } ] } diff --git a/packages/kyc-ui/src/hooks/form.ts b/packages/kyc-ui/src/hooks/form.ts @@ -92,6 +92,7 @@ function constructFormHandler<T>( const path = fieldId.split("."); function updater(newValue: unknown) { + console.log("----",path, newValue) updateForm(setValueDeeper(form, path, newValue)); } diff --git a/packages/kyc-ui/src/pages/FillForm.tsx b/packages/kyc-ui/src/pages/FillForm.tsx @@ -24,6 +24,7 @@ import { import { Button, FormMetadata, + FormProvider, InternationalizationAPI, LocalNotificationBanner, RenderAllFieldsByUiConfig, @@ -89,13 +90,8 @@ export function FillForm({ : undefined; const { forms } = useUiFormsContext(); - const allForms = customForm ? [...forms, customForm] : forms - const theForm = searchForm( - i18n, - allForms, - formId, - requirement.context, - ); + const allForms = customForm ? [...forms, customForm] : forms; + const theForm = searchForm(i18n, allForms, formId, requirement.context); if (!theForm) { return <div>form with id {formId} not found</div>; } @@ -107,12 +103,13 @@ export function FillForm({ const requiredFields: Array<UIHandlerId> = []; theForm.config.design.forEach((section) => { - Array.prototype.push.apply(shape, getShapeFromFields(section.fields)); + Array.prototype.push.apply(shape, getShapeFromFields(section.fields, "")); Array.prototype.push.apply( requiredFields, getRequiredFields(section.fields), ); }); + const [form, state] = useFormState<FormType>(shape, {}, (st) => { const partialErrors = undefinedIfEmpty<FormErrors<FormType>>({}); @@ -186,43 +183,44 @@ export function FillForm({ <div class="rounded-lg bg-white px-5 py-6 shadow m-4"> <LocalNotificationBanner notification={notification} /> <div class="space-y-10 divide-y -mt-5 divide-gray-900/10"> - {theForm.config.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 class="px-4 sm:px-0"> - <h2 class="text-base font-semibold leading-7 text-gray-900"> - {section.title} - </h2> - {section.description && ( - <p class="mt-1 text-sm leading-6 text-gray-600"> - {section.description} - </p> - )} - </div> - <div class="bg-white shadow-sm ring-1 ring-gray-900/5 rounded-md md:col-span-2"> - <div class="p-3"> - <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6"> - <RenderAllFieldsByUiConfig - key={i} - fields={convertUiField( - i18n, - section.fields, - form, - getConverterById, - )} - /> + {theForm.config.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 class="px-4 sm:px-0"> + <h2 class="text-base font-semibold leading-7 text-gray-900"> + {section.title} + </h2> + {section.description && ( + <p class="mt-1 text-sm leading-6 text-gray-600"> + {section.description} + </p> + )} + </div> + <div class="bg-white shadow-sm ring-1 ring-gray-900/5 rounded-md md:col-span-2"> + <div class="p-3"> + <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6"> + <RenderAllFieldsByUiConfig + key={i} + fields={convertUiField( + i18n, + section.fields, + form, + getConverterById, + )} + /> + </div> </div> </div> </div> - </div> - ); - })} + ); + })} </div> + <pre>{JSON.stringify(state.result, undefined, 2)}</pre> <div class="mt-6 flex items-center justify-end gap-x-6"> <button onClick={onComplete} @@ -262,7 +260,10 @@ function getRequiredFields(fields: UIFormElementConfig[]): Array<UIHandlerId> { }); return shape; } -function getShapeFromFields(fields: UIFormElementConfig[]): Array<UIHandlerId> { +function getShapeFromFields( + fields: UIFormElementConfig[], + parent: string, +): Array<UIHandlerId> { const shape: Array<UIHandlerId> = []; fields.forEach((field) => { if ("id" in field) { @@ -273,7 +274,10 @@ function getShapeFromFields(fields: UIFormElementConfig[]): Array<UIHandlerId> { } shape.push(field.id); } else if (field.type === "group") { - Array.prototype.push.apply(shape, getShapeFromFields(field.fields)); + Array.prototype.push.apply( + shape, + getShapeFromFields(field.fields, parent), + ); } }); return shape; diff --git a/packages/kyc-ui/src/pages/TriggerKyc.tsx b/packages/kyc-ui/src/pages/TriggerKyc.tsx @@ -131,25 +131,25 @@ export function TriggerKyc({ onKycStarted }: Props): VNode { useEffect(() => { if (!kycAccount) return; - const paytoHash = kycAccount + const paytoHash = kycAccount; async function check() { - const {signingKey} = await accountPromise; + const { signingKey } = await accountPromise; const result = await lib.exchange.checkKycStatus(signingKey, paytoHash); if (result.type === "ok") { if (result.body) { - onKycStarted(result.body.access_token) + onKycStarted(result.body.access_token); } else { - console.log("empty body") + console.log("empty body"); } } else { - switch(result.case) { - case HttpStatusCode.Forbidden:{ + switch (result.case) { + case HttpStatusCode.Forbidden: { notify({ type: "error", title: i18n.str`could not create token`, description: i18n.str`access denied`, when: AbsoluteTime.now(), - }) + }); } case HttpStatusCode.NotFound: { notify({ @@ -157,7 +157,7 @@ export function TriggerKyc({ onKycStarted }: Props): VNode { title: i18n.str`could not create token`, description: i18n.str`not found`, when: AbsoluteTime.now(), - }) + }); } case HttpStatusCode.Conflict: { notify({ @@ -165,49 +165,52 @@ export function TriggerKyc({ onKycStarted }: Props): VNode { title: i18n.str`could not create token`, description: i18n.str`conflict`, when: AbsoluteTime.now(), - }) - + }); } } } } - check() + check(); }, [kycAccount]); - const submitHandler = - theForm === undefined || state.status === "fail" - ? undefined - : withErrorHandler( - async () => { - const account = await accountPromise; + function triggerAmount(amount:AmountJson) { + return withErrorHandler( + async () => { + const account = await accountPromise; - return lib.exchange.notifyKycBalanceLimit( - account, - Amounts.stringify(state.result.amount), - ); - }, - (res) => { - notify({ - type: "info", - title: i18n.str`No kyc required`, - when: AbsoluteTime.now(), - }); - }, - (fail) => { - switch (fail.case) { - case HttpStatusCode.Forbidden: - return i18n.str`Access denied trying to test balance.`; - case HttpStatusCode.UnavailableForLegalReasons: - setKycAccount(fail.body.h_payto); - return i18n.str`Unavailable For Legal Reasons`; - default: - assertUnreachable(fail); - } - }, + return lib.exchange.notifyKycBalanceLimit( + account, + Amounts.stringify(amount), ); + }, + (res) => { + notify({ + type: "info", + title: i18n.str`No kyc required`, + when: AbsoluteTime.now(), + }); + }, + (fail) => { + switch (fail.case) { + case HttpStatusCode.Forbidden: + return i18n.str`Access denied trying to test balance.`; + case HttpStatusCode.UnavailableForLegalReasons: + setKycAccount(fail.body.h_payto); + return i18n.str`Unavailable For Legal Reasons`; + default: + assertUnreachable(fail); + } + }, + ) + } + + const sendFormValue = + theForm === undefined || state.status === "fail" + ? undefined + : triggerAmount(state.result.amount); if (kycAccount) { - return <div>loading...</div> + return <div>loading...</div>; } return ( @@ -260,13 +263,114 @@ export function TriggerKyc({ onKycStarted }: Props): VNode { </button> <Button type="submit" - handler={submitHandler} + handler={sendFormValue} // disabled={!submitHandler} class="disabled:opacity-50 disabled:cursor-default rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" > <i18n.Translate>Confirm</i18n.Translate> </Button> </div> + + <div class="grid grid-cols-1 gap-x-8 gap-y-4 "> + <p> + <i18n.Translate> + This actions will trigger wallet balance kyc above 1000000 + threshold, the exchange should be properly configured to trigger the + desired kyc flow. + </i18n.Translate> + </p> + <div> + <Button + type="submit" + handler={triggerAmount(Amounts.parseOrThrow(`${config.currency}:1000070`))} + // disabled={!submitHandler} + class="disabled:opacity-50 disabled:cursor-default rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" + > + <i18n.Translate>Trigger TOPS Terms of service</i18n.Translate> + </Button> + </div> + <div> + <Button + type="submit" + handler={triggerAmount(Amounts.parseOrThrow(`${config.currency}:1000080`))} + // disabled={!submitHandler} + class="disabled:opacity-50 disabled:cursor-default rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" + > + <i18n.Translate>Trigger GLS onboarding</i18n.Translate> + </Button> + </div> + <div> + <Button + type="submit" + handler={triggerAmount(Amounts.parseOrThrow(`${config.currency}:1000000`))} + // disabled={!submitHandler} + class="disabled:opacity-50 disabled:cursor-default rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" + > + <i18n.Translate>Trigger VQF Start</i18n.Translate> + </Button> + </div> + <div> + <Button + type="submit" + handler={triggerAmount(Amounts.parseOrThrow(`${config.currency}:1000010`))} + // disabled={!submitHandler} + class="disabled:opacity-50 disabled:cursor-default rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" + > + <i18n.Translate>Trigger VQF Natural</i18n.Translate> + </Button> + </div> + <div> + <Button + type="submit" + handler={triggerAmount(Amounts.parseOrThrow(`${config.currency}:1000020`))} + // disabled={!submitHandler} + class="disabled:opacity-50 disabled:cursor-default rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" + > + <i18n.Translate>Trigger VQF operational</i18n.Translate> + </Button> + </div> + <div> + <Button + type="submit" + handler={triggerAmount(Amounts.parseOrThrow(`${config.currency}:1000030`))} + // disabled={!submitHandler} + class="disabled:opacity-50 disabled:cursor-default rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" + > + <i18n.Translate>Trigger VQF foundation</i18n.Translate> + </Button> + </div> + <div> + <Button + type="submit" + handler={triggerAmount(Amounts.parseOrThrow(`${config.currency}:1000040`))} + // disabled={!submitHandler} + class="disabled:opacity-50 disabled:cursor-default rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" + > + <i18n.Translate>Trigger VQF insurance</i18n.Translate> + </Button> + </div> + <div> + <Button + type="submit" + handler={triggerAmount(Amounts.parseOrThrow(`${config.currency}:1000050`))} + // disabled={!submitHandler} + class="disabled:opacity-50 disabled:cursor-default rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" + > + <i18n.Translate>Trigger VQF trust</i18n.Translate> + </Button> + </div> + <div> + <Button + type="submit" + handler={triggerAmount(Amounts.parseOrThrow(`${config.currency}:1000060`))} + // disabled={!submitHandler} + class="disabled:opacity-50 disabled:cursor-default rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" + > + <i18n.Translate>Trigger VQF other</i18n.Translate> + </Button> + </div> + + </div> </div> ); } diff --git a/packages/web-util/src/forms/InputArray.stories.tsx b/packages/web-util/src/forms/InputArray.stories.tsx @@ -61,20 +61,21 @@ const form: FlexibleForm_Deprecated<TargetObject> = { { 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, + 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: "people" as UIHandlerId, labelFieldId: "name" as UIHandlerId, }, ], @@ -82,7 +83,57 @@ const form: FlexibleForm_Deprecated<TargetObject> = { ], }; -export const SimpleComment = tests.createExample(TestedComponent, { +export const FormWithArray = tests.createExample(TestedComponent, { initial, form, }); + +const initial2: any = { +}; + +const form2: FlexibleForm_Deprecated<TargetObject> = { + design: [ + { + title: "Personal information" as TranslatedString, + fields: [ + { + type: "text", + id: "PERSON_FULL_NAME" as UIHandlerId, + label: "Full name", + required: true, + }, + ], + }, + { + title: "Representatives" as TranslatedString, + fields: [ + { + type: "array", + id: "BUSINESS_LEGAL_REPRESENTATIVES" as UIHandlerId, + labelFieldId: "PERSON_FULL_NAME" as UIHandlerId, + label: + "List of natural persons that are legal representatives or shareholders", + fields: [ + { + type: "text", + id: "PERSON_FULL_NAME" as UIHandlerId, + label: "Name", + required: true, + }, + { + type: "text", + id: "PERSON_DATE_OF_BIRTH" as UIHandlerId, + label: "Date of birth", + required: true, + }, + ], + }, + ], + }, + ], +}; + +export const NonMixingProperties = tests.createExample(TestedComponent, { + initial: initial2, + form: form2, +}); diff --git a/packages/web-util/src/forms/InputArray.tsx b/packages/web-util/src/forms/InputArray.tsx @@ -5,6 +5,7 @@ import { FormProvider, UIFormProps } from "./FormProvider.js"; import { LabelWithTooltipMaybeRequired } from "./InputLine.js"; import { RenderAllFieldsByUiConfig, UIFormField } from "./forms.js"; import { useField } from "./useField.js"; +import { UIFormElementConfig, UIHandlerId } from "./ui-form.js"; function Option({ label, @@ -79,6 +80,28 @@ export function noHandlerPropsAndNoContextForField( ); } +function getShapeFromFields( + fields: UIFormElementConfig[], +): Array<UIHandlerId> { + const shape: Array<UIHandlerId> = []; + fields.forEach((field) => { + if ("id" in field) { + // FIXME: this should be a validation when loading the form + // consistency check + if (shape.indexOf(field.id) !== -1) { + throw Error(`already present: ${field.id}`); + } + shape.push(field.id); + } else if (field.type === "group") { + Array.prototype.push.apply( + shape, + getShapeFromFields(field.fields), + ); + } + }); + return shape; +} + export function InputArray<T extends object, K extends keyof T>( props: { fields: UIFormField[]; @@ -102,6 +125,16 @@ export function InputArray<T extends object, K extends keyof T>( const selected = selectedIndex === undefined ? undefined : list[selectedIndex]; + const shape: Array<UIHandlerId> = []; + const requiredFields: Array<UIHandlerId> = []; + + Array.prototype.push.apply(shape, getShapeFromFields(fields)); + Array.prototype.push.apply( + requiredFields, + getRequiredFields(fields), + ); + + return ( <div class="sm:col-span-6"> <LabelWithTooltipMaybeRequired @@ -153,36 +186,36 @@ export function InputArray<T extends object, K extends keyof T>( * 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 + // <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); - }} - > + // // 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} /> + <RenderAllFieldsByUiConfig fields={fields} /> </div> </div> - </FormProvider> + // </FormProvider> )} {selectedIndex !== undefined && ( <div class="flex items-center justify-end gap-x-6"> diff --git a/packages/web-util/src/forms/InputLine.tsx b/packages/web-util/src/forms/InputLine.tsx @@ -164,7 +164,7 @@ export function InputLine<T extends object, K extends keyof T>( //FIXME: remove deprecated const fieldCtx = useField<T, K>(props.name); const { value, onChange, state, error } = - props.handler ?? fieldCtx ?? noHandlerPropsAndNoContextForField(props.name); + props.handler ?? noHandlerPropsAndNoContextForField(props.name); // const [text, setText] = useState(""); const fromString: (s: string) => any = diff --git a/packages/web-util/src/forms/forms.ts b/packages/web-util/src/forms/forms.ts @@ -19,7 +19,7 @@ import { UIFieldElementDescription, } from "../index.browser.js"; import { assertUnreachable, TranslatedString } from "@gnu-taler/taler-util"; -import { UIFormFieldBaseConfig, UIFormElementConfig } from "./ui-form.js"; +import { UIFormFieldBaseConfig, UIFormElementConfig, UIHandlerId } from "./ui-form.js"; import { HtmlIframe } from "./HtmlIframe.js"; /** * Constrain the type with the ui props @@ -215,7 +215,7 @@ export function convertUiField( fields: convertUiField( i18n_, config.fields, - form, + (form as any)[config.id].value ?? {}, getConverterById, ), }, @@ -349,6 +349,7 @@ function converInputFieldsProps( getConverterById: GetConverterById, ) { const names = p.id.split("."); + console.log("NAMES", names, getValueDeeper2(form, names), form !== undefined) return { converter: getConverterById(p.converterId, p), handler: getValueDeeper2(form, names),