taler-typescript-core

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

commit 867049abeacecfbec73094d9963ee57889887260
parent ec9d6255e6b6113e60802a643bc68d9b6c0d8d44
Author: Sebastian <sebasjm@gmail.com>
Date:   Tue, 20 Aug 2024 01:43:44 -0300

first form test

Diffstat:
Mpackages/aml-backoffice-ui/src/ExchangeAmlFrame.tsx | 4++--
Mpackages/kyc-ui/src/Routing.tsx | 29++++++-----------------------
Mpackages/kyc-ui/src/forms/index.ts | 38+++++++++++++++++++++-----------------
Mpackages/kyc-ui/src/forms/personal-info.ts | 85+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Mpackages/kyc-ui/src/pages/FillForm.tsx | 170++++++++++++++++++++++++++++++++++++-------------------------------------------
Mpackages/kyc-ui/src/pages/Start.tsx | 378++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
6 files changed, 389 insertions(+), 315 deletions(-)

diff --git a/packages/aml-backoffice-ui/src/ExchangeAmlFrame.tsx b/packages/aml-backoffice-ui/src/ExchangeAmlFrame.tsx @@ -110,7 +110,7 @@ export function ExchangeAmlFrame({ children, officer, }: { - officer?: OfficerState, + officer?: OfficerState; children?: ComponentChildren; }): VNode { const { i18n } = useTranslationContext(); @@ -133,7 +133,7 @@ export function ExchangeAmlFrame({ }, [error]); const [preferences, updatePreferences] = usePreferences(); - const settings = useUiSettingsContext() + const settings = useUiSettingsContext(); return ( <div diff --git a/packages/kyc-ui/src/Routing.tsx b/packages/kyc-ui/src/Routing.tsx @@ -17,7 +17,7 @@ import { urlPattern, useCurrentLocation, - useNavigationContext + useNavigationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; @@ -44,10 +44,6 @@ const publicPages = { /\/info\/(?<token>[0-9A-Za-z]+)/, ({ token }) => `#/start/${token}`, ), - fillForm: urlPattern<{ token: string, formId: string }>( - /\/fill-form\/(?<token>[0-9A-Za-z]+)\/(?<formId>[0-9A-Za-z\-]+)/, - ({ token, formId }) => `#/fill-form/${token}/${formId}`, - ), }; function safeGetParam( @@ -76,33 +72,20 @@ function PublicRounting(): VNode { switch (location.name) { case undefined: { - return <div>not found</div> + return <div>not found</div>; } case "start": { - const toFillForm = urlPattern<{ formId: string }>( - /\/fill-form\/(?<token>[0-9A-Za-z]+)\/(?<formId>[0-9A-Za-z]+)/, - ({ formId }) => `#/fill-form/${location.values.token}/${formId}`, - ) return ( <Start //FIX: validate token token={location.values.token as AccessToken} - routeFillForm={toFillForm} - onCreated={() => { - navigateTo(publicPages.completed.url({})); - }} - /> - ); - } - case "fillForm": { - return ( - <FillForm - //FIX: validate token - token={location.values.token as AccessToken} - formId={location.values.formId} + // onCreated={() => { + // navigateTo(publicPages.completed.url({})); + // }} /> ); } + case "completed": { return <CallengeCompleted />; } diff --git a/packages/kyc-ui/src/forms/index.ts b/packages/kyc-ui/src/forms/index.ts @@ -13,21 +13,26 @@ 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 { FormMetadata, InternationalizationAPI } from "@gnu-taler/web-util/browser"; +import { + FormMetadata, + InternationalizationAPI, +} from "@gnu-taler/web-util/browser"; import { simplest } from "./simplest.js"; +import { personalInfo } from "./personal-info.js"; -export const preloadedForms: (i18n: InternationalizationAPI) => Array<FormMetadata> = (i18n) => [ - { - label: i18n.str`Simple comment`, - id: "__simple_comment", - version: 1, - config: simplest(i18n), - }, - { - label: i18n.str`Personal info`, - id: "personal-info", - version: 1, - config: simplest(i18n), - }, - -] -\ No newline at end of file +export const preloadedForms: ( + i18n: InternationalizationAPI, +) => Array<FormMetadata> = (i18n) => [ + { + label: i18n.str`Simple comment`, + id: "__simple_comment", + version: 1, + config: simplest(i18n), + }, + { + label: i18n.str`Personal info`, + id: "personal-info", + version: 1, + config: personalInfo(i18n), + }, +]; diff --git a/packages/kyc-ui/src/forms/personal-info.ts b/packages/kyc-ui/src/forms/personal-info.ts @@ -14,34 +14,56 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ - import type { - DoubleColumnForm, - DoubleColumnFormSection, - InternationalizationAPI, - UIHandlerId, - } from "@gnu-taler/web-util/browser"; - - export const personalInfo = (i18n: InternationalizationAPI): DoubleColumnForm => ({ - type: "double-column" as const, - design: [ - { - title: i18n.str`Simple form`, - fields: [ - { - type: "textArea", - id: ".full_name" as UIHandlerId, - name: "full_name", - label: i18n.str`Full Name`, - }, - { - type: "textArea", - id: ".birthdate" as UIHandlerId, - name: "birthdate", - label: i18n.str`Birthdate`, - }, - ], - }, - ], - }); - - -\ No newline at end of file +import type { + DoubleColumnForm, + DoubleColumnFormSection, + InternationalizationAPI, + UIHandlerId, +} from "@gnu-taler/web-util/browser"; + +export const personalInfo = ( + i18n: InternationalizationAPI, +): DoubleColumnForm => ({ + type: "double-column" as const, + design: [ + { + title: i18n.str`Simple form`, + fields: [ + // { + // type: "absoluteTimeText", + // name: "dateOfDeath", + // label: i18n.str`Date of death`, + // pattern: "dd/MM/yyyy", + // // help: i18n.str`if deceased. format 'dd/MM/yyyy'`, + // help: i18n.str`if deceased'`, + // id: ".birthdate" as UIHandlerId, + // }, + { + type: "choiceStacked", + name: "trucker", + id: ".trucker" as UIHandlerId, + required: true, + label: i18n.str`Are you a cross-border truck driver?`, + choices: [ + { + label: i18n.str`Yes`, + value: "yes", + }, + { + label: i18n.str`No`, + value: "no", + }, + ], + }, + { + type: "amount", + id: ".money" as UIHandlerId, + currency: "YEIN", + name: "money", + converterId: "Taler.Amount", + label: i18n.str`How much is in your pockets?`, + }, + ], + }, + ], +}); diff --git a/packages/kyc-ui/src/pages/FillForm.tsx b/packages/kyc-ui/src/pages/FillForm.tsx @@ -16,56 +16,74 @@ import { AbsoluteTime, AccessToken, - AmountJson, - Amounts + HttpStatusCode, + KycRequirementInformation, + assertUnreachable, } from "@gnu-taler/taler-util"; import { Button, - convertUiField, FormMetadata, - getConverterById, InternationalizationAPI, LocalNotificationBanner, RenderAllFieldsByUiConfig, UIFormElementConfig, UIHandlerId, + convertUiField, + getConverterById, useExchangeApiContext, useLocalNotificationHandler, - useTranslationContext + useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { preloadedForms } from "../forms/index.js"; -import { FormErrors, useFormState, validateRequiredFields } from "../hooks/form.js"; +import { + FormErrors, + useFormState, + validateRequiredFields, +} from "../hooks/form.js"; import { undefinedIfEmpty } from "./Start.js"; type Props = { token: AccessToken; formId: string; + requirement: KycRequirementInformation; + onComplete: () => void; }; type FormType = { - when: AbsoluteTime; // state: TalerExchangeApi.AmlState; - threshold: AmountJson; - comment: string; }; -export function FillForm({token, formId}:Props):VNode { +type KycFormMetadata = { + id: string; + version: number; + when: AbsoluteTime; +}; + +type KycForm = { + header: KycFormMetadata; + payload: object; +}; + +export function FillForm({ + token, + formId, + requirement, + onComplete, +}: Props): VNode { const { i18n } = useTranslationContext(); - const { config } = useExchangeApiContext(); + const { config, lib } = useExchangeApiContext(); // const { forms } = useUiFormsContext(); const [notification, withErrorHandler] = useLocalNotificationHandler(); - const initial: FormType = { - when: AbsoluteTime.now(), - threshold: Amounts.zeroOfCurrency(config.currency), - comment: "", - }; - const theForm = searchForm(i18n, [], formId); if (!theForm) { return <div>form with id {formId} not found</div>; } + const reqId = requirement.id; + if (!reqId) { + return <div>no id for this form, can't upload</div>; + } const shape: Array<UIHandlerId> = []; const requiredFields: Array<UIHandlerId> = []; @@ -76,11 +94,8 @@ export function FillForm({token, formId}:Props):VNode { getRequiredFields(section.fields), ); }); - const [form, state] = useFormState<FormType>(shape, initial, (st) => { - const partialErrors = undefinedIfEmpty<FormErrors<FormType>>({ - threshold: !st.threshold ? i18n.str`required` : undefined, - when: !st.when ? i18n.str`required` : undefined, - }); + const [form, state] = useFormState<FormType>(shape, {}, (st) => { + const partialErrors = undefinedIfEmpty<FormErrors<FormType>>({}); const errors = undefinedIfEmpty<FormErrors<FormType> | undefined>( validateRequiredFields(partialErrors, st, requiredFields), @@ -103,58 +118,40 @@ export function FillForm({token, formId}:Props):VNode { const validatedForm = state.status !== "ok" ? undefined : state.result; const submitHandler = - validatedForm === undefined - ? undefined - : withErrorHandler( - async () => { - // const justification: Justification = { - // id: theForm.id, - // label: theForm.label, - // version: theForm.version, - // value: validatedForm, - // }; + validatedForm === undefined + ? undefined + : withErrorHandler( + async () => { + const information: KycForm = { + header: { + id: theForm.id, + version: theForm.version, + when: AbsoluteTime.now(), + }, + payload: validatedForm, + }; - // const decision: Omit<TalerExchangeApi.AmlDecisionRequest, "officer_sig"> = - // { - // justification: JSON.stringify(justification), - // decision_time: TalerProtocolTimestamp.now(), - // h_payto: account, - // keep_investigating: false, - // new_rules: { - // custom_measures: {}, - // expiration_time: { - // t_s: "never" - // }, - // rules: [], - // successor_measure: undefined - // }, - // properties: {}, - // new_measure: undefined, - // }; + return lib.exchange.uploadKycForm(reqId, information); + }, + (res) => { + onComplete(); + }, + (fail) => { + switch (fail.case) { + case HttpStatusCode.PayloadTooLarge: + return i18n.str`The form is too big for the server, try uploading smaller files.`; + case HttpStatusCode.NotFound: + return i18n.str`The account was not found`; + case HttpStatusCode.Conflict: + return i18n.str`Officer disabled or more recent decision was already submitted.`; + default: + assertUnreachable(fail); + } + }, + ); - return { - type: "ok", - body: {} - } - }, - () => { - // window.location.href = privatePages.cases.url({}); - }, - // (fail) => { - // switch (fail.case) { - // case HttpStatusCode.Forbidden: - // return i18n.str`Wrong credentials for "${officer.account}"`; - // case HttpStatusCode.NotFound: - // return i18n.str`The account was not found`; - // case HttpStatusCode.Conflict: - // return i18n.str`Officer disabled or more recent decision was already submitted.`; - // default: - // assertUnreachable(fail); - // } - // }, - ); - - return <Fragment> + return ( + <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) => { @@ -195,12 +192,12 @@ export function FillForm({token, formId}:Props):VNode { </div> <div class="mt-6 flex items-center justify-end gap-x-6"> - <a - // href={privatePages.caseDetails.url({ cid: account })} + <button + onClick={onComplete} class="text-sm font-semibold leading-6 text-gray-900" > <i18n.Translate>Cancel</i18n.Translate> - </a> + </button> <Button type="submit" handler={submitHandler} @@ -210,13 +207,11 @@ export function FillForm({token, formId}:Props):VNode { <i18n.Translate>Confirm</i18n.Translate> </Button> </div> - </Fragment> - + </div> + ); } -function getRequiredFields( - fields: UIFormElementConfig[], -): Array<UIHandlerId> { +function getRequiredFields(fields: UIFormElementConfig[]): Array<UIHandlerId> { const shape: Array<UIHandlerId> = []; fields.forEach((field) => { if ("id" in field) { @@ -230,17 +225,12 @@ function getRequiredFields( } shape.push(field.id); } else if (field.type === "group") { - Array.prototype.push.apply( - shape, - getRequiredFields(field.fields), - ); + Array.prototype.push.apply(shape, getRequiredFields(field.fields)); } }); return shape; } -function getShapeFromFields( - fields: UIFormElementConfig[], -): Array<UIHandlerId> { +function getShapeFromFields(fields: UIFormElementConfig[]): Array<UIHandlerId> { const shape: Array<UIHandlerId> = []; fields.forEach((field) => { if ("id" in field) { @@ -251,10 +241,7 @@ function getShapeFromFields( } 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)); } }); return shape; @@ -275,4 +262,3 @@ function searchForm( } return undefined; } - diff --git a/packages/kyc-ui/src/pages/Start.tsx b/packages/kyc-ui/src/pages/Start.tsx @@ -18,40 +18,37 @@ import { HttpStatusCode, KycRequirementInformation, TalerError, - assertUnreachable + assertUnreachable, } from "@gnu-taler/taler-util"; import { Attention, - FormMetadata, - InternationalizationAPI, Loading, LocalNotificationBanner, - RouteDefinition, useLocalNotificationHandler, - useTranslationContext + useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; +import { useState } from "preact/hooks"; import { ErrorLoadingWithDebug } from "../components/ErrorLoadingWithDebug.js"; -import { preloadedForms } from "../forms/index.js"; import { useKycInfo } from "../hooks/kyc.js"; +import { FillForm } from "./FillForm.js"; type Props = { token: AccessToken; - onCreated: () => void; - routeFillForm: RouteDefinition<{ formId: string }>; }; -export function Start({ +function ShowReqList({ token, - routeFillForm, - onCreated, -}: Props): VNode { + onFormSelected, +}: { + token: AccessToken; + onFormSelected: (r: KycRequirementInformation) => void; +}): VNode { const { i18n } = useTranslationContext(); const [notification, withErrorHandler] = useLocalNotificationHandler(); // const { lib } = useExchangeApiContext(); // const { state, start } = useSessionState(); - const result = useKycInfo({ accessToken: token }) - + const result = useKycInfo({ accessToken: token }); if (!result) { return <Loading />; } @@ -82,37 +79,37 @@ export function Start({ // : undefined, }); - const onStart = - !!errors - ? undefined - : withErrorHandler( - async () => { - return { - type: "ok", - body: {}, - } - // return lib.exchange.uploadKycForm( - // "clientId", - // createRFC8959AccessTokenEncoded(password), - // ); - }, - (ok) => { - // start({ - // nonce: ok.body.nonce, - // clientId, - // redirectURL: url, - // state: encodeCrock(randomBytes(32)), - // }); + // const onStart = + // !!errors + // ? undefined + // : withErrorHandler( + // async () => { + // return { + // type: "ok", + // body: {}, + // } + // // return lib.exchange.uploadKycForm( + // // "clientId", + // // createRFC8959AccessTokenEncoded(password), + // // ); + // }, + // (ok) => { + // // start({ + // // nonce: ok.body.nonce, + // // clientId, + // // redirectURL: url, + // // state: encodeCrock(randomBytes(32)), + // // }); - onCreated(); - }, - // () => { - // // switch (fail.case) { - // // case HttpStatusCode.NotFound: - // // return i18n.str`Client doesn't exist.`; - // // } - // }, - ); + // onCreated(); + // }, + // // () => { + // // // switch (fail.case) { + // // // case HttpStatusCode.NotFound: + // // // return i18n.str`Client doesn't exist.`; + // // // } + // // }, + // ); // const requirements: typeof result.body.requirements = [{ // description: "this is the form description, click to show the form field bla bla bla", @@ -132,30 +129,23 @@ export function Start({ // }] const requirements = result.body.requirements; if (!result.body.requirements.length) { - return <Fragment> - <LocalNotificationBanner notification={notification} /> - <div class="isolate bg-white px-6 py-12"> - <div class="mx-auto max-w-2xl text-center"> - <h2 class="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl"> - <i18n.Translate> - No requirements for this account - </i18n.Translate> - </h2> - </div> - <div class="m-8"> - <Attention - title={i18n.str`Kyc completed`} - type="success" - > - <i18n.Translate> - You can close this now - </i18n.Translate> - </Attention> + return ( + <Fragment> + <LocalNotificationBanner notification={notification} /> + <div class="isolate bg-white px-6 py-12"> + <div class="mx-auto max-w-2xl text-center"> + <h2 class="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl"> + <i18n.Translate>No requirements for this account</i18n.Translate> + </h2> + </div> + <div class="m-8"> + <Attention title={i18n.str`Kyc completed`} type="success"> + <i18n.Translate>You can close this now</i18n.Translate> + </Attention> + </div> </div> - </div> - - - </Fragment> + </Fragment> + ); } return ( <Fragment> @@ -171,107 +161,195 @@ export function Start({ </div> <div class="mt-8"> - <ul role="list" class=" divide-y divide-gray-100 overflow-hidden bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl"> + <ul + role="list" + class=" divide-y divide-gray-100 overflow-hidden bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl" + > {requirements.map((req, idx) => { - return <li key={idx} class="relative flex justify-between gap-x-6 px-4 py-5 hover:bg-gray-50 sm:px-6"> - <RequirementRow requirement={req} routeFillForm={routeFillForm} /> - </li> + return ( + <li + key={idx} + class="relative flex justify-between gap-x-6 px-4 py-5 hover:bg-gray-50 sm:px-6" + > + <RequirementRow + requirement={req} + onFormSelected={() => { + onFormSelected(req); + }} + /> + </li> + ); })} </ul> </div> - </div> </Fragment> ); } +export function Start({ token }: Props): VNode { + const [req, setReq] = useState<KycRequirementInformation>(); + if (!req) { + return <ShowReqList token={token} onFormSelected={(r) => setReq(r)} />; + } + return ( + <FillForm + formId={req.form} + requirement={req} + token={token} + onComplete={() => { + setReq(undefined); + }} + /> + ); +} - -function RequirementRow({ requirement: req, routeFillForm }: { requirement: KycRequirementInformation, routeFillForm: RouteDefinition<{ formId: string }> }): VNode { - const { i18n } = useTranslationContext() +function RequirementRow({ + requirement: req, + onFormSelected, +}: { + requirement: KycRequirementInformation; + onFormSelected: () => void; +}): VNode { + const { i18n } = useTranslationContext(); switch (req.form) { case "INFO": { - return <Fragment> - <div class="flex min-w-0 gap-x-4"> - <div class="inline-block h-10 w-10 rounded-full"> - - <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6"> - <path stroke-linecap="round" stroke-linejoin="round" d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z" /> - </svg> - </div> - <div class="min-w-0 flex-auto"> - <p class="text-sm font-semibold leading-6 text-gray-900"> - <span class="absolute inset-x-0 -top-px bottom-0"></span> - <i18n.Translate>Information</i18n.Translate> - </p> - <p class="mt-1 flex text-xs leading-5 text-gray-500"> - {req.description} - </p> + return ( + <Fragment> + <div class="flex min-w-0 gap-x-4"> + <div class="inline-block h-10 w-10 rounded-full"> + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + class="size-6" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z" + /> + </svg> + </div> + <div class="min-w-0 flex-auto"> + <p class="text-sm font-semibold leading-6 text-gray-900"> + <span class="absolute inset-x-0 -top-px bottom-0"></span> + <i18n.Translate>Information</i18n.Translate> + </p> + <p class="mt-1 flex text-xs leading-5 text-gray-500"> + {req.description} + </p> + </div> </div> - </div> - </Fragment> + </Fragment> + ); } case "LINK": { - return <Fragment> - <div class="flex min-w-0 gap-x-4"> - <div class="inline-block h-10 w-10 rounded-full"> - <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6"> - <path stroke-linecap="round" stroke-linejoin="round" d="M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" /> - </svg> + return ( + <Fragment> + <div class="flex min-w-0 gap-x-4"> + <div class="inline-block h-10 w-10 rounded-full"> + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + class="size-6" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" + /> + </svg> + </div> + <div class="min-w-0 flex-auto"> + <p class="text-sm font-semibold leading-6 text-gray-900"> + <a href="#"> + <span class="absolute inset-x-0 -top-px bottom-0"></span> + <i18n.Translate>Begin KYC process</i18n.Translate> + </a> + </p> + <p class="mt-1 flex text-xs leading-5 text-gray-500"> + {req.description} + </p> + </div> </div> - <div class="min-w-0 flex-auto"> - <p class="text-sm font-semibold leading-6 text-gray-900"> - <a href="#"> - <span class="absolute inset-x-0 -top-px bottom-0"></span> - <i18n.Translate>Begin KYC process</i18n.Translate> - </a> - </p> - <p class="mt-1 flex text-xs leading-5 text-gray-500"> - {req.description} - </p> - </div> - </div> - <div class="flex shrink-0 items-center gap-x-4"> - <div class="hidden sm:flex sm:flex-col sm:items-end"> - <p class="text-sm leading-6 text-gray-900"> - <i18n.Translate>Start</i18n.Translate> - </p> + <div class="flex shrink-0 items-center gap-x-4"> + <div class="hidden sm:flex sm:flex-col sm:items-end"> + <p class="text-sm leading-6 text-gray-900"> + <i18n.Translate>Start</i18n.Translate> + </p> + </div> + <svg + class="h-5 w-5 flex-none text-gray-400" + viewBox="0 0 20 20" + fill="currentColor" + aria-hidden="true" + > + <path + fill-rule="evenodd" + d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" + clip-rule="evenodd" + /> + </svg> </div> - <svg class="h-5 w-5 flex-none text-gray-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> - <path fill-rule="evenodd" d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" clip-rule="evenodd" /> - </svg> - </div> - </Fragment> + </Fragment> + ); } default: { - return <Fragment> - <div class="flex min-w-0 gap-x-4"> - <div class="inline-block h-10 w-10 rounded-full"> - - <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6"> - <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" /> - </svg> + return ( + <Fragment> + <div class="flex min-w-0 gap-x-4"> + <div class="inline-block h-10 w-10 rounded-full"> + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + class="size-6" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" + /> + </svg> + </div> + <div class="min-w-0 flex-auto"> + <p class="text-sm font-semibold leading-6 text-gray-900"> + <button onClick={onFormSelected}> + <span class="absolute inset-x-0 -top-px bottom-0"></span> + <i18n.Translate>Form</i18n.Translate> + </button> + </p> + <p class="mt-1 flex text-xs leading-5 text-gray-500"> + {req.description} + </p> + </div> </div> - <div class="min-w-0 flex-auto"> - <p class="text-sm font-semibold leading-6 text-gray-900"> - <a href={routeFillForm.url({ formId: req.form })}> - <span class="absolute inset-x-0 -top-px bottom-0"></span> - <i18n.Translate>Form</i18n.Translate> - </a> - </p> - <p class="mt-1 flex text-xs leading-5 text-gray-500"> - {req.description} - </p> - </div> - </div> - <div class="flex shrink-0 items-center gap-x-4"> - <div class="hidden sm:flex sm:flex-col sm:items-end"> - <p class="text-sm leading-6 text-gray-900">Fill form</p> + <div class="flex shrink-0 items-center gap-x-4"> + <div class="hidden sm:flex sm:flex-col sm:items-end"> + <p class="text-sm leading-6 text-gray-900">Fill form</p> + </div> + <svg + class="h-5 w-5 flex-none text-gray-400" + viewBox="0 0 20 20" + fill="currentColor" + aria-hidden="true" + > + <path + fill-rule="evenodd" + d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" + clip-rule="evenodd" + /> + </svg> </div> - <svg class="h-5 w-5 flex-none text-gray-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> - <path fill-rule="evenodd" d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" clip-rule="evenodd" /> - </svg> - </div> - </Fragment> + </Fragment> + ); } } } @@ -293,7 +371,9 @@ export function doAutoFocus(element: HTMLElement | null): void { } } -export function undefinedIfEmpty<T extends object | undefined>(obj: T): T | undefined { +export function undefinedIfEmpty<T extends object | undefined>( + obj: T, +): T | undefined { if (obj === undefined) return undefined; return Object.keys(obj).some( (k) => (obj as Record<string, T>)[k] !== undefined,