fix: enhance error handling and loading state management in Form and Table components

This commit is contained in:
faisolavolut 2025-03-06 13:58:51 +07:00
parent dfabe899d6
commit 32a24c9df1
7 changed files with 429 additions and 304 deletions

View File

@ -55,7 +55,10 @@ export const Form: React.FC<any> = ({
)} )}
/> />
{toastMessage ? `${toastMessage}...` : "Saving..."} {toastMessage ? `${toastMessage}...` : "Saving..."}
</> </>,
{
duration: Infinity,
}
); );
local.btn_ready = false; local.btn_ready = false;
local.render(); local.render();
@ -167,42 +170,50 @@ export const Form: React.FC<any> = ({
await onSubmit(local); await onSubmit(local);
} }
setTimeout(() => { setTimeout(() => {
toast.success( toast.dismiss();
<div setTimeout(() => {
className={cx( toast.success(
"cursor-pointer flex flex-col select-none items-stretch flex-1 w-full" <div
)} className={cx(
onClick={() => { "cursor-pointer flex flex-col select-none items-stretch flex-1 w-full"
toast.dismiss(); )}
}} onClick={() => {
> toast.dismiss();
<div className="flex text-green-700 items-center success-title font-semibold"> }}
<Check className="h-6 w-6 mr-1 " /> >
<div className="flex text-green-700 items-center success-title font-semibold">
<Check className="h-6 w-6 mr-1 " />
{toastMessage ? `${toastMessage} success` : "Record Saved"} {toastMessage ? `${toastMessage} success` : "Record Saved"}
</div>
</div> </div>
</div> );
); }, 100);
}, 100); }, 100);
} catch (ex: any) { } catch (ex: any) {
const msg = get(ex, "response.data.meta.message") || ex.message; const msg = get(ex, "response.data.meta.message") || ex.message;
toast.error( setTimeout(() => {
<div className="flex flex-col w-full"> toast.dismiss();
<div className="flex text-red-600 items-center"> setTimeout(() => {
<AlertTriangle className="h-4 w-4 mr-1" /> toast.error(
{toastMessage <div className="flex flex-col w-full">
? `${toastMessage} failed ${msg}.` <div className="flex text-red-600 items-center">
: `Submit Failed ${msg}.`} <AlertTriangle className="h-4 w-4 mr-1" />
</div> {toastMessage
</div>, ? `${toastMessage} failed ${msg}.`
{ : `Submit Failed ${msg}.`}
dismissible: true, </div>
className: css` </div>,
background: #ffecec; {
border: 2px solid red; dismissible: true,
`, className: css`
} background: #ffecec;
); border: 2px solid red;
`,
}
);
}, 100);
}, 100);
} }
local.btn_ready = true; local.btn_ready = true;
local.render(); local.render();
@ -229,13 +240,18 @@ export const Form: React.FC<any> = ({
)} )}
/> />
{"Loading..."} {"Loading..."}
</> </>,
{
duration: Infinity,
}
); );
local.data = null; local.data = null;
local.render(); local.render();
const res = await onLoad(); try {
const res = await onLoad();
local.data = res;
} catch (ex) {}
local.ready = true; local.ready = true;
local.data = res;
local.render(); local.render();
if (typeof afterLoad === "function") { if (typeof afterLoad === "function") {
afterLoad(local); afterLoad(local);
@ -243,22 +259,6 @@ export const Form: React.FC<any> = ({
setTimeout(() => { setTimeout(() => {
toast.dismiss(); toast.dismiss();
}, 100); }, 100);
// if (res instanceof Promise) {
// res.then((data) => {
// local.ready = true;
// local.data = data;
// local.render(); // Panggil render setelah data diperbarui
// // toast.dismiss();
// // toast.success("Data Loaded Successfully!");
// });
// } else {
// local.ready = true;
// local.data = res;
// local.render(); // Panggil render untuk memicu re-render
// toast.dismiss();
// toast.success("Data Loaded Successfully!");
// }
}, },
fields: {} as any, fields: {} as any,
render: () => {}, render: () => {},
@ -295,19 +295,46 @@ export const Form: React.FC<any> = ({
)} )}
/> />
{"Loading..."} {"Loading..."}
</> </>,
{
duration: Infinity,
}
); );
const res = await onLoad(); let res = null as any;
try {
res = await onLoad();
local.data = res;
setTimeout(() => {
toast.dismiss();
}, 100);
} catch (ex: any) {
const msg = get(ex, "response.data.meta.message") || ex.message;
setTimeout(() => {
toast.dismiss();
setTimeout(() => {
toast.error(
<div className="flex flex-col w-full">
<div className="flex text-red-600 items-center">
<AlertTriangle className="h-4 w-4 mr-1" />
{`Failed ${msg}.`}
</div>
</div>,
{
dismissible: true,
className: css`
background: #ffecec;
border: 2px solid red;
`,
}
);
}, 100);
}, 100);
}
local.ready = true; local.ready = true;
local.data = res;
local.render(); local.render();
if (typeof afterLoad === "function") { if (typeof afterLoad === "function") {
await afterLoad(local); await afterLoad(local);
} }
setTimeout(() => {
toast.dismiss();
}, 100);
}; };
run(); run();
}, []); }, []);

View File

@ -1,7 +1,9 @@
"use client"; "use client";
import { notFound } from "next/navigation";
import { ScrollArea } from "../ui/scroll-area"; import { ScrollArea } from "../ui/scroll-area";
import { Form } from "./Form"; import { Form } from "./Form";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import get from "lodash.get";
export const FormBetter: React.FC<any> = ({ export const FormBetter: React.FC<any> = ({
children, children,
@ -19,6 +21,10 @@ export const FormBetter: React.FC<any> = ({
const [fm, setFM] = useState<any>({ const [fm, setFM] = useState<any>({
data: null as any, data: null as any,
}); });
const [show, setShow] = useState(true as boolean);
if (!show) {
notFound();
}
useEffect(() => {}, [fm.data]); useEffect(() => {}, [fm.data]);
return ( return (
<div className="flex flex-col flex-grow gap-y-3 "> <div className="flex flex-col flex-grow gap-y-3 ">
@ -37,7 +43,17 @@ export const FormBetter: React.FC<any> = ({
children, children,
header, header,
onTitle, onTitle,
onLoad, onLoad: async () => {
try {
const res = await onLoad();
return res;
} catch (ex: any) {
setShow(false);
throw new Error(
get(ex, "response.data.meta.message") || ex.message
);
}
},
onSubmit, onSubmit,
onFooter, onFooter,
showResize, showResize,

View File

@ -4,6 +4,7 @@ import { useLocal } from "@/lib/utils/use-local";
import { toast } from "sonner"; import { toast } from "sonner";
import { Loader2 } from "lucide-react"; import { Loader2 } from "lucide-react";
import { ScrollArea } from "../ui/scroll-area"; import { ScrollArea } from "../ui/scroll-area";
import get from "lodash.get";
export const ListBetter: React.FC<any> = ({ export const ListBetter: React.FC<any> = ({
autoPagination = true, autoPagination = true,
@ -82,36 +83,46 @@ export const ListBetter: React.FC<any> = ({
)} )}
/> />
{"Loading..."} {"Loading..."}
</> </>,
{
duration: Infinity,
}
); );
local.ready = false; local.ready = false;
local.render(); local.render();
if (typeof onCount === "function") { try {
const res = await onCount(); if (typeof onCount === "function") {
local.count = res; const res = await onCount();
local.maxPage = Math.ceil(res / take); local.count = res;
local.paging = 1; local.maxPage = Math.ceil(res / take);
local.render(); local.paging = 1;
} local.render();
if (Array.isArray(onLoad)) { }
let res = onLoad; if (Array.isArray(onLoad)) {
local.data = res; let res = onLoad;
local.render(); local.data = res;
setData(res); local.render();
} else { setData(res);
let res: any = await onLoad({ } else {
search: local.search, let res: any = await onLoad({
sort: local.sort, search: local.search,
take, sort: local.sort,
paging: 1, take,
}); paging: 1,
local.data = res; });
local.render(); local.data = res;
setData(res); local.render();
setTimeout(() => { setData(res);
toast.dismiss(); setTimeout(() => {
}, 100); toast.dismiss();
}, 100);
}
} catch (ex: any) {
console.error(get(ex, "response.data.meta.message") || ex.message);
} }
setTimeout(() => {
toast.dismiss();
}, 100);
local.ready = true; local.ready = true;
local.render(); local.render();
}, },
@ -135,28 +146,38 @@ export const ListBetter: React.FC<any> = ({
)} )}
/> />
{"Loading..."} {"Loading..."}
</> </>,
{
duration: Infinity,
}
); );
const listData = local.data || []; const listData = local.data || [];
if (Array.isArray(onLoad)) { try {
let res = onLoad; if (Array.isArray(onLoad)) {
local.data = listData.concat(res); let res = onLoad;
local.render(); local.data = listData.concat(res);
setData(res); local.render();
} else { setData(res);
let res: any = await onLoad({ } else {
search: local.search, let res: any = await onLoad({
sort: local.sort, search: local.search,
take, sort: local.sort,
paging: local.paging, take,
}); paging: local.paging,
local.data = listData.concat(res); });
local.render(); local.data = listData.concat(res);
setData(res); local.render();
setTimeout(() => { setData(res);
toast.dismiss(); setTimeout(() => {
}, 100); toast.dismiss();
}, 100);
}
} catch (ex: any) {
console.error(get(ex, "response.data.meta.message") || ex.message);
} }
setTimeout(() => {
toast.dismiss();
}, 100);
}, },
}); });
useEffect(() => { useEffect(() => {
@ -180,42 +201,49 @@ export const ListBetter: React.FC<any> = ({
)} )}
/> />
{"Loading..."} {"Loading..."}
</> </>,
{
duration: Infinity,
}
); );
local.ready = false; local.ready = false;
local.render(); local.render();
if (typeof onCount === "function") { try {
const res = await onCount(); if (typeof onCount === "function") {
setMaxPage(Math.ceil(res / take)); const res = await onCount();
local.maxPage = Math.ceil(res / take); setMaxPage(Math.ceil(res / take));
local.count = res; local.maxPage = Math.ceil(res / take);
local.render(); local.count = res;
}
if (mode === "form") {
local.data = fm.data?.[name] || [];
local.render();
setData(fm.data?.[name] || []);
} else {
if (Array.isArray(onLoad)) {
local.data = onLoad;
local.render(); local.render();
setData(onLoad);
} else if (typeof onLoad === "function") {
let res: any = await onLoad({
search: local.search,
sort: local.sort,
take,
paging: 1,
});
local.data = res;
local.render();
setData(local.data);
} else {
let res = onLoad;
local.data = res;
local.render();
setData(local.data);
} }
if (mode === "form") {
local.data = fm.data?.[name] || [];
local.render();
setData(fm.data?.[name] || []);
} else {
if (Array.isArray(onLoad)) {
local.data = onLoad;
local.render();
setData(onLoad);
} else if (typeof onLoad === "function") {
let res: any = await onLoad({
search: local.search,
sort: local.sort,
take,
paging: 1,
});
local.data = res;
local.render();
setData(local.data);
} else {
let res = onLoad;
local.data = res;
local.render();
setData(local.data);
}
}
} catch (ex: any) {
console.error(get(ex, "response.data.meta.message") || ex.message);
} }
if (typeof onInit === "function") { if (typeof onInit === "function") {
onInit(local); onInit(local);

View File

@ -10,6 +10,7 @@ import { getNumber } from "@/lib/utils/getNumber";
import { formatMoney } from "@/lib/components/form/field/TypeInput"; import { formatMoney } from "@/lib/components/form/field/TypeInput";
import "react-resizable/css/styles.css"; import "react-resizable/css/styles.css";
import { Resizable } from "react-resizable"; import { Resizable } from "react-resizable";
import get from "lodash.get";
export const TableEditBetter: React.FC<any> = ({ export const TableEditBetter: React.FC<any> = ({
name, name,
column, column,
@ -101,37 +102,44 @@ export const TableEditBetter: React.FC<any> = ({
)} )}
/> />
{"Loading..."} {"Loading..."}
</> </>,
); {
if (Array.isArray(onLoad)) { duration: Infinity,
local.data = onLoad;
local.render();
setData(onLoad);
} else {
const res: any = onLoad({
search: local.search,
sort: local.sort,
take,
paging: 1,
});
if (res instanceof Promise) {
res.then((e) => {
local.data = e;
local.render();
setData(e);
setTimeout(() => {
toast.dismiss();
}, 100);
});
} else {
local.data = res;
local.render();
setData(res);
setTimeout(() => {
toast.dismiss();
}, 100);
} }
);
try {
if (Array.isArray(onLoad)) {
local.data = onLoad;
local.render();
setData(onLoad);
} else {
const res: any = onLoad({
search: local.search,
sort: local.sort,
take,
paging: 1,
});
if (res instanceof Promise) {
res.then((e) => {
local.data = e;
local.render();
setData(e);
setTimeout(() => {
toast.dismiss();
}, 100);
});
} else {
local.data = res;
local.render();
setData(res);
}
}
} catch (ex: any) {
console.error(get(ex, "response.data.meta.message") || ex.message);
} }
setTimeout(() => {
toast.dismiss();
}, 100);
}, },
}); });
// const cloneListFM = (data: any[]) => { // const cloneListFM = (data: any[]) => {

View File

@ -193,7 +193,10 @@ export const TableList = <T extends object>({
)} )}
/> />
{"Loading..."} {"Loading..."}
</> </>,
{
duration: Infinity,
}
); );
if (typeof onCount === "function") { if (typeof onCount === "function") {
const params = await events("onload-param", { const params = await events("onload-param", {
@ -255,7 +258,10 @@ export const TableList = <T extends object>({
)} )}
/> />
{"Loading..."} {"Loading..."}
</> </>,
{
duration: Infinity,
}
); );
if (Array.isArray(onLoad)) { if (Array.isArray(onLoad)) {
let res = onLoad; let res = onLoad;
@ -285,6 +291,9 @@ export const TableList = <T extends object>({
toast.dismiss(); toast.dismiss();
}, 100); }, 100);
} }
setTimeout(() => {
toast.dismiss();
}, 100);
}, },
}); });
const cloneListFM = (data: any[]) => { const cloneListFM = (data: any[]) => {
@ -351,61 +360,66 @@ export const TableList = <T extends object>({
)} )}
/> />
{"Loading..."} {"Loading..."}
</> </>,
{
duration: Infinity,
}
); );
try { try {
if (typeof onCount === "function") { try {
const params = await events("onload-param", { if (typeof onCount === "function") {
take: 1, const params = await events("onload-param", {
paging: 1, take: 1,
search: local.search, paging: 1,
...local.filter, search: local.search,
...local.fieldResultFilter, ...local.filter,
}); ...local.fieldResultFilter,
const res = await onCount(params); });
local.count = res; const res = await onCount(params);
local.count = res;
local.render();
}
} catch (ex) {}
if (mode === "form") {
local.data = fm.data?.[name] || [];
cloneListFM(fm.data?.[name] || []);
local.render(); local.render();
setData(fm.data?.[name] || []);
} else {
if (Array.isArray(onLoad)) {
local.data = onLoad;
cloneListFM(onLoad);
local.render();
setData(onLoad);
} else if (typeof onLoad === "function") {
let res: any = await onLoad({
search: local.search,
sort: local.sort,
take,
paging: 1,
...local.filter,
...local.fieldResultFilter,
});
if (!autoPagination) {
res = paginateArray(res, take, 1);
}
if (!local.count) {
local.count = res?.length;
}
local.data = res;
local.render();
setData(local.data);
} else {
let res: any[] = onLoad;
if (!autoPagination) {
res = paginateArray(res, take, 1);
}
local.data = res;
local.render();
setData(local.data);
}
} }
} catch (ex) {} } catch (ex) {}
if (mode === "form") {
local.data = fm.data?.[name] || [];
cloneListFM(fm.data?.[name] || []);
local.render();
setData(fm.data?.[name] || []);
} else {
if (Array.isArray(onLoad)) {
local.data = onLoad;
cloneListFM(onLoad);
local.render();
setData(onLoad);
} else if (typeof onLoad === "function") {
let res: any = await onLoad({
search: local.search,
sort: local.sort,
take,
paging: 1,
...local.filter,
...local.fieldResultFilter,
});
if (!autoPagination) {
res = paginateArray(res, take, 1);
}
if (!local.count) {
local.count = res?.length;
}
local.data = res;
local.render();
setData(local.data);
} else {
let res: any[] = onLoad;
if (!autoPagination) {
res = paginateArray(res, take, 1);
}
local.data = res;
local.render();
setData(local.data);
}
}
setTimeout(() => { setTimeout(() => {
toast.dismiss(); toast.dismiss();
}, 100); }, 100);

View File

@ -111,39 +111,52 @@ export const TableListBetter: React.FC<any> = ({
)} )}
/> />
{"Loading..."} {"Loading..."}
</> </>,
{
duration: Infinity,
}
); );
if (Array.isArray(onLoad)) { try {
local.data = onLoad; if (Array.isArray(onLoad)) {
local.render(); local.data = onLoad;
setData(onLoad);
} else {
const res: any = onLoad({
search: local.search,
sort: local.sort,
take,
paging: 1,
});
if (res instanceof Promise) {
res.then((e) => {
local.data = e;
cloneListFM(e);
local.render();
setData(e);
setTimeout(() => {
toast.dismiss();
}, 2000);
});
} else {
local.data = res;
cloneListFM(res);
local.render(); local.render();
setData(res); setData(onLoad);
setTimeout(() => { setTimeout(() => {
toast.dismiss(); toast.dismiss();
}, 2000); }, 100);
} else {
const res: any = onLoad({
search: local.search,
sort: local.sort,
take,
paging: 1,
});
if (res instanceof Promise) {
res.then((e) => {
local.data = e;
cloneListFM(e);
local.render();
setData(e);
setTimeout(() => {
toast.dismiss();
}, 100);
});
} else {
local.data = res;
cloneListFM(res);
local.render();
setData(res);
setTimeout(() => {
toast.dismiss();
}, 100);
}
} }
} catch (ex: any) {
console.error(get(ex, "response.data.meta.message") || ex.message);
} }
setTimeout(() => {
toast.dismiss();
}, 100);
}, },
}); });
const cloneListFM = (data: any[]) => { const cloneListFM = (data: any[]) => {
@ -173,35 +186,48 @@ export const TableListBetter: React.FC<any> = ({
)} )}
/> />
{"Loading..."} {"Loading..."}
</> </>,
{
duration: Infinity,
}
); );
if (typeof onCount === "function") { try {
const res = await onCount(); if (typeof onCount === "function") {
local.count = res; const res = await onCount();
local.count = res;
local.render(); local.render();
} }
if (Array.isArray(onLoad)) { if (Array.isArray(onLoad)) {
local.data = onLoad; local.data = onLoad;
cloneListFM(onLoad); cloneListFM(onLoad);
local.render(); local.render();
setData(onLoad); setData(onLoad);
} else { setTimeout(() => {
const res: any = await onLoad({ toast.dismiss();
search: local.search, }, 100);
sort: local.sort, } else {
take, const res: any = await onLoad({
paging: 1, search: local.search,
}); sort: local.sort,
local.data = res; take,
cloneListFM(res); paging: 1,
local.render(); });
setData(res); local.data = res;
setTimeout(() => { cloneListFM(res);
toast.dismiss(); local.render();
}, 2000); setData(res);
setTimeout(() => {
toast.dismiss();
}, 100);
}
} catch (ex: any) {
console.error(get(ex, "response.data.meta.message") || ex.message);
} }
setTimeout(() => {
toast.dismiss();
}, 100);
}; };
if (typeof onInit === "function") { if (typeof onInit === "function") {
onInit(local); onInit(local);

View File

@ -46,27 +46,31 @@ export const actionToast = async (data: {
)} )}
/> />
{msg_load ? msg_load : " Load..."} {msg_load ? msg_load : " Load..."}
</> </>,
{
duration: Infinity,
}
); );
if (typeof task === "function") await task(); if (typeof task === "function") await task();
setTimeout(() => { setTimeout(() => {
toast.dismiss(); toast.dismiss();
toast.success( setTimeout(() => {
<div toast.success(
className={cx( <div
"cursor-pointer flex flex-col select-none items-stretch flex-1 w-full" className={cx(
)} "cursor-pointer flex flex-col select-none items-stretch flex-1 w-full"
onClick={() => { )}
toast.dismiss(); onClick={() => {
}} toast.dismiss();
> }}
<div className="flex text-green-700 items-center success-title font-semibold"> >
<Check className="h-6 w-6 mr-1 " /> <div className="flex text-green-700 items-center success-title font-semibold">
{msg_succes ? msg_succes : " Success"} <Check className="h-6 w-6 mr-1 " />
{msg_succes ? msg_succes : " Success"}
</div>
</div> </div>
</div> );
); }, 100);
if (typeof after === "function") after(); if (typeof after === "function") after();
if (typeof success === "function") success(); if (typeof success === "function") success();
}, 100); }, 100);
@ -74,26 +78,28 @@ export const actionToast = async (data: {
setTimeout(() => { setTimeout(() => {
if (typeof failed === "function") failed(); if (typeof failed === "function") failed();
toast.dismiss(); toast.dismiss();
toast.error( setTimeout(() => {
<div className="flex flex-col w-full"> toast.error(
<div className="flex text-red-600 items-center"> <div className="flex flex-col w-full">
{hidden_icon !== true ? ( <div className="flex text-red-600 items-center">
<AlertTriangle className="h-4 w-4 mr-1" /> {hidden_icon !== true ? (
) : ( <AlertTriangle className="h-4 w-4 mr-1" />
<></> ) : (
)} <></>
{msg_error ? msg_error : " Failed"}{" "} )}
{get(ex, "response.data.meta.message") || ex.message}. {msg_error ? msg_error : " Failed"}{" "}
</div> {get(ex, "response.data.meta.message") || ex.message}.
</div>, </div>
{ </div>,
dismissible: true, {
className: css` dismissible: true,
background: #ffecec; className: css`
border: 2px solid red; background: #ffecec;
`, border: 2px solid red;
} `,
); }
);
}, 100);
}, 100); }, 100);
} }
}; };