diff options
author | Sebastian <sebasjm@gmail.com> | 2023-05-11 15:49:28 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2023-05-11 15:49:41 -0300 |
commit | fcfba4322ad0e27d7912fe5fcda6f4f362442e03 (patch) | |
tree | d678b2e271c5e955e88dbba44d52c802ba4a71cd /packages | |
parent | 359b9860a615b8421dcbb72b763b20f3cae24083 (diff) | |
download | wallet-core-fcfba4322ad0e27d7912fe5fcda6f4f362442e03.tar.xz |
fix some ui in arrays and created input file
Diffstat (limited to 'packages')
-rw-r--r-- | packages/exchange-backoffice-ui/src/Dashborad.tsx | 26 | ||||
-rw-r--r-- | packages/exchange-backoffice-ui/src/Form.tsx | 780 | ||||
-rw-r--r-- | packages/exchange-backoffice-ui/src/NiceForm.tsx | 822 | ||||
-rw-r--r-- | packages/exchange-backoffice-ui/src/forms/InputArray.tsx | 191 | ||||
-rw-r--r-- | packages/exchange-backoffice-ui/src/forms/InputFile.tsx | 99 | ||||
-rw-r--r-- | packages/exchange-backoffice-ui/src/forms/forms.ts | 4 |
6 files changed, 1052 insertions, 870 deletions
diff --git a/packages/exchange-backoffice-ui/src/Dashborad.tsx b/packages/exchange-backoffice-ui/src/Dashborad.tsx index 19ea4a31c..a8bac8ece 100644 --- a/packages/exchange-backoffice-ui/src/Dashborad.tsx +++ b/packages/exchange-backoffice-ui/src/Dashborad.tsx @@ -17,7 +17,7 @@ import { MagnifyingGlassIcon, } from "@heroicons/react/20/solid"; import { useRef, useState } from "preact/hooks"; -import { Form } from "./Form.js"; +import { NiceForm } from "./NiceForm.js"; const navigation = [ { name: "Dashboard", href: "#", icon: HomeIcon, current: true }, @@ -41,6 +41,28 @@ function classNames(...classes: string[]) { return classes.filter(Boolean).join(" "); } +/** + * mapping route to view + * not found (error page) + * nested, index element, relative routes + * link interception + * form POST interception, call action + * fromData => Object.fromEntries + * segments in the URL + * navigationState: idle, submitting, loading + * form GET interception: does a navigateTo + * form GET Sync: + * 1.- back after submit: useEffect to sync URL to form + * 2.- refresh after submit: input default value + * useSubmit for form submission onChange, history replace + * + * post form without redirect + * + * + * @param param0 + * @returns + */ + export function Dashboard({ children, }: { @@ -380,7 +402,7 @@ export function Dashboard({ <main class="py-10"> <div class="px-4 sm:px-6 lg:px-8"> <div class="mx-auto max-w-3xl"> - <Form + <NiceForm onUpdate={(v) => { if (!logRef.current) return; logRef.current.innerHTML = JSON.stringify(v, undefined, 1); diff --git a/packages/exchange-backoffice-ui/src/Form.tsx b/packages/exchange-backoffice-ui/src/Form.tsx deleted file mode 100644 index e8c6af369..000000000 --- a/packages/exchange-backoffice-ui/src/Form.tsx +++ /dev/null @@ -1,780 +0,0 @@ -import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h } from "preact"; -import { useRef, useState } from "preact/hooks"; -import { FormProvider, FormState } from "./forms/FormProvider.js"; -import { DoubleColumnForm, RenderAllFieldsByUiConfig } from "./forms/forms.js"; -import { CircleStackIcon } from "@heroicons/react/24/outline"; - -namespace Form902_1e { - interface LegalEntityCustomer { - companyName: string; - domicile: string; - contactPerson: string; - telephone: string; - email: string; - document: string; - } - interface NaturalCustomer { - fullName: string; - address: string; - telephone: string; - email: string; - dateOfBirth: AbsoluteTime; - nationality: string; - document: string; - companyName: string; - office: string; - companyDocument: string; - } - - interface Person { - fullName: string; - address: string; - dateOfBirth: AbsoluteTime; - nationality: string; - typeOfAuthorization: string; - document: string; - powerOfAttorneyArrangements: string; - } - - interface Acceptance { - when: AbsoluteTime; - acceptedBy: "face-to-face" | "authenticated-copy"; - typeOfCorrespondence: string; - language: string[]; - furtherInformation: string; - thirdPartyFullName: string; - thirdPartyAddress: string; - } - - interface Filler { - fullName: string; - when: AbsoluteTime; - } - - interface BeneficialOwner { - establishment: - | "natural-person" - | "foundation" - | "trust" - | "insurance-wrapper" - | "other"; - } - - interface CashTransactions { - typeOfBusiness: "money-exchange" | "money-and-asset-transfer" | "other"; - otherTypeOfBusiness: string; - purpose: string; - } - - // interface Enclosures { - // costumerIdentificationDocuments: string; - // documentOfPersons: string; - - // } - - export interface Form { - filler: Filler; - customerType: "natural" | "legal"; - naturalCustomer: NaturalCustomer; - legalCustomer: LegalEntityCustomer; - businessEstablisher: Array<Person>; - acceptance: Acceptance; - beneficialOwner: BeneficialOwner; - embargoEvaluation: string; - cashTransactions: CashTransactions; - // enclosures: Enclosures; - } -} - -const languageList = [ - { - label: "Mandarin Chinese" as TranslatedString, - value: "cmn", - }, - { - label: "Spanish" as TranslatedString, - value: "spa", - }, - { - label: "English" as TranslatedString, - value: "eng", - }, - { - label: "Hindi" as TranslatedString, - value: "hin", - }, - { - label: "Portuguese" as TranslatedString, - value: "por", - }, - { - label: "Bengali" as TranslatedString, - value: "ben", - }, - { - label: "Russian" as TranslatedString, - value: "rus", - }, - { - label: "Japanese" as TranslatedString, - value: "jpn", - }, - { - label: "Yue" as TranslatedString, - value: "yue", - }, - { - label: "Vietnamese" as TranslatedString, - value: "vie", - }, - { - label: "Turkish" as TranslatedString, - value: "tur", - }, - { - label: "Wu" as TranslatedString, - value: "wuu", - }, - { - label: "Marathi" as TranslatedString, - value: "mar", - }, - { - label: "Telugu" as TranslatedString, - value: "ten", - }, - { - label: "Korean" as TranslatedString, - value: "kor", - }, - { - label: "French" as TranslatedString, - value: "fra", - }, - { - label: "Tamil" as TranslatedString, - value: "tam", - }, - { - label: "Egyptian Arabic" as TranslatedString, - value: "arz", - }, - { - label: "Standard German" as TranslatedString, - value: "deu", - }, - { - label: "Urdu" as TranslatedString, - value: "urd", - }, - { - label: "Javanese" as TranslatedString, - value: "jav", - }, - { - label: "Punjabi" as TranslatedString, - value: "pan", - }, - { - label: "Italian" as TranslatedString, - value: "ita", - }, - { - label: "Gujarati" as TranslatedString, - value: "guj", - }, - { - label: "Iranian Persian" as TranslatedString, - value: "pes", - }, - { - label: "Bhojpuri" as TranslatedString, - value: "bho", - }, - { - label: "Hausa" as TranslatedString, - value: "hau", - }, -]; - -const firstForm: DoubleColumnForm = [ - { - title: "This form was completed by" as TranslatedString, - description: - "The customer has to be identified on entering into a permanent business relationship or on concluding a cash transaction, which meets the according threshold." as TranslatedString, - fields: [ - { - type: "text", - props: { - name: "filler.fullName", - label: "Full name" as TranslatedString, - }, - }, - { - type: "date", - props: { - name: "filler.when", - pattern: "dd/MM/yyyy", - label: "Date" as TranslatedString, - help: "format 'dd/MM/yyyy'" as TranslatedString, - }, - }, - ], - }, - // { - // title: "Information on customer" as TranslatedString, - // description: - // "The customer is the person with whom the member concludes the contract with regard to the financial service provided (civil law). Does the member act as director of a domiciliary company, this domiciliary company is the customer." as TranslatedString, - // fields: [ - // { - // type: "choiceStacked", - // props: { - // name: "customerType", - // label: "Type of customer" as TranslatedString, - // required: true, - // choices: [ - // { - // label: "Natural person" as TranslatedString, - // value: "natural", - // }, - // { - // label: "Legal entity" as TranslatedString, - // value: "legal", - // }, - // ], - // }, - // }, - // { - // type: "text", - // props: { - // name: "naturalCustomer.fullName", - // label: "Full name" as TranslatedString, - // required: true, - // }, - // }, - // { - // type: "text", - // props: { - // name: "naturalCustomer.address", - // label: "Residential address" as TranslatedString, - // required: true, - // }, - // }, - // { - // type: "integer", - // props: { - // name: "naturalCustomer.telephone", - // label: "Telephone" as TranslatedString, - // }, - // }, - // { - // type: "text", - // props: { - // name: "naturalCustomer.email", - // label: "E-mail" as TranslatedString, - // }, - // }, - // { - // type: "text", - // props: { - // name: "naturalCustomer.dateOfBirth", - // label: "Date of birth" as TranslatedString, - // required: true, - // }, - // }, - // { - // type: "text", - // props: { - // name: "naturalCustomer.nationality", - // label: "Nationality" as TranslatedString, - // required: true, - // }, - // }, - // { - // type: "text", - // props: { - // name: "naturalCustomer.document", - // label: "Identification document" as TranslatedString, - // required: true, - // }, - // }, - // { - // type: "text", - // props: { - // name: "naturalCustomer.companyName", - // label: "Company name" as TranslatedString, - // }, - // }, - // { - // type: "text", - // props: { - // name: "naturalCustomer.office", - // label: "Registered office" as TranslatedString, - // }, - // }, - // { - // type: "text", - // props: { - // name: "naturalCustomer.companyDocument", - // label: "Company identification document" as TranslatedString, - // }, - // }, - // { - // type: "text", - // props: { - // name: "legalCustomer.companyName", - // label: "Company name" as TranslatedString, - // required: true, - // }, - // }, - // { - // type: "text", - // props: { - // name: "legalCustomer.domicile", - // label: "Domicile" as TranslatedString, - // required: true, - // }, - // }, - // { - // type: "text", - // props: { - // name: "legalCustomer.contactPerson", - // label: "Contact person" as TranslatedString, - // }, - // }, - // { - // type: "text", - // props: { - // name: "legalCustomer.telephone", - // label: "Telephone" as TranslatedString, - // }, - // }, - // { - // type: "text", - // props: { - // name: "legalCustomer.email", - // label: "E-mail" as TranslatedString, - // }, - // }, - // { - // type: "text", - // props: { - // name: "legalCustomer.document", - // label: "Identification document" as TranslatedString, - // }, - // }, - // ], - // }, - // { - // title: - // "Information on the natural persons who establish the business relationship for legal entities and partnerships" as TranslatedString, - // description: - // "For legal entities and partnerships the identity of the natural persons who establish the business relationship must be verified." as TranslatedString, - // fields: [ - // { - // type: "array", - // props: { - // name: "businessEstablisher", - // label: "Persons" as TranslatedString, - // required: true, - // tooltip: "hola" as TranslatedString, - // placeholder: "this is the placeholder" as TranslatedString, - // fields: [ - // { - // type: "text", - // props: { - // name: "fullName", - // label: "Full name" as TranslatedString, - // required: true, - // }, - // }, - // { - // type: "text", - // props: { - // name: "address", - // label: "Residential address" as TranslatedString, - // required: true, - // }, - // }, - // { - // type: "date", - // props: { - // name: "dateOfBirth", - // label: "Date of birth" as TranslatedString, - // required: true, - // help: "format 'dd/MM/yyyy'" as TranslatedString, - // }, - // }, - // { - // type: "text", - // props: { - // name: "nationality", - // label: "Nationality" as TranslatedString, - // required: true, - // }, - // }, - // { - // type: "text", - // props: { - // name: "typeOfAuthorization", - // label: - // "Type of authorization (signatory of representation)" as TranslatedString, - // required: true, - // }, - // }, - // { - // type: "text", - // props: { - // name: "document", - // label: "Identification document" as TranslatedString, - // required: true, - // }, - // }, - // { - // type: "text", - // props: { - // name: "powerOfAttorneyArrangements", - // label: "Residential address" as TranslatedString, - // required: true, - // }, - // }, - // ], - // labelField: "fullName", - // }, - // }, - // ], - // }, - { - title: "Acceptance of business relationship" as TranslatedString, - fields: [ - { - type: "date", - props: { - name: "acceptance.when", - pattern: "dd/MM/yyyy", - label: "Date (conclusion of contract)" as TranslatedString, - help: "format 'dd/MM/yyyy'" as TranslatedString, - }, - }, - { - type: "choiceStacked", - props: { - name: "acceptance.acceptedBy", - label: "Accepted by" as TranslatedString, - required: true, - choices: [ - { - label: "Face-to-face meeting with customer" as TranslatedString, - value: "face-to-face", - }, - { - label: - "Correspondence: authenticated copy of identification document obtained" as TranslatedString, - value: "correspondence-document", - }, - { - label: - "Correspondence: residential address validated" as TranslatedString, - value: "correspondence-address", - }, - ], - }, - }, - { - type: "choiceStacked", - props: { - name: "acceptance.typeOfCorrespondence", - label: "Type of correspondence" as TranslatedString, - choices: [ - { - label: "to the customer" as TranslatedString, - value: "face-to-face", - }, - { - label: "hold at bank" as TranslatedString, - value: "correspondence-document", - }, - { - label: "to a third party" as TranslatedString, - value: "correspondence-address", - }, - ], - }, - }, - { - type: "text", - props: { - name: "acceptance.thirdPartyFullName", - label: "Third party full name" as TranslatedString, - required: true, - }, - }, - { - type: "text", - props: { - name: "acceptance.thirdPartyAddress", - label: "Third party address" as TranslatedString, - required: true, - }, - }, - { - type: "selectMultiple", - props: { - name: "acceptance.language", - label: "Languages" as TranslatedString, - choices: languageList, - unique: true, - }, - }, - { - type: "textArea", - props: { - name: "acceptance.furtherInformation", - label: "Further information" as TranslatedString, - }, - }, - ], - }, - // { - // title: - // "Information on the beneficial owner of the assets and/or controlling person" as TranslatedString, - // description: - // "Establishment of the beneficial owner of the assets and/or controlling person" as TranslatedString, - // fields: [ - // { - // type: "choiceStacked", - // props: { - // name: "establishment", - // label: "The customer is" as TranslatedString, - // required: true, - // choices: [ - // { - // label: - // "a natural person and there are no doubts that this person is the sole beneficial owner of the assets" as TranslatedString, - // value: "natural", - // }, - // { - // label: - // "a foundation (or a similar construct; incl. underlying companies)" as TranslatedString, - // value: "foundation", - // }, - // { - // label: "a trust (incl. underlying companies)" as TranslatedString, - // value: "trust", - // }, - // { - // label: - // "a life insurance policy with separately managed accounts/securities accounts" as TranslatedString, - // value: "insurance-wrapper", - // }, - // { - // label: "all other cases" as TranslatedString, - // value: "other", - // }, - // ], - // }, - // }, - // ], - // }, - // { - // title: - // "Evaluation with regard to embargo procedures/terrorism lists on establishing the business relationship" as TranslatedString, - // description: - // "Verification whether the customer, beneficial owners of the assets, controlling persons, authorized representatives or other involved persons are listed on an embargo/terrorism list (date of verification/result)" as TranslatedString, - // fields: [ - // { - // type: "textArea", - // props: { - // name: "embargoEvaluation", - // help: "The evaluation must be made at the beginning of the business relationship and has to be repeated in the case of permanent business relationship every time the according lists are updated." as TranslatedString, - // label: "Evaluation" as TranslatedString, - // }, - // }, - // ], - // }, - // { - // title: - // "In the case of cash transactions/occasional customers: Information on type and purpose of business relationship" as TranslatedString, - // description: - // "These details are only necessary for occasional customers, i.e. money exchange, money and asset transfer or other cash transactions provided that no customer profile (VQF doc. No. 902.5) is created" as TranslatedString, - // fields: [ - // { - // type: "choiceStacked", - // props: { - // name: "cashTransactions.typeOfBusiness", - // label: "Type of business relationship" as TranslatedString, - // choices: [ - // { - // label: "Money exchange" as TranslatedString, - // value: "money-exchange", - // }, - // { - // label: "Money and asset transfer" as TranslatedString, - // value: "money-and-asset-transfer", - // }, - // { - // label: - // "Other cash transactions. Specify below" as TranslatedString, - // value: "other", - // }, - // ], - // }, - // }, - // { - // type: "text", - // props: { - // name: "cashTransactions.otherTypeOfBusiness", - // required: true, - // label: "Specify other cash transactions:" as TranslatedString, - // }, - // }, - // { - // type: "textArea", - // props: { - // name: "cashTransactions.purpose", - // label: - // "Purpose of the business relationship (purpose of service requested)" as TranslatedString, - // }, - // }, - // ], - // }, - // { - // title: "Enclosures" as TranslatedString, - // fields: [ - // { - // type: "text", - // props: { - // label: "Customer identification documents" as TranslatedString, - // name: "ASd", - // }, - // }, - // ], - // }, -]; - -function formBehavior(v: Form902_1e.Form): FormState<Form902_1e.Form> { - return { - filler: { - fullName: { - disabled: true, - }, - when: { - disabled: true, - }, - }, - acceptance: { - thirdPartyFullName: { - hidden: v.acceptance?.typeOfCorrespondence !== "correspondence-address", - }, - thirdPartyAddress: { - hidden: v.acceptance?.typeOfCorrespondence !== "correspondence-address", - }, - }, - cashTransactions: { - otherTypeOfBusiness: { - hidden: v.cashTransactions?.typeOfBusiness !== "other", - }, - }, - naturalCustomer: { - fullName: { - hidden: v.customerType !== "natural", - }, - address: { - hidden: v.customerType !== "natural", - }, - telephone: { - hidden: v.customerType !== "natural", - }, - email: { - hidden: v.customerType !== "natural", - }, - dateOfBirth: { - hidden: v.customerType !== "natural", - }, - nationality: { - hidden: v.customerType !== "natural", - }, - document: { - hidden: v.customerType !== "natural", - }, - companyName: { - hidden: v.customerType !== "natural", - }, - office: { - hidden: v.customerType !== "natural", - }, - companyDocument: { - hidden: v.customerType !== "natural", - }, - }, - legalCustomer: { - companyName: { - hidden: v.customerType !== "legal", - }, - contactPerson: { - hidden: v.customerType !== "legal", - }, - document: { - hidden: v.customerType !== "legal", - }, - domicile: { - hidden: v.customerType !== "legal", - }, - email: { - hidden: v.customerType !== "legal", - }, - telephone: { - hidden: v.customerType !== "legal", - }, - }, - }; -} - -export function Form({ onUpdate }: { onUpdate: (d: any) => void }) { - const { i18n } = useTranslationContext(); - const initial: Form902_1e.Form = { - filler: { - fullName: "Sebastian Marchano", - when: { - t_ms: Date.now(), - }, - }, - } as Form902_1e.Form; - return ( - <Fragment> - <FormProvider - initialValue={initial} - onUpdate={onUpdate} - computeFormState={formBehavior} - > - <div class="space-y-10 divide-y -mt-5 divide-gray-900/10"> - {firstForm.map((section) => { - return ( - <div 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 sm:rounded-xl md:col-span-2"> - <div class="px-4 py-6 sm:p-8"> - <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6"> - <RenderAllFieldsByUiConfig fields={section.fields} /> - </div> - </div> - </div> - </div> - ); - })} - </div> - </FormProvider> - </Fragment> - ); -} diff --git a/packages/exchange-backoffice-ui/src/NiceForm.tsx b/packages/exchange-backoffice-ui/src/NiceForm.tsx new file mode 100644 index 000000000..1e096eddb --- /dev/null +++ b/packages/exchange-backoffice-ui/src/NiceForm.tsx @@ -0,0 +1,822 @@ +import { AbsoluteTime, TranslatedString } from "@gnu-taler/taler-util"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, h } from "preact"; +import { useRef, useState } from "preact/hooks"; +import { FormProvider, FormState } from "./forms/FormProvider.js"; +import { DoubleColumnForm, RenderAllFieldsByUiConfig } from "./forms/forms.js"; +import { CircleStackIcon } from "@heroicons/react/24/outline"; + +namespace Form902_1e { + interface LegalEntityCustomer { + companyName: string; + domicile: string; + contactPerson: string; + telephone: string; + email: string; + document: string; + documentAttachment: string; + } + interface NaturalCustomer { + fullName: string; + address: string; + telephone: string; + email: string; + dateOfBirth: AbsoluteTime; + nationality: string; + document: string; + documentAttachment: string; + companyName: string; + office: string; + companyDocument: string; + companyDocumentAttachment: string; + } + + interface Person { + fullName: string; + address: string; + dateOfBirth: AbsoluteTime; + nationality: string; + typeOfAuthorization: string; + document: string; + documentAttachment: string; + powerOfAttorneyArrangements: string; + } + + interface Acceptance { + when: AbsoluteTime; + acceptedBy: "face-to-face" | "authenticated-copy"; + typeOfCorrespondence: string; + language: string[]; + furtherInformation: string; + thirdPartyFullName: string; + thirdPartyAddress: string; + } + + interface Filler { + fullName: string; + when: AbsoluteTime; + } + + interface BeneficialOwner { + establishment: + | "natural-person" + | "foundation" + | "trust" + | "insurance-wrapper" + | "other"; + } + + interface CashTransactions { + typeOfBusiness: "money-exchange" | "money-and-asset-transfer" | "other"; + otherTypeOfBusiness: string; + purpose: string; + } + + export interface Form { + filler: Filler; + customerType: "natural" | "legal"; + naturalCustomer: NaturalCustomer; + legalCustomer: LegalEntityCustomer; + businessEstablisher: Array<Person>; + acceptance: Acceptance; + beneficialOwner: BeneficialOwner; + embargoEvaluation: string; + cashTransactions: CashTransactions; + // enclosures: Enclosures; + } +} + +const languageList = [ + { + label: "Mandarin Chinese" as TranslatedString, + value: "cmn", + }, + { + label: "Spanish" as TranslatedString, + value: "spa", + }, + { + label: "English" as TranslatedString, + value: "eng", + }, + { + label: "Hindi" as TranslatedString, + value: "hin", + }, + { + label: "Portuguese" as TranslatedString, + value: "por", + }, + { + label: "Bengali" as TranslatedString, + value: "ben", + }, + { + label: "Russian" as TranslatedString, + value: "rus", + }, + { + label: "Japanese" as TranslatedString, + value: "jpn", + }, + { + label: "Yue" as TranslatedString, + value: "yue", + }, + { + label: "Vietnamese" as TranslatedString, + value: "vie", + }, + { + label: "Turkish" as TranslatedString, + value: "tur", + }, + { + label: "Wu" as TranslatedString, + value: "wuu", + }, + { + label: "Marathi" as TranslatedString, + value: "mar", + }, + { + label: "Telugu" as TranslatedString, + value: "ten", + }, + { + label: "Korean" as TranslatedString, + value: "kor", + }, + { + label: "French" as TranslatedString, + value: "fra", + }, + { + label: "Tamil" as TranslatedString, + value: "tam", + }, + { + label: "Egyptian Arabic" as TranslatedString, + value: "arz", + }, + { + label: "Standard German" as TranslatedString, + value: "deu", + }, + { + label: "Urdu" as TranslatedString, + value: "urd", + }, + { + label: "Javanese" as TranslatedString, + value: "jav", + }, + { + label: "Punjabi" as TranslatedString, + value: "pan", + }, + { + label: "Italian" as TranslatedString, + value: "ita", + }, + { + label: "Gujarati" as TranslatedString, + value: "guj", + }, + { + label: "Iranian Persian" as TranslatedString, + value: "pes", + }, + { + label: "Bhojpuri" as TranslatedString, + value: "bho", + }, + { + label: "Hausa" as TranslatedString, + value: "hau", + }, +]; + +const form902_1e: DoubleColumnForm = [ + { + title: "This form was completed by" as TranslatedString, + description: + "The customer has to be identified on entering into a permanent business relationship or on concluding a cash transaction, which meets the according threshold." as TranslatedString, + fields: [ + { + type: "text", + props: { + name: "filler.fullName", + label: "Full name" as TranslatedString, + }, + }, + { + type: "date", + props: { + name: "filler.when", + pattern: "dd/MM/yyyy", + label: "Date" as TranslatedString, + help: "format 'dd/MM/yyyy'" as TranslatedString, + }, + }, + ], + }, + { + title: "Information on customer" as TranslatedString, + description: + "The customer is the person with whom the member concludes the contract with regard to the financial service provided (civil law). Does the member act as director of a domiciliary company, this domiciliary company is the customer." as TranslatedString, + fields: [ + { + type: "choiceStacked", + props: { + name: "customerType", + label: "Type of customer" as TranslatedString, + required: true, + choices: [ + { + label: "Natural person" as TranslatedString, + value: "natural", + }, + { + label: "Legal entity" as TranslatedString, + value: "legal", + }, + ], + }, + }, + { + type: "text", + props: { + name: "naturalCustomer.fullName", + label: "Full name" as TranslatedString, + required: true, + }, + }, + { + type: "text", + props: { + name: "naturalCustomer.address", + label: "Residential address" as TranslatedString, + required: true, + }, + }, + { + type: "integer", + props: { + name: "naturalCustomer.telephone", + label: "Telephone" as TranslatedString, + }, + }, + { + type: "text", + props: { + name: "naturalCustomer.email", + label: "E-mail" as TranslatedString, + }, + }, + { + type: "date", + props: { + name: "naturalCustomer.dateOfBirth", + label: "Date of birth" as TranslatedString, + required: true, + }, + }, + { + type: "text", + props: { + name: "naturalCustomer.nationality", + label: "Nationality" as TranslatedString, + required: true, + }, + }, + { + type: "text", + props: { + name: "naturalCustomer.document", + label: "Identification document" as TranslatedString, + required: true, + }, + }, + { + type: "file", + props: { + name: "naturalCustomer.documentAttachment", + label: "Document attachment" as TranslatedString, + required: true, + maxBites: 2 * 1024 * 1024, + accept: ".png", + help: "Max size of 2 mega bytes" as TranslatedString, + }, + }, + { + type: "text", + props: { + name: "naturalCustomer.companyName", + label: "Company name" as TranslatedString, + }, + }, + { + type: "text", + props: { + name: "naturalCustomer.office", + label: "Registered office" as TranslatedString, + }, + }, + { + type: "text", + props: { + name: "naturalCustomer.companyDocument", + label: "Company identification document" as TranslatedString, + }, + }, + { + type: "file", + props: { + name: "naturalCustomer.companyDocumentAttachment", + label: "Document attachment" as TranslatedString, + required: true, + maxBites: 2 * 1024 * 1024, + accept: ".png", + help: "Max size of 2 mega bytes" as TranslatedString, + }, + }, + { + type: "text", + props: { + name: "legalCustomer.companyName", + label: "Company name" as TranslatedString, + required: true, + }, + }, + { + type: "text", + props: { + name: "legalCustomer.domicile", + label: "Domicile" as TranslatedString, + required: true, + }, + }, + { + type: "text", + props: { + name: "legalCustomer.contactPerson", + label: "Contact person" as TranslatedString, + }, + }, + { + type: "text", + props: { + name: "legalCustomer.telephone", + label: "Telephone" as TranslatedString, + }, + }, + { + type: "text", + props: { + name: "legalCustomer.email", + label: "E-mail" as TranslatedString, + }, + }, + { + type: "text", + props: { + name: "legalCustomer.document", + label: "Identification document" as TranslatedString, + }, + }, + { + type: "file", + props: { + name: "legalCustomer.documentAttachment", + label: "Document attachment" as TranslatedString, + required: true, + maxBites: 2 * 1024 * 1024, + accept: ".png", + help: "Max size of 2 mega bytes" as TranslatedString, + }, + }, + ], + }, + { + title: + "Information on the natural persons who establish the business relationship for legal entities and partnerships" as TranslatedString, + description: + "For legal entities and partnerships the identity of the natural persons who establish the business relationship must be verified." as TranslatedString, + fields: [ + { + type: "array", + props: { + name: "businessEstablisher", + label: "Persons" as TranslatedString, + required: true, + tooltip: "hola" as TranslatedString, + placeholder: "this is the placeholder" as TranslatedString, + fields: [ + { + type: "text", + props: { + name: "fullName", + label: "Full name" as TranslatedString, + required: true, + }, + }, + { + type: "text", + props: { + name: "address", + label: "Residential address" as TranslatedString, + required: true, + }, + }, + { + type: "date", + props: { + name: "dateOfBirth", + label: "Date of birth" as TranslatedString, + required: true, + help: "format 'dd/MM/yyyy'" as TranslatedString, + }, + }, + { + type: "text", + props: { + name: "nationality", + label: "Nationality" as TranslatedString, + required: true, + }, + }, + { + type: "text", + props: { + name: "typeOfAuthorization", + label: + "Type of authorization (signatory of representation)" as TranslatedString, + required: true, + }, + }, + { + type: "text", + props: { + name: "document", + label: "Identification document" as TranslatedString, + required: true, + }, + }, + { + type: "file", + props: { + name: "documentAttachment", + label: "Document attachment" as TranslatedString, + required: true, + maxBites: 2 * 1024 * 1024, + accept: ".png", + help: "Max size of 2 mega bytes" as TranslatedString, + }, + }, + { + type: "text", + props: { + name: "powerOfAttorneyArrangements", + label: "Residential address" as TranslatedString, + required: true, + }, + }, + ], + labelField: "fullName", + }, + }, + ], + }, + { + title: "Acceptance of business relationship" as TranslatedString, + fields: [ + { + type: "date", + props: { + name: "acceptance.when", + pattern: "dd/MM/yyyy", + label: "Date (conclusion of contract)" as TranslatedString, + help: "format 'dd/MM/yyyy'" as TranslatedString, + }, + }, + { + type: "choiceStacked", + props: { + name: "acceptance.acceptedBy", + label: "Accepted by" as TranslatedString, + required: true, + choices: [ + { + label: "Face-to-face meeting with customer" as TranslatedString, + value: "face-to-face", + }, + { + label: + "Correspondence: authenticated copy of identification document obtained" as TranslatedString, + value: "correspondence-document", + }, + { + label: + "Correspondence: residential address validated" as TranslatedString, + value: "correspondence-address", + }, + ], + }, + }, + { + type: "choiceStacked", + props: { + name: "acceptance.typeOfCorrespondence", + label: "Type of correspondence" as TranslatedString, + choices: [ + { + label: "to the customer" as TranslatedString, + value: "face-to-face", + }, + { + label: "hold at bank" as TranslatedString, + value: "correspondence-document", + }, + { + label: "to a third party" as TranslatedString, + value: "correspondence-address", + }, + ], + }, + }, + { + type: "text", + props: { + name: "acceptance.thirdPartyFullName", + label: "Third party full name" as TranslatedString, + required: true, + }, + }, + { + type: "text", + props: { + name: "acceptance.thirdPartyAddress", + label: "Third party address" as TranslatedString, + required: true, + }, + }, + { + type: "selectMultiple", + props: { + name: "acceptance.language", + label: "Languages" as TranslatedString, + choices: languageList, + unique: true, + }, + }, + { + type: "textArea", + props: { + name: "acceptance.furtherInformation", + label: "Further information" as TranslatedString, + }, + }, + ], + }, + { + title: + "Information on the beneficial owner of the assets and/or controlling person" as TranslatedString, + description: + "Establishment of the beneficial owner of the assets and/or controlling person" as TranslatedString, + fields: [ + { + type: "choiceStacked", + props: { + name: "establishment", + label: "The customer is" as TranslatedString, + required: true, + choices: [ + { + label: + "a natural person and there are no doubts that this person is the sole beneficial owner of the assets" as TranslatedString, + value: "natural", + }, + { + label: + "a foundation (or a similar construct; incl. underlying companies)" as TranslatedString, + value: "foundation", + }, + { + label: "a trust (incl. underlying companies)" as TranslatedString, + value: "trust", + }, + { + label: + "a life insurance policy with separately managed accounts/securities accounts" as TranslatedString, + value: "insurance-wrapper", + }, + { + label: "all other cases" as TranslatedString, + value: "other", + }, + ], + }, + }, + ], + }, + { + title: + "Evaluation with regard to embargo procedures/terrorism lists on establishing the business relationship" as TranslatedString, + description: + "Verification whether the customer, beneficial owners of the assets, controlling persons, authorized representatives or other involved persons are listed on an embargo/terrorism list (date of verification/result)" as TranslatedString, + fields: [ + { + type: "textArea", + props: { + name: "embargoEvaluation", + help: "The evaluation must be made at the beginning of the business relationship and has to be repeated in the case of permanent business relationship every time the according lists are updated." as TranslatedString, + label: "Evaluation" as TranslatedString, + }, + }, + ], + }, + { + title: + "In the case of cash transactions/occasional customers: Information on type and purpose of business relationship" as TranslatedString, + description: + "These details are only necessary for occasional customers, i.e. money exchange, money and asset transfer or other cash transactions provided that no customer profile (VQF doc. No. 902.5) is created" as TranslatedString, + fields: [ + { + type: "choiceStacked", + props: { + name: "cashTransactions.typeOfBusiness", + label: "Type of business relationship" as TranslatedString, + choices: [ + { + label: "Money exchange" as TranslatedString, + value: "money-exchange", + }, + { + label: "Money and asset transfer" as TranslatedString, + value: "money-and-asset-transfer", + }, + { + label: + "Other cash transactions. Specify below" as TranslatedString, + value: "other", + }, + ], + }, + }, + { + type: "text", + props: { + name: "cashTransactions.otherTypeOfBusiness", + required: true, + label: "Specify other cash transactions:" as TranslatedString, + }, + }, + { + type: "textArea", + props: { + name: "cashTransactions.purpose", + label: + "Purpose of the business relationship (purpose of service requested)" as TranslatedString, + }, + }, + ], + }, +]; + +function formBehavior(v: Form902_1e.Form): FormState<Form902_1e.Form> { + return { + filler: { + fullName: { + disabled: true, + }, + when: { + disabled: true, + }, + }, + acceptance: { + thirdPartyFullName: { + hidden: v.acceptance?.typeOfCorrespondence !== "correspondence-address", + }, + thirdPartyAddress: { + hidden: v.acceptance?.typeOfCorrespondence !== "correspondence-address", + }, + }, + cashTransactions: { + otherTypeOfBusiness: { + hidden: v.cashTransactions?.typeOfBusiness !== "other", + }, + }, + naturalCustomer: { + fullName: { + hidden: v.customerType !== "natural", + }, + address: { + hidden: v.customerType !== "natural", + }, + telephone: { + hidden: v.customerType !== "natural", + }, + email: { + hidden: v.customerType !== "natural", + }, + dateOfBirth: { + hidden: v.customerType !== "natural", + }, + nationality: { + hidden: v.customerType !== "natural", + }, + document: { + hidden: v.customerType !== "natural", + }, + companyName: { + hidden: v.customerType !== "natural", + }, + office: { + hidden: v.customerType !== "natural", + }, + companyDocument: { + hidden: v.customerType !== "natural", + }, + }, + legalCustomer: { + companyName: { + hidden: v.customerType !== "legal", + }, + contactPerson: { + hidden: v.customerType !== "legal", + }, + document: { + hidden: v.customerType !== "legal", + }, + domicile: { + hidden: v.customerType !== "legal", + }, + email: { + hidden: v.customerType !== "legal", + }, + telephone: { + hidden: v.customerType !== "legal", + }, + documentAttachment: { + hidden: v.customerType !== "legal", + }, + }, + }; +} + +export function NiceForm({ onUpdate }: { onUpdate: (d: any) => void }) { + const { i18n } = useTranslationContext(); + const initial: Form902_1e.Form = { + filler: { + fullName: "Sebastian Marchano", + when: { + t_ms: Date.now(), + }, + }, + customerType: "natural", + naturalCustomer: { + fullName: "javier", + }, + businessEstablisher: [ + { + fullName: "sebastian marchano", + }, + ], + } as Form902_1e.Form; + return ( + <Fragment> + <FormProvider + initialValue={initial} + onUpdate={onUpdate} + computeFormState={formBehavior} + > + <div class="space-y-10 divide-y -mt-5 divide-gray-900/10"> + {form902_1e.map((section) => { + return ( + <div 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 fields={section.fields} /> + </div> + </div> + </div> + </div> + ); + })} + </div> + </FormProvider> + </Fragment> + ); +} diff --git a/packages/exchange-backoffice-ui/src/forms/InputArray.tsx b/packages/exchange-backoffice-ui/src/forms/InputArray.tsx index 2447c9989..b2905a2a7 100644 --- a/packages/exchange-backoffice-ui/src/forms/InputArray.tsx +++ b/packages/exchange-backoffice-ui/src/forms/InputArray.tsx @@ -4,6 +4,72 @@ import { FormProvider } from "./FormProvider.js"; import { LabelWithTooltipMaybeRequired, UIFormProps } from "./InputLine.js"; import { RenderAllFieldsByUiConfig, UIFormField } from "./forms.js"; import { useField } from "./useField.js"; +import { TranslatedString } from "@gnu-taler/taler-util"; + +function Option({ + label, + disabled, + isFirst, + isLast, + isSelected, + onClick, +}: { + label: TranslatedString; + isFirst?: boolean; + isLast?: boolean; + isSelected?: boolean; + disabled?: boolean; + onClick: () => void; +}): VNode { + let clazz = "relative flex border p-4 focus:outline-none disabled:text-grey"; + if (isFirst) { + clazz += " rounded-tl-md rounded-tr-md "; + } + if (isLast) { + clazz += " rounded-bl-md rounded-br-md "; + } + if (isSelected) { + clazz += " z-10 border-indigo-200 bg-indigo-50 "; + } else { + clazz += " border-gray-200"; + } + if (disabled) { + clazz += + " cursor-not-allowed bg-gray-50 text-gray-500 ring-gray-200 text-gray"; + } else { + clazz += " cursor-pointer"; + } + return ( + <label class={clazz}> + <input + type="radio" + name="privacy-setting" + checked={isSelected} + disabled={disabled} + onClick={onClick} + class="mt-0.5 h-4 w-4 shrink-0 text-indigo-600 disabled:cursor-not-allowed disabled:bg-gray-50 disabled:text-gray-500 disabled:ring-gray-200 focus:ring-indigo-600" + aria-labelledby="privacy-setting-0-label" + aria-describedby="privacy-setting-0-description" + /> + <span class="ml-3 flex flex-col"> + <span + id="privacy-setting-0-label" + disabled + class="block text-sm font-medium" + > + {label} + </span> + {/* <!-- Checked: "text-indigo-700", Not Checked: "text-gray-500" --> */} + {/* <span + id="privacy-setting-0-description" + class="block text-sm" + > + This project would be available to anyone who has the link + </span> */} + </span> + </label> + ); +} export function InputArray( props: { @@ -17,10 +83,7 @@ export function InputArray( const [selectedIndex, setSelected] = useState<number | undefined>(undefined); const selected = selectedIndex === undefined ? undefined : list[selectedIndex]; - const [subForm, updateSubForm] = useState(selected ?? {}); - useEffect(() => { - updateSubForm(selected); - }, [selected]); + return ( <div class="sm:col-span-6"> <LabelWithTooltipMaybeRequired @@ -29,95 +92,47 @@ export function InputArray( tooltip={tooltip} /> - <div class="flex mb-4 items-center pt-3"> - <div class="flex-auto"> - {selectedIndex !== undefined && ( - <button - type="button" - onClick={() => { - setSelected(undefined); - }} - class="block rounded-md bg-white px-3 py-2 text-center text-sm font-semibold shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50" - > - Cancel - </button> - )} - </div> - <div class="flex-none"> - {selectedIndex === undefined && ( - <button - type="button" - onClick={() => { - setSelected(list.length); - }} - class="block rounded-md bg-indigo-600 px-3 py-2 text-center text-sm text-white shadow-sm hover:bg-indigo-500 " - > - Add - </button> - )} - </div> - </div> <div class="-space-y-px rounded-md bg-white "> {list.map((v, idx) => { - const isFirst = idx === 0; - const isLast = idx === list.length - 1; - const isSelected = selectedIndex === idx; - const disabled = selectedIndex !== undefined && !isSelected; - let clazz = - "relative flex border p-4 focus:outline-none disabled:text-grey"; - if (isFirst) { - clazz += " rounded-tl-md rounded-tr-md "; - } - if (isLast) { - clazz += " rounded-bl-md rounded-br-md "; - } - if (isSelected) { - clazz += " z-10 border-indigo-200 bg-indigo-50 "; - } else { - clazz += " border-gray-200"; - } - if (disabled) { - clazz += - " cursor-not-allowed bg-gray-50 text-gray-500 ring-gray-200 text-gray"; - } else { - clazz += " cursor-pointer"; - } return ( - <label class={clazz}> - <Fragment> - <input - type="radio" - name="privacy-setting" - checked={isSelected} - disabled={disabled} - onClick={() => setSelected(idx)} - class="mt-0.5 h-4 w-4 shrink-0 text-indigo-600 disabled:cursor-not-allowed disabled:bg-gray-50 disabled:text-gray-500 disabled:ring-gray-200 focus:ring-indigo-600" - aria-labelledby="privacy-setting-0-label" - aria-describedby="privacy-setting-0-description" - /> - <span class="ml-3 flex flex-col"> - <span - id="privacy-setting-0-label" - disabled - class="block text-sm font-medium" - > - {v[labelField]} - </span> - {/* <!-- Checked: "text-indigo-700", Not Checked: "text-gray-500" --> */} - {/* <span - id="privacy-setting-0-description" - class="block text-sm" - > - This project would be available to anyone who has the link - </span> */} - </span> - </Fragment> - </label> + <Option + label={v[labelField]} + isSelected={selectedIndex === idx} + isLast={idx === list.length - 1} + disabled={selectedIndex !== undefined && selectedIndex !== idx} + isFirst={idx === 0} + onClick={() => { + setSelected(selectedIndex === idx ? undefined : idx); + }} + /> ); })} + <div class="pt-2"> + <Option + label={"Add..." as TranslatedString} + isSelected={selectedIndex === list.length} + isLast + isFirst + disabled={ + selectedIndex !== undefined && selectedIndex !== list.length + } + onClick={() => { + setSelected( + selectedIndex === list.length ? undefined : list.length, + ); + }} + /> + </div> </div> {selectedIndex !== undefined && ( - <FormProvider initialValue={subForm} onUpdate={updateSubForm}> + <FormProvider + initialValue={selected} + onUpdate={(v) => { + const newValue = [...list]; + newValue.splice(selectedIndex, 1, v); + onChange(newValue); + }} + > <div class="px-4 py-6"> <div class="grid grid-cols-1 gap-y-8 "> <RenderAllFieldsByUiConfig fields={fields} /> @@ -143,7 +158,7 @@ export function InputArray( </button> )} </div> - <div class="flex-none"> + {/* <div class="flex-none"> <button type="button" onClick={() => { @@ -156,7 +171,7 @@ export function InputArray( > Confirm </button> - </div> + </div> */} </div> )} </div> diff --git a/packages/exchange-backoffice-ui/src/forms/InputFile.tsx b/packages/exchange-backoffice-ui/src/forms/InputFile.tsx new file mode 100644 index 000000000..749c8b264 --- /dev/null +++ b/packages/exchange-backoffice-ui/src/forms/InputFile.tsx @@ -0,0 +1,99 @@ +import { VNode, h } from "preact"; +import { + InputLine, + LabelWithTooltipMaybeRequired, + UIFormProps, +} from "./InputLine.js"; +import { useField } from "./useField.js"; + +export function InputFile( + props: { maxBites: number; accept?: string } & UIFormProps<string>, +): VNode { + const { + name, + label, + placeholder, + tooltip, + required, + help, + maxBites, + accept, + } = props; + const { value, onChange } = useField<{ [s: string]: string }>(name); + + return ( + <div class="col-span-full"> + <LabelWithTooltipMaybeRequired + label={label} + tooltip={tooltip} + required={required} + /> + {!value || !value.startsWith("data:image/") ? ( + <div class="mt-2 flex justify-center rounded-lg border border-dashed border-gray-900/25 py-1"> + <div class="text-center"> + <svg + class="mx-auto h-12 w-12 text-gray-300" + viewBox="0 0 24 24" + fill="currentColor" + aria-hidden="true" + > + <path + fill-rule="evenodd" + d="M1.5 6a2.25 2.25 0 012.25-2.25h16.5A2.25 2.25 0 0122.5 6v12a2.25 2.25 0 01-2.25 2.25H3.75A2.25 2.25 0 011.5 18V6zM3 16.06V18c0 .414.336.75.75.75h16.5A.75.75 0 0021 18v-1.94l-2.69-2.689a1.5 1.5 0 00-2.12 0l-.88.879.97.97a.75.75 0 11-1.06 1.06l-5.16-5.159a1.5 1.5 0 00-2.12 0L3 16.061zm10.125-7.81a1.125 1.125 0 112.25 0 1.125 1.125 0 01-2.25 0z" + clip-rule="evenodd" + /> + </svg> + <div class="my-2 flex text-sm leading-6 text-gray-600"> + <label + for="file-upload" + class="relative cursor-pointer rounded-md bg-white font-semibold text-indigo-600 focus-within:outline-none focus-within:ring-2 focus-within:ring-indigo-600 focus-within:ring-offset-2 hover:text-indigo-500" + > + <span>Upload a file</span> + <input + id="file-upload" + name="file-upload" + type="file" + class="sr-only" + accept={accept} + onChange={(e) => { + const f: FileList | null = e.currentTarget.files; + if (!f || f.length != 1) { + return onChange(undefined!); + } + if (f[0].size > maxBites) { + return onChange(undefined!); + } + return f[0].arrayBuffer().then((b) => { + const b64 = window.btoa( + new Uint8Array(b).reduce( + (data, byte) => data + String.fromCharCode(byte), + "", + ), + ); + return onChange(`data:${f[0].type};base64,${b64}` as any); + }); + }} + /> + </label> + {/* <p class="pl-1">or drag and drop</p> */} + </div> + </div> + </div> + ) : ( + <div class="mt-2 flex justify-center rounded-lg border border-dashed border-gray-900/25 relative"> + <img src={value} class=" h-24 w-full object-cover relative" /> + + <div + class="opacity-0 hover:opacity-70 duration-300 absolute rounded-lg border inset-0 z-10 flex justify-center text-xl items-center bg-black text-white cursor-pointer " + onClick={() => { + onChange(undefined!); + }} + > + Clear + </div> + </div> + )} + {help && <p class="text-xs leading-5 text-gray-600 mt-2">{help}</p>} + </div> + ); +} diff --git a/packages/exchange-backoffice-ui/src/forms/forms.ts b/packages/exchange-backoffice-ui/src/forms/forms.ts index 1e59e0cf2..c2cb0799e 100644 --- a/packages/exchange-backoffice-ui/src/forms/forms.ts +++ b/packages/exchange-backoffice-ui/src/forms/forms.ts @@ -7,6 +7,7 @@ import { InputChoiceStacked } from "./InputChoice.js"; import { InputArray } from "./InputArray.js"; import { InputSelectMultiple } from "./InputSelectMultiple.js"; import { InputTextArea } from "./InputTextArea.js"; +import { InputFile } from "./InputFile.js"; export type DoubleColumnForm = DoubleColumnFormSection[]; @@ -22,6 +23,7 @@ type DoubleColumnFormSection = { type FieldType = { separator: {}; array: Parameters<typeof InputArray>[0]; + file: Parameters<typeof InputFile>[0]; selectMultiple: Parameters<typeof InputSelectMultiple>[0]; text: Parameters<typeof InputText>[0]; textArea: Parameters<typeof InputTextArea>[0]; @@ -36,6 +38,7 @@ type FieldType = { export type UIFormField = | { type: "separator"; props: FieldType["separator"] } | { type: "array"; props: FieldType["array"] } + | { type: "file"; props: FieldType["file"] } | { type: "selectMultiple"; props: FieldType["selectMultiple"] } | { type: "text"; props: FieldType["text"] } | { type: "textArea"; props: FieldType["textArea"] } @@ -62,6 +65,7 @@ const UIFormConfiguration: UIFormFieldMap = { separator: Separator, array: InputArray, text: InputText, + file: InputFile, textArea: InputTextArea, date: InputDate, choiceStacked: InputChoiceStacked, |