merchant-backoffice

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

commit aa3858a3e35d592377d3fbb23ec862044e99330a
parent 045cd4bf9f76846718fdd105829db4f2f3d3e9fe
Author: Sebastian <sebasjm@gmail.com>
Date:   Thu, 11 Feb 2021 14:33:25 -0300

refactor table, taking out the modal

Diffstat:
Msrc/components/auth/LoginPage.tsx | 2+-
Asrc/routes/instances/CardTable.tsx | 135+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/routes/instances/CreateModal.tsx | 9++++-----
Dsrc/routes/instances/DeleteAllModal.tsx | 30------------------------------
Msrc/routes/instances/DeleteModal.tsx | 6+++---
Dsrc/routes/instances/Table.tsx | 154-------------------------------------------------------------------------------
Msrc/routes/instances/UpdateModal.tsx | 2+-
Msrc/routes/instances/View.tsx | 44+++++++++++++++++++++++++++++++++++++-------
8 files changed, 181 insertions(+), 201 deletions(-)

diff --git a/src/components/auth/LoginPage.tsx b/src/components/auth/LoginPage.tsx @@ -23,7 +23,7 @@ export default function LoginPage({ onLogIn }: Props): VNode { <div class="field-body"> <div class="field"> <p class="control is-expanded has-icons-left"> - <input class="input" type="text" placeholder="abcdef" name="id" value={token} onInput={e => update(e?.currentTarget.value)} /> + <input class="input" type="text" placeholder="abcdef" name="id" value={token} onInput={(e): void => update(e?.currentTarget.value)} /> </p> </div> </div> diff --git a/src/routes/instances/CardTable.tsx b/src/routes/instances/CardTable.tsx @@ -0,0 +1,134 @@ +import { h, VNode } from "preact"; +import { useEffect, useState, StateUpdater } from "preact/hooks"; +import { MerchantBackend, WidthId as WithId } from "../../declaration"; + +interface Props { + instances: MerchantBackend.Instances.Instance[]; + onSelect: (id: string | null, action: 'UPDATE' | 'DELETE') => void; + onCreate: () => void; + selected: MerchantBackend.Instances.QueryInstancesResponse & WithId | undefined; +} + +function toggleSelected<T>(id: T): (prev: T[]) => T[] { + return (prev: T[]): T[] => prev.indexOf(id) == -1 ? [...prev, id] : prev.filter(e => e != id) +} + +interface Actions { + element: MerchantBackend.Instances.Instance; + type: 'DELETE' | 'UPDATE'; +} + +function notEmpty<TValue>(value: TValue | null | undefined): value is TValue { + return value !== null && value !== undefined; +} + +function buildActions(intances: MerchantBackend.Instances.Instance[], selected: string[], action: 'DELETE'): Actions[] { + return selected.map(id => intances.find(i => i.id === id)) + .filter(notEmpty) + .map(id => ({ element: id, type: action })) +} + +const EmptyTable = (): VNode => ( + <div class="content has-text-grey has-text-centered"> + <p> + <span class="icon is-large"><i class="mdi mdi-emoticon-sad mdi-48px" /></span> + </p> + <p>No instance configured yet, setup one pressing the + button </p> + </div> +) + +interface TableProps { + rowSelection: string[]; + instances: MerchantBackend.Instances.Instance[]; + onSelect: (id: string | null, action: 'UPDATE' | 'DELETE') => void; + rowSelectionHandler: StateUpdater<string[]>; +} + +const Table = ({ rowSelection, rowSelectionHandler, instances, onSelect }: TableProps): VNode => ( + <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> + <thead> + <tr> + <th class="is-checkbox-cell"> + <label class="b-checkbox checkbox"> + <input type="checkbox" checked={rowSelection.length === instances.length} onClick={(): void => rowSelectionHandler(rowSelection.length === instances.length ? [] : instances.map(i => i.id))} /> + <span class="check" /> + </label> + </th> + <th>id</th> + <th>name</th> + <th>public key</th> + <th>payments</th> + <th /> + </tr> + </thead> + <tbody> + {instances.map(i => { + return <tr> + <td class="is-checkbox-cell"> + <label class="b-checkbox checkbox"> + <input type="checkbox" checked={rowSelection.indexOf(i.id) != -1} onClick={(): void => rowSelectionHandler(toggleSelected(i.id))} /> + <span class="check" /> + </label> + </td> + <td >{i.id}</td> + <td >{i.name}</td> + <td >{i.merchant_pub}</td> + <td >{i.payment_targets}</td> + <td class="is-actions-cell"> + <div class="buttons is-right"> + <button class="button is-small is-primary" type="button" onClick={(): void => onSelect(i.id, 'UPDATE')}> + <span class="icon"><i class="mdi mdi-eye" /></span> + </button> + <button class="button is-small is-danger jb-modal" type="button" onClick={(): void => onSelect(i.id, 'DELETE')}> + <span class="icon"><i class="mdi mdi-trash-can" /></span> + </button> + </div> + </td> + </tr> + })} + + </tbody> + </table>) + + +export default function CardTable({ instances, onCreate, onSelect, selected }: Props): VNode { + const [actionQueue, actionQueueHandler] = useState<Actions[]>([]); + const [rowSelection, rowSelectionHandler] = useState<string[]>([]) + + useEffect(() => { + if (actionQueue.length > 0 && !selected && actionQueue[0].type == 'DELETE') { + onSelect(actionQueue[0].element.id, 'DELETE') + actionQueueHandler(actionQueue.slice(1)) + } + }, [actionQueue, selected, onSelect]) + + useEffect(() => { + if (actionQueue.length > 0 && !selected && actionQueue[0].type == 'UPDATE') { + onSelect(actionQueue[0].element.id, 'UPDATE') + actionQueueHandler(actionQueue.slice(1)) + } + }, [actionQueue, selected, onSelect]) + + + return <div class="card has-table"> + <header class="card-header"> + <p class="card-header-title"><span class="icon"><i class="mdi mdi-account-multiple" /></span>Instances</p> + <button class={rowSelection.length > 0 ? "card-header-icon" : "card-header-icon is-hidden"} type="button" onClick={(): void => actionQueueHandler(buildActions(instances, rowSelection, 'DELETE'))} > + <span class="icon"><i class="mdi mdi-trash-can" /></span> + </button> + <button class="card-header-icon" type="button" onClick={onCreate}> + <span class="icon"><i class="mdi mdi-plus" /></span> + </button> + </header> + <div class="card-content"> + <div class="b-table has-pagination"> + <div class="table-wrapper has-mobile-cards"> + {instances.length > 0 ? + <Table instances={instances} onSelect={onSelect} rowSelection={rowSelection} rowSelectionHandler={rowSelectionHandler} /> : + <EmptyTable /> + } + </div> + </div> + </div> + </div> +} +\ No newline at end of file diff --git a/src/routes/instances/CreateModal.tsx b/src/routes/instances/CreateModal.tsx @@ -36,7 +36,6 @@ const schema = yup.object().shape({ }); interface Props { - active: boolean; onCancel: () => void; onConfirm: (i: MerchantBackend.Instances.InstanceConfigurationMessage) => void; } @@ -45,8 +44,8 @@ interface KeyValue { [key: string]: string; } -export default function CreateModal({ active, onCancel, onConfirm }: Props): VNode { - const [value, valueHandler] = useState((active || {}) as any) +export default function CreateModal({ onCancel, onConfirm }: Props): VNode { + const [value, valueHandler] = useState({} as any) const [errors, setErrors] = useState<KeyValue>({}) const submit = (): void => { @@ -60,7 +59,7 @@ export default function CreateModal({ active, onCancel, onConfirm }: Props): VNo } } - return <ConfirmModal description="create instance" active={active} onConfirm={submit} onCancel={onCancel}> + return <ConfirmModal description="create instance" active onConfirm={submit} onCancel={onCancel}> {Object.keys(schema.fields).map(f => { const info = schema.fields[f].describe() @@ -73,7 +72,7 @@ export default function CreateModal({ active, onCancel, onConfirm }: Props): VNo <div class="field-body"> <div class="field"> <p class="control is-expanded has-icons-left"> - <input class="input" type="text" placeholder={info?.meta?.placeholder} readonly={info?.meta?.readonly} name={f} value={value[f]} onChange={e => valueHandler((prev: any) => ({ ...prev, [f]: e.currentTarget.value }))} /> + <input class="input" type="text" placeholder={info?.meta?.placeholder} readonly={info?.meta?.readonly} name={f} value={value[f]} onChange={(e): void => valueHandler((prev: any) => ({ ...prev, [f]: e.currentTarget.value }))} /> {info?.meta?.help} </p> {errors[f] ? <p class="help is-danger">{errors[f]}</p> : null} diff --git a/src/routes/instances/DeleteAllModal.tsx b/src/routes/instances/DeleteAllModal.tsx @@ -1,29 +0,0 @@ -import { h, VNode } from "preact"; -import { MerchantBackend } from "../../declaration"; - -interface Props { - element: MerchantBackend.Instances.Instance | null; - onCancel: () => void; - onConfirm: (i: MerchantBackend.Instances.Instance) => void; -} - -export default function DeleteAllModal({ element, onCancel, onConfirm }: Props): VNode { - return <div class={element ? "modal is-active is-clipped" : "modal"}> - <div class="modal-background jb-modal-close" onClick={e => onCancel()} /> - <div class="modal-card"> - <header class="modal-card-head"> - <p class="modal-card-title">Confirm action</p> - <button class="delete jb-modal-close" aria-label="close" onClick={e => onCancel()} /> - </header> - <section class="modal-card-body"> - <p>This will permanently delete instance "{element?.name}" with id <b>{element?.id}</b></p> - <p>Please confirm this action</p> - </section> - <footer class="modal-card-foot"> - <button class="button jb-modal-close" onClick={e => onCancel()} >Cancel</button> - <button class="button is-danger jb-modal-close" onClick={element ? e => onConfirm(element) : undefined} >Delete</button> - </footer> - </div> - <button class="modal-close is-large jb-modal-close" aria-label="close" onClick={e => onCancel()} /> - </div> -} -\ No newline at end of file diff --git a/src/routes/instances/DeleteModal.tsx b/src/routes/instances/DeleteModal.tsx @@ -1,11 +1,11 @@ import { h, VNode } from "preact"; -import { MerchantBackend } from "../../declaration"; +import { MerchantBackend, WidthId } from "../../declaration"; import ConfirmModal from "./ConfirmModal"; interface Props { - element: MerchantBackend.Instances.Instance; + element: MerchantBackend.Instances.QueryInstancesResponse & WidthId; onCancel: () => void; - onConfirm: (i: MerchantBackend.Instances.Instance) => void; + onConfirm: (i: MerchantBackend.Instances.QueryInstancesResponse & WidthId) => void; } export default function DeleteModal({ element, onCancel, onConfirm }: Props): VNode { diff --git a/src/routes/instances/Table.tsx b/src/routes/instances/Table.tsx @@ -1,153 +0,0 @@ -import { h, VNode } from "preact"; -import { useEffect, useState, StateUpdater } from "preact/hooks"; -import { MerchantBackend, WidthId as WithId } from "../../declaration"; -import DeleteModal from './DeleteModal' -import UpdateModal from './UpdateModal' -import CreateModal from './CreateModal' - -interface Props { - instances: MerchantBackend.Instances.Instance[]; - onCreate: (d: MerchantBackend.Instances.InstanceConfigurationMessage) => void; - onUpdate: (id: string, d: MerchantBackend.Instances.InstanceReconfigurationMessage) => void; - onDelete: (id: string) => void; - onSelect: (id: string | null) => void; - selected: MerchantBackend.Instances.QueryInstancesResponse & WithId | undefined; -} - -function toggleSelected<T>(id: T): (prev: T[]) => T[] { - return (prev: T[]): T[] => prev.indexOf(id) == -1 ? [...prev, id] : prev.filter(e => e != id) -} - -interface Actions { - element: MerchantBackend.Instances.Instance; - type: 'DELETE' | 'UPDATE'; -} - -function notEmpty<TValue>(value: TValue | null | undefined): value is TValue { - return value !== null && value !== undefined; -} - -function buildActions(intances: MerchantBackend.Instances.Instance[], selected: string[], action: 'DELETE'): Actions[] { - return selected.map(id => intances.find(i => i.id === id)) - .filter(notEmpty) - .map(id => ({ element: id, type: action })) -} - -const EmptyTable = () => <div class="content has-text-grey has-text-centered"> - <p> - <span class="icon is-large"><i class="mdi mdi-emoticon-sad mdi-48px" /></span> - </p> - <p>No instance configured yet, setup one pressing the + button </p> -</div> - -interface TableProps { - rowSelection: string[]; - instances: MerchantBackend.Instances.Instance[]; - onSelect: (id: string | null) => void; - rowSelectionHandler: StateUpdater<string[]>; - toBeDeletedHandler: StateUpdater<MerchantBackend.Instances.Instance | null>; -} - -const Table = ({ rowSelection, rowSelectionHandler, instances, onSelect, toBeDeletedHandler }: TableProps): VNode => ( - <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> - <thead> - <tr> - <th class="is-checkbox-cell"> - <label class="b-checkbox checkbox"> - <input type="checkbox" checked={rowSelection.length === instances.length} onClick={e => rowSelectionHandler(rowSelection.length === instances.length ? [] : instances.map(i => i.id))} /> - <span class="check" /> - </label> - </th> - <th>id</th> - <th>name</th> - <th>public key</th> - <th>payments</th> - <th /> - </tr> - </thead> - <tbody> - {instances.map(i => { - return <tr> - <td class="is-checkbox-cell"> - <label class="b-checkbox checkbox"> - <input type="checkbox" checked={rowSelection.indexOf(i.id) != -1} onClick={e => rowSelectionHandler(toggleSelected(i.id))} /> - <span class="check" /> - </label> - </td> - <td >{i.id}</td> - <td >{i.name}</td> - <td >{i.merchant_pub}</td> - <td >{i.payment_targets}</td> - <td class="is-actions-cell"> - <div class="buttons is-right"> - <button class="button is-small is-primary" type="button" onClick={e => onSelect(i.id)}> - <span class="icon"><i class="mdi mdi-eye" /></span> - </button> - <button class="button is-small is-danger jb-modal" type="button" onClick={e => toBeDeletedHandler(i)}> - <span class="icon"><i class="mdi mdi-trash-can" /></span> - </button> - </div> - </td> - </tr> - })} - - </tbody> - </table>) - - -export default function CardTable({ instances, onCreate, onDelete, onSelect, onUpdate, selected }: Props): VNode { - const [toBeCreated, toBeCreatedHandler] = useState<boolean>(false) - const [actionQueue, actionQueueHandler] = useState<Actions[]>([]); - const [toBeDeleted, toBeDeletedHandler] = useState<MerchantBackend.Instances.Instance | null>(null) - - const [rowSelection, rowSelectionHandler] = useState<string[]>([]) - - useEffect(() => { - if (actionQueue.length > 0 && !toBeDeleted && actionQueue[0].type == 'DELETE') { - toBeDeletedHandler(actionQueue[0].element) - actionQueueHandler(actionQueue.slice(1)) - } - }, [actionQueue, toBeDeleted]) - - useEffect(() => { - if (actionQueue.length > 0 && !selected && actionQueue[0].type == 'UPDATE') { - onSelect(actionQueue[0].element.id) - actionQueueHandler(actionQueue.slice(1)) - } - }, [actionQueue, selected]) - - - return <div class="card has-table"> - <header class="card-header"> - <p class="card-header-title"><span class="icon"><i class="mdi mdi-account-multiple" /></span>Instances</p> - <button class={rowSelection.length > 0 ? "card-header-icon" : "card-header-icon is-hidden"} type="button" onClick={e => actionQueueHandler(buildActions(instances, rowSelection, 'DELETE'))} > - <span class="icon"><i class="mdi mdi-trash-can" /></span> - </button> - <button class="card-header-icon" type="button" onClick={e => toBeCreatedHandler(true)}> - <span class="icon"><i class="mdi mdi-plus" /></span> - </button> - </header> - <div class="card-content"> - <div class="b-table has-pagination"> - <div class="table-wrapper has-mobile-cards"> - {instances.length > 0 ? - <Table instances={instances} onSelect={onSelect} rowSelection={rowSelection} rowSelectionHandler={rowSelectionHandler} toBeDeletedHandler={toBeDeletedHandler} /> : - <EmptyTable /> - } - </div> - {toBeDeleted ? <DeleteModal element={toBeDeleted} onCancel={() => toBeDeletedHandler(null)} onConfirm={(i) => { - onDelete(i.id) - toBeDeletedHandler(null); - }} /> : null} - {selected ? <UpdateModal element={selected} onCancel={() => onSelect(null)} onConfirm={(i) => { - onUpdate(selected.id, i) - onSelect(null); - }} /> : null} - {toBeCreated ? <CreateModal active={toBeCreated} onCancel={() => toBeCreatedHandler(false)} onConfirm={(i) => { - onCreate(i) - toBeCreatedHandler(false); - }} /> : null} - </div> - </div> - </div > -} -\ No newline at end of file diff --git a/src/routes/instances/UpdateModal.tsx b/src/routes/instances/UpdateModal.tsx @@ -66,7 +66,7 @@ export default function UpdateModal({ element, onCancel, onConfirm }: Props): VN <input class="input" type="text" placeholder={info?.meta?.placeholder} readonly={info?.meta?.readonly} name={f} value={value[f]} - onChange={e => valueHandler((prev: any) => ({ ...prev, [f]: e.currentTarget.value }))} /> + onChange={(e): void => valueHandler((prev: any) => ({ ...prev, [f]: e.currentTarget.value }))} /> {info?.meta?.help} </p> {errors[f] ? <p class="help is-danger">{errors[f]}</p> : null} diff --git a/src/routes/instances/View.tsx b/src/routes/instances/View.tsx @@ -1,6 +1,10 @@ import { h, VNode } from "preact"; +import { useState } from "preact/hooks"; import { MerchantBackend, WidthId } from "../../declaration"; -import Table from './Table'; +import Table from './CardTable'; +import DeleteModal from './DeleteModal' +import UpdateModal from './UpdateModal' +import CreateModal from './CreateModal' interface Props { instances: MerchantBackend.Instances.Instance[]; @@ -13,6 +17,14 @@ interface Props { } export default function View({ instances, isLoading, onCreate, onDelete, onSelect, onUpdate, selected }: Props): VNode { + const [create, setCreate] = useState<boolean>(false) + const [action, setAction] = useState<'UPDATE' | 'DELETE' | null>(null) + + const onSelectAction = (id: string | null, action?: 'UPDATE' | 'DELETE'): void => { + onSelect(id) + setAction(action || null) + } + return <div id="app"> <section class="section is-title-bar"> @@ -27,7 +39,8 @@ export default function View({ instances, isLoading, onCreate, onDelete, onSelec </div> </div> </section> - <section class={ isLoading ? "hero is-hero-bar" : "hero is-hero-bar is-loading" }> + + <section class={isLoading ? "hero is-hero-bar" : "hero is-hero-bar is-loading"}> <div class="hero-body"> <div class="level"> <div class="level-left"> @@ -42,12 +55,29 @@ export default function View({ instances, isLoading, onCreate, onDelete, onSelec </div> </section> <section class="section is-main-section"> - <Table instances={instances} onCreate={onCreate} - onUpdate={onUpdate} - onDelete={onDelete} onSelect={onSelect} - selected={selected} - /> + <Table instances={instances} onSelect={onSelectAction} selected={selected} onCreate={(): void => setCreate(true)} /> </section> + {selected && action === 'DELETE' ? + <DeleteModal element={selected} onCancel={(): void => onSelectAction(null)} onConfirm={(i): void => { + onDelete(i.id) + onSelectAction(null); + }} + /> + : null} + + {selected && action === 'UPDATE' ? + <UpdateModal element={selected} onCancel={(): void => onSelectAction(null)} onConfirm={(i): void => { + onUpdate(selected.id, i); + onSelectAction(null); + }} + /> + : null} + + {create ? <CreateModal onCancel={(): void => setCreate(false)} onConfirm={(i): void => { + onCreate(i) + setCreate(false); + }} /> : null} + </div > } \ No newline at end of file