This commit is contained in:
rizky 2024-04-12 23:12:08 -07:00
parent 72bcba5bb5
commit 26c1402bd1
14 changed files with 359 additions and 79 deletions

View File

@ -1,14 +1,12 @@
import { useLocal } from "@/utils/use-local"; import { useLocal } from "@/utils/use-local";
import { FC, Fragment, useEffect, useRef } from "react"; import get from "lodash.get";
import { FMInternal, FMProps } from "./typings"; import { FC, useRef } from "react";
import { formReload } from "./utils/reload";
import { formInit } from "./utils/init";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
import { Toaster } from "sonner"; import { Toaster } from "sonner";
import get from "lodash.get"; import { FMInternal, FMProps } from "./typings";
import { Field } from "./field/Field";
import { getProp } from "../../..";
import { editorFormData } from "./utils/ed-data"; import { editorFormData } from "./utils/ed-data";
import { formInit } from "./utils/init";
import { formReload } from "./utils/reload";
const editorFormWidth = {} as Record<string, { w: number; f: any }>; const editorFormWidth = {} as Record<string, { w: number; f: any }>;
@ -35,6 +33,7 @@ export const Form: FC<FMProps> = (props) => {
done: [], done: [],
}, },
}, },
field_def: {},
props: {} as any, props: {} as any,
size: { size: {
width: editorFormWidth[props.item.id] width: editorFormWidth[props.item.id]

View File

@ -36,6 +36,7 @@ export const Field: FC<FieldProp> = (arg) => {
w === "auto" && fm.size.field === "full" && "c-w-full", w === "auto" && fm.size.field === "full" && "c-w-full",
w === "auto" && fm.size.field === "half" && "c-w-1/2", w === "auto" && fm.size.field === "half" && "c-w-1/2",
w === "full" && "c-w-full", w === "full" && "c-w-full",
w === "¾" && "c-w-3/4",
w === "½" && "c-w-1/2", w === "½" && "c-w-1/2",
w === "⅓" && "c-w-1/3", w === "⅓" && "c-w-1/3",
w === "¼" && "c-w-1/4", w === "¼" && "c-w-1/4",

View File

@ -19,7 +19,7 @@ export const FieldInput: FC<{
_item: any; _item: any;
_meta: any; _meta: any;
_sync: (mitem: any, item: any) => void; _sync: (mitem: any, item: any) => void;
}> = ({ field, fm, PassProp, child, _meta, _item }) => { }> = ({ field, fm, PassProp, child, _meta, _item, _sync }) => {
const prefix = typeof field.prefix === "function" ? field.prefix() : null; const prefix = typeof field.prefix === "function" ? field.prefix() : null;
const suffix = typeof field.suffix === "function" ? field.suffix() : null; const suffix = typeof field.suffix === "function" ? field.suffix() : null;
const errors = fm.error.get(field.name); const errors = fm.error.get(field.name);
@ -31,26 +31,35 @@ export const FieldInput: FC<{
let found = null as any; let found = null as any;
if (childs && childs.length > 0) { if (childs && childs.length > 0) {
for (const child of childs) { for (const child of childs) {
if (child.component?.id === fieldMapping[field.type].id) { const mp = (fieldMapping as any)[field.type];
if (child.component?.id === mp.id) {
found = child; found = child;
const item = createItem({ if (mp.props) {
component: { id: "--", props: fieldMapping[field.type].props }, const item = createItem({
}); component: {
id: "--",
props:
typeof mp.props === "function" ? mp.props(fm, field) : mp.props,
},
});
const props = found.component.props; const props = found.component.props;
let should_update = false; let should_update = false;
for (const [k, v] of Object.entries(item.component.props) as any) { for (const [k, v] of Object.entries(item.component.props) as any) {
if (props[k] && props[k].valueBuilt === v.valueBuilt) { if (props[k] && props[k].valueBuilt === v.valueBuilt) {
continue; continue;
} else { } else {
props[k] = v; if (field.prop && !field.prop[k]) {
should_update = true; props[k] = v;
should_update = true;
}
}
} }
}
if (should_update) { if (should_update) {
updateFieldMItem(_meta, found); updateFieldMItem(_meta, found, _sync);
}
} }
} }
} }
@ -58,14 +67,14 @@ export const FieldInput: FC<{
useEffect(() => { useEffect(() => {
if (isEditor && !found) { if (isEditor && !found) {
genFieldMitem({ _meta, _item, field, fm }); genFieldMitem({ _meta, _item, _sync, field, fm });
} }
}, []); }, []);
return ( return (
<div <div
className={cx( className={cx(
"field-inner c-flex c-flex-1 c-flex-row c-rounded c-border c-text-sm", "field-outer c-flex c-flex-1 c-flex-row c-rounded c-border c-text-sm",
fm.status === "loading" fm.status === "loading"
? css` ? css`
border-color: transparent; border-color: transparent;

View File

@ -1,12 +1,29 @@
import { FieldProp } from "../typings"; import { FMLocal, FieldInternal, FieldLocal, FieldProp } from "../typings";
export const fieldMapping: Record< export const fieldMapping: {
FieldProp["type"], [K in FieldProp["type"]]: {
{ id: string; props: any } id: string;
> = { props?:
| Record<string, any>
| ((
fm: FMLocal,
field: FieldInternal<K> & {
render: () => void;
}
) => Record<string, any>);
};
} = {
text: { id: "ca7ac237-8f22-4492-bb9d-4b715b1f5c25", props: { type: "text" } }, text: { id: "ca7ac237-8f22-4492-bb9d-4b715b1f5c25", props: { type: "text" } },
number: { relation: {
id: "ca7ac237-8f22-4492-bb9d-4b715b1f5c25", id: "69263ca0-61a1-4899-ad5f-059ac12b94d1",
props: { type: "number" }, props: (fm, field) => {
const rel = fm.field_def[field.name];
if (rel) {
if (field.prop && !field.prop.type) {
return { type: rel.type };
}
}
return {};
},
}, },
} as any; };

150
comps/form/field/raw/Dropdown.tsx Executable file
View File

@ -0,0 +1,150 @@
import { Popover } from "@/comps/custom/Popover";
import { useLocal } from "@/utils/use-local";
import { ChevronDown } from "lucide-react";
import { FC, ReactNode } from "react";
type OptionItem = { value: string; label: string; el?: ReactNode };
export const RawDropdown: FC<{
options: OptionItem[];
className?: string;
value: string;
onFocus?: () => void;
onBlur?: () => void;
onChange?: (value: string) => void;
}> = ({ value, options, className, onFocus, onBlur, onChange }) => {
const local = useLocal({
open: false,
input: {
value: "",
el: null as any,
},
filter: "",
width: 0,
selected: undefined as undefined | OptionItem,
});
let filtered = options;
if (local.filter) {
filtered = options.filter((e) => {
if (e.label.toLowerCase().includes(local.filter)) return true;
return false;
});
}
local.selected = options.find((e) => e.value === value);
return (
<Popover
open={local.open}
onOpenChange={() => {
local.open = false;
local.render();
}}
arrow={false}
className={cx("c-rounded-sm c-bg-white")}
content={
<div
className={cx(
"c-text-sm",
css`
width: ${local.width || 100}px;
`
)}
>
<>
{filtered.map((item, idx) => {
return (
<div
tabIndex={0}
key={item.value + "_" + idx}
className={cx(
"c-px-3 c-py-1 cursor-pointer option-item",
item.value === value
? "c-bg-blue-600 c-text-white"
: "hover:c-bg-blue-50",
idx > 0 && "c-border-t",
idx === 0 && "c-rounded-t-sm",
idx === filtered.length - 1 && "c-rounded-b-sm"
)}
onClick={() => {
if (onChange) onChange(item.value);
}}
>
{item.label}
</div>
);
})}
</>
</div>
}
>
<div
className={cx(
"c-relative",
className,
css`
cursor: pointer !important;
`
)}
tabIndex={0}
onFocus={() => {
local.open = true;
if (local.selected) local.input.value = local.selected.label;
local.filter = "";
local.render();
setTimeout(() => {
local.input.el?.focus();
});
}}
ref={(el) => {
if (local.width === 0 && el) {
const box = el.getBoundingClientRect();
if (box && box.width) {
local.width = box.width;
local.render();
}
}
}}
>
<div className="c-w-full c-h-full c-relative">
<input
spellCheck={false}
value={local.open ? local.input.value : "Halo"}
className={cx(
"c-absolute c-inset-0 c-w-full c-h-full c-outline-none c-p-0",
local.open
? "c-cursor-pointer"
: "c-pointer-events-none c-invisible"
)}
onChange={(e) => {
local.input.value = e.currentTarget.value;
local.filter = local.input.value.toLowerCase();
local.render();
}}
ref={(el) => {
local.input.el = el;
}}
type="text"
onFocus={onFocus}
onBlur={onBlur}
/>
{!local.open && local.selected && (
<div className="c-absolute c-inset-0 c-z-10 c-w-full c-h-full c-text-sm c-flex c-items-center">
{local.selected.el || local.selected.label}
</div>
)}
</div>
<div
className={cx(
"c-absolute c-pointer-events-none c-z-10 c-inset-0 c-left-auto c-flex c-items-center ",
"c-bg-white c-justify-center c-w-6 c-mr-1 c-my-2"
)}
>
<ChevronDown size={14} />
</div>
</div>
</Popover>
);
};

View File

@ -0,0 +1,36 @@
import { useLocal } from "@/utils/use-local";
import { FC } from "react";
import { FMLocal, FieldLocal } from "../../typings";
import { RawDropdown } from "../raw/Dropdown";
export type PropTypeRelation = {
type: "has-one" | "has-many";
};
export const FieldTypeRelation: FC<{
field: FieldLocal;
fm: FMLocal;
prop: PropTypeRelation;
}> = ({ field, fm, prop }) => {
const input = useLocal({});
const value = fm.data[field.name];
field.input = input;
field.prop = prop;
return (
<>
<RawDropdown
options={[{ label: "Halo", value: "halo" }]}
value={"halo"}
className="c-flex-1 c-bg-transparent c-outline-none c-px-2 c-text-sm c-w-full c-h-full"
onFocus={() => {
field.focused = true;
field.render();
}}
onBlur={() => {
field.focused = false;
field.render();
}}
/>
</>
);
};

View File

@ -1,14 +1,21 @@
import { FC } from "react"; import { FC } from "react";
import { FMLocal, FieldLocal } from "../../typings"; import { FMLocal, FieldLocal } from "../../typings";
import { useLocal } from "@/utils/use-local";
export type PropTypeText = {
type: "text" | "password" | "number";
};
export const FieldTypeText: FC<{ export const FieldTypeText: FC<{
field: FieldLocal; field: FieldLocal;
fm: FMLocal; fm: FMLocal;
prop: { prop: PropTypeText;
type: "text" | "password" | "number";
};
}> = ({ field, fm, prop }) => { }> = ({ field, fm, prop }) => {
const input = useLocal({});
const value = fm.data[field.name]; const value = fm.data[field.name];
field.input = input;
field.prop = prop;
return ( return (
<input <input
type={prop.type} type={prop.type}
@ -18,7 +25,7 @@ export const FieldTypeText: FC<{
}} }}
value={value || ""} value={value || ""}
disabled={field.disabled} disabled={field.disabled}
className="c-flex-1 c-rounded c-bg-transparent c-outline-none c-px-2 c-text-sm" className="c-flex-1 c-bg-transparent c-outline-none c-px-2 c-text-sm"
spellCheck={false} spellCheck={false}
onFocus={() => { onFocus={() => {
field.focused = true; field.focused = true;

View File

@ -1,8 +1,10 @@
import { GFCol } from "@/gen/utils";
import { ReactNode } from "react"; import { ReactNode } from "react";
import { SliderOptions } from "../form-old/Slider/types";
import { FieldOptions } from "../form-old/type"; import { FieldOptions } from "../form-old/type";
import { FormHook } from "../form-old/utils/utils"; import { FormHook } from "../form-old/utils/utils";
import { editorFormData } from "./utils/ed-data"; import { editorFormData } from "./utils/ed-data";
import { PropTypeText } from "./field/type/TypeText";
import { PropTypeRelation } from "./field/type/TypeRelation";
export type FMProps = { export type FMProps = {
on_init: (arg: { fm: FMLocal; submit: any; reload: any }) => any; on_init: (arg: { fm: FMLocal; submit: any; reload: any }) => any;
@ -18,6 +20,7 @@ export type FMProps = {
item: any; item: any;
label_mode: "vertical" | "horizontal" | "hidden"; label_mode: "vertical" | "horizontal" | "hidden";
label_width: number; label_width: number;
gen_fields: any;
}; };
export type FieldProp = { export type FieldProp = {
@ -26,20 +29,18 @@ export type FieldProp = {
desc?: string; desc?: string;
props?: any; props?: any;
fm: FMLocal; fm: FMLocal;
type: type: "text" | "relation";
| "text" // | "number"
| "number" // | "textarea"
| "textarea" // | "dropdown"
| "dropdown" // | "password"
| "relation" // | "radio"
| "password" // | "date"
| "radio" // | "datetime"
| "date" // | "money"
| "datetime" // | "slider"
| "money" // | "master-link"
| "slider" // | "custom";
| "master-link"
| "custom";
required: "y" | "n"; required: "y" | "n";
required_msg: (name: string) => string; required_msg: (name: string) => string;
options: FieldOptions; options: FieldOptions;
@ -50,7 +51,7 @@ export type FieldProp = {
selection: "single" | "multi"; selection: "single" | "multi";
prefix: any; prefix: any;
suffix: any; suffix: any;
width: "auto" | "full" | "½" | "⅓" | "¼"; width: "auto" | "full" | "¾" | "½" | "⅓" | "¼";
_meta: any; _meta: any;
_item: any; _item: any;
_sync: any; _sync: any;
@ -65,6 +66,7 @@ export type FMInternal = {
on_change: (name: string, new_value: any) => void; on_change: (name: string, new_value: any) => void;
}; };
fields: Record<string, FieldLocal>; fields: Record<string, FieldLocal>;
field_def: Record<string, GFCol>;
error: { error: {
readonly list: { name: string; error: string[] }[]; readonly list: { name: string; error: string[] }[];
set: (name: string, error: string[]) => void; set: (name: string, error: string[]) => void;
@ -87,10 +89,16 @@ export type FMInternal = {
}; };
export type FMLocal = FMInternal & { render: () => void }; export type FMLocal = FMInternal & { render: () => void };
export type FieldInternal = { type FieldInternalProp = {
text: PropTypeText;
number: PropTypeText;
relation: PropTypeRelation;
};
export type FieldInternal<T extends FieldProp["type"]> = {
status: "init" | "loading" | "ready"; status: "init" | "loading" | "ready";
name: FieldProp["name"]; name: FieldProp["name"];
type: FieldProp["type"]; type: T;
label: FieldProp["label"]; label: FieldProp["label"];
desc: FieldProp["desc"]; desc: FieldProp["desc"];
prefix: FieldProp["prefix"]; prefix: FieldProp["prefix"];
@ -100,9 +108,16 @@ export type FieldInternal = {
focused: boolean; focused: boolean;
disabled: boolean; disabled: boolean;
required_msg: FieldProp["required_msg"]; required_msg: FieldProp["required_msg"];
col?: GFCol;
Child: () => ReactNode; Child: () => ReactNode;
input: Record<string, any> & {
render: () => void;
};
prop?: FieldInternalProp[T];
};
export type FieldLocal = FieldInternal<any> & {
render: () => void;
}; };
export type FieldLocal = FieldInternal & { render: () => void };
export const formType = (active: { item_id: string }, meta: any) => { export const formType = (active: { item_id: string }, meta: any) => {
let data = "null as any"; let data = "null as any";

View File

@ -1,12 +1,17 @@
import { newField } from "@/gen/gen_form/new_field";
import { FMLocal, FieldLocal } from "../typings"; import { FMLocal, FieldLocal } from "../typings";
import get from "lodash.get";
import { fieldMapping } from "../field/mapping";
import { createItem } from "@/gen/utils";
export const genFieldMitem = (arg: { export const genFieldMitem = (arg: {
_meta: any; _meta: any;
_item: any; _item: any;
_sync: any;
fm: FMLocal; fm: FMLocal;
field: FieldLocal; field: FieldLocal;
}) => { }) => {
const { _meta, _item, fm, field } = arg; const { _meta, _item, _sync, fm, field } = arg;
const m = _meta[_item.id]; const m = _meta[_item.id];
if (m) { if (m) {
const mitem = m.mitem; const mitem = m.mitem;
@ -18,16 +23,27 @@ export const genFieldMitem = (arg: {
?.get("content") ?.get("content")
?.get("childs"); ?.get("childs");
// console.log(field.name, childs); const col = fm.field_def[field.name];
if (col) {
const component = fieldMapping[field.type as "text"];
if (component) {
const item = createItem({
component: component as any,
});
_sync(childs, [...childs.toJSON(), item]);
}
}
} }
} }
}; };
export const updateFieldMItem = (_meta: any, _item: any) => {
export const updateFieldMItem = (_meta: any, _item: any, _sync: any) => {
const m = _meta[_item.id]; const m = _meta[_item.id];
if (m) { if (m) {
const mitem = m.mitem; const mitem = m.mitem;
if (mitem) { if (mitem) {
_sync(mitem, _item);
} }
} }
}; };

View File

@ -1,9 +1,10 @@
import { parseGenField } from "@/gen/utils";
import get from "lodash.get";
import { Loader2 } from "lucide-react"; import { Loader2 } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
import { FMLocal, FMProps } from "../typings"; import { FMLocal, FMProps } from "../typings";
import { formError } from "./error";
import { editorFormData } from "./ed-data"; import { editorFormData } from "./ed-data";
import get from "lodash.get"; import { formError } from "./error";
export const formInit = (fm: FMLocal, props: FMProps) => { export const formInit = (fm: FMLocal, props: FMProps) => {
for (const [k, v] of Object.entries(props)) { for (const [k, v] of Object.entries(props)) {
@ -13,6 +14,14 @@ export const formInit = (fm: FMLocal, props: FMProps) => {
const { on_load, sonar } = fm.props; const { on_load, sonar } = fm.props;
fm.error = formError(fm); fm.error = formError(fm);
if (isEditor) {
fm.field_def = {};
const defs = parseGenField(fm.props.gen_fields);
for (const d of defs) {
fm.field_def[d.name] = d;
}
}
fm.reload = () => { fm.reload = () => {
fm.status = isEditor ? "ready" : "loading"; fm.status = isEditor ? "ready" : "loading";
fm.render(); fm.render();

View File

@ -3,11 +3,12 @@ import { FMLocal, FieldInternal, FieldProp } from "../typings";
import { useEffect } from "react"; import { useEffect } from "react";
export const useField = (arg: FieldProp) => { export const useField = (arg: FieldProp) => {
const field = useLocal<FieldInternal>({ const field = useLocal<FieldInternal<typeof arg.type>>({
status: "init", status: "init",
Child: () => { Child: () => {
return <arg.PassProp>{arg.child}</arg.PassProp>; return <arg.PassProp>{arg.child}</arg.PassProp>;
}, },
input: {},
} as any); } as any);
const update_field = { const update_field = {

View File

@ -1,4 +1,5 @@
export { FieldTypeText } from "./comps/form/field/type/TypeText"; export { FieldTypeText } from "./comps/form/field/type/TypeText";
export { FieldTypeRelation } from "./comps/form/field/type/TypeRelation";
export { Form } from "@/comps/form/Form"; export { Form } from "@/comps/form/Form";
export { Field } from "@/comps/form/field/Field"; export { Field } from "@/comps/form/field/Field";
export { formType } from "@/comps/form/typings"; export { formType } from "@/comps/form/typings";

View File

@ -1,9 +1,8 @@
import get from "lodash.get"; import { codeBuild } from "../master_detail/utils";
import { GFCol as Col, GFCol, formatName, parseGenField } from "../utils"; import { GFCol, parseGenField } from "../utils";
import { NewFieldArg, newField } from "./new_field"; import { newField } from "./new_field";
import { on_load } from "./on_load"; import { on_load } from "./on_load";
import { on_submit } from "./on_submit"; import { on_submit } from "./on_submit";
import { codeBuild } from "../master_detail/utils";
export const gen_form = async (modify: (data: any) => void, data: any) => { export const gen_form = async (modify: (data: any) => void, data: any) => {
const table = JSON.parse(data.gen_table.value); const table = JSON.parse(data.gen_table.value);
@ -57,7 +56,7 @@ export const gen_form = async (modify: (data: any) => void, data: any) => {
} }
result["body"] = data["body"]; result["body"] = data["body"];
result.body.content.childs = fields.map(newField); result.body.content.childs = fields.filter((e) => !e.is_pk).map(newField);
} }
modify(result); modify(result);
}; };

View File

@ -29,23 +29,43 @@ export type NewFieldArg = {
}; };
export const newField = (arg: GFCol) => { export const newField = (arg: GFCol) => {
const childs = [];
let type = "text";
if (["int", "string"].includes(arg.type)) {
childs.push(
createItem({
component: {
id: "ca7ac237-8f22-4492-bb9d-4b715b1f5c25",
props: {
type: arg.type === "int" ? "number" : "text",
},
},
})
);
} else if (["has-many", "has-one"].includes(arg.type)) {
type = "relation";
childs.push(
createItem({
component: {
id: "69263ca0-61a1-4899-ad5f-059ac12b94d1",
props: {
type: arg.type,
},
},
})
);
}
const item = createItem({ const item = createItem({
component: { component: {
id: "32550d01-42a3-4b15-a04a-2c2d5c3c8e67", id: "32550d01-42a3-4b15-a04a-2c2d5c3c8e67",
props: { props: {
name: arg.name, name: arg.name,
label: formatName(arg.name), label: formatName(arg.name),
type,
child: { child: {
childs: [ childs,
createItem({
component: {
id: "ca7ac237-8f22-4492-bb9d-4b715b1f5c25",
props: {
type: "text",
},
},
}),
],
}, },
}, },
}, },