diff options
Diffstat (limited to 'packages/aml-backoffice-ui/src/pages')
-rw-r--r-- | packages/aml-backoffice-ui/src/pages/Cases.tsx | 42 | ||||
-rw-r--r-- | packages/aml-backoffice-ui/src/pages/CreateAccount.tsx | 252 |
2 files changed, 219 insertions, 75 deletions
diff --git a/packages/aml-backoffice-ui/src/pages/Cases.tsx b/packages/aml-backoffice-ui/src/pages/Cases.tsx index e928b831f..6b59b2736 100644 --- a/packages/aml-backoffice-ui/src/pages/Cases.tsx +++ b/packages/aml-backoffice-ui/src/pages/Cases.tsx @@ -20,19 +20,20 @@ import { assertUnreachable, } from "@gnu-taler/taler-util"; import { + Attention, ErrorLoading, Loading, createNewForm, useTranslationContext, } from "@gnu-taler/web-util/browser"; -import { VNode, h } from "preact"; +import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { useCases } from "../hooks/useCases.js"; +import { privatePages } from "../Routing.js"; import { amlStateConverter } from "../utils/converter.js"; import { AmlExchangeBackend } from "../utils/types.js"; import { Officer } from "./Officer.js"; -import { privatePages } from "../Routing.js"; export function CasesUI({ records, @@ -130,7 +131,9 @@ export function CasesUI({ <td class="whitespace-nowrap px-3 py-5 text-sm text-gray-500 "> <div class="text-gray-900"> <a - href={privatePages.caseDetails.url({ cid: r.h_payto })} + href={privatePages.caseDetails.url({ + cid: r.h_payto, + })} class="text-indigo-600 hover:text-indigo-900" > {r.h_payto.substring(0, 16)}... @@ -187,6 +190,7 @@ export function Cases() { ); const list = useCases(stateFilter); + const { i18n } = useTranslationContext(); if (!list) { return <Loading />; @@ -197,8 +201,36 @@ export function Cases() { if (list.type === "fail") { switch (list.case) { - case HttpStatusCode.Unauthorized: - case HttpStatusCode.Forbidden: + case HttpStatusCode.Forbidden: { + return ( + <Fragment> + <Attention + type="danger" + title={i18n.str`Operation denied`} + > + <i18n.Translate> + This account doesnt have access. Request account activation sending your public key. + </i18n.Translate> + </Attention> + <Officer /> + </Fragment> + ); + } + case HttpStatusCode.Unauthorized: { + return ( + <Fragment> + <Attention + type="danger" + title={i18n.str`Operation denied`} + > + <i18n.Translate> + This account is not allowed to perform list the cases. + </i18n.Translate> + </Attention> + <Officer /> + </Fragment> + ); + } case HttpStatusCode.NotFound: case HttpStatusCode.Conflict: return <Officer />; diff --git a/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx b/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx index 9afd0d212..094e78531 100644 --- a/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx +++ b/packages/aml-backoffice-ui/src/pages/CreateAccount.tsx @@ -13,29 +13,113 @@ 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 { TranslatedString } from "@gnu-taler/taler-util"; import { - createNewForm, - notifyError, + Button, + InternationalizationAPI, + LocalNotificationBanner, + ShowInputErrorLabel, + useLocalNotificationHandler, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { VNode, h } from "preact"; +import { + FormErrors, + FormStatus, + FormValues, + RecursivePartial, + useFormState, +} from "../hooks/form.js"; +import { useOfficer } from "../hooks/officer.js"; import { usePreferences } from "../hooks/preferences.js"; +type FormType = { + password: string; + repeat: string; +}; +function createFormValidator( + i18n: InternationalizationAPI, + allowInsecurePassword: boolean, +) { + return function check( + state: RecursivePartial<FormValues<FormType>>, + ): FormStatus<FormType> { + const errors = undefinedIfEmpty<FormErrors<FormType>>({ + password: !state.password + ? i18n.str`required` + : allowInsecurePassword + ? undefined + : state.password.length < 8 + ? i18n.str`should have at least 8 characters` + : !state.password.match(/[a-z]/) && state.password.match(/[A-Z]/) + ? i18n.str`should have lowercase and uppercase characters` + : !state.password.match(/\d/) + ? i18n.str`should have numbers` + : !state.password.match(/[^a-zA-Z\d]/) + ? i18n.str`should have at least one character which is not a number or letter` + : undefined, + + repeat: !state.repeat + ? i18n.str`required` + : state.password !== state.repeat + ? i18n.str`doesn't match` + : undefined, + }); + + if (errors === undefined) { + return { + status: "ok", + result: state as FormType, + errors, + }; + } + return { + status: "fail", + result: state, + errors, + }; + }; +} + +export function undefinedIfEmpty<T extends object>(obj: T): T | undefined { + return Object.keys(obj).some( + (k) => (obj as Record<string, T>)[k] !== undefined, + ) + ? obj + : undefined; +} + export function CreateAccount({ onNewAccount, }: { - onNewAccount: (password: string) => void; + onNewAccount: () => void; }): VNode { const { i18n } = useTranslationContext(); - const Form = createNewForm<{ - password: string; - repeat: string; - }>(); + // const Form = createNewForm<FormType>(); const [settings] = usePreferences(); - + const officer = useOfficer(); + + const [notification, withErrorHandler] = useLocalNotificationHandler(); + + const [form, status] = useFormState<FormType>( + { + password: undefined, + repeat: undefined, + }, + createFormValidator(i18n, settings.allowInsecurePassword), + ); + + const createAccountHandler = + status.status === "fail" || officer.state !== "not-found" + ? undefined + : withErrorHandler( + async () => officer.create(form.password!.value!), + onNewAccount, + ); + return ( <div class="flex min-h-full flex-col "> + <LocalNotificationBanner notification={notification} /> + <div class="sm:mx-auto sm:w-full sm:max-w-md"> <h2 class="mt-6 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900"> <i18n.Translate>Create account</i18n.Translate> @@ -44,79 +128,107 @@ export function CreateAccount({ <div class="mt-10 sm:mx-auto sm:w-full sm:max-w-[480px] "> <div class="bg-gray-100 px-6 py-6 shadow sm:rounded-lg sm:px-12"> - <Form.Provider - computeFormState={(v) => { - return { - password: { - error: !v.password - ? i18n.str`required` - : settings.allowInsecurePassword - ? undefined - : v.password.length < 8 - ? i18n.str`should have at least 8 characters` - : !v.password.match(/[a-z]/) && - v.password.match(/[A-Z]/) - ? i18n.str`should have lowercase and uppercase characters` - : !v.password.match(/\d/) - ? i18n.str`should have numbers` - : !v.password.match(/[^a-zA-Z\d]/) - ? i18n.str`should have at least one character which is not a number or letter` - : undefined, - }, - repeat: { - error: !v.repeat - ? i18n.str`required` - : v.repeat !== v.password - ? i18n.str`doesn't match` - : undefined, - }, - }; - }} - onSubmit={async (v, s) => { - const error = s?.password?.error ?? s?.repeat?.error; - if (error) { - notifyError( - i18n.str`Can't create account`, - error as TranslatedString, - ); - } else { - onNewAccount(v.password!); - } + <form + class="space-y-6" + noValidate + onSubmit={(e) => { + e.preventDefault(); }} + autoCapitalize="none" + autoCorrect="off" > - <div class="mb-4"> - <Form.InputLine - label={i18n.str`Password`} - name="password" - type="password" - help={ - settings.allowInsecurePassword - ? i18n.str`short password are insecure, turn off insecure password in settings` - : i18n.str`lower and upper case letters, number and special character` - } - required - /> + <div> + <label + for="password" + class="block text-sm font-medium leading-6 text-gray-900" + > + <i18n.Translate>Password</i18n.Translate> + </label> + <div class="mt-2"> + <input + ref={doAutoFocus} + type="text" + name="password" + id="password" + class="block w-full disabled:bg-gray-200 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" + value={form.password?.value ?? ""} + enterkeyhint="next" + placeholder="strong password" + autocomplete="password" + title={i18n.str`Password`} + required + onChange={(e) => { + console.log("ASDASD", form.password?.onUpdate); + form.password?.onUpdate(e.currentTarget.value); + }} + /> + <ShowInputErrorLabel + message={form.password?.error} + isDirty={form.password?.value !== undefined} + /> + </div> </div> - <div class="mb-4"> - <Form.InputLine - label={i18n.str`Repeat password`} - name="repeat" - type="password" - required - /> + + <div> + <label + for="repeat" + class="block text-sm font-medium leading-6 text-gray-900" + > + <i18n.Translate>Repeat password</i18n.Translate> + </label> + <div class="mt-2"> + <input + type="text" + name="repeat" + id="repeat" + class="block w-full disabled:bg-gray-200 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" + value={form.repeat?.value ?? ""} + enterkeyhint="next" + placeholder="identification" + autocomplete="repeat" + title={i18n.str`Repeat password`} + required + onChange={(e): void => { + form.repeat?.onUpdate(e.currentTarget.value); + }} + /> + <ShowInputErrorLabel + message={form.repeat?.error} + isDirty={form.repeat?.value !== undefined} + /> + </div> </div> <div class="mt-8"> - <button + <Button type="submit" - class="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 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" + disabled={!createAccountHandler} + class="disabled:opacity-50 disabled:cursor-default flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 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" + handler={createAccountHandler} > <i18n.Translate>Create</i18n.Translate> - </button> + </Button> </div> - </Form.Provider> + </form> </div> </div> </div> ); } + +/** + * Show the element when the load ended + * @param element + */ +export function doAutoFocus(element: HTMLElement | null) { + if (element) { + setTimeout(() => { + element.focus({ preventScroll: true }); + element.scrollIntoView({ + behavior: "smooth", + block: "center", + inline: "center", + }); + }, 100); + } +} |