diff --git a/comps/form/Form.tsx b/comps/form/Form.tsx index 4f32596..a2c06f7 100755 --- a/comps/form/Form.tsx +++ b/comps/form/Form.tsx @@ -28,20 +28,9 @@ export const Form: FC = (props) => { events: { on_change(name: string, new_value: any) {}, }, + internal: {}, submit: null as any, error: {} as any, - internal: { - reload: { - timeout: null as any, - promises: [], - done: [], - }, - submit: { - timeout: null as any, - promises: [], - done: [], - }, - }, field_def: {}, props: {} as any, size: { @@ -164,6 +153,7 @@ export const Form: FC = (props) => { formInit(fm, props); fm.reload(); } + if (document.getElementsByClassName("prasi-toaster").length === 0) { const elemDiv = document.createElement("div"); elemDiv.className = "prasi-toaster"; diff --git a/comps/form/gen/gen-form/on-submit.ts b/comps/form/gen/gen-form/on-submit.ts index 8fc00ae..dcbb3b6 100755 --- a/comps/form/gen/gen-form/on-submit.ts +++ b/comps/form/gen/gen-form/on-submit.ts @@ -149,13 +149,13 @@ ${ fm.status = "ready"; fm.data = form; md.selected = form; + if (md.props.mode !== "full") md.master.reload({ toast: false }); md.render(); fm.render(); if (fm.props.back_on_save === "y") { md.selected = null; md.tab.active = "master"; - md.internal.action_should_refresh = true; md.params.apply(); md.render(); } diff --git a/comps/form/typings.ts b/comps/form/typings.ts index d48b61f..8e8ff09 100755 --- a/comps/form/typings.ts +++ b/comps/form/typings.ts @@ -139,16 +139,6 @@ export type FMInternal = { clear: (name?: string) => void; }; internal: { - reload: { - timeout: ReturnType; - promises: Promise[]; - done: any[]; - }; - submit: { - promises: Promise[]; - timeout: ReturnType; - done: any[]; - }; original_render?: () => void; }; props: Exclude; diff --git a/comps/form/utils/init.tsx b/comps/form/utils/init.tsx index b1baec0..a8ffabd 100755 --- a/comps/form/utils/init.tsx +++ b/comps/form/utils/init.tsx @@ -1,10 +1,10 @@ import { parseGenField } from "@/gen/utils"; import get from "lodash.get"; import { AlertTriangle, Check, Loader2 } from "lucide-react"; -import { toast } from "sonner"; import { FMLocal, FMProps } from "../typings"; import { editorFormData } from "./ed-data"; import { formError } from "./error"; +import { toast } from "lib/comps/ui/toast"; export const formInit = (fm: FMLocal, props: FMProps) => { for (const [k, v] of Object.entries(props)) { @@ -19,77 +19,165 @@ export const formInit = (fm: FMLocal, props: FMProps) => { fm.field_def[d.name] = d; } - fm.reload = () => { + fm.reload = async () => { fm.status = isEditor ? "ready" : "loading"; fm.render(); - const promise = new Promise((done) => { - fm.internal.reload.done.push(done); - clearTimeout(fm.internal.reload.timeout); - fm.internal.reload.timeout = setTimeout(async () => { - if (sonar === "on" && !isEditor) { - setTimeout(() => { - toast.dismiss(); - toast.loading( - <> - - Loading data... - - ); - }); + if (sonar === "on" && !isEditor) { + toast.dismiss(); + toast.loading( + <> + + Loading data... + + ); + } + + let should_load = true; + if (isEditor) { + const item_id = props.item.id; + if (item_id) { + const cache = editorFormData[item_id]; + if ( + cache && + cache.on_load === get(props.item, "component.props.on_load.value") + ) { + fm.data = cache.data; + should_load = false; + } + } + } + if (should_load) { + if (!on_load) { + console.error("Form on_load is empty. Please re-generate the form."); + } else { + const on_load_result = on_load({ fm }); + let result = undefined; + if ( + typeof on_load_result === "object" && + on_load_result instanceof Promise + ) { + result = await on_load_result; + } else { + result = on_load_result; } - let should_load = true; + fm.data = result; + + if (result === undefined) fm.data = {}; + if (isEditor) { const item_id = props.item.id; if (item_id) { - const cache = editorFormData[item_id]; - if ( - cache && - cache.on_load === get(props.item, "component.props.on_load.value") - ) { - fm.data = cache.data; - should_load = false; - } + editorFormData[item_id] = { + data: fm.data, + on_load: get(props.item, "component.props.on_load.value"), + }; } } - if (should_load) { - if (!on_load) { - console.error("Form on_load is empty. Please re-generate the form."); - } else { - const on_load_result = on_load({ fm }); - let result = undefined; - if ( - typeof on_load_result === "object" && - on_load_result instanceof Promise - ) { - result = await on_load_result; - } else { - result = on_load_result; + } + } + + toast.dismiss(); + + if (fm.is_newly_created) { + fm.is_newly_created = false; + toast.success( +
+ + Saved +
, + { + className: css` + background: #e4ffed; + border: 2px solid green; + `, + } + ); + } + + fm.status = "ready"; + fm.render(); + }; + + fm.submit = async () => { + if (fm.status !== "ready") { + return; + } + if (typeof fm.props.on_submit === "function") { + fm.status = "saving"; + fm.render(); + + if (fm.props.sonar === "on" && !isEditor) { + toast.loading( + <> + + Submitting... + + ); + } + + const form = JSON.parse(JSON.stringify(fm.data)); + + if (fm.deps.md) { + const md = fm.deps.md; + const last = md.params.links[md.params.links.length - 1]; + if (last) { + const pk = Object.values(fm.field_def).find((e) => e.is_pk); + if (pk) { + let obj = last.update; + if (!fm.data[pk.name]) { + obj = last.create; } - fm.data = result; - - if (result === undefined) fm.data = {}; - - if (isEditor) { - const item_id = props.item.id; - if (item_id) { - editorFormData[item_id] = { - data: fm.data, - on_load: get(props.item, "component.props.on_load.value"), - }; + if (typeof obj === "object" && obj) { + for (const [k, v] of Object.entries(obj)) { + form[k] = v; } } } } + } - fm.internal.reload.done.map((e) => e()); - setTimeout(() => { - toast.dismiss(); + const success = await fm.props.on_submit({ + fm, + form, + error: fm.error.object, + }); - if (fm.is_newly_created) { - fm.is_newly_created = false; + toast.dismiss(); + + if (!success) { + fm.status = "ready"; + fm.render(); + } + + if (fm.props.sonar === "on" && !isEditor) { + toast.dismiss(); + + if (!success) { + const errors = Object.keys(fm.error.list); + const count = errors.length; + console.log(fm.error.list); + toast.error( +
+ + Save Failed + {count > 0 && + `, please correct + ${count} errors`} + . +
, + { + dismissible: true, + className: css` + background: #ffecec; + border: 2px solid red; + `, + } + ); + } else { + if (!fm.is_newly_created) { toast.success(
@@ -103,127 +191,10 @@ export const formInit = (fm: FMLocal, props: FMProps) => { } ); } - }, 100); - - fm.status = "ready"; - fm.render(); - }, 50); - }); - fm.internal.reload.promises.push(promise); - return promise; - }; - - fm.submit = () => { - const promise = new Promise(async (done) => { - fm.internal.submit.done.push(done); - clearTimeout(fm.internal.submit.timeout); - fm.internal.submit.timeout = setTimeout(async () => { - const done_all = (val: boolean) => { - for (const d of fm.internal.submit.done) { - d(val); - } - fm.internal.submit.done = []; - fm.render(); - }; - - if (typeof fm.props.on_submit === "function") { - fm.status = "saving"; - fm.render(); - - if (fm.props.sonar === "on" && !isEditor) { - toast.loading( - <> - - Submitting... - - ); - } - - const form = JSON.parse(JSON.stringify(fm.data)); - - if (fm.deps.md) { - const md = fm.deps.md; - const last = md.params.links[md.params.links.length - 1]; - if (last) { - const pk = Object.values(fm.field_def).find((e) => e.is_pk); - if (pk) { - let obj = last.update; - if (!fm.data[pk.name]) { - obj = last.create; - } - - if (typeof obj === "object" && obj) { - for (const [k, v] of Object.entries(obj)) { - form[k] = v; - } - } - } - } - } - - const success = await fm.props.on_submit({ - fm, - form, - error: fm.error.object, - }); - - toast.dismiss(); - done_all(success); - - if (!success) { - fm.status = "ready"; - fm.render(); - } - - if (fm.props.sonar === "on" && !isEditor) { - setTimeout(() => { - toast.dismiss(); - - if (!success) { - const errors = Object.keys(fm.error.list); - const count = errors.length; - console.log(fm.error.list); - toast.error( -
- - Save Failed - {count > 0 && - `, please correct - ${count} errors`} - . -
, - { - dismissible: true, - className: css` - background: #ffecec; - border: 2px solid red; - `, - } - ); - } else { - if (!fm.is_newly_created) { - toast.success( -
- - Saved -
, - { - className: css` - background: #e4ffed; - border: 2px solid green; - `, - } - ); - } - } - }, 100); - } } - }, 100); - }); - fm.internal.submit.promises.push(promise); - - return promise; + } + return success; + } }; if (typeof fm.props.on_init === "function") { fm.props.on_init({ fm, submit: fm.submit, reload: fm.reload }); diff --git a/comps/list/TableList.tsx b/comps/list/TableList.tsx index 06b7df8..a71614b 100755 --- a/comps/list/TableList.tsx +++ b/comps/list/TableList.tsx @@ -23,13 +23,14 @@ import DataGrid, { } from "react-data-grid"; import "react-data-grid/lib/styles.css"; import { createPortal } from "react-dom"; -import { Toaster, toast } from "sonner"; +import { Toaster } from "sonner"; import { call_prasi_events } from "../../.."; import { filterWhere } from "../filter/parser/filter-where"; import { getFilter } from "../filter/utils/get-filter"; import { MDLocal } from "../md/utils/typings"; import { Skeleton } from "../ui/skeleton"; import { sortTree } from "./utils/sort-tree"; +import { toast } from "../ui/toast"; type OnRowClick = (arg: { row: any; @@ -37,6 +38,8 @@ type OnRowClick = (arg: { idx: any; event: React.MouseEvent; }) => void; +let EMPTY_SET = new Set() as ReadonlySet; + type SelectedRow = (arg: { row: any; rows: any[]; idx: any }) => boolean; type TableListProp = { child: any; @@ -147,6 +150,7 @@ export const TableList: FC = ({ | "init" | "error", where: null as any, + should_toast: true, paging: { take: 0, skip: 0, @@ -235,75 +239,82 @@ export const TableList: FC = ({ }, }); - const reload = useCallback(() => { - if (typeof on_load === "function") { - local.status = "loading"; - local.render(); + const reload = useCallback( + (arg?: { toast: boolean }) => { + let should_toast = true; + if (arg?.toast === false) should_toast = false; + local.should_toast = should_toast; - const orderBy = local.sort.orderBy || undefined; - const where = filterWhere(filter_name, __props); - - if (md) { - const last = md.params.links[md.params.links.length - 1]; - if (last && last.where) { - for (const [k, v] of Object.entries(last.where)) { - where[k] = v; - } - } - } - - call_prasi_events("tablelist", "where", [__props?.gen__table, where]); - - const load_args: any = { - async reload() {}, - orderBy, - where, - paging: { - take: local.paging.take > 0 ? local.paging.take : undefined, - skip: local.paging.skip, - }, - }; - if (id_parent) { - load_args.paging = {}; - } - const result = on_load({ ...load_args, mode: "query" }); - const callback = (data: any[]) => { - if (local.paging.skip === 0) { - local.data = data; - } else { - local.data = [...local.data, ...data]; - } - - local.status = "ready"; + if (typeof on_load === "function") { + local.status = "loading"; local.render(); - }; - if (result instanceof Promise) { - (async () => { - try { - callback(await result); - } catch (e) { - console.error(e); - local.status = "error"; - toast.dismiss(); - toast.error( -
- - Failed to load data -
, - { - dismissible: true, - className: css` - background: #ffecec; - border: 2px solid red; - `, - } - ); + const orderBy = local.sort.orderBy || undefined; + const where = filterWhere(filter_name, __props); + + if (md) { + const last = md.params.links[md.params.links.length - 1]; + if (last && last.where) { + for (const [k, v] of Object.entries(last.where)) { + where[k] = v; + } } - })(); - } else callback(result); - } - }, [on_load, local.sort.orderBy, local.paging.take, local.paging.skip]); + } + + call_prasi_events("tablelist", "where", [__props?.gen__table, where]); + + const load_args: any = { + async reload() {}, + orderBy, + where, + paging: { + take: local.paging.take > 0 ? local.paging.take : undefined, + skip: local.paging.skip, + }, + }; + if (id_parent) { + load_args.paging = {}; + } + const result = on_load({ ...load_args, mode: "query" }); + const callback = (data: any[]) => { + if (local.paging.skip === 0) { + local.data = data; + } else { + local.data = [...local.data, ...data]; + } + + local.status = "ready"; + local.render(); + }; + + if (result instanceof Promise) { + (async () => { + try { + callback(await result); + } catch (e) { + console.error(e); + local.status = "error"; + toast.dismiss(); + toast.error( +
+ + Failed to load data +
, + { + dismissible: true, + className: css` + background: #ffecec; + border: 2px solid red; + `, + } + ); + } + })(); + } else callback(result); + } + }, + [on_load, local.sort.orderBy, local.paging.take, local.paging.skip] + ); if (md) { md.master.reload = reload; @@ -540,16 +551,20 @@ export const TableList: FC = ({ if (!isEditor) { if (local.status === "loading") { - toast.dismiss(); - toast.loading( - <> - - Loading {local.paging.skip === 0 ? "Data" : "more rows"} ... - , - { - dismissible: true, - } - ); + if (local.should_toast) { + toast.dismiss(); + toast.loading( + <> + + Loading {local.paging.skip === 0 ? "Data" : "more rows"} ... + , + { + dismissible: true, + } + ); + } else { + local.should_toast = true; + } } else { if (local.status !== "error") { toast.dismiss(); @@ -682,7 +697,7 @@ export const TableList: FC = ({ rows={data} className="rdg-light" onScroll={local.paging.scroll} - selectedRows={new Set() as ReadonlySet} + selectedRows={EMPTY_SET} onSelectedCellChange={() => {}} onSelectedRowsChange={() => {}} headerRowHeight={show_header === false ? 0 : undefined} @@ -722,7 +737,10 @@ export const TableList: FC = ({ isRowSelected={true} className={cx( props.className, - isSelect && "row-selected" + (isSelect || + md?.selected?.[local.pk?.name || ""] === + props.row[local.pk?.name || ""]) && + "row-selected" )} /> ); @@ -911,7 +929,7 @@ const dataGridStyle = (local: { height: number }) => css` } .row-selected { - background: #e2f1ff; + background: #bddfff !important; } `; diff --git a/comps/md/MasterDetail.tsx b/comps/md/MasterDetail.tsx index 0e1f7e9..41b2315 100755 --- a/comps/md/MasterDetail.tsx +++ b/comps/md/MasterDetail.tsx @@ -27,6 +27,7 @@ export const MasterDetail: FC = (arg) => { on_init, _item, title, + detail_size, } = arg; const _ref = useRef({ PassProp, item: _item, childs: {} }); const mdr = _ref.current; @@ -47,7 +48,7 @@ export const MasterDetail: FC = (arg) => { active: "", list: [], }, - internal: { action_should_refresh: true }, + internal: { action_should_refresh: false }, childs: {}, props: { mode, @@ -70,11 +71,8 @@ export const MasterDetail: FC = (arg) => { masterDetailApplyParams(md); }, }, + detail_size: Number(detail_size || "400"), master: { render() {}, reload() {} }, - panel: { - size: 25, - min_size: 0, - }, }); mdr.PassProp = PassProp; diff --git a/comps/md/gen/md-form.ts b/comps/md/gen/md-form.ts index a30bd48..872c711 100755 --- a/comps/md/gen/md-form.ts +++ b/comps/md/gen/md-form.ts @@ -86,10 +86,8 @@ export const generateMDForm = async ( onClick: () => { md.selected = null; md.tab.active = "master"; - md.internal.action_should_refresh = true; md.params.apply(); md.render(); - }, }, ]; diff --git a/comps/md/gen/md-list.ts b/comps/md/gen/md-list.ts index 137779b..8e73bf2 100755 --- a/comps/md/gen/md-list.ts +++ b/comps/md/gen/md-list.ts @@ -38,7 +38,6 @@ export const generateMDList = async ( value: `\ ({ row, rows, idx, event }: OnRowClick) => { md.selected = row; -md.internal.action_should_refresh = true; md.tab.active = "detail"; md.params.apply(); md.render(); diff --git a/comps/md/mode/h-split.tsx b/comps/md/mode/h-split.tsx index b315926..442c0a0 100755 --- a/comps/md/mode/h-split.tsx +++ b/comps/md/mode/h-split.tsx @@ -3,35 +3,40 @@ import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels"; import { MDMaster } from "../parts/MDMaster"; import { MDDetail } from "../parts/MDDetail"; import { MDLocal, MDRef } from "../utils/typings"; +import { getPathname } from "@/utils/pathname"; export const ModeHSplit: FC<{ md: MDLocal; mdr: MDRef }> = ({ md, mdr }) => { return (
- + - <> - - { - if (e < 80) { - localStorage.setItem(`prasi-md-h-${md.name}`, e.toString()); + {(md.selected || isEditor) && ( + <> + + - - - + onResize={(e) => { + if (e < 80 && !isEditor) { + localStorage.setItem( + `prasi-md-${getPathname({ hash: false })}${md.name}`, + e.toString() + ); + } + }} + > + + + + )}
); diff --git a/comps/md/mode/v-split.tsx b/comps/md/mode/v-split.tsx index d16a484..ef487f3 100755 --- a/comps/md/mode/v-split.tsx +++ b/comps/md/mode/v-split.tsx @@ -1,37 +1,42 @@ import { FC } from "react"; +import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels"; +import { MDDetail } from "../parts/MDDetail"; import { MDMaster } from "../parts/MDMaster"; import { MDLocal, MDRef } from "../utils/typings"; -import { MDDetail, should_show_tab } from "../parts/MDDetail"; -import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels"; +import { getPathname } from "@/utils/pathname"; export const ModeVSplit: FC<{ md: MDLocal; mdr: MDRef }> = ({ md, mdr }) => { return (
- + - <> - - { - if (e < 80) { - localStorage.setItem(`prasi-md-h-${md.name}`, e.toString()); + {(md.selected || isEditor) && ( + <> + + - - - + onResize={(e) => { + if (e < 80 && !isEditor) { + localStorage.setItem( + `prasi-md-${getPathname({ hash: false })}${md.name}`, + e.toString() + ); + } + }} + > + + + + )}
); diff --git a/comps/md/parts/MDDetail.tsx b/comps/md/parts/MDDetail.tsx index 8705505..6b474e9 100755 --- a/comps/md/parts/MDDetail.tsx +++ b/comps/md/parts/MDDetail.tsx @@ -3,6 +3,7 @@ import { FC, useEffect } from "react"; import { breadcrumbPrefix } from "../utils/md-hash"; import { MDLocal, MDRef } from "../utils/typings"; import { MDHeader } from "./MDHeader"; +import { hashSum } from "lib/utils/hash-sum"; export const should_show_tab = (md: MDLocal) => { if (isEditor) { @@ -12,6 +13,7 @@ export const should_show_tab = (md: MDLocal) => { }; export const MDDetail: FC<{ md: MDLocal; mdr: MDRef }> = ({ md, mdr }) => { + const local = useLocal({ selected: "", synced: false }); const detail = md.childs[md.tab.active]; const PassProp = mdr.PassProp; if (!detail) { diff --git a/comps/md/utils/typings.ts b/comps/md/utils/typings.ts index 6441f8b..dc65f56 100755 --- a/comps/md/utils/typings.ts +++ b/comps/md/utils/typings.ts @@ -24,6 +24,7 @@ export type MDProps = { gen_fields: any; footer: any; gen_table: string; + detail_size: string; on_init: (md: MDLocal) => void; _item: PrasiItem; deps?: any[]; @@ -54,7 +55,11 @@ export type MDLocalInternal = { list: string[]; }; internal: { action_should_refresh: boolean }; - master: { reload: () => void; render: () => void }; + master: { + reload: (arg?: { toast: boolean }) => void; + render: () => void; + pk?: string; + }; params: { links: LinkParam[]; hash: Record; @@ -74,6 +79,7 @@ export type MDLocalInternal = { item: PrasiItem; }; deps?: object; + detail_size: number; childs: Record< string, { @@ -87,10 +93,6 @@ export type MDLocalInternal = { list?: any; } >; - panel: { - size: number; - min_size: number; - }; }; export type MDRef = { PassProp: any; @@ -133,7 +135,7 @@ export const MasterDetailType = `const md = { apply: () => void; }; props: { - mode: "full" | "h-split" | "v-split"; + mode: string; show_head: "always" | "only-master" | "only-child" | "hidden"; tab_mode: "h-tab" | "v-tab" | "hidden"; editor_tab: string; @@ -143,7 +145,7 @@ export const MasterDetailType = `const md = { }; internal: { action_should_refresh: boolean }; render: () => void; - master: { reload: () => void; render: () => void }; + master: { reload: (arg?:{toast: boolean}) => void; render: () => void }; pk?: { name: string; type: string; @@ -168,9 +170,6 @@ export const MasterDetailType = `const md = { md?: md; } >; - panel: { - size: number; - min_size: number; - }; + detail_size: number; deps: any };`; diff --git a/comps/ui/toast.tsx b/comps/ui/toast.tsx new file mode 100755 index 0000000..c805c9e --- /dev/null +++ b/comps/ui/toast.tsx @@ -0,0 +1,47 @@ +import { ReactElement } from "react"; +import { toast as sonner } from "sonner"; +const timer = { + timeout: null as any, + done: false, + limit: 400, +}; + +export const toast = { + dismiss: () => { + if (!timer.timeout) { + sonner.dismiss(); + } else { + clearTimeout(timer.timeout); + } + }, + loading: ( + el: ReactElement, + props?: { dismissible?: boolean; className?: string } + ) => { + clearTimeout(timer.timeout); + timer.timeout = setTimeout(() => { + sonner.loading(el, props); + timer.timeout = null; + }, timer.limit); + }, + success: ( + el: ReactElement, + props?: { dismissible?: boolean; className?: string } + ) => { + clearTimeout(timer.timeout); + timer.timeout = setTimeout(() => { + sonner.success(el, props); + timer.timeout = null; + }, timer.limit); + }, + error: ( + el: ReactElement, + props?: { dismissible?: boolean; className?: string } + ) => { + clearTimeout(timer.timeout); + timer.timeout = setTimeout(() => { + sonner.error(el, props); + timer.timeout = null; + }, timer.limit); + }, +};