import { cn } from "@/utils"; import { fields_map } from "@/utils/format-value"; import { useLocal } from "@/utils/use-local"; import get from "lodash.get"; import { Loader2 } from "lucide-react"; import { FC, useEffect } from "react"; import DataGrid, { ColumnOrColumnGroup, Row, SortColumn, } from "react-data-grid"; import "react-data-grid/lib/styles.css"; import { createPortal } from "react-dom"; import { Toaster, toast } from "sonner"; import { getProp } from "../md/utils/get-prop"; import { Skeleton } from "../ui/skeleton"; import { sortTree } from "./sort-tree"; import { GFCol, parseGenField } from "@/gen/utils"; type OnRowClick = (arg: { row: any; rows: any[]; idx: any; event: React.MouseEvent; }) => void; type TableListProp = { child: any; PassProp: any; name: string; 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[]; row_click: OnRowClick; id_parent?: string; }; export const TableList: FC = ({ name, on_load, child, PassProp, mode, _meta, gen_fields, row_click, id_parent, }) => { const local = useLocal({ el: null as null | HTMLDivElement, width: 0, height: 0, rob: new ResizeObserver(([e]) => { local.height = e.contentRect.height; local.width = e.contentRect.width; if (local.status === "ready") local.status = "resizing"; local.render(); }), pk: null as null | GFCol, scrolled: false, data: [] as any[], 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 gf = JSON.stringify(gen_fields); const fields = fields_map.get(gf); if (fields) { const rel = fields?.find((e) => e.name === columnKey); if (rel && rel.checked) { should_set = false; if (rel.type === "has-many") { local.sort.orderBy = { [columnKey]: { _count: direction === "ASC" ? "asc" : "desc", }, }; } else { const field = rel.checked.find((e) => !e.is_pk); if (field) { 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 (isEditor) return; (async () => { if (local.status === "reload" && typeof on_load === "function") { local.status = "loading"; local.render(); const orderBy = local.sort.orderBy || undefined; const load_args: any = { async reload() {}, orderBy, paging: { take: local.paging.take, 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) result.then(callback); else callback(result); } })(); }, [local.status, on_load, local.sort.orderBy]); const raw_childs = get( child, "props.meta.item.component.props.child.content.childs" ); let childs: any[] = []; let sub_name = "fields"; if (mode === "table") sub_name = "columns"; const mode_child = raw_childs.find( (e: any) => e.name === sub_name || e.name === mode ); if (mode_child) { const meta = _meta[mode_child.id]; if (meta && meta.item.childs) { childs = meta.item.childs; } } const columns: ColumnOrColumnGroup[] = []; for (const child of childs) { const key = getProp(child, "name", {}); const name = getProp(child, "title", {}); const width = parseInt(getProp(child, "width", {})); columns.push({ key, name, width: width > 0 ? width : undefined, resizable: true, sortable: true, renderCell(props) { return ( {child} ); }, }); } if (local.status === "resizing" && !isEditor) { local.status = "ready"; local.render(); 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 (local.status === "init") { const fields = parseGenField(gen_fields); for (const field of fields) { if (field.is_pk) { local.pk = field; } } } if (isEditor && local.status !== "ready") { if (local.data.length === 0) { const load_args: any = { async reload() {}, paging: { take: local.paging.take, skip: local.paging.skip }, }; if (id_parent) load_args.paging = {}; if (typeof on_load === "function") { local.data = on_load({ ...load_args, mode: "query" }) as any; } } local.status = "ready"; } let selected_idx = -1; let data = local.data || []; if (id_parent && local.pk && local.sort.columns.length === 0) { data = sortTree(local.data, id_parent, local.pk.name); } 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)} /> ) : ( <> { if ( !isEditor && typeof row_click === "function" ) { row_click({ event: ev, idx: props.rowIdx, row: props.row, rows: local.data, }); } }} isRowSelected={is_selected} className={cx( props.className, is_selected && "row-selected" )} /> ); }, noRowsFallback: (
No Data
), } } /> )}
); } 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 ); }