fixing lib

This commit is contained in:
rizky 2024-08-07 07:04:29 -07:00
parent ab03212230
commit 2e6d2b5388
13 changed files with 361 additions and 339 deletions

View File

@ -28,20 +28,9 @@ export const Form: FC<FMProps> = (props) => {
events: {
on_change(name: string, new_value: any) {},
},
internal: {},
submit: null as any,
error: {} as any,
internal: {
reload: {
timeout: null as any,
promises: [],
done: [],
},
submit: {
timeout: null as any,
promises: [],
done: [],
},
},
field_def: {},
props: {} as any,
size: {
@ -164,6 +153,7 @@ export const Form: FC<FMProps> = (props) => {
formInit(fm, props);
fm.reload();
}
if (document.getElementsByClassName("prasi-toaster").length === 0) {
const elemDiv = document.createElement("div");
elemDiv.className = "prasi-toaster";

View File

@ -149,13 +149,13 @@ ${
fm.status = "ready";
fm.data = form;
md.selected = form;
if (md.props.mode !== "full") md.master.reload({ toast: false });
md.render();
fm.render();
if (fm.props.back_on_save === "y") {
md.selected = null;
md.tab.active = "master";
md.internal.action_should_refresh = true;
md.params.apply();
md.render();
}

View File

@ -139,16 +139,6 @@ export type FMInternal = {
clear: (name?: string) => void;
};
internal: {
reload: {
timeout: ReturnType<typeof setTimeout>;
promises: Promise<void>[];
done: any[];
};
submit: {
promises: Promise<boolean>[];
timeout: ReturnType<typeof setTimeout>;
done: any[];
};
original_render?: () => void;
};
props: Exclude<FMProps, "body" | "PassProp">;

View File

@ -1,10 +1,10 @@
import { parseGenField } from "@/gen/utils";
import get from "lodash.get";
import { AlertTriangle, Check, Loader2 } from "lucide-react";
import { toast } from "sonner";
import { FMLocal, FMProps } from "../typings";
import { editorFormData } from "./ed-data";
import { formError } from "./error";
import { toast } from "lib/comps/ui/toast";
export const formInit = (fm: FMLocal, props: FMProps) => {
for (const [k, v] of Object.entries(props)) {
@ -19,77 +19,165 @@ export const formInit = (fm: FMLocal, props: FMProps) => {
fm.field_def[d.name] = d;
}
fm.reload = () => {
fm.reload = async () => {
fm.status = isEditor ? "ready" : "loading";
fm.render();
const promise = new Promise<void>((done) => {
fm.internal.reload.done.push(done);
clearTimeout(fm.internal.reload.timeout);
fm.internal.reload.timeout = setTimeout(async () => {
if (sonar === "on" && !isEditor) {
setTimeout(() => {
toast.dismiss();
toast.loading(
<>
<Loader2 className="c-h-4 c-w-4 c-animate-spin" />
Loading data...
</>
);
});
if (sonar === "on" && !isEditor) {
toast.dismiss();
toast.loading(
<>
<Loader2 className="c-h-4 c-w-4 c-animate-spin" />
Loading data...
</>
);
}
let should_load = true;
if (isEditor) {
const item_id = props.item.id;
if (item_id) {
const cache = editorFormData[item_id];
if (
cache &&
cache.on_load === get(props.item, "component.props.on_load.value")
) {
fm.data = cache.data;
should_load = false;
}
}
}
if (should_load) {
if (!on_load) {
console.error("Form on_load is empty. Please re-generate the form.");
} else {
const on_load_result = on_load({ fm });
let result = undefined;
if (
typeof on_load_result === "object" &&
on_load_result instanceof Promise
) {
result = await on_load_result;
} else {
result = on_load_result;
}
let should_load = true;
fm.data = result;
if (result === undefined) fm.data = {};
if (isEditor) {
const item_id = props.item.id;
if (item_id) {
const cache = editorFormData[item_id];
if (
cache &&
cache.on_load === get(props.item, "component.props.on_load.value")
) {
fm.data = cache.data;
should_load = false;
}
editorFormData[item_id] = {
data: fm.data,
on_load: get(props.item, "component.props.on_load.value"),
};
}
}
if (should_load) {
if (!on_load) {
console.error("Form on_load is empty. Please re-generate the form.");
} else {
const on_load_result = on_load({ fm });
let result = undefined;
if (
typeof on_load_result === "object" &&
on_load_result instanceof Promise
) {
result = await on_load_result;
} else {
result = on_load_result;
}
}
toast.dismiss();
if (fm.is_newly_created) {
fm.is_newly_created = false;
toast.success(
<div className="c-flex c-text-green-700 c-items-center">
<Check className="c-h-4 c-w-4 c-mr-1 " />
Saved
</div>,
{
className: css`
background: #e4ffed;
border: 2px solid green;
`,
}
);
}
fm.status = "ready";
fm.render();
};
fm.submit = async () => {
if (fm.status !== "ready") {
return;
}
if (typeof fm.props.on_submit === "function") {
fm.status = "saving";
fm.render();
if (fm.props.sonar === "on" && !isEditor) {
toast.loading(
<>
<Loader2 className="c-h-4 c-w-4 c-animate-spin" />
Submitting...
</>
);
}
const form = JSON.parse(JSON.stringify(fm.data));
if (fm.deps.md) {
const md = fm.deps.md;
const last = md.params.links[md.params.links.length - 1];
if (last) {
const pk = Object.values(fm.field_def).find((e) => e.is_pk);
if (pk) {
let obj = last.update;
if (!fm.data[pk.name]) {
obj = last.create;
}
fm.data = result;
if (result === undefined) fm.data = {};
if (isEditor) {
const item_id = props.item.id;
if (item_id) {
editorFormData[item_id] = {
data: fm.data,
on_load: get(props.item, "component.props.on_load.value"),
};
if (typeof obj === "object" && obj) {
for (const [k, v] of Object.entries(obj)) {
form[k] = v;
}
}
}
}
}
fm.internal.reload.done.map((e) => e());
setTimeout(() => {
toast.dismiss();
const success = await fm.props.on_submit({
fm,
form,
error: fm.error.object,
});
if (fm.is_newly_created) {
fm.is_newly_created = false;
toast.dismiss();
if (!success) {
fm.status = "ready";
fm.render();
}
if (fm.props.sonar === "on" && !isEditor) {
toast.dismiss();
if (!success) {
const errors = Object.keys(fm.error.list);
const count = errors.length;
console.log(fm.error.list);
toast.error(
<div className="c-flex c-text-red-600 c-items-center">
<AlertTriangle className="c-h-4 c-w-4 c-mr-1" />
Save Failed
{count > 0 &&
`, please correct
${count} errors`}
.
</div>,
{
dismissible: true,
className: css`
background: #ffecec;
border: 2px solid red;
`,
}
);
} else {
if (!fm.is_newly_created) {
toast.success(
<div className="c-flex c-text-green-700 c-items-center">
<Check className="c-h-4 c-w-4 c-mr-1 " />
@ -103,127 +191,10 @@ export const formInit = (fm: FMLocal, props: FMProps) => {
}
);
}
}, 100);
fm.status = "ready";
fm.render();
}, 50);
});
fm.internal.reload.promises.push(promise);
return promise;
};
fm.submit = () => {
const promise = new Promise<boolean>(async (done) => {
fm.internal.submit.done.push(done);
clearTimeout(fm.internal.submit.timeout);
fm.internal.submit.timeout = setTimeout(async () => {
const done_all = (val: boolean) => {
for (const d of fm.internal.submit.done) {
d(val);
}
fm.internal.submit.done = [];
fm.render();
};
if (typeof fm.props.on_submit === "function") {
fm.status = "saving";
fm.render();
if (fm.props.sonar === "on" && !isEditor) {
toast.loading(
<>
<Loader2 className="c-h-4 c-w-4 c-animate-spin" />
Submitting...
</>
);
}
const form = JSON.parse(JSON.stringify(fm.data));
if (fm.deps.md) {
const md = fm.deps.md;
const last = md.params.links[md.params.links.length - 1];
if (last) {
const pk = Object.values(fm.field_def).find((e) => e.is_pk);
if (pk) {
let obj = last.update;
if (!fm.data[pk.name]) {
obj = last.create;
}
if (typeof obj === "object" && obj) {
for (const [k, v] of Object.entries(obj)) {
form[k] = v;
}
}
}
}
}
const success = await fm.props.on_submit({
fm,
form,
error: fm.error.object,
});
toast.dismiss();
done_all(success);
if (!success) {
fm.status = "ready";
fm.render();
}
if (fm.props.sonar === "on" && !isEditor) {
setTimeout(() => {
toast.dismiss();
if (!success) {
const errors = Object.keys(fm.error.list);
const count = errors.length;
console.log(fm.error.list);
toast.error(
<div className="c-flex c-text-red-600 c-items-center">
<AlertTriangle className="c-h-4 c-w-4 c-mr-1" />
Save Failed
{count > 0 &&
`, please correct
${count} errors`}
.
</div>,
{
dismissible: true,
className: css`
background: #ffecec;
border: 2px solid red;
`,
}
);
} else {
if (!fm.is_newly_created) {
toast.success(
<div className="c-flex c-text-green-700 c-items-center">
<Check className="c-h-4 c-w-4 c-mr-1 " />
Saved
</div>,
{
className: css`
background: #e4ffed;
border: 2px solid green;
`,
}
);
}
}
}, 100);
}
}
}, 100);
});
fm.internal.submit.promises.push(promise);
return promise;
}
return success;
}
};
if (typeof fm.props.on_init === "function") {
fm.props.on_init({ fm, submit: fm.submit, reload: fm.reload });

View File

@ -23,13 +23,14 @@ import DataGrid, {
} from "react-data-grid";
import "react-data-grid/lib/styles.css";
import { createPortal } from "react-dom";
import { Toaster, toast } from "sonner";
import { Toaster } from "sonner";
import { call_prasi_events } from "../../..";
import { filterWhere } from "../filter/parser/filter-where";
import { getFilter } from "../filter/utils/get-filter";
import { MDLocal } from "../md/utils/typings";
import { Skeleton } from "../ui/skeleton";
import { sortTree } from "./utils/sort-tree";
import { toast } from "../ui/toast";
type OnRowClick = (arg: {
row: any;
@ -37,6 +38,8 @@ type OnRowClick = (arg: {
idx: any;
event: React.MouseEvent<HTMLDivElement>;
}) => void;
let EMPTY_SET = new Set() as ReadonlySet<any>;
type SelectedRow = (arg: { row: any; rows: any[]; idx: any }) => boolean;
type TableListProp = {
child: any;
@ -147,6 +150,7 @@ export const TableList: FC<TableListProp> = ({
| "init"
| "error",
where: null as any,
should_toast: true,
paging: {
take: 0,
skip: 0,
@ -235,75 +239,82 @@ export const TableList: FC<TableListProp> = ({
},
});
const reload = useCallback(() => {
if (typeof on_load === "function") {
local.status = "loading";
local.render();
const reload = useCallback(
(arg?: { toast: boolean }) => {
let should_toast = true;
if (arg?.toast === false) should_toast = false;
local.should_toast = should_toast;
const orderBy = local.sort.orderBy || undefined;
const where = filterWhere(filter_name, __props);
if (md) {
const last = md.params.links[md.params.links.length - 1];
if (last && last.where) {
for (const [k, v] of Object.entries(last.where)) {
where[k] = v;
}
}
}
call_prasi_events("tablelist", "where", [__props?.gen__table, where]);
const load_args: any = {
async reload() {},
orderBy,
where,
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";
if (typeof on_load === "function") {
local.status = "loading";
local.render();
};
if (result instanceof Promise) {
(async () => {
try {
callback(await result);
} 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;
`,
}
);
const orderBy = local.sort.orderBy || undefined;
const where = filterWhere(filter_name, __props);
if (md) {
const last = md.params.links[md.params.links.length - 1];
if (last && last.where) {
for (const [k, v] of Object.entries(last.where)) {
where[k] = v;
}
}
})();
} else callback(result);
}
}, [on_load, local.sort.orderBy, local.paging.take, local.paging.skip]);
}
call_prasi_events("tablelist", "where", [__props?.gen__table, where]);
const load_args: any = {
async reload() {},
orderBy,
where,
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";
local.render();
};
if (result instanceof Promise) {
(async () => {
try {
callback(await result);
} 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);
}
},
[on_load, local.sort.orderBy, local.paging.take, local.paging.skip]
);
if (md) {
md.master.reload = reload;
@ -540,16 +551,20 @@ export const TableList: FC<TableListProp> = ({
if (!isEditor) {
if (local.status === "loading") {
toast.dismiss();
toast.loading(
<>
<Loader2 className="c-h-4 c-w-4 c-animate-spin" />
Loading {local.paging.skip === 0 ? "Data" : "more rows"} ...
</>,
{
dismissible: true,
}
);
if (local.should_toast) {
toast.dismiss();
toast.loading(
<>
<Loader2 className="c-h-4 c-w-4 c-animate-spin" />
Loading {local.paging.skip === 0 ? "Data" : "more rows"} ...
</>,
{
dismissible: true,
}
);
} else {
local.should_toast = true;
}
} else {
if (local.status !== "error") {
toast.dismiss();
@ -682,7 +697,7 @@ export const TableList: FC<TableListProp> = ({
rows={data}
className="rdg-light"
onScroll={local.paging.scroll}
selectedRows={new Set() as ReadonlySet<any>}
selectedRows={EMPTY_SET}
onSelectedCellChange={() => {}}
onSelectedRowsChange={() => {}}
headerRowHeight={show_header === false ? 0 : undefined}
@ -722,7 +737,10 @@ export const TableList: FC<TableListProp> = ({
isRowSelected={true}
className={cx(
props.className,
isSelect && "row-selected"
(isSelect ||
md?.selected?.[local.pk?.name || ""] ===
props.row[local.pk?.name || ""]) &&
"row-selected"
)}
/>
);
@ -911,7 +929,7 @@ const dataGridStyle = (local: { height: number }) => css`
}
.row-selected {
background: #e2f1ff;
background: #bddfff !important;
}
`;

View File

@ -27,6 +27,7 @@ export const MasterDetail: FC<MDProps> = (arg) => {
on_init,
_item,
title,
detail_size,
} = arg;
const _ref = useRef({ PassProp, item: _item, childs: {} });
const mdr = _ref.current;
@ -47,7 +48,7 @@ export const MasterDetail: FC<MDProps> = (arg) => {
active: "",
list: [],
},
internal: { action_should_refresh: true },
internal: { action_should_refresh: false },
childs: {},
props: {
mode,
@ -70,11 +71,8 @@ export const MasterDetail: FC<MDProps> = (arg) => {
masterDetailApplyParams(md);
},
},
detail_size: Number(detail_size || "400"),
master: { render() {}, reload() {} },
panel: {
size: 25,
min_size: 0,
},
});
mdr.PassProp = PassProp;

View File

@ -86,10 +86,8 @@ export const generateMDForm = async (
onClick: () => {
md.selected = null;
md.tab.active = "master";
md.internal.action_should_refresh = true;
md.params.apply();
md.render();
},
},
];

View File

@ -38,7 +38,6 @@ export const generateMDList = async (
value: `\
({ row, rows, idx, event }: OnRowClick) => {
md.selected = row;
md.internal.action_should_refresh = true;
md.tab.active = "detail";
md.params.apply();
md.render();

View File

@ -3,35 +3,40 @@ import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";
import { MDMaster } from "../parts/MDMaster";
import { MDDetail } from "../parts/MDDetail";
import { MDLocal, MDRef } from "../utils/typings";
import { getPathname } from "@/utils/pathname";
export const ModeHSplit: FC<{ md: MDLocal; mdr: MDRef }> = ({ md, mdr }) => {
return (
<div className={cx("c-flex-1")}>
<PanelGroup direction="horizontal">
<Panel
className="c-border-r c-flex"
defaultSize={md.panel.size}
minSize={md.panel.min_size}
>
<Panel className="c-border-r c-flex">
<MDMaster md={md} mdr={mdr} />
</Panel>
<>
<PanelResizeHandle />
<Panel
className="c-flex c-flex-col c-items-stretch c-w-10"
defaultSize={
parseInt(localStorage.getItem(`prasi-md-h-${md.name}`) || "") ||
undefined
}
onResize={(e) => {
if (e < 80) {
localStorage.setItem(`prasi-md-h-${md.name}`, e.toString());
{(md.selected || isEditor) && (
<>
<PanelResizeHandle />
<Panel
className="c-flex c-flex-col c-items-stretch c-w-10"
defaultSize={
Number(
localStorage.getItem(
`prasi-md-${getPathname({ hash: false })}${md.name}`
)
) || md.detail_size
}
}}
>
<MDDetail md={md} mdr={mdr} />
</Panel>
</>
onResize={(e) => {
if (e < 80 && !isEditor) {
localStorage.setItem(
`prasi-md-${getPathname({ hash: false })}${md.name}`,
e.toString()
);
}
}}
>
<MDDetail md={md} mdr={mdr} />
</Panel>
</>
)}
</PanelGroup>
</div>
);

View File

@ -1,37 +1,42 @@
import { FC } from "react";
import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";
import { MDDetail } from "../parts/MDDetail";
import { MDMaster } from "../parts/MDMaster";
import { MDLocal, MDRef } from "../utils/typings";
import { MDDetail, should_show_tab } from "../parts/MDDetail";
import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";
import { getPathname } from "@/utils/pathname";
export const ModeVSplit: FC<{ md: MDLocal; mdr: MDRef }> = ({ md, mdr }) => {
return (
<div className={cx("c-flex-1")}>
<PanelGroup direction="vertical">
<Panel
className="c-border-b"
defaultSize={md.panel.size}
minSize={md.panel.min_size}
>
<Panel className="c-border-b c-flex">
<MDMaster md={md} mdr={mdr} />
</Panel>
<>
<PanelResizeHandle />
<Panel
className="c-flex c-flex-col c-items-stretch"
defaultSize={
parseInt(localStorage.getItem(`prasi-md-h-${md.name}`) || "") ||
undefined
}
onResize={(e) => {
if (e < 80) {
localStorage.setItem(`prasi-md-h-${md.name}`, e.toString());
{(md.selected || isEditor) && (
<>
<PanelResizeHandle />
<Panel
className="c-flex c-flex-col c-items-stretch"
defaultSize={
Number(
localStorage.getItem(
`prasi-md-${getPathname({ hash: false })}${md.name}`
)
) || md.detail_size
}
}}
>
<MDDetail md={md} mdr={mdr} />
</Panel>
</>
onResize={(e) => {
if (e < 80 && !isEditor) {
localStorage.setItem(
`prasi-md-${getPathname({ hash: false })}${md.name}`,
e.toString()
);
}
}}
>
<MDDetail md={md} mdr={mdr} />
</Panel>
</>
)}
</PanelGroup>
</div>
);

View File

@ -3,6 +3,7 @@ import { FC, useEffect } from "react";
import { breadcrumbPrefix } from "../utils/md-hash";
import { MDLocal, MDRef } from "../utils/typings";
import { MDHeader } from "./MDHeader";
import { hashSum } from "lib/utils/hash-sum";
export const should_show_tab = (md: MDLocal) => {
if (isEditor) {
@ -12,6 +13,7 @@ export const should_show_tab = (md: MDLocal) => {
};
export const MDDetail: FC<{ md: MDLocal; mdr: MDRef }> = ({ md, mdr }) => {
const local = useLocal({ selected: "", synced: false });
const detail = md.childs[md.tab.active];
const PassProp = mdr.PassProp;
if (!detail) {

View File

@ -24,6 +24,7 @@ export type MDProps = {
gen_fields: any;
footer: any;
gen_table: string;
detail_size: string;
on_init: (md: MDLocal) => void;
_item: PrasiItem;
deps?: any[];
@ -54,7 +55,11 @@ export type MDLocalInternal = {
list: string[];
};
internal: { action_should_refresh: boolean };
master: { reload: () => void; render: () => void };
master: {
reload: (arg?: { toast: boolean }) => void;
render: () => void;
pk?: string;
};
params: {
links: LinkParam[];
hash: Record<string, any>;
@ -74,6 +79,7 @@ export type MDLocalInternal = {
item: PrasiItem;
};
deps?: object;
detail_size: number;
childs: Record<
string,
{
@ -87,10 +93,6 @@ export type MDLocalInternal = {
list?: any;
}
>;
panel: {
size: number;
min_size: number;
};
};
export type MDRef = {
PassProp: any;
@ -133,7 +135,7 @@ export const MasterDetailType = `const md = {
apply: () => void;
};
props: {
mode: "full" | "h-split" | "v-split";
mode: string;
show_head: "always" | "only-master" | "only-child" | "hidden";
tab_mode: "h-tab" | "v-tab" | "hidden";
editor_tab: string;
@ -143,7 +145,7 @@ export const MasterDetailType = `const md = {
};
internal: { action_should_refresh: boolean };
render: () => void;
master: { reload: () => void; render: () => void };
master: { reload: (arg?:{toast: boolean}) => void; render: () => void };
pk?: {
name: string;
type: string;
@ -168,9 +170,6 @@ export const MasterDetailType = `const md = {
md?: md;
}
>;
panel: {
size: number;
min_size: number;
};
detail_size: number;
deps: any
};`;

47
comps/ui/toast.tsx Executable file
View File

@ -0,0 +1,47 @@
import { ReactElement } from "react";
import { toast as sonner } from "sonner";
const timer = {
timeout: null as any,
done: false,
limit: 400,
};
export const toast = {
dismiss: () => {
if (!timer.timeout) {
sonner.dismiss();
} else {
clearTimeout(timer.timeout);
}
},
loading: (
el: ReactElement,
props?: { dismissible?: boolean; className?: string }
) => {
clearTimeout(timer.timeout);
timer.timeout = setTimeout(() => {
sonner.loading(el, props);
timer.timeout = null;
}, timer.limit);
},
success: (
el: ReactElement,
props?: { dismissible?: boolean; className?: string }
) => {
clearTimeout(timer.timeout);
timer.timeout = setTimeout(() => {
sonner.success(el, props);
timer.timeout = null;
}, timer.limit);
},
error: (
el: ReactElement,
props?: { dismissible?: boolean; className?: string }
) => {
clearTimeout(timer.timeout);
timer.timeout = setTimeout(() => {
sonner.error(el, props);
timer.timeout = null;
}, timer.limit);
},
};