fixing table list loading link params

This commit is contained in:
rizky 2024-08-15 11:11:15 -07:00
parent 732aca1abf
commit 59c8ddc716
3 changed files with 297 additions and 215 deletions

View File

@ -36,6 +36,7 @@ import { MDLocal } from "../md/utils/typings";
import { Skeleton } from "../ui/skeleton"; import { Skeleton } from "../ui/skeleton";
import { toast } from "../ui/toast"; import { toast } from "../ui/toast";
import { sortTree } from "./utils/sort-tree"; import { sortTree } from "./utils/sort-tree";
import { getPathname } from "lib/exports";
type OnRowClick = (arg: { type OnRowClick = (arg: {
row: any; row: any;
@ -129,235 +130,262 @@ export const TableList: FC<TableListProp> = ({
if (ls_sort) { if (ls_sort) {
ls_sort = JSON.parse(ls_sort as any); ls_sort = JSON.parse(ls_sort as any);
} }
const local = useLocal({ const local = useLocal(
selectedRows: [] as { {
pk: string | number; times: 0,
rows: any; selectedRows: [] as {
}[], pk: string | number;
el: null as null | HTMLDivElement, rows: any;
width: 0, }[],
height: 0, el: null as null | HTMLDivElement,
selectedRowIds: [] as (string | number)[], width: 0,
rob: new ResizeObserver(([e]) => { height: 0,
local.height = e.contentRect.height; selectedRowIds: [] as (string | number)[],
local.width = e.contentRect.width; rob: new ResizeObserver(([e]) => {
if (local.status === "ready") local.status = "resizing"; local.height = e.contentRect.height;
local.render(); local.width = e.contentRect.width;
}), if (local.status === "ready") local.status = "resizing";
pk: null as null | GFCol, local.render();
scrolled: false, }),
data: [] as any[], pk: null as null | GFCol,
status: "init" as scrolled: false,
| "loading" data: [] as any[],
| "ready" status: "init" as
| "resizing" | "loading"
| "reload" | "ready"
| "init" | "resizing"
| "error", | "reload"
where: null as any, | "init"
firstKey: "", | "error",
should_toast: true, where: null as any,
paging: { firstKey: "",
take: 0, should_toast: true,
skip: 0, paging: {
timeout: null as any, take: 0,
total: 0, skip: 0,
scroll: (currentTarget: HTMLDivElement) => { timeout: null as any,
if (local.status === "loading" || !isAtBottom(currentTarget)) return; total: 0,
if (local.data.length >= local.paging.skip + local.paging.take) { scroll: (currentTarget: HTMLDivElement) => {
local.paging.skip += local.paging.take; if (local.status === "loading" || !isAtBottom(currentTarget)) return;
local.status = "reload"; if (local.data.length >= local.paging.skip + local.paging.take) {
local.render(); local.paging.skip += local.paging.take;
} local.status = "reload";
}, local.render();
},
grid_ref: null as null | HTMLDivElement,
collapsed: new Set<number>(),
cached_row: new WeakMap<any, ReactElement>(),
filtering: "" as ReactNode | string | true,
reload: (arg?: { toast: boolean }) => {
return new Promise<void>((done) => {
let should_toast = true;
if (arg?.toast === false) should_toast = false;
local.should_toast = should_toast;
local.filtering = "";
if (typeof on_load === "function") {
local.status = "loading";
local.render();
const orderBy = local.sort.orderBy || undefined;
const where = filterWhere(filter_name, __props);
if (where?.OR?.length > 0) {
const key = Object.keys(where.OR[0])[0];
if (key && where.OR[0][key]) {
let filtering = where.OR[0][key].contains;
if (typeof local.filtering === "string") {
filtering = filtering.slice(1, -1);
} else {
filtering = "";
}
if (filtering) {
local.filtering = (
<div className="c-pt-2">
Searching for: <pre>"{filtering.trim()}"</pre>
</div>
);
}
}
} }
},
},
grid_ref: null as null | HTMLDivElement,
collapsed: new Set<number>(),
cached_row: new WeakMap<any, ReactElement>(),
filtering: "" as ReactNode | string | true,
reloading: null as any,
reload: (arg?: { toast: boolean }) => {
if (local.reloading) return local.reloading;
if (md) { local.reloading = new Promise<void>(async (done) => {
const last = md.params.links[md.params.links.length - 1]; let should_toast = true;
if (arg?.toast === false) should_toast = false;
local.should_toast = should_toast;
if (last && last.where) { local.filtering = "";
if ((last.name && last.name === md.name) || !last.name) { if (typeof on_load === "function") {
for (const [k, v] of Object.entries(last.where)) { local.status = "loading";
where[k] = v; local.render();
const orderBy = local.sort.orderBy || undefined;
const where = filterWhere(filter_name, __props);
if (where?.OR?.length > 0) {
const key = Object.keys(where.OR[0])[0];
if (key && where.OR[0][key]) {
let filtering = where.OR[0][key].contains;
if (typeof local.filtering === "string") {
filtering = filtering.slice(1, -1);
} else {
filtering = "";
}
if (filtering) {
local.filtering = (
<div className="c-pt-2">
Searching for: <pre>"{filtering.trim()}"</pre>
</div>
);
} }
} }
} }
}
call_prasi_events("tablelist", "where", [__props?.gen__table, where]); if (md) {
if (md.header.loading) {
await new Promise<void>((resolve) => {
const ival = setInterval(() => {
if (!md.header.loading) {
clearInterval(ival);
resolve();
}
}, 10);
});
}
const last = md.params.links[md.params.links.length - 1];
const load_args: any = { if (last && last.where) {
async reload() {}, if ((last.name && last.name === md.name) || !last.name) {
orderBy, for (const [k, v] of Object.entries(last.where)) {
where, where[k] = v;
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"; call_prasi_events("tablelist", "where", [
local.render(); __props?.gen__table,
done(); where,
]);
if (local.grid_ref && !id_parent) local.paging.scroll(local.grid_ref); const load_args: any = {
}; async reload() {},
orderBy,
if (result instanceof Promise) { where,
(async () => { paging: {
try { take: local.paging.take > 0 ? local.paging.take : undefined,
callback(await result); skip: local.paging.skip,
} catch (e) { },
console.error(e); };
local.status = "error"; if (id_parent) {
toast.dismiss(); load_args.paging = {};
toast.error( }
<div className="c-flex c-text-red-600 c-items-center"> const result = on_load({ ...load_args, mode: "query" });
<AlertTriangle className="c-h-4 c-w-4 c-mr-1" /> const callback = (data: any[]) => {
Failed to load data if (local.paging.skip === 0) {
</div>, local.data = data;
{ } else {
dismissible: true, local.data = [...local.data, ...data];
className: css`
background: #ffecec;
border: 2px solid red;
`,
}
);
} }
})();
} else callback(result);
}
});
},
sort: {
columns: (ls_sort?.columns || []) as SortColumn[],
on_change: (cols: SortColumn[]) => {
if (feature?.find((e) => e === "sorting")) {
local.sort.columns = cols;
local.paging.skip = 0;
if (cols.length > 0) {
let { columnKey, direction } = cols[0];
if (columnKey.includes(".")) { local.status = "ready";
let root: any = {}; local.reloading = null;
set(root, columnKey, direction === "ASC" ? "asc" : "desc"); local.render();
local.sort.orderBy = root; done();
} else {
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") { if (local.grid_ref && !id_parent)
local.sort.orderBy = { local.paging.scroll(local.grid_ref);
[columnKey]: { };
_count: direction === "ASC" ? "asc" : "desc",
}, if (result instanceof Promise) {
}; (async () => {
} else { try {
const field = rel.checked.find((e) => !e.is_pk); callback(await result);
if (field) { } catch (e) {
console.error(e);
local.status = "error";
toast.dismiss();
toast.error(
<div className="c-flex c-text-red-600 c-items-center">
<AlertTriangle className="c-h-4 c-w-4 c-mr-1" />
Failed to load data
</div>,
{
dismissible: true,
className: css`
background: #ffecec;
border: 2px solid red;
`,
}
);
}
})();
} else callback(result);
}
});
return local.reloading;
},
sort: {
columns: (ls_sort?.columns || []) as SortColumn[],
on_change: (cols: SortColumn[]) => {
if (feature?.find((e) => e === "sorting")) {
local.sort.columns = cols;
local.paging.skip = 0;
if (cols.length > 0) {
let { columnKey, direction } = cols[0];
if (columnKey.includes(".")) {
let root: any = {};
set(root, columnKey, direction === "ASC" ? "asc" : "desc");
local.sort.orderBy = root;
} else {
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 = { local.sort.orderBy = {
[columnKey]: { [columnKey]: {
[field.name]: direction === "ASC" ? "asc" : "desc", _count: direction === "ASC" ? "asc" : "desc",
},
};
} else if (rel.relation) {
local.sort.orderBy = {
[columnKey]: {
[rel.relation.to.fields[0]]:
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",
},
};
} else if (rel.relation) {
local.sort.orderBy = {
[columnKey]: {
[rel.relation.to.fields[0]]:
direction === "ASC" ? "asc" : "desc",
},
};
}
} }
} }
} }
}
if (should_set) { if (should_set) {
local.sort.orderBy = { local.sort.orderBy = {
[columnKey]: direction === "ASC" ? "asc" : "desc", [columnKey]: direction === "ASC" ? "asc" : "desc",
}; };
}
} }
} else {
local.sort.orderBy = null;
} }
} else { localStorage.setItem(
local.sort.orderBy = null; `sort-${location.pathname}-${location.hash}-${name}`,
} JSON.stringify({
localStorage.setItem( columns: local.sort.columns,
`sort-${location.pathname}-${location.hash}-${name}`, orderBy: local.sort.orderBy,
JSON.stringify({ })
columns: local.sort.columns, );
orderBy: local.sort.orderBy,
})
);
local.status = "reload"; local.status = "reload";
local.render(); local.render();
} }
},
orderBy: (ls_sort?.orderBy || null) as null | Record<
string,
"asc" | "desc" | Record<string, "asc" | "desc">
>,
},
soft_delete: {
field: null as any,
}, },
orderBy: (ls_sort?.orderBy || null) as null | Record<
string,
"asc" | "desc" | Record<string, "asc" | "desc">
>,
}, },
soft_delete: { ({ setDelayedRender }) => {
field: null as any, setDelayedRender(true);
}, }
}); );
const reload = local.reload; const reload = local.reload;
if (md) { if (md) {
md.master.list = local;
md.master.reload = reload; md.master.reload = reload;
} }
@ -712,9 +740,12 @@ export const TableList: FC<TableListProp> = ({
if (!isEditor) { if (!isEditor) {
let should_toast = true; let should_toast = true;
if (md && md.props.mode !== "full") { if (md) {
should_toast = false; if (md.props.mode !== "full") {
should_toast = false;
}
} }
if (should_toast) { if (should_toast) {
if (local.status === "loading") { if (local.status === "loading") {
toast.dismiss(); toast.dismiss();
@ -730,12 +761,6 @@ export const TableList: FC<TableListProp> = ({
} }
} }
if (local.status === "resizing" && !isEditor) {
local.status = "ready";
local.render();
return null;
}
if (document.getElementsByClassName("prasi-toaster").length === 0) { if (document.getElementsByClassName("prasi-toaster").length === 0) {
const elemDiv = document.createElement("div"); const elemDiv = document.createElement("div");
elemDiv.className = "prasi-toaster"; elemDiv.className = "prasi-toaster";
@ -744,6 +769,11 @@ export const TableList: FC<TableListProp> = ({
const toaster_el = document.getElementsByClassName("prasi-toaster")[0]; const toaster_el = document.getElementsByClassName("prasi-toaster")[0];
if (mode === "table") { if (mode === "table") {
if (local.status === "resizing" && !isEditor) {
local.status = "ready";
local.render();
}
return ( return (
<div <div
className={cx( className={cx(
@ -929,7 +959,26 @@ export const TableList: FC<TableListProp> = ({
<Skeleton className={cx("c-w-[180px] c-h-[11px]")} /> <Skeleton className={cx("c-w-[180px] c-h-[11px]")} />
</div> </div>
) : ( ) : (
<div className="c-absolute c-inset-0"> <div
className={cx(
"c-absolute c-inset-0",
css`
@keyframes flasher {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0px);
}
}
.list-row {
animation: flasher 0.5s;
}
`
)}
>
<> <>
{Array.isArray(data) && data.length > 0 ? ( {Array.isArray(data) && data.length > 0 ? (
<div <div
@ -942,7 +991,7 @@ export const TableList: FC<TableListProp> = ({
{data.map((e, idx) => { {data.map((e, idx) => {
return ( return (
<div <div
className="c-flex-grow c-flex" className={cx("list-row c-flex-grow c-flex")}
onClick={(ev) => { onClick={(ev) => {
if (!isEditor && typeof row_click === "function") { if (!isEditor && typeof row_click === "function") {
row_click({ row_click({
@ -1047,9 +1096,14 @@ const genRows = (total: number) => {
}; };
const dataGridStyle = (local: { height: number }) => css` const dataGridStyle = (local: { height: number }) => css`
.rdg { ${local.height
block-size: ${local.height}px; ? css`
} .rdg {
block-size: ${local.height}px;
}
`
: ``}
div[role="row"]:hover { div[role="row"]:hover {
background: #e2f1ff; background: #e2f1ff;
.num-edit { .num-edit {

View File

@ -58,6 +58,7 @@ export type MDLocalInternal = {
master: { master: {
reload: (arg?: { toast: boolean }) => void; reload: (arg?: { toast: boolean }) => void;
render: () => void; render: () => void;
list?: any;
pk?: string; pk?: string;
}; };
params: { params: {

View File

@ -4,6 +4,7 @@ export const useLocal = <T extends object>(
data: T, data: T,
effect?: (arg: { effect?: (arg: {
init: boolean; init: boolean;
setDelayedRender: (arg: boolean) => void;
}) => Promise<void | (() => void)> | void | (() => void), }) => Promise<void | (() => void)> | void | (() => void),
deps?: any[] deps?: any[]
): { ): {
@ -19,12 +20,20 @@ export const useLocal = <T extends object>(
_loading: {} as any, _loading: {} as any,
lastRender: 0, lastRender: 0,
lastRenderCount: 0, lastRenderCount: 0,
delayedRender: false,
delayedRenderTimeout: null as any,
}); });
const local = _.current; const local = _.current;
useEffect(() => { useEffect(() => {
local.ready = true; local.ready = true;
if (effect) effect({ init: true }); if (effect)
effect({
init: true,
setDelayedRender(arg) {
local.delayedRender = arg;
},
});
}, []); }, []);
if (local.ready === false) { if (local.ready === false) {
@ -32,6 +41,17 @@ export const useLocal = <T extends object>(
local.data.render = () => { local.data.render = () => {
if (local.ready) { if (local.ready) {
if (local.delayedRender) {
if (Date.now() - local.lastRender > 100) {
local.lastRender = Date.now();
_render({});
} else {
clearTimeout(local.delayedRenderTimeout);
local.delayedRenderTimeout = setTimeout(local.data.render, 50);
}
return;
}
if (Date.now() - local.lastRender < 300) { if (Date.now() - local.lastRender < 300) {
local.lastRenderCount++; local.lastRenderCount++;
} else { } else {
@ -39,7 +59,9 @@ export const useLocal = <T extends object>(
} }
if (local.lastRenderCount > 300) { if (local.lastRenderCount > 300) {
throw new Error("local.render more than 300 times in less than 300ms"); throw new Error(
"local.render more than 300 times in less than 300ms"
);
} }
local.lastRender = Date.now(); local.lastRender = Date.now();
@ -54,7 +76,12 @@ export const useLocal = <T extends object>(
if (effect) { if (effect) {
setTimeout(() => { setTimeout(() => {
effect({ init: false }); effect({
init: false,
setDelayedRender(arg) {
local.delayedRender = arg;
},
});
}); });
} }
break; break;