prasi-lib/comps/list/TableList.tsx

320 lines
8.4 KiB
TypeScript
Executable File

import { useLocal } from "@/utils/use-local";
import get from "lodash.get";
import { FC, useEffect } from "react";
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: (arg: {
reload: () => Promise<void>;
orderBy?: Record<string, "asc" | "desc" | Record<string, "asc" | "desc">>;
paging: { take: number; skip: number };
mode: "count" | "query";
}) => Promise<any[]>;
mode: "table" | "list" | "grid";
_meta: Record<string, any>;
gen_fields: string[];
};
export const TableList: FC<TableListProp> = ({
name,
on_load,
child,
PassProp,
mode,
_meta,
gen_fields,
}) => {
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();
}),
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<HTMLDivElement>) => {
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<string, "asc" | "desc">
>,
},
});
useEffect(() => {
(async () => {
if (local.status === "reload") {
local.status = "loading";
local.render();
const orderBy = local.sort.orderBy || undefined;
const load_args = {
async reload() {},
orderBy,
paging: { take: local.paging.take, skip: local.paging.skip },
};
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[] = [];
const mode_child = raw_childs.find((e: any) => e.name === mode);
if (mode_child) {
const meta = _meta[mode_child.id];
if (meta && meta.item.childs) {
childs = meta.item.childs;
}
}
const columns: ColumnOrColumnGroup<any>[] = [];
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 (
<PassProp
idx={props.rowIdx}
row={props.row}
col={{
name: props.column.key,
value: props.row[props.column.key],
}}
rows={local.data}
>
{child}
</PassProp>
);
},
});
}
if (local.status === "resizing") {
local.status = "ready";
local.render();
return null;
}
if (!isEditor) {
if (local.status === "loading") {
toast.loading(
<>
<Loader2 className="c-h-4 c-w-4 c-animate-spin" />
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 (
<div
className={cx(
"c-w-full c-h-full c-flex-1 c-relative c-overflow-hidden",
dataGridStyle(local)
)}
ref={(el) => {
if (!local.el && el) {
local.el = el;
local.rob.observe(el);
}
}}
>
{local.status !== "ready" && (
<div className="c-flex c-flex-col c-space-y-2 c-m-4 c-absolute c-left-0 c-top-0">
<Skeleton className={cx("c-w-[200px] c-h-[11px]")} />
<Skeleton className={cx("c-w-[170px] c-h-[11px]")} />
<Skeleton className={cx("c-w-[180px] c-h-[11px]")} />
</div>
)}
<div className="c-absolute c-inset-0">
{toaster_el && createPortal(<Toaster cn={cn} />, toaster_el)}
{local.status === "init" ? (
<DataGrid
style={{ opacity: 0 }}
columns={[
{
key: "_",
name: "",
renderCell({ rowIdx }) {
if (local.paging.take < rowIdx) {
local.paging.take = rowIdx;
}
clearTimeout(local.paging.timeout);
local.paging.timeout = setTimeout(() => {
local.status = "reload";
local.paging.take = local.paging.take * 5;
local.render();
}, 100);
return <></>;
},
},
]}
rows={genRows(200)}
/>
) : (
<>
<DataGrid
sortColumns={local.sort.columns}
onSortColumnsChange={local.sort.on_change}
columns={columns}
rows={local.data || []}
onScroll={local.paging.scroll}
/>
</>
)}
</div>
</div>
);
} 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<HTMLDivElement>): boolean {
return (
currentTarget.scrollTop + 10 >=
currentTarget.scrollHeight - currentTarget.clientHeight
);
}