merchant-backoffice

ZZZ: Inactive/Deprecated
Log | Files | Refs | Submodules | README

commit 64451fe1b74508c3eda71dbc8f3d4101a255a330
parent bf20cbfeb441dfc4d9a109e6e4502ebc37a10b02
Author: Sebastian <sebasjm@gmail.com>
Date:   Tue, 23 Feb 2021 18:03:09 -0300

refactor src/index to split routes

Diffstat:
MCHANGELOG.md | 1+
Apackages/frontend/src/ApplicationReadyRoutes.tsx | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/frontend/src/InstanceRoutes.tsx | 87+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/frontend/src/components/auth/index.tsx | 2+-
Mpackages/frontend/src/components/notifications/index.tsx | 3+--
Mpackages/frontend/src/components/yup/YupField.tsx | 208+++----------------------------------------------------------------------------
Apackages/frontend/src/components/yup/YupInput.tsx | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/frontend/src/components/yup/YupInputArray.tsx | 81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/frontend/src/components/yup/YupInputSecured.tsx | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/frontend/src/components/yup/YupInputWithAddon.tsx | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/frontend/src/components/yup/YupObjectInput.tsx | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/frontend/src/declaration.d.ts | 13-------------
Mpackages/frontend/src/hooks/index.ts | 12+++++++++++-
Mpackages/frontend/src/hooks/notifications.ts | 2+-
Mpackages/frontend/src/index.tsx | 220+++++++++++--------------------------------------------------------------------
Mpackages/frontend/src/routes/instances/details/index.tsx | 2+-
Mpackages/frontend/src/routes/instances/list/index.tsx | 13+++++++------
Mpackages/frontend/src/routes/login/index.tsx | 2+-
Mpackages/frontend/src/schemas/index.ts | 2+-
Rpackages/frontend/src/constants.ts -> packages/frontend/src/utils/constants.ts | 0
Apackages/frontend/src/utils/functions.ts | 26++++++++++++++++++++++++++
Apackages/frontend/src/utils/types.ts | 30++++++++++++++++++++++++++++++
Mpackages/frontend/tests/functions/regex.test.ts | 2+-
23 files changed, 610 insertions(+), 419 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - validate on change everything - all button to the right - feature: input as date format + - bug: there is missing a mutate call when updating to remove the instance from cache - add order section - add product section diff --git a/packages/frontend/src/ApplicationReadyRoutes.tsx b/packages/frontend/src/ApplicationReadyRoutes.tsx @@ -0,0 +1,68 @@ +import { h, VNode } from 'preact'; +import { useContext } from "preact/hooks"; +import { Route, Router, route } from 'preact-router'; +import { useMessageTemplate } from 'preact-messages'; +import { Notification } from "./utils/types"; +import { BackendContext } from './context/backend'; +import { SwrError } from "./hooks/backend"; +import { InstanceRoutes } from "./InstanceRoutes"; +import { RootPaths, Redirect } from "./index"; +import NotFoundPage from './routes/notfound'; +import LoginPage from './routes/login'; +import InstanceListPage from './routes/instances/list'; +import InstanceCreatePage from "./routes/instances/create"; + +export function ApplicationReadyRoutes({ pushNotification, addTokenCleaner }: { pushNotification: (n: Notification) => void; addTokenCleaner: any; }): VNode { + const { changeBackend, updateToken } = useContext(BackendContext); + + const updateLoginStatus = (url: string, token?: string) => { + changeBackend(url); + if (token) + updateToken(token); + }; + const i18n = useMessageTemplate(); + + return <Router> + <Route path={RootPaths.root} component={Redirect} + to={RootPaths.list_instances} /> + + <Route path={RootPaths.list_instances} component={InstanceListPage} + + onCreate={() => { + route(RootPaths.new_instance); + }} + + onUpdate={(id: string): void => { + route(`/instance/${id}/`); + }} + + onUnauthorized={() => <LoginPage + withMessage={{ message: i18n`Access denied`, description: i18n`Check your token is valid`, type: 'ERROR', }} + onConfirm={updateLoginStatus} />} + + onError={(error: SwrError) => { + pushNotification({ message: i18n`error`, params: error, type: 'ERROR' }); + return <div />; + }} /> + + <Route path={RootPaths.new_instance} component={InstanceCreatePage} + + onBack={() => route(RootPaths.list_instances)} + + onConfirm={() => { + pushNotification({ message: i18n`create_success`, type: 'SUCCESS' }); + route(RootPaths.list_instances); + }} + + onError={(error: any) => { + pushNotification({ message: i18n`create_error`, type: 'ERROR', params: error }); + }} /> + + <Route path={RootPaths.instance_id_route} component={InstanceRoutes} + pushNotification={pushNotification} + addTokenCleaner={addTokenCleaner} /> + + <Route default component={NotFoundPage} /> + + </Router>; +} diff --git a/packages/frontend/src/InstanceRoutes.tsx b/packages/frontend/src/InstanceRoutes.tsx @@ -0,0 +1,87 @@ +import { h, VNode } from 'preact'; +import { useCallback, useContext, useEffect } from "preact/hooks"; +import { Route, Router, route } from 'preact-router'; +import { useMessageTemplate } from 'preact-messages'; +import { useBackendInstanceToken } from './hooks'; +import { BackendContext, InstanceContext } from './context/backend'; +import { SwrError } from "./hooks/backend"; +import { InstancePaths } from "./index"; +import { Notification } from './utils/types'; +import NotFoundPage from './routes/notfound'; +import LoginPage from './routes/login'; +import InstanceDetailsPage from "./routes/instances/details"; +import InstanceUpdatePage from "./routes/instances/update"; + +export interface Props { + id: string; + pushNotification: (n: Notification) => void; + addTokenCleaner: any; +} + +export function InstanceRoutes({ id, pushNotification, addTokenCleaner }: Props): VNode { + const [token, updateToken] = useBackendInstanceToken(id); + const { changeBackend } = useContext(BackendContext); + const cleaner = useCallback(() => { updateToken(undefined); }, [id]); + const i18n = useMessageTemplate(''); + + useEffect(() => { + addTokenCleaner(cleaner); + }, [addTokenCleaner, cleaner]); + + const updateLoginStatus = (url: string, token?: string) => { + changeBackend(url); + if (token) + updateToken(token); + }; + + return <InstanceContext.Provider value={{ id, token }}> + <Router> + <Route path={InstancePaths.details} + component={InstanceDetailsPage} + pushNotification={pushNotification} + + onUnauthorized={() => <LoginPage + withMessage={{ message: i18n`Access denied`, description: i18n`Check your token is valid`, type: 'ERROR', }} + onConfirm={updateLoginStatus} />} + + onUpdate={() => { + route(`/instance/${id}/update`); + }} + + onDelete={() => { + route(`/instances`); + }} + + onLoadError={(e: SwrError) => { + pushNotification({ message: i18n`update_load_error`, type: 'ERROR', params: e }); + route(`/instance/${id}/`); + return <div />; + }} /> + + <Route path={InstancePaths.update} + component={InstanceUpdatePage} + // pushNotification={pushNotification} + onUnauthorized={() => <LoginPage + withMessage={{ message: i18n`Access denied`, description: i18n`Check your token is valid`, type: 'ERROR', }} + onConfirm={updateLoginStatus} />} + onLoadError={(e: SwrError) => { + pushNotification({ message: i18n`update_load_error`, type: 'ERROR', params: e }); + route(`/instance/${id}/`); + return <div />; + }} + onBack={() => { + route(`/instance/${id}/`); + }} + onConfirm={() => { + pushNotification({ message: i18n`create_success`, type: 'SUCCESS' }); + route(`/instance/${id}/`); + }} + onUpdateError={(e: Error) => { + pushNotification({ message: i18n`update_error`, type: 'ERROR', params: e }); + }} /> + + <Route default component={NotFoundPage} /> + </Router> + </InstanceContext.Provider>; + +} diff --git a/packages/frontend/src/components/auth/index.tsx b/packages/frontend/src/components/auth/index.tsx @@ -23,7 +23,7 @@ import { h, VNode } from "preact"; import { Message } from "preact-messages"; import { useContext, useState } from "preact/hooks"; import { BackendContext } from "../../context/backend"; -import { Notification } from "../../declaration"; +import { Notification } from "../../utils/types"; interface Props { withMessage?: Notification; diff --git a/packages/frontend/src/components/notifications/index.tsx b/packages/frontend/src/components/notifications/index.tsx @@ -20,8 +20,7 @@ */ import { h, VNode } from "preact"; -import { Message } from "preact-messages"; -import { MessageType, Notification } from "../../declaration"; +import { MessageType, Notification } from "../../utils/types"; interface Props { notifications: Notification[]; diff --git a/packages/frontend/src/components/yup/YupField.tsx b/packages/frontend/src/components/yup/YupField.tsx @@ -20,36 +20,20 @@ */ import { h, VNode } from "preact"; -import { Message, useMessage } from "preact-messages"; -import { StateUpdater, useContext, useState } from "preact/hooks"; +import { StateUpdater, useContext } from "preact/hooks"; import { intervalToDuration, formatDuration } from 'date-fns' import { BackendContext, ConfigContext } from '../../context/backend'; +import { YupObjectInput } from "./YupObjectInput"; +import { YupInput } from "./YupInput"; +import { YupInputArray } from "./YupInputArray"; +import { YupInputWithAddon } from "./YupInputWithAddon"; +import { YupInputSecured } from "./YupInputSecured"; function readableDuration(duration?: number): string { if (!duration) return "" return formatDuration(intervalToDuration({ start: 0, end: duration })) } -// customFormatDuration({ start: 0, end: 10800 * 1000}) // 3 hours -// customFormatDuration({ start: 0, end: 108000 * 1000}) // 1 day 6 hours - -interface PropsInputInternal { - name: string; - value: string; - readonly?: boolean; - errors: any; - onChange: any; -} - -interface PropsObject { - name: string; - info: any; - value: any; - errors: any; - onChange: any; - readonly?: boolean; -} - interface Props { name: string; field: string; @@ -93,183 +77,3 @@ export function YupField({ name, field, errors, object, valueHandler, info, read } } - -function YupObjectInput({ name, info, value, errors, onChange, readonly }: PropsObject): VNode { - const [active, setActive] = useState(false) - return <div class="card"> - <header class="card-header"> - <p class="card-header-title"> - <Message id={`fields.instance.${name}.label`} /> - </p> - <button class="card-header-icon" aria-label="more options" onClick={(): void => setActive(!active)}> - <span class="icon"> - {active ? - <i class="mdi mdi-arrow-up" /> : - <i class="mdi mdi-arrow-down" />} - </span> - </button> - </header> - <div class={active ? "card-content" : "is-hidden"}> - <div class="content"> - {Object.keys(info.fields).map(f => <YupField name={`${name}.${f}`} - field={f} errors={errors} object={value} - valueHandler={onChange} info={info.fields[f]} - readonly={readonly} - />)} - </div> - </div> - </div> -} - -function YupInput({ name, readonly, value, errors, onChange }: PropsInputInternal): VNode { - const placeholder = useMessage(`fields.instance.${name}.placeholder`) - const tooltip = useMessage(`fields.instance.${name}.tooltip`) - - return <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label"> - <Message id={`fields.instance.${name}.label`} /> - {tooltip && <span class="icon" data-tooltip={tooltip}> - <i class="mdi mdi-information" /> - </span>} - </label> - </div> - <div class="field-body"> - <div class="field"> - <p class="control"> - <input class={errors[name] ? "input is-danger" : "input"} type="text" - placeholder={placeholder} readonly={readonly} - name={name} value={value} disabled={readonly} - onChange={(e): void => onChange(e.currentTarget.value)} /> - <Message id={`fields.instance.${name}.help`} > </Message> - </p> - {errors[name] ? <p class="help is-danger"> - <Message id={`validation.${errors[name].type}`} fields={errors[name].params}>{errors[name].message} </Message> - </p> : null} - </div> - </div> - </div> -} - -function YupInputArray({ name, readonly, value, errors, onChange }: PropsInputInternal): VNode { - const placeholder = useMessage(`fields.instance.${name}.placeholder`) - const tooltip = useMessage(`fields.instance.${name}.tooltip`) - - const array = value as unknown as string[] || [] - const [currentValue, setCurrentValue] = useState('') - - return <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label"> - <Message id={`fields.instance.${name}.label`} /> - {tooltip && <span class="icon" data-tooltip={tooltip}> - <i class="mdi mdi-information" /> - </span>} - </label> - </div> - <div class="field-body"> - <div class="field"> - <div class="field has-addons"> - <p class="control"> - <input class={errors[name] ? "input is-danger" : "input"} type="text" - placeholder={placeholder} readonly={readonly} disabled={readonly} - name={name} value={currentValue} - onChange={(e): void => setCurrentValue(e.currentTarget.value)} /> - <Message id={`fields.instance.${name}.help`} > </Message> - </p> - <p class="control"> - <button class="button is-info" onClick={(): void => { - onChange([currentValue, ...array]) - setCurrentValue('') - }} >add</button> - </p> - </div> - {errors[name] ? <p class="help is-danger"> - <Message id={`validation.${errors[name].type}`} fields={errors[name].params}>{errors[name].message}</Message> - </p> : null} - {array.map(v => <div class="tags has-addons"> - <span class="tag is-medium is-info">{v}</span> - <a class="tag is-medium is-danger is-delete" onClick={() => { - onChange(array.filter(f => f !== v)) - setCurrentValue(v) - }} /> - </div> - )} - </div> - - </div> - </div> -} - -function YupInputWithAddon({ name, readonly, value, errors, onChange, addon, atTheEnd }: PropsInputInternal & { addon: string; atTheEnd?: boolean }): VNode { - const placeholder = useMessage(`fields.instance.${name}.placeholder`) - const tooltip = useMessage(`fields.instance.${name}.tooltip`) - - return <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label"> - <Message id={`fields.instance.${name}.label`} /> - {tooltip && <span class="icon" data-tooltip={tooltip}> - <i class="mdi mdi-information" /> - </span>} - </label> - </div> - <div class="field-body"> - <div class="field"> - <div class="field has-addons"> - {!atTheEnd && <div class="control"> - <a class="button is-static">{addon}</a> - </div>} - <p class="control is-expanded"> - <input class={errors[name] ? "input is-danger" : "input"} type="text" - placeholder={placeholder} readonly={readonly} disabled={readonly} - name={name} value={value} - onChange={(e): void => onChange(e.currentTarget.value)} /> - <Message id={`fields.instance.${name}.help`} > </Message> - </p> - {atTheEnd && <div class="control"> - <a class="button is-static">{addon}</a> - </div>} - </div> - {errors[name] ? <p class="help is-danger"><Message id={`validation.${errors[name].type}`} fields={errors[name].params}>{errors[name].message}</Message></p> : null} - </div> - </div> - </div> -} - -function YupInputSecured({ name, readonly, value, errors, onChange }: PropsInputInternal): VNode { - const placeholder = useMessage(`fields.instance.${name}.placeholder`, {}) - const tooltip = useMessage(`fields.instance.${name}.tooltip`, {}) - - const [active, setActive] = useState(false) - - return <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label"> - <Message id={`fields.instance.${name}.label`} /> - {tooltip && <span class="icon" data-tooltip={tooltip}> - <i class="mdi mdi-information" /> - </span>} - </label> - </div> - <div class="field-body"> - <div class="field"> - <div class="field has-addons"> - <label class="b-checkbox checkbox"> - <input type="checkbox" checked={active} onClick={(): void => { onChange(''); setActive(!active) }} /> - <span class="check" /> - </label> - <p class="control"> - <input class="input" type="text" - placeholder={placeholder} readonly={readonly || !active} - disabled={readonly || !active} - name={name} value={value} - onChange={(e): void => onChange(e.currentTarget.value)} /> - <Message id={`fields.instance.${name}.help`}> </Message> - </p> - </div> - {errors[name] ? <p class="help is-danger"><Message id={`validation.${errors[name].type}`} fields={errors[name].params}>{errors[name].message}</Message></p> : null} - </div> - </div> - </div> -} diff --git a/packages/frontend/src/components/yup/YupInput.tsx b/packages/frontend/src/components/yup/YupInput.tsx @@ -0,0 +1,60 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** +* +* @author Sebastian Javier Marchano (sebasjm) +*/ +import { h, VNode } from "preact"; +import { Message, useMessage } from "preact-messages"; + +interface Props { + name: string; + value: string; + readonly?: boolean; + errors: any; + onChange: any; +} + +export function YupInput({ name, readonly, value, errors, onChange }: Props): VNode { + const placeholder = useMessage(`fields.instance.${name}.placeholder`); + const tooltip = useMessage(`fields.instance.${name}.tooltip`); + + return <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label"> + <Message id={`fields.instance.${name}.label`} /> + {tooltip && <span class="icon" data-tooltip={tooltip}> + <i class="mdi mdi-information" /> + </span>} + </label> + </div> + <div class="field-body"> + <div class="field"> + <p class="control"> + <input class={errors[name] ? "input is-danger" : "input"} type="text" + placeholder={placeholder} readonly={readonly} + name={name} value={value} disabled={readonly} + onChange={(e): void => onChange(e.currentTarget.value)} /> + <Message id={`fields.instance.${name}.help`}> </Message> + </p> + {errors[name] ? <p class="help is-danger"> + <Message id={`validation.${errors[name].type}`} fields={errors[name].params}>{errors[name].message} </Message> + </p> : null} + </div> + </div> + </div>; +} diff --git a/packages/frontend/src/components/yup/YupInputArray.tsx b/packages/frontend/src/components/yup/YupInputArray.tsx @@ -0,0 +1,81 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** +* +* @author Sebastian Javier Marchano (sebasjm) +*/ +import { h, VNode } from "preact"; +import { Message, useMessage } from "preact-messages"; +import { useState } from "preact/hooks"; + +export interface Props { + name: string; + value: string; + readonly?: boolean; + errors: any; + onChange: any; +} + +export function YupInputArray({ name, readonly, value, errors, onChange }: Props): VNode { + const placeholder = useMessage(`fields.instance.${name}.placeholder`); + const tooltip = useMessage(`fields.instance.${name}.tooltip`); + + const array = value as unknown as string[] || []; + const [currentValue, setCurrentValue] = useState(''); + + return <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label"> + <Message id={`fields.instance.${name}.label`} /> + {tooltip && <span class="icon" data-tooltip={tooltip}> + <i class="mdi mdi-information" /> + </span>} + </label> + </div> + <div class="field-body"> + <div class="field"> + <div class="field has-addons"> + <p class="control"> + <input class={errors[name] ? "input is-danger" : "input"} type="text" + placeholder={placeholder} readonly={readonly} disabled={readonly} + name={name} value={currentValue} + onChange={(e): void => setCurrentValue(e.currentTarget.value)} /> + <Message id={`fields.instance.${name}.help`}> </Message> + </p> + <p class="control"> + <button class="button is-info" onClick={(): void => { + onChange([currentValue, ...array]); + setCurrentValue(''); + }}>add</button> + </p> + </div> + {errors[name] ? <p class="help is-danger"> + <Message id={`validation.${errors[name].type}`} fields={errors[name].params}>{errors[name].message}</Message> + </p> : null} + {array.map(v => <div class="tags has-addons"> + <span class="tag is-medium is-info">{v}</span> + <a class="tag is-medium is-danger is-delete" onClick={() => { + onChange(array.filter(f => f !== v)); + setCurrentValue(v); + }} /> + </div> + )} + </div> + + </div> + </div>; +} diff --git a/packages/frontend/src/components/yup/YupInputSecured.tsx b/packages/frontend/src/components/yup/YupInputSecured.tsx @@ -0,0 +1,68 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** +* +* @author Sebastian Javier Marchano (sebasjm) +*/ +import { h, VNode } from "preact"; +import { Message, useMessage } from "preact-messages"; +import { useState } from "preact/hooks"; + +export interface Props { + name: string; + value: string; + readonly?: boolean; + errors: any; + onChange: any; +} + +export function YupInputSecured({ name, readonly, value, errors, onChange }: Props): VNode { + const placeholder = useMessage(`fields.instance.${name}.placeholder`, {}); + const tooltip = useMessage(`fields.instance.${name}.tooltip`, {}); + + const [active, setActive] = useState(false); + + return <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label"> + <Message id={`fields.instance.${name}.label`} /> + {tooltip && <span class="icon" data-tooltip={tooltip}> + <i class="mdi mdi-information" /> + </span>} + </label> + </div> + <div class="field-body"> + <div class="field"> + <div class="field has-addons"> + <label class="b-checkbox checkbox"> + <input type="checkbox" checked={active} onClick={(): void => { onChange(''); setActive(!active); }} /> + <span class="check" /> + </label> + <p class="control"> + <input class="input" type="text" + placeholder={placeholder} readonly={readonly || !active} + disabled={readonly || !active} + name={name} value={value} + onChange={(e): void => onChange(e.currentTarget.value)} /> + <Message id={`fields.instance.${name}.help`}> </Message> + </p> + </div> + {errors[name] ? <p class="help is-danger"><Message id={`validation.${errors[name].type}`} fields={errors[name].params}>{errors[name].message}</Message></p> : null} + </div> + </div> + </div>; +} diff --git a/packages/frontend/src/components/yup/YupInputWithAddon.tsx b/packages/frontend/src/components/yup/YupInputWithAddon.tsx @@ -0,0 +1,68 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** +* +* @author Sebastian Javier Marchano (sebasjm) +*/ +import { h, VNode } from "preact"; +import { Message, useMessage } from "preact-messages"; + +export interface Props { + name: string; + value: string; + readonly?: boolean; + errors: any; + onChange: any; + addon: string; + atTheEnd?: boolean; +} + +export function YupInputWithAddon({ name, readonly, value, errors, onChange, addon, atTheEnd }: Props): VNode { + const placeholder = useMessage(`fields.instance.${name}.placeholder`); + const tooltip = useMessage(`fields.instance.${name}.tooltip`); + + return <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label"> + <Message id={`fields.instance.${name}.label`} /> + {tooltip && <span class="icon" data-tooltip={tooltip}> + <i class="mdi mdi-information" /> + </span>} + </label> + </div> + <div class="field-body"> + <div class="field"> + <div class="field has-addons"> + {!atTheEnd && <div class="control"> + <a class="button is-static">{addon}</a> + </div>} + <p class="control is-expanded"> + <input class={errors[name] ? "input is-danger" : "input"} type="text" + placeholder={placeholder} readonly={readonly} disabled={readonly} + name={name} value={value} + onChange={(e): void => onChange(e.currentTarget.value)} /> + <Message id={`fields.instance.${name}.help`}> </Message> + </p> + {atTheEnd && <div class="control"> + <a class="button is-static">{addon}</a> + </div>} + </div> + {errors[name] ? <p class="help is-danger"><Message id={`validation.${errors[name].type}`} fields={errors[name].params}>{errors[name].message}</Message></p> : null} + </div> + </div> + </div>; +} diff --git a/packages/frontend/src/components/yup/YupObjectInput.tsx b/packages/frontend/src/components/yup/YupObjectInput.tsx @@ -0,0 +1,59 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** +* +* @author Sebastian Javier Marchano (sebasjm) +*/ +import { h, VNode } from "preact"; +import { Message } from "preact-messages"; +import { useState } from "preact/hooks"; +import { YupField } from "./YupField"; + +export interface PropsObject { + name: string; + info: any; + value: any; + errors: any; + onChange: any; + readonly?: boolean; +} + +export function YupObjectInput({ name, info, value, errors, onChange, readonly }: PropsObject): VNode { + const [active, setActive] = useState(false); + return <div class="card"> + <header class="card-header"> + <p class="card-header-title"> + <Message id={`fields.instance.${name}.label`} /> + </p> + <button class="card-header-icon" aria-label="more options" onClick={(): void => setActive(!active)}> + <span class="icon"> + {active ? + <i class="mdi mdi-arrow-up" /> : + <i class="mdi mdi-arrow-down" />} + </span> + </button> + </header> + <div class={active ? "card-content" : "is-hidden"}> + <div class="content"> + {Object.keys(info.fields).map(f => <YupField name={`${name}.${f}`} + field={f} errors={errors} object={value} + valueHandler={onChange} info={info.fields[f]} + readonly={readonly} />)} + </div> + </div> + </div>; +} diff --git a/packages/frontend/src/declaration.d.ts b/packages/frontend/src/declaration.d.ts @@ -20,19 +20,6 @@ */ -interface KeyValue { - [key: string]: string; -} - -interface Notification { - message: string; - description?: string; - type: MessageType; - params?: any; -} - -type ValueOrFunction<T> = T | ((p: T) => T) -type MessageType = 'INFO' | 'WARN' | 'ERROR' | 'SUCCESS' type EddsaPublicKey = string; type RelativeTime = Duration; diff --git a/packages/frontend/src/hooks/index.ts b/packages/frontend/src/hooks/index.ts @@ -20,7 +20,16 @@ */ import { StateUpdater, useEffect, useState } from "preact/hooks"; -import { ValueOrFunction } from '../declaration'; +import { ValueOrFunction } from '../utils/types'; + + +export function useBackendContextState() { + const [lang, setLang] = useLang() + const [url, changeBackend] = useBackendURL(); + const [token, updateToken] = useBackendDefaultToken(); + + return { url, token, changeBackend, updateToken, lang, setLang } +} export function useBackendURL(): [string, StateUpdater<string>] { const [value, setter] = useNotNullLocalStorage('backend-url', typeof window !== 'undefined' ? window.location.origin : '') @@ -112,3 +121,4 @@ export function useNotNullLocalStorage(key: string, initialValue: string): [stri return [storedValue, setValue]; } + diff --git a/packages/frontend/src/hooks/notifications.ts b/packages/frontend/src/hooks/notifications.ts @@ -20,7 +20,7 @@ */ import { useState } from "preact/hooks"; -import { Notification } from '../declaration'; +import { Notification } from '../utils/types'; interface Result { notifications: Notification[]; diff --git a/packages/frontend/src/index.tsx b/packages/frontend/src/index.tsx @@ -23,54 +23,60 @@ import "./scss/main.scss" import { h, VNode } from 'preact'; import { useCallback, useContext, useEffect, useState } from "preact/hooks"; -import { Route, Router, route } from 'preact-router'; -import { MessageError, MessageProvider, useMessageTemplate } from 'preact-messages'; +import { Route, route } from 'preact-router'; +import { MessageProvider, useMessageTemplate } from 'preact-messages'; -import { Notification } from "./declaration"; import { Sidebar } from './components/sidebar'; import { NavigationBar } from './components/navbar'; import { Notifications } from './components/notifications'; import * as messages from './messages' -import { useBackendURL, useBackendDefaultToken, useLang, useBackendInstanceToken } from './hooks'; +import { useBackendContextState } from './hooks'; import { useNotifications } from "./hooks/notifications"; -import { BackendContext, ConfigContext, InstanceContext } from './context/backend'; -import { SwrError, useBackendConfig } from "./hooks/backend"; +import { BackendContext, ConfigContext } from './context/backend'; +import { useBackendConfig } from "./hooks/backend"; +import { hasKey, onTranslationError } from "./utils/functions"; -import NotFoundPage from './routes/notfound'; -import Login from './routes/login'; -import Instances from './routes/instances/list'; -import Create from "./routes/instances/create"; -import Details from "./routes/instances/details"; -import Update from "./routes/instances/update"; +import LoginPage from './routes/login'; +import { ApplicationReadyRoutes } from "./ApplicationReadyRoutes"; -enum RootPages { +export enum RootPaths { root = '/', - instances = '/instances', - new = '/new', + list_instances = '/instances', + new_instance = '/new', instance_id_route = '/instance/:id/:rest*', } -enum InstancePages { +export enum InstancePaths { details = '/instance/:id/', update = '/instance/:id/update', } -function Redirect({ to }: { to: string }): null { +export function Redirect({ to }: { to: string }): null { useEffect(() => { route(to, true) }) return null } +export default function Application(): VNode { + const state = useBackendContextState() + return ( + <BackendContext.Provider value={state}> + <MessageProvider locale={state.lang} onError={onTranslationError} messages={hasKey(messages, state.lang) ? messages[state.lang] : messages.en} pathSep={null as any} > + <ApplicationStatusRoutes /> + </MessageProvider > + </BackendContext.Provider> + ); +} -function AppRouting(): VNode { +function ApplicationStatusRoutes(): VNode { const { notifications, pushNotification, removeNotification } = useNotifications() const { lang, setLang, changeBackend, updateToken } = useContext(BackendContext) const backendConfig = useBackendConfig(); const i18n = useMessageTemplate('') - const LoginWithError = () => <Login + const LoginWithError = () => <LoginPage withMessage={{ message: i18n`Couldnt access the server`, type: 'ERROR', @@ -79,187 +85,24 @@ function AppRouting(): VNode { onConfirm={(url: string, token?: string) => { changeBackend(url) if (token) updateToken(token) - route(RootPages.instances) + route(RootPaths.list_instances) }} /> - const cleaner = useCallback(() =>{updateToken(undefined)},[]) - + const cleaner = useCallback(() => { updateToken(undefined) }, []) const [cleaners, setCleaners] = useState([cleaner]) - const addTokenCleaner = (c:() => void) => setCleaners(cs => [...cs,c]) - const addTokenCleanerNemo = useCallback((c:() => void) =>{addTokenCleaner(c)},[cleaner]) + const addTokenCleaner = (c: () => void) => setCleaners(cs => [...cs, c]) + const addTokenCleanerNemo = useCallback((c: () => void) => { addTokenCleaner(c) }, [cleaner]) return <div id="app"> <ConfigContext.Provider value={backendConfig.data || {}}> - <NavigationBar lang={lang} setLang={setLang} onLogout={() => { cleaners.forEach( c => c() ) }} /> + <NavigationBar lang={lang} setLang={setLang} onLogout={() => { cleaners.forEach(c => c()) }} /> <Sidebar /> <Notifications notifications={notifications} removeNotification={removeNotification} /> {!backendConfig.data ? <Route default component={LoginWithError} /> : - <Route default component={AppReady} pushNotification={pushNotification} addTokenCleaner={addTokenCleanerNemo} /> + <Route default component={ApplicationReadyRoutes} pushNotification={pushNotification} addTokenCleaner={addTokenCleanerNemo} /> } </ConfigContext.Provider> </div> } - -function AppReady({ pushNotification,addTokenCleaner }: { pushNotification: (n: Notification) => void, addTokenCleaner: any }): VNode { - const { changeBackend, updateToken } = useContext(BackendContext) - - const updateLoginStatus = (url: string, token?: string) => { - changeBackend(url) - if (token) updateToken(token) - } - const i18n = useMessageTemplate('') - - return <Router> - <Route path={RootPages.root} component={Redirect} to={RootPages.instances} /> - - <Route path={RootPages.instances} - component={Instances} - - onCreate={() => { - route(RootPages.new) - }} - - onUpdate={(id: string): void => { - route(`/instance/${id}/`) - }} - - onUnauthorized={() => <Login - withMessage={{ message: i18n`Access denied`, description: i18n`Check your token is valid`, type: 'ERROR', }} - onConfirm={updateLoginStatus} - />} - - onError={(error: SwrError) => { - pushNotification({ message: i18n`error`, params: error, type: 'ERROR' }) - return <div /> - }} - /> - - <Route path={RootPages.new} - component={Create} - onBack={() => route(RootPages.instances)} - - onConfirm={() => { - pushNotification({ message: i18n`create_success`, type: 'SUCCESS' }) - route(RootPages.instances) - }} - - onError={(error: any) => { - pushNotification({ message: i18n`create_error`, type: 'ERROR', params: error }) - }} - /> - - <Route path={RootPages.instance_id_route} component={SubPages} pushNotification={pushNotification} addTokenCleaner={addTokenCleaner} /> - - <Route default component={NotFoundPage} /> - - </Router> -} - -function hasKey<O>(obj: O, key: string | number | symbol): key is keyof O { - return key in obj -} - -function useBackendContextState() { - const [lang, setLang] = useLang() - const [url, changeBackend] = useBackendURL(); - const [token, updateToken] = useBackendDefaultToken(); - - return { url, token, changeBackend, updateToken, lang, setLang } -} - -function onTranslationError(error: MessageError) { - if (typeof window === "undefined") return; - (window as any)['missing_locale'] = ([] as string[]).concat((window as any)['missing_locale']).concat(error.path.join()) -} - -export default function Application(): VNode { - const state = useBackendContextState() - - return ( - <BackendContext.Provider value={state}> - <MessageProvider locale={state.lang} onError={onTranslationError} messages={hasKey(messages, state.lang) ? messages[state.lang] : messages.en} pathSep={null as any} > - <AppRouting /> - </MessageProvider > - </BackendContext.Provider> - ); -} -interface SubPagesProps { - id: string; - pushNotification: (n: Notification) => void; - addTokenCleaner: any; -} - - -function SubPages({ id, pushNotification, addTokenCleaner }: SubPagesProps): VNode { - const [token, updateToken] = useBackendInstanceToken(id); - const { changeBackend } = useContext(BackendContext) - const cleaner = useCallback(() =>{updateToken(undefined)},[id]) - const i18n = useMessageTemplate('') - - useEffect(() => { - addTokenCleaner(cleaner) - }, [addTokenCleaner, cleaner]) - - const updateLoginStatus = (url: string, token?: string) => { - changeBackend(url) - if (token) updateToken(token) - } - - return <InstanceContext.Provider value={{id, token}}> - <Router> - <Route path={InstancePages.details} - component={Details} - pushNotification={pushNotification} - - onUnauthorized={() => <Login - withMessage={{ message: i18n`Access denied`, description: i18n`Check your token is valid`, type: 'ERROR', }} - onConfirm={updateLoginStatus} - />} - - onUpdate={() => { - route(`/instance/${id}/update`) - }} - - onDelete={() => { - route(`/instances`) - }} - - onLoadError={(e: SwrError) => { - pushNotification({ message: i18n`update_load_error`, type: 'ERROR', params: e }) - route(`/instance/${id}/`) - return <div /> - }} - /> - - <Route path={InstancePages.update} - component={Update} - // pushNotification={pushNotification} - - onUnauthorized={() => <Login - withMessage={{ message: i18n`Access denied`, description: i18n`Check your token is valid`, type: 'ERROR', }} - onConfirm={updateLoginStatus} - />} - onLoadError={(e: SwrError) => { - pushNotification({ message: i18n`update_load_error`, type: 'ERROR', params: e }) - route(`/instance/${id}/`) - return <div /> - }} - onBack={() => { - route(`/instance/${id}/`) - }} - onConfirm={() => { - pushNotification({ message: i18n`create_success`, type: 'SUCCESS' }) - route(`/instance/${id}/`) - }} - onUpdateError={(e: Error) => { - pushNotification({ message: i18n`update_error`, type: 'ERROR', params: e }) - }} - /> - - <Route default component={NotFoundPage} /> - </Router> - </InstanceContext.Provider> - -} -\ No newline at end of file diff --git a/packages/frontend/src/routes/instances/details/index.tsx b/packages/frontend/src/routes/instances/details/index.tsx @@ -16,7 +16,7 @@ import { Fragment, h, VNode } from "preact"; import { useContext, useState } from "preact/hooks"; import { InstanceContext } from "../../../context/backend"; -import { Notification } from "../../../declaration"; +import { Notification } from "../../../utils/types"; import { useBackendInstance, useBackendInstanceMutateAPI, SwrError } from "../../../hooks/backend"; import { DeleteModal } from "../list/DeleteModal"; import { DetailPage } from "./DetailPage"; diff --git a/packages/frontend/src/routes/instances/list/index.tsx b/packages/frontend/src/routes/instances/list/index.tsx @@ -23,7 +23,8 @@ import { Fragment, h, VNode } from 'preact'; import { View } from './View'; import { useBackendInstances, useBackendInstanceMutateAPI, SwrError } from '../../../hooks/backend'; import { useState } from 'preact/hooks'; -import { MerchantBackend, Notification } from '../../../declaration'; +import { MerchantBackend } from '../../../declaration'; +import { Notification } from '../../../utils/types'; import { DeleteModal } from './DeleteModal'; interface Props { pushNotification: (n: Notification) => void; @@ -43,7 +44,7 @@ export default function Instances({ pushNotification, onUnauthorized, onError, o if (!list.data) { if (list.unauthorized) return onUnauthorized() - if (list.error) return onError(list.error) + if (list.error) return onError(list.error) } return <Fragment> @@ -54,9 +55,9 @@ export default function Instances({ pushNotification, onUnauthorized, onError, o onUpdate={onUpdate} selected={!!deleting} /> - {deleting && <DeleteModal - element={deleting} - onCancel={() => setDeleting(null) } + {deleting && <DeleteModal + element={deleting} + onCancel={() => setDeleting(null)} onConfirm={async (): Promise<void> => { try { await deleteInstance() @@ -65,7 +66,7 @@ export default function Instances({ pushNotification, onUnauthorized, onError, o pushNotification({ message: 'delete_error', type: 'ERROR', params: error }) } setDeleting(null) - }} + }} />} </Fragment>; } diff --git a/packages/frontend/src/routes/login/index.tsx b/packages/frontend/src/routes/login/index.tsx @@ -20,7 +20,7 @@ */ import { h, VNode } from "preact"; import { LoginModal } from '../../components/auth'; -import { Notification } from "../../declaration"; +import { Notification } from "../../utils/types"; interface Props { withMessage?: Notification; diff --git a/packages/frontend/src/schemas/index.ts b/packages/frontend/src/schemas/index.ts @@ -20,7 +20,7 @@ */ import * as yup from 'yup'; -import { AMOUNT_REGEX, PAYTO_REGEX } from "../constants"; +import { AMOUNT_REGEX, PAYTO_REGEX } from "../utils/constants"; yup.setLocale({ mixed: { diff --git a/packages/frontend/src/constants.ts b/packages/frontend/src/utils/constants.ts diff --git a/packages/frontend/src/utils/functions.ts b/packages/frontend/src/utils/functions.ts @@ -0,0 +1,26 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +import { MessageError } from "preact-messages"; + +export function hasKey<O>(obj: O, key: string | number | symbol): key is keyof O { + return key in obj +} + +export function onTranslationError(error: MessageError) { + if (typeof window === "undefined") return; + (window as any)['missing_locale'] = ([] as string[]).concat((window as any)['missing_locale']).concat(error.path.join()) +} diff --git a/packages/frontend/src/utils/types.ts b/packages/frontend/src/utils/types.ts @@ -0,0 +1,30 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +export interface KeyValue { + [key: string]: string; +} + +export interface Notification { + message: string; + description?: string; + type: MessageType; + params?: any; +} + +export type ValueOrFunction<T> = T | ((p: T) => T) +export type MessageType = 'INFO' | 'WARN' | 'ERROR' | 'SUCCESS' + diff --git a/packages/frontend/tests/functions/regex.test.ts b/packages/frontend/tests/functions/regex.test.ts @@ -19,7 +19,7 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { AMOUNT_REGEX, PAYTO_REGEX } from "../../src/constants"; +import { AMOUNT_REGEX, PAYTO_REGEX } from "../../src/utils/constants"; describe('payto uri format', () => { const valids = [