diff --git a/comps/form/Date/index.tsx b/comps/form-old/Date/index.tsx similarity index 100% rename from comps/form/Date/index.tsx rename to comps/form-old/Date/index.tsx diff --git a/comps/form/Datetime/index.tsx b/comps/form-old/Datetime/index.tsx similarity index 100% rename from comps/form/Datetime/index.tsx rename to comps/form-old/Datetime/index.tsx diff --git a/comps/form/Dropdown/index.tsx b/comps/form-old/Dropdown/index.tsx similarity index 100% rename from comps/form/Dropdown/index.tsx rename to comps/form-old/Dropdown/index.tsx diff --git a/comps/form/Dropdown/relation.tsx b/comps/form-old/Dropdown/relation.tsx similarity index 96% rename from comps/form/Dropdown/relation.tsx rename to comps/form-old/Dropdown/relation.tsx index df01dcc..873f622 100755 --- a/comps/form/Dropdown/relation.tsx +++ b/comps/form-old/Dropdown/relation.tsx @@ -51,11 +51,13 @@ export const Relation: FC = ({ 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 = {}; diff --git a/comps/form/Field.tsx b/comps/form-old/Field.tsx similarity index 100% rename from comps/form/Field.tsx rename to comps/form-old/Field.tsx diff --git a/comps/form-old/Form.tsx b/comps/form-old/Form.tsx new file mode 100755 index 0000000..167f1a7 --- /dev/null +++ b/comps/form-old/Form.tsx @@ -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; + 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({ + 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((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( + <> + + 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( +
+ + Save Failed, please correct{" "} + {Object.keys(form.hook.formState.errors).length} errors. +
, + { + dismissible: true, + className: css` + background: #ffecec; + border: 2px solid red; + `, + } + ); + } else { + toast.success( +
+ + Done +
, + { + 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( + <> + + 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 ( +
+ + +
+ ); + + return ( + + {toaster_el && createPortal(, toaster_el)} +
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(); + }} + > + + {body} + +
+
+ ); +}; + +const FormInternal = (props: any) => { + useEffect(() => { + return () => { + if (props.form && props.form.unload) { + props.form.unload(); + } + }; + }, []); + return ; +}; diff --git a/comps/form/InputMoney/index.tsx b/comps/form-old/InputMoney/index.tsx similarity index 100% rename from comps/form/InputMoney/index.tsx rename to comps/form-old/InputMoney/index.tsx diff --git a/comps/form/Radio/index.tsx b/comps/form-old/Radio/index.tsx similarity index 100% rename from comps/form/Radio/index.tsx rename to comps/form-old/Radio/index.tsx diff --git a/comps/form/Slider/types.ts b/comps/form-old/Slider/types.ts similarity index 100% rename from comps/form/Slider/types.ts rename to comps/form-old/Slider/types.ts diff --git a/comps/form/type.ts b/comps/form-old/type.ts similarity index 100% rename from comps/form/type.ts rename to comps/form-old/type.ts diff --git a/comps/form/utils/utils.ts b/comps/form-old/utils/utils.ts similarity index 100% rename from comps/form/utils/utils.ts rename to comps/form-old/utils/utils.ts diff --git a/comps/form/Form.tsx b/comps/form/Form.tsx index 167f1a7..11ee7e1 100755 --- a/comps/form/Form.tsx +++ b/comps/form/Form.tsx @@ -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; - 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({ - 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((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( - <> - - 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( -
- - Save Failed, please correct{" "} - {Object.keys(form.hook.formState.errors).length} errors. -
, - { - dismissible: true, - className: css` - background: #ffecec; - border: 2px solid red; - `, - } - ); - } else { - toast.success( -
- - Done -
, - { - 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 = (props) => { + const { PassProp, body } = props; + const fm = useLocal({ + 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( - <> - - 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 ( -
- - -
- ); - return ( - - {toaster_el && createPortal(, toaster_el)} -
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(); - }} - > - - {body} - -
-
+ <> + {toaster_el && createPortal(, toaster_el)} + {body} + ); }; - -const FormInternal = (props: any) => { - useEffect(() => { - return () => { - if (props.form && props.form.unload) { - props.form.unload(); - } - }; - }, []); - return ; -}; diff --git a/comps/form/typings.ts b/comps/form/typings.ts new file mode 100755 index 0000000..d4953b9 --- /dev/null +++ b/comps/form/typings.ts @@ -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; + 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; + error: { + list: { name: string; error: string }[]; + set: (name: string, error: string) => void; + get: (name: string, error: string) => void; + clear: () => void; + }; + internal: { + reload: { + timeout: ReturnType; + promises: Promise[]; + done: any[]; + }; + }; + props: Exclude; +}; +export type FMLocal = FMInternal & { render: () => void }; + +export const FormType = `{ + status: "init" | "loading" | "saving" | "ready" +}`; diff --git a/comps/form/utils/error.ts b/comps/form/utils/error.ts new file mode 100755 index 0000000..287507a --- /dev/null +++ b/comps/form/utils/error.ts @@ -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; +}; diff --git a/comps/form/utils/init.tsx b/comps/form/utils/init.tsx new file mode 100755 index 0000000..f3d8011 --- /dev/null +++ b/comps/form/utils/init.tsx @@ -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((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( + <> + + Loading data... + + ); + }); + } + + const res = on_load({ fm }); + + fm.internal.reload.done.map((e) => e()); + }, 50); + }); + fm.internal.reload.promises.push(promise); + return promise; + }; +}; diff --git a/comps/form/utils/reload.ts b/comps/form/utils/reload.ts new file mode 100755 index 0000000..4246e68 --- /dev/null +++ b/comps/form/utils/reload.ts @@ -0,0 +1,3 @@ +import { FMLocal } from "../typings"; + +export const formReload = (fm: FMLocal) => {}; diff --git a/comps/list/List.tsx b/comps/list/old/List.tsx similarity index 98% rename from comps/list/List.tsx rename to comps/list/old/List.tsx index 5fd5344..7ab224c 100755 --- a/comps/list/List.tsx +++ b/comps/list/old/List.tsx @@ -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 = { diff --git a/comps/list/Table.tsx b/comps/list/old/Table.tsx similarity index 99% rename from comps/list/Table.tsx rename to comps/list/old/Table.tsx index a622c09..28c4fe7 100755 --- a/comps/list/Table.tsx +++ b/comps/list/old/Table.tsx @@ -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; diff --git a/comps/list/typings.ts b/comps/list/typings.ts new file mode 100755 index 0000000..0afda92 --- /dev/null +++ b/comps/list/typings.ts @@ -0,0 +1,3 @@ +export type TLInternal = {}; +export type TL = TLInternal & { render: (force?: boolean) => void }; +export type TableListType = `{}`; diff --git a/comps/md/utils/typings.ts b/comps/md/utils/typings.ts index 543bcbe..e010374 100755 --- a/comps/md/utils/typings.ts +++ b/comps/md/utils/typings.ts @@ -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; diff --git a/data.ts b/data.ts index 446d0b8..a760f94 100755 --- a/data.ts +++ b/data.ts @@ -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"; diff --git a/exports.tsx b/exports.tsx index a9bf81b..448a79d 100755 --- a/exports.tsx +++ b/exports.tsx @@ -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";