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 { FC, Fragment, useEffect, useRef } from "react";
import { FMInternal, FMProps } from "./typings";
import { formReload } from "./utils/reload";
import { formInit } from "./utils/init";
import get from "lodash.get";
import { FC, useRef } from "react";
import { createPortal } from "react-dom";
import { Toaster } from "sonner";
import get from "lodash.get";
import { Field } from "./field/Field";
import { getProp } from "../../..";
import { FMInternal, FMProps } from "./typings";
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 }>;
@ -35,6 +33,7 @@ export const Form: FC<FMProps> = (props) => {
done: [],
},
},
field_def: {},
props: {} as any,
size: {
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 === "half" && "c-w-1/2",
w === "full" && "c-w-full",
w === "¾" && "c-w-3/4",
w === "½" && "c-w-1/2",
w === "⅓" && "c-w-1/3",
w === "¼" && "c-w-1/4",

View File

@ -19,7 +19,7 @@ export const FieldInput: FC<{
_item: any;
_meta: any;
_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 suffix = typeof field.suffix === "function" ? field.suffix() : null;
const errors = fm.error.get(field.name);
@ -31,26 +31,35 @@ export const FieldInput: FC<{
let found = null as any;
if (childs && childs.length > 0) {
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;
const item = createItem({
component: { id: "--", props: fieldMapping[field.type].props },
});
if (mp.props) {
const item = createItem({
component: {
id: "--",
props:
typeof mp.props === "function" ? mp.props(fm, field) : mp.props,
},
});
const props = found.component.props;
let should_update = false;
for (const [k, v] of Object.entries(item.component.props) as any) {
if (props[k] && props[k].valueBuilt === v.valueBuilt) {
continue;
} else {
props[k] = v;
should_update = true;
const props = found.component.props;
let should_update = false;
for (const [k, v] of Object.entries(item.component.props) as any) {
if (props[k] && props[k].valueBuilt === v.valueBuilt) {
continue;
} else {
if (field.prop && !field.prop[k]) {
props[k] = v;
should_update = true;
}
}
}
}
if (should_update) {
updateFieldMItem(_meta, found);
if (should_update) {
updateFieldMItem(_meta, found, _sync);
}
}
}
}
@ -58,14 +67,14 @@ export const FieldInput: FC<{
useEffect(() => {
if (isEditor && !found) {
genFieldMitem({ _meta, _item, field, fm });
genFieldMitem({ _meta, _item, _sync, field, fm });
}
}, []);
return (
<div
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"
? css`
border-color: transparent;

View File

@ -1,12 +1,29 @@
import { FieldProp } from "../typings";
import { FMLocal, FieldInternal, FieldLocal, FieldProp } from "../typings";
export const fieldMapping: Record<
FieldProp["type"],
{ id: string; props: any }
> = {
export const fieldMapping: {
[K in FieldProp["type"]]: {
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" } },
number: {
id: "ca7ac237-8f22-4492-bb9d-4b715b1f5c25",
props: { type: "number" },
relation: {
id: "69263ca0-61a1-4899-ad5f-059ac12b94d1",
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 { FMLocal, FieldLocal } from "../../typings";
import { useLocal } from "@/utils/use-local";
export type PropTypeText = {
type: "text" | "password" | "number";
};
export const FieldTypeText: FC<{
field: FieldLocal;
fm: FMLocal;
prop: {
type: "text" | "password" | "number";
};
prop: PropTypeText;
}> = ({ field, fm, prop }) => {
const input = useLocal({});
const value = fm.data[field.name];
field.input = input;
field.prop = prop;
return (
<input
type={prop.type}
@ -18,7 +25,7 @@ export const FieldTypeText: FC<{
}}
value={value || ""}
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}
onFocus={() => {
field.focused = true;

View File

@ -1,8 +1,10 @@
import { GFCol } from "@/gen/utils";
import { ReactNode } from "react";
import { SliderOptions } from "../form-old/Slider/types";
import { FieldOptions } from "../form-old/type";
import { FormHook } from "../form-old/utils/utils";
import { editorFormData } from "./utils/ed-data";
import { PropTypeText } from "./field/type/TypeText";
import { PropTypeRelation } from "./field/type/TypeRelation";
export type FMProps = {
on_init: (arg: { fm: FMLocal; submit: any; reload: any }) => any;
@ -18,6 +20,7 @@ export type FMProps = {
item: any;
label_mode: "vertical" | "horizontal" | "hidden";
label_width: number;
gen_fields: any;
};
export type FieldProp = {
@ -26,20 +29,18 @@ export type FieldProp = {
desc?: string;
props?: any;
fm: FMLocal;
type:
| "text"
| "number"
| "textarea"
| "dropdown"
| "relation"
| "password"
| "radio"
| "date"
| "datetime"
| "money"
| "slider"
| "master-link"
| "custom";
type: "text" | "relation";
// | "number"
// | "textarea"
// | "dropdown"
// | "password"
// | "radio"
// | "date"
// | "datetime"
// | "money"
// | "slider"
// | "master-link"
// | "custom";
required: "y" | "n";
required_msg: (name: string) => string;
options: FieldOptions;
@ -50,7 +51,7 @@ export type FieldProp = {
selection: "single" | "multi";
prefix: any;
suffix: any;
width: "auto" | "full" | "½" | "⅓" | "¼";
width: "auto" | "full" | "¾" | "½" | "⅓" | "¼";
_meta: any;
_item: any;
_sync: any;
@ -65,6 +66,7 @@ export type FMInternal = {
on_change: (name: string, new_value: any) => void;
};
fields: Record<string, FieldLocal>;
field_def: Record<string, GFCol>;
error: {
readonly list: { name: string; error: string[] }[];
set: (name: string, error: string[]) => void;
@ -87,10 +89,16 @@ export type FMInternal = {
};
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";
name: FieldProp["name"];
type: FieldProp["type"];
type: T;
label: FieldProp["label"];
desc: FieldProp["desc"];
prefix: FieldProp["prefix"];
@ -100,9 +108,16 @@ export type FieldInternal = {
focused: boolean;
disabled: boolean;
required_msg: FieldProp["required_msg"];
col?: GFCol;
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) => {
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 get from "lodash.get";
import { fieldMapping } from "../field/mapping";
import { createItem } from "@/gen/utils";
export const genFieldMitem = (arg: {
_meta: any;
_item: any;
_sync: any;
fm: FMLocal;
field: FieldLocal;
}) => {
const { _meta, _item, fm, field } = arg;
const { _meta, _item, _sync, fm, field } = arg;
const m = _meta[_item.id];
if (m) {
const mitem = m.mitem;
@ -18,16 +23,27 @@ export const genFieldMitem = (arg: {
?.get("content")
?.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];
if (m) {
const mitem = m.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 { toast } from "sonner";
import { FMLocal, FMProps } from "../typings";
import { formError } from "./error";
import { editorFormData } from "./ed-data";
import get from "lodash.get";
import { formError } from "./error";
export const formInit = (fm: FMLocal, props: FMProps) => {
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;
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.status = isEditor ? "ready" : "loading";
fm.render();

View File

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

View File

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

View File

@ -1,9 +1,8 @@
import get from "lodash.get";
import { GFCol as Col, GFCol, formatName, parseGenField } from "../utils";
import { NewFieldArg, newField } from "./new_field";
import { codeBuild } from "../master_detail/utils";
import { GFCol, parseGenField } from "../utils";
import { newField } from "./new_field";
import { on_load } from "./on_load";
import { on_submit } from "./on_submit";
import { codeBuild } from "../master_detail/utils";
export const gen_form = async (modify: (data: any) => void, data: any) => {
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.content.childs = fields.map(newField);
result.body.content.childs = fields.filter((e) => !e.is_pk).map(newField);
}
modify(result);
};

View File

@ -29,23 +29,43 @@ export type NewFieldArg = {
};
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({
component: {
id: "32550d01-42a3-4b15-a04a-2c2d5c3c8e67",
props: {
name: arg.name,
label: formatName(arg.name),
type,
child: {
childs: [
createItem({
component: {
id: "ca7ac237-8f22-4492-bb9d-4b715b1f5c25",
props: {
type: "text",
},
},
}),
],
childs,
},
},
},