diff --git a/comps/list/TableList.tsx b/comps/list/TableList.tsx index a63a185..96a9ffa 100755 --- a/comps/list/TableList.tsx +++ b/comps/list/TableList.tsx @@ -1,16 +1,29 @@ import { useLocal } from "@/utils/use-local"; import get from "lodash.get"; import { FC, useEffect } from "react"; -import DataGrid, { ColumnOrColumnGroup } from "react-data-grid"; +import DataGrid, { ColumnOrColumnGroup, SortColumn } from "react-data-grid"; import "react-data-grid/lib/styles.css"; import { getProp } from "../md/utils/get-prop"; +import { Toaster, toast } from "sonner"; +import { createPortal } from "react-dom"; +import { cn } from "@/utils"; +import { Loader2 } from "lucide-react"; +import { fields_map } from "@/utils/format-value"; +import { Skeleton } from "../ui/skeleton"; type TableListProp = { child: any; PassProp: any; name: string; - on_load: () => Promise; + on_load: (arg: { + reload: () => Promise; + orderBy?: Record>; + paging: { take: number; skip: number }; + mode: "count" | "query"; + }) => Promise; mode: "table" | "list" | "grid"; + _meta: Record; + gen_fields: string[]; }; export const TableList: FC = ({ @@ -19,6 +32,8 @@ export const TableList: FC = ({ child, PassProp, mode, + _meta, + gen_fields, }) => { const local = useLocal({ el: null as null | HTMLDivElement, @@ -27,30 +42,97 @@ export const TableList: FC = ({ rob: new ResizeObserver(([e]) => { local.height = e.contentRect.height; local.width = e.contentRect.width; + if (local.status === "ready") local.status = "resizing"; local.render(); }), scrolled: false, data: [] as any[], - status: "ready" as "loading" | "ready", + status: "init" as "loading" | "ready" | "resizing" | "reload" | "init", + paging: { + take: 0, + skip: 0, + timeout: null as any, + total: 0, + scroll: (event: React.UIEvent) => { + if (local.status === "loading" || !isAtBottom(event)) return; + if (local.data.length >= local.paging.skip + local.paging.take) { + local.paging.skip += local.paging.take; + local.status = "reload"; + local.render(); + } + }, + }, + sort: { + columns: [] as SortColumn[], + on_change: (cols: SortColumn[]) => { + local.sort.columns = cols; + local.paging.skip = 0; + + if (cols.length > 0) { + const { columnKey, direction } = cols[0]; + + let should_set = true; + const fields = fields_map.get(gen_fields); + if (fields) { + const rel = fields?.find((e) => e.name === columnKey); + if (rel && rel.checked) { + const field = rel.checked.find((e) => !e.is_pk); + if (field) { + should_set = false; + local.sort.orderBy = { + [columnKey]: { + [field.name]: direction === "ASC" ? "asc" : "desc", + }, + }; + } + } + } + + if (should_set) { + local.sort.orderBy = { + [columnKey]: direction === "ASC" ? "asc" : "desc", + }; + } + } else { + local.sort.orderBy = null; + } + local.status = "reload"; + local.render(); + }, + orderBy: null as null | Record< + string, + "asc" | "desc" | Record + >, + }, }); useEffect(() => { - if (local.status === "ready") { - local.status = "loading"; - local.render(); - - const result = on_load(); - const callback = (data: any[]) => { - local.data = data; + (async () => { + if (local.status === "reload") { + local.status = "loading"; local.render(); - }; - if (result instanceof Promise) result.then(callback); - else callback(result); + const orderBy = local.sort.orderBy || undefined; + const load_args = { + async reload() {}, + orderBy, + paging: { take: local.paging.take, skip: local.paging.skip }, + }; - local.status = "ready"; - local.render(); - } - }, [on_load]); + 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) result.then(callback); + else callback(result); + } + })(); + }, [local.status, on_load, local.sort.orderBy]); const raw_childs = get( child, @@ -60,11 +142,12 @@ export const TableList: FC = ({ let childs: any[] = []; const mode_child = raw_childs.find((e: any) => e.name === mode); - - if (mode_child && mode_child.childs) { - childs = mode_child.childs; + if (mode_child) { + const meta = _meta[mode_child.id]; + if (meta && meta.item.childs) { + childs = meta.item.childs; + } } - console.log(raw_childs); const columns: ColumnOrColumnGroup[] = []; for (const child of childs) { @@ -76,6 +159,7 @@ export const TableList: FC = ({ name, width: width > 0 ? width : undefined, resizable: true, + sortable: true, renderCell(props) { return ( = ({ }); } - return ( -
{ - if (!local.el && el) { - local.el = el; - local.rob.observe(el); + return null; + } + + if (!isEditor) { + if (local.status === "loading") { + toast.loading( + <> + + Loading {local.paging.skip === 0 ? "Data" : "more rows"} ... + , + { + dismissible: true, + className: css` + background: #e4f7ff; + `, } - }} - > - -
- ); + ); + } else { + toast.dismiss(); + } + } + + if (document.getElementsByClassName("prasi-toaster").length === 0) { + const elemDiv = document.createElement("div"); + elemDiv.className = "prasi-toaster"; + document.body.appendChild(elemDiv); + } + const toaster_el = document.getElementsByClassName("prasi-toaster")[0]; + + if (mode === "table") { + return ( +
{ + if (!local.el && el) { + local.el = el; + local.rob.observe(el); + } + }} + > + {local.status !== "ready" && ( +
+ + + +
+ )} +
+ {toaster_el && createPortal(, toaster_el)} + {local.status === "init" ? ( + { + local.status = "reload"; + local.paging.take = local.paging.take * 5; + local.render(); + }, 100); + return <>; + }, + }, + ]} + rows={genRows(200)} + /> + ) : ( + <> + + + )} +
+
+ ); + } else { + } }; + +const genRows = (total: number) => { + const result = [] as any[]; + for (let i = 0; i < total; i++) { + result.push({ _: i }); + } + return result; +}; + +const dataGridStyle = (local: { height: number }) => css` + .rdg { + block-size: ${local.height}px; + } + div[role="row"]:hover { + background: #e2f1ff; + .num-edit { + display: flex; + } + .num-idx { + display: none; + } + } + div[role="columnheader"] span svg { + margin: 12px 2px; + } + div[aria-selected="true"] { + outline: none; + } + div[role="gridcell"] { + padding-inline: 0px; + } + + .row-selected { + background: #e2f1ff; + } +`; + +function isAtBottom({ currentTarget }: React.UIEvent): boolean { + return ( + currentTarget.scrollTop + 10 >= + currentTarget.scrollHeight - currentTarget.clientHeight + ); +} diff --git a/gen/gen_table_list/gen_table_list.tsx b/gen/gen_table_list/gen_table_list.tsx index 531be22..72fe7ab 100755 --- a/gen/gen_table_list/gen_table_list.tsx +++ b/gen/gen_table_list/gen_table_list.tsx @@ -60,20 +60,43 @@ export const gen_table_list = ( } ); - result["child"].content.childs = [ - createItem({ - name: arg.mode, - childs: [ - { + const child = createItem({ + name: arg.mode, + childs: columns + .map((e) => { + if (e.is_pk) return; + return { component: { id: "297023a4-d552-464a-971d-f40dcd940b77", props: { - name: "muku", + name: e.name, + title: formatName(e.name), + child: { + name: "cell", + padding: { + l: 8, + b: 0, + t: 0, + r: 8, + }, + adv: { + js: `\ +
+ +
`, + jsBuilt: `\ +render(React.createElement("div", Object.assign({}, props, { className: cx(props.className, "") }),React.createElement(FormatValue, { value: col.value, name: col.name, gen_fields: gen_fields }))); + `, + }, + }, }, }, - }, - ], - }), + }; + }) + .filter((e) => e) as any, + }); + result["child"].content.childs = [ + child, ...result["child"].content.childs, ]; } diff --git a/gen/gen_table_list/on_load.ts b/gen/gen_table_list/on_load.ts index 5fb8b6a..e94e6f1 100755 --- a/gen/gen_table_list/on_load.ts +++ b/gen/gen_table_list/on_load.ts @@ -29,20 +29,30 @@ export const on_load = ({ return `\ async (arg: TableOnLoad) => { - if (isEditor) return [${JSON.stringify(sample)}]; + if (isEditor) return sampleData; + + if (arg.mode === 'count') { + return await db.${table}.count(); + } const items = await db.${table}.findMany({ select: ${JSON.stringify(select, null, 2).split("\n").join("\n ")}, - orderBy: { + orderBy: arg.orderBy || { ${pk}: "desc" - } + }, + ...arg.paging, }); return items; } +const sampleData = [${JSON.stringify(sample, null, 2)}] + type TableOnLoad = { reload: () => Promise; + orderBy?: Record; + paging: { take: number; skip: number }; + mode: 'count' | 'query' } `; }; diff --git a/gen/utils.ts b/gen/utils.ts index 4570637..f1f305a 100755 --- a/gen/utils.ts +++ b/gen/utils.ts @@ -22,8 +22,13 @@ export const formatName = (name: string) => { type SimplifiedItem = { name?: string; - component?: { id: string; props: Record }; + component?: { id: string; props: Record }; childs?: SimplifiedItem[]; + adv?: { + js: string; + jsBuilt: string; + }; + padding?: any; }; export const createItem = (arg: SimplifiedItem): any => { @@ -34,11 +39,32 @@ export const createItem = (arg: SimplifiedItem): any => { if (arg.component.props) { for (const [k, v] of Object.entries(arg.component.props)) { - component.props[k] = { - type: "string", - value: JSON.stringify(v), - valueBuilt: JSON.stringify(v), - }; + if (typeof v === "object") { + component.props[k] = { + meta: { + type: "content-element", + }, + content: { + id: createId(), + dim: { + h: "full", + w: "full", + }, + padding: arg.padding, + type: "item", + name: k, + ...v, + }, + value: "", + valueBuilt: "", + }; + } else { + component.props[k] = { + type: "string", + value: JSON.stringify(v), + valueBuilt: JSON.stringify(v), + }; + } } } } @@ -49,10 +75,13 @@ export const createItem = (arg: SimplifiedItem): any => { h: "full", w: "full", }, + padding: arg.padding, name: arg.name || "item", type: "item", component, - script: {}, + script: { + ...arg.adv, + }, childs: arg.childs?.map(createItem), }; }; diff --git a/utils/format-value.tsx b/utils/format-value.tsx new file mode 100755 index 0000000..e95e492 --- /dev/null +++ b/utils/format-value.tsx @@ -0,0 +1,49 @@ +import { GFCol } from "@/gen/utils"; +import { FC } from "react"; + +export const fields_map = new WeakMap< + string[], + (GFCol & { checked?: GFCol[] })[] +>(); + +export const FormatValue: FC<{ + value: any; + name: string; + gen_fields: string[]; +}> = (prop) => { + const { value, gen_fields, name } = prop; + + if (!fields_map.has(gen_fields)) { + fields_map.set( + gen_fields, + gen_fields.map((e: any) => { + if (typeof e === "string") { + return JSON.parse(e); + } else { + return { + ...JSON.parse(e.value), + checked: e.checked.map(JSON.parse), + }; + } + }) + ); + } + + const fields = fields_map.get(gen_fields); + + if (typeof value === "object" && value) { + const rel = fields?.find((e) => e.name === name); + if (rel && rel.checked) { + const result = rel.checked + .filter((e) => !e.is_pk) + .map((e) => { + return value[e.name]; + }) + .join(" - "); + return result; + } + + return JSON.stringify(value); + } + return <>{value}; +};