This commit is contained in:
rizky 2024-03-30 23:10:56 -07:00
parent 6c17b8711d
commit 59f10f944a
5 changed files with 371 additions and 74 deletions

View File

@ -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<any[]>;
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> = ({
@ -19,6 +32,8 @@ export const TableList: FC<TableListProp> = ({
child,
PassProp,
mode,
_meta,
gen_fields,
}) => {
const local = useLocal({
el: null as null | HTMLDivElement,
@ -27,30 +42,97 @@ export const TableList: FC<TableListProp> = ({
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<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(() => {
if (local.status === "ready") {
(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();
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 = "ready";
local.render();
}
}, [on_load]);
})();
}, [local.status, on_load, local.sort.orderBy]);
const raw_childs = get(
child,
@ -60,11 +142,12 @@ export const TableList: FC<TableListProp> = ({
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<any>[] = [];
for (const child of childs) {
@ -76,6 +159,7 @@ export const TableList: FC<TableListProp> = ({
name,
width: width > 0 ? width : undefined,
resizable: true,
sortable: true,
renderCell(props) {
return (
<PassProp
@ -94,11 +178,115 @@ export const TableList: FC<TableListProp> = ({
});
}
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",
css`
"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 {
@ -114,20 +302,18 @@ export const TableList: FC<TableListProp> = ({
div[aria-selected="true"] {
outline: none;
}
div[role="gridcell"] {
padding-inline: 0px;
}
.row-selected {
background: #e2f1ff;
}
`
)}
ref={(el) => {
if (!local.el && el) {
local.el = el;
local.rob.observe(el);
}
}}
>
<DataGrid columns={columns} rows={local.data || []} />
</div>
`;
function isAtBottom({ currentTarget }: React.UIEvent<HTMLDivElement>): boolean {
return (
currentTarget.scrollTop + 10 >=
currentTarget.scrollHeight - currentTarget.clientHeight
);
};
}

View File

@ -60,20 +60,43 @@ export const gen_table_list = (
}
);
result["child"].content.childs = [
createItem({
const child = createItem({
name: arg.mode,
childs: [
{
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: `\
<div {...props} className={cx(props.className, "")}>
<FormatValue value={col.value} name={col.name} gen_fields={gen_fields} />
</div>`,
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,
];
}

View File

@ -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<void>;
orderBy?: Record<string, "asc" | "desc">;
paging: { take: number; skip: number };
mode: 'count' | 'query'
}
`;
};

View File

@ -22,8 +22,13 @@ export const formatName = (name: string) => {
type SimplifiedItem = {
name?: string;
component?: { id: string; props: Record<string, any> };
component?: { id: string; props: Record<string, string | SimplifiedItem> };
childs?: SimplifiedItem[];
adv?: {
js: string;
jsBuilt: string;
};
padding?: any;
};
export const createItem = (arg: SimplifiedItem): any => {
@ -34,6 +39,26 @@ export const createItem = (arg: SimplifiedItem): any => {
if (arg.component.props) {
for (const [k, v] of Object.entries(arg.component.props)) {
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),
@ -42,6 +67,7 @@ export const createItem = (arg: SimplifiedItem): any => {
}
}
}
}
return {
id: createId(),
@ -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),
};
};

49
utils/format-value.tsx Executable file
View File

@ -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}</>;
};