This commit is contained in:
rizky 2024-04-08 00:22:31 -07:00
parent 4a1cdf7fcd
commit 121ec5f0ed
22 changed files with 423 additions and 283 deletions

View File

@ -51,11 +51,13 @@ export const Relation: FC<RelationProps> = ({
const select = {} as any;
local.pk_field = "";
for (const f of relation.fields) {
if (f.startsWith("::")) {
select[f.substring(2)] = true;
local.pk_field = f.substring(2);
} else {
select[f] = true;
if (typeof f === "string") {
if (f.startsWith("::")) {
select[f.substring(2)] = true;
local.pk_field = f.substring(2);
} else {
select[f] = true;
}
}
}
let q = {};

286
comps/form-old/Form.tsx Executable file
View File

@ -0,0 +1,286 @@
import { Form as FForm } from "@/comps/ui/form";
import { Toaster } from "@/comps/ui/sonner";
import { cn } from "@/utils";
import { useLocal } from "@/utils/use-local";
import { AlertTriangle, Check, Loader2 } from "lucide-react";
import { FC, useEffect } from "react";
import { createPortal } from "react-dom";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { Skeleton } from "../ui/skeleton";
import { FormHook } from "./utils/utils";
export const Form: FC<{
on_init: (arg: { submit: any; reload: any }) => any;
on_load: () => any;
on_submit: (arg: { form: any; error: any }) => Promise<any>;
body: any;
form: FormHook;
PassProp: any;
cache: () => any;
sonar: "on" | "off";
layout: "auto" | "1-col" | "2-col";
}> = ({
on_init,
on_load,
body,
form,
PassProp,
on_submit,
cache,
layout: _layout,
sonar,
}) => {
const form_hook = useForm<any>({
defaultValues: {},
});
const local = useLocal({
el: null as any,
submit_timeout: null as any,
submit_done: [] as any[],
layout: "unknown" as "unknown" | "2-col" | "1-col",
init: false,
});
form.hook = form_hook;
if (!form.cache && typeof cache === "function") {
try {
form.cache = cache() || {};
} catch (e) {}
}
if (!form.cache) form.cache = {};
if (!form.validation) {
form.validation = {};
}
if (!form.label) {
form.label = {};
}
let layout = _layout || "auto";
if (layout !== "auto") local.layout = layout;
const submit = () => {
return new Promise<boolean>((done) => {
local.submit_done.push(done);
const done_all = (val: boolean) => {
for (const d of local.submit_done) {
d(val);
}
local.submit_done = [];
local.render();
};
clearTimeout(local.submit_timeout);
local.submit_timeout = setTimeout(async () => {
if (sonar === "on") {
toast.loading(
<>
<Loader2 className="c-h-4 c-w-4 c-animate-spin" />
Processing ...
</>,
{
dismissible: true,
className: css`
background: #e4f7ff;
`,
}
);
}
const data = form.hook.getValues();
form.hook.clearErrors();
for (const [k, v] of Object.entries(form.validation)) {
if (v === "required") {
if (!data[k]) {
const error = {
type: "required",
message: `${form.label[k] || k} is required.`,
};
form.hook.formState.errors[k] = error;
form.hook.setError(k, error);
}
}
}
const res = on_submit({
form: data,
error: form.hook.formState.errors,
});
const success = await res;
toast.dismiss();
done_all(success);
if (sonar === "on") {
setTimeout(() => {
toast.dismiss();
if (!success) {
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, please correct{" "}
{Object.keys(form.hook.formState.errors).length} errors.
</div>,
{
dismissible: true,
className: css`
background: #ffecec;
border: 2px solid red;
`,
}
);
} else {
toast.success(
<div className="c-flex c-text-blue-700 c-items-center">
<Check className="c-h-4 c-w-4 c-mr-1 " />
Done
</div>,
{
className: css`
background: #e4f5ff;
border: 2px solid blue;
`,
}
);
}
}, 100);
}
}, 50);
});
};
if (!local.init) {
local.init = true;
on_init({
submit,
reload: () => {
local.init = false;
form.unload = () => {
form.hook.clearErrors();
form.hook.reset();
delete form.unload;
local.render();
};
local.render();
},
});
const res = on_load();
const loaded = (values: any) => {
setTimeout(() => {
toast.dismiss();
}, 100);
if (!!values) {
for (const [k, v] of Object.entries(values)) {
form.hook.setValue(k, v);
}
}
local.render();
};
if (res instanceof Promise) {
setTimeout(() => {
if (!isEditor && sonar === "on") {
toast.loading(
<>
<Loader2 className="c-h-4 c-w-4 c-animate-spin" />
Loading data...
</>
);
}
res.then(loaded);
});
} else {
loaded(res);
}
}
form.submit = submit;
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 (form.unload)
return (
<div className="c-p-6 c-flex c-flex-col c-space-y-2 c-w-full c-flex-1 c-items-start">
<Skeleton className="c-h-3 c-w-[50%]" />
<Skeleton className="c-h-3 c-w-[40%]" />
</div>
);
return (
<FormInternal {...form_hook} form={form}>
{toaster_el && createPortal(<Toaster cn={cn} />, toaster_el)}
<form
className={cx(
"flex-1 flex flex-col w-full items-stretch relative overflow-auto",
css`
.c-text-destructive {
color: red;
}
`,
local.layout === "unknown" && "c-hidden",
local.layout === "2-col" &&
css`
> div {
flex-direction: row;
flex-wrap: wrap;
> div {
width: 50%;
}
}
`
)}
ref={(el) => {
if (el) form.ref = el;
if (el && layout === "auto" && local.layout === "unknown") {
let cur: any = el;
let i = 0;
while (cur.parentNode && cur.getBoundingClientRect().width === 0) {
cur = cur.parentNode;
i++;
if (i > 10) {
break;
}
}
if (cur.getBoundingClientRect().width < 500) {
local.layout = "1-col";
} else {
local.layout = "2-col";
}
local.render(true);
}
}}
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
submit();
}}
>
<PassProp submit={submit} data={form_hook.getValues()}>
{body}
</PassProp>
</form>
</FormInternal>
);
};
const FormInternal = (props: any) => {
useEffect(() => {
return () => {
if (props.form && props.form.unload) {
props.form.unload();
}
};
}, []);
return <FForm {...props} />;
};

View File

@ -1,204 +1,34 @@
import { Form as FForm } from "@/comps/ui/form";
import { Toaster } from "@/comps/ui/sonner";
import { cn } from "@/utils";
import { useLocal } from "@/utils/use-local";
import { AlertTriangle, Check, Loader2 } from "lucide-react";
import { FC, useEffect } from "react";
import { FC } from "react";
import { FMInternal, FMProps } from "./typings";
import { formReload } from "./utils/reload";
import { formInit } from "./utils/init";
import { createPortal } from "react-dom";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { Skeleton } from "../ui/skeleton";
import { FormHook } from "./utils/utils";
import { Toaster } from "sonner";
export const Form: FC<{
on_init: (arg: { submit: any; reload: any }) => any;
on_load: () => any;
on_submit: (arg: { form: any; error: any }) => Promise<any>;
body: any;
form: FormHook;
PassProp: any;
cache: () => any;
sonar: "on" | "off";
layout: "auto" | "1-col" | "2-col";
}> = ({
on_init,
on_load,
body,
form,
PassProp,
on_submit,
cache,
layout: _layout,
sonar,
}) => {
const form_hook = useForm<any>({
defaultValues: {},
});
const local = useLocal({
el: null as any,
submit_timeout: null as any,
submit_done: [] as any[],
layout: "unknown" as "unknown" | "2-col" | "1-col",
init: false,
});
form.hook = form_hook;
if (!form.cache && typeof cache === "function") {
try {
form.cache = cache() || {};
} catch (e) {}
}
if (!form.cache) form.cache = {};
if (!form.validation) {
form.validation = {};
}
if (!form.label) {
form.label = {};
}
let layout = _layout || "auto";
if (layout !== "auto") local.layout = layout;
const submit = () => {
return new Promise<boolean>((done) => {
local.submit_done.push(done);
const done_all = (val: boolean) => {
for (const d of local.submit_done) {
d(val);
}
local.submit_done = [];
local.render();
};
clearTimeout(local.submit_timeout);
local.submit_timeout = setTimeout(async () => {
if (sonar === "on") {
toast.loading(
<>
<Loader2 className="c-h-4 c-w-4 c-animate-spin" />
Processing ...
</>,
{
dismissible: true,
className: css`
background: #e4f7ff;
`,
}
);
}
const data = form.hook.getValues();
form.hook.clearErrors();
for (const [k, v] of Object.entries(form.validation)) {
if (v === "required") {
if (!data[k]) {
const error = {
type: "required",
message: `${form.label[k] || k} is required.`,
};
form.hook.formState.errors[k] = error;
form.hook.setError(k, error);
}
}
}
const res = on_submit({
form: data,
error: form.hook.formState.errors,
});
const success = await res;
toast.dismiss();
done_all(success);
if (sonar === "on") {
setTimeout(() => {
toast.dismiss();
if (!success) {
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, please correct{" "}
{Object.keys(form.hook.formState.errors).length} errors.
</div>,
{
dismissible: true,
className: css`
background: #ffecec;
border: 2px solid red;
`,
}
);
} else {
toast.success(
<div className="c-flex c-text-blue-700 c-items-center">
<Check className="c-h-4 c-w-4 c-mr-1 " />
Done
</div>,
{
className: css`
background: #e4f5ff;
border: 2px solid blue;
`,
}
);
}
}, 100);
}
}, 50);
});
};
if (!local.init) {
local.init = true;
on_init({
submit,
reload: () => {
local.init = false;
form.unload = () => {
form.hook.clearErrors();
form.hook.reset();
delete form.unload;
local.render();
};
local.render();
export const Form: FC<FMProps> = (props) => {
const { PassProp, body } = props;
const fm = useLocal<FMInternal>({
data: "",
status: "init",
reload: async () => {
formReload(fm);
},
error: {} as any,
internal: {
reload: {
timeout: null as any,
promises: [],
done: [],
},
});
},
props: {} as any,
});
const res = on_load();
const loaded = (values: any) => {
setTimeout(() => {
toast.dismiss();
}, 100);
if (!!values) {
for (const [k, v] of Object.entries(values)) {
form.hook.setValue(k, v);
}
}
local.render();
};
if (res instanceof Promise) {
setTimeout(() => {
if (!isEditor && sonar === "on") {
toast.loading(
<>
<Loader2 className="c-h-4 c-w-4 c-animate-spin" />
Loading data...
</>
);
}
res.then(loaded);
});
} else {
loaded(res);
}
if (fm.status === "init") {
formInit(fm, props);
}
form.submit = submit;
if (document.getElementsByClassName("prasi-toaster").length === 0) {
const elemDiv = document.createElement("div");
elemDiv.className = "prasi-toaster";
@ -206,81 +36,10 @@ export const Form: FC<{
}
const toaster_el = document.getElementsByClassName("prasi-toaster")[0];
if (form.unload)
return (
<div className="c-p-6 c-flex c-flex-col c-space-y-2 c-w-full c-flex-1 c-items-start">
<Skeleton className="c-h-3 c-w-[50%]" />
<Skeleton className="c-h-3 c-w-[40%]" />
</div>
);
return (
<FormInternal {...form_hook} form={form}>
{toaster_el && createPortal(<Toaster cn={cn} />, toaster_el)}
<form
className={cx(
"flex-1 flex flex-col w-full items-stretch relative overflow-auto",
css`
.c-text-destructive {
color: red;
}
`,
local.layout === "unknown" && "c-hidden",
local.layout === "2-col" &&
css`
> div {
flex-direction: row;
flex-wrap: wrap;
> div {
width: 50%;
}
}
`
)}
ref={(el) => {
if (el) form.ref = el;
if (el && layout === "auto" && local.layout === "unknown") {
let cur: any = el;
let i = 0;
while (cur.parentNode && cur.getBoundingClientRect().width === 0) {
cur = cur.parentNode;
i++;
if (i > 10) {
break;
}
}
if (cur.getBoundingClientRect().width < 500) {
local.layout = "1-col";
} else {
local.layout = "2-col";
}
local.render(true);
}
}}
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
submit();
}}
>
<PassProp submit={submit} data={form_hook.getValues()}>
{body}
</PassProp>
</form>
</FormInternal>
<>
{toaster_el && createPortal(<Toaster cn={cx} />, toaster_el)}
<PassProp>{body}</PassProp>
</>
);
};
const FormInternal = (props: any) => {
useEffect(() => {
return () => {
if (props.form && props.form.unload) {
props.form.unload();
}
};
}, []);
return <FForm {...props} />;
};

37
comps/form/typings.ts Executable file
View File

@ -0,0 +1,37 @@
import { FormHook } from "../form-old/utils/utils";
export type FMProps = {
on_init: (arg: { submit: any; reload: any }) => any;
on_load: () => any;
on_submit: (arg: { form: any; error: any }) => Promise<any>;
body: any;
form: FormHook;
PassProp: any;
sonar: "on" | "off";
layout: "auto" | "1-col" | "2-col";
};
export type FMInternal = {
status: "init" | "loading" | "saving" | "ready";
data: any;
reload: () => Promise<void>;
error: {
list: { name: string; error: string }[];
set: (name: string, error: string) => void;
get: (name: string, error: string) => void;
clear: () => void;
};
internal: {
reload: {
timeout: ReturnType<typeof setTimeout>;
promises: Promise<void>[];
done: any[];
};
};
props: Exclude<FMProps, "body" | "PassProp">;
};
export type FMLocal = FMInternal & { render: () => void };
export const FormType = `{
status: "init" | "loading" | "saving" | "ready"
}`;

10
comps/form/utils/error.ts Executable file
View File

@ -0,0 +1,10 @@
import { FMLocal } from "../typings";
export const formError = (fm: FMLocal) => {
const error = {} as FMLocal["error"];
error.list = [];
error.clear = () => {};
error.set = () => {};
error.get = () => {};
return error;
};

38
comps/form/utils/init.tsx Executable file
View File

@ -0,0 +1,38 @@
import { Loader2 } from "lucide-react";
import { toast } from "sonner";
import { FMLocal, FMProps } from "../typings";
import { formError } from "./error";
export const formInit = (fm: FMLocal, props: FMProps) => {
for (const [k, v] of Object.entries(props)) {
if (["PassProp", "body"].includes(k)) continue;
(fm.props as any)[k] = v;
}
const { on_load, sonar } = fm.props;
fm.error = formError(fm);
fm.reload = () => {
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.loading(
<>
<Loader2 className="c-h-4 c-w-4 c-animate-spin" />
Loading data...
</>
);
});
}
const res = on_load({ fm });
fm.internal.reload.done.map((e) => e());
}, 50);
});
fm.internal.reload.promises.push(promise);
return promise;
};
};

3
comps/form/utils/reload.ts Executable file
View File

@ -0,0 +1,3 @@
import { FMLocal } from "../typings";
export const formReload = (fm: FMLocal) => {};

View File

@ -1,6 +1,6 @@
import { useLocal } from "@/utils/use-local";
import { FC, ReactElement, useEffect } from "react";
import { Skeleton } from "../ui/skeleton";
import { Skeleton } from "../../ui/skeleton";
import get from "lodash.get";
type ListProp = {

View File

@ -7,7 +7,7 @@ import DataGrid, {
SortColumn,
} from "react-data-grid";
import "react-data-grid/lib/styles.css";
import { Skeleton } from "../ui/skeleton";
import { Skeleton } from "../../ui/skeleton";
type OnRowClick = {
row: any;

3
comps/list/typings.ts Executable file
View File

@ -0,0 +1,3 @@
export type TLInternal = {};
export type TL = TLInternal & { render: (force?: boolean) => void };
export type TableListType = `{}`;

View File

@ -1,6 +1,6 @@
import { BreadItem } from "@/comps/custom/Breadcrumb";
import { GFCol } from "@/gen/utils";
import { ReactElement, ReactNode } from "react";
import { ReactNode } from "react";
export type MDActions = {
action?: string;

View File

@ -1,9 +1,11 @@
export { getProp } from "./comps/md/utils/get-prop";
export { MasterDetailType } from "./comps/md/utils/typings";
export { getProp } from "@/comps/md/utils/get-prop";
export { MasterDetailType } from "@/comps/md/utils/typings";
export { TableList } from "@/comps/list/TableList";
export { MasterDetail } from "@/comps/md/MasterDetail";
export { MDAction } from "./comps/md/MDAction";
export { Form } from "@/comps/form/Form";
export { Field } from "@/comps/form/Field";
export { FormType } from "@/comps/form/typings";
export { Field } from "@/comps/form-old/Field";
export { prasi_gen } from "@/gen/prasi_gen";
export { FormatValue } from "@/utils/format-value";
export { TableListType } from "@/comps/list/typings";

View File

@ -1,11 +1,11 @@
export { Card } from "@/comps/custom/Card";
export { Detail } from "@/comps/custom/Detail";
export { Tab } from "@/comps/custom/Tab";
export { Field } from "@/comps/form/Field";
export { Field } from "@/comps/form-old/Field";
export { Form } from "@/comps/form/Form";
export { formatMoney } from "@/comps/form/InputMoney";
export { formatMoney } from "@/comps/form-old/InputMoney";
export { icon } from "@/comps/icon";
export { List } from "@/comps/list/List";
export { List } from "@/comps/list/old/List";
export { Slider } from "@/comps/ui/slider";
export * from "@/utils/date";
export { Button, FloatButton } from "@/comps/ui/button";