remove form old
This commit is contained in:
parent
38ea3224d2
commit
7f6f5106c7
|
|
@ -1,16 +0,0 @@
|
|||
import { FC } from "react";
|
||||
|
||||
export const Date: FC<{
|
||||
on_select: (val: any) => void;
|
||||
}> = ({ on_select }) => {
|
||||
return (
|
||||
<input
|
||||
id="date"
|
||||
className="c-w-full c-flex c-justify-center c-border c-px-3 c-py-2 c-rounded-lg c-cursor-pointer"
|
||||
type="date"
|
||||
onChange={(event) => {
|
||||
on_select(event.target.value);
|
||||
}}
|
||||
></input>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
import { FC } from "react";
|
||||
|
||||
export const Datetime: FC<{
|
||||
on_select: (val: any) => void;
|
||||
}> = ({ on_select }) => {
|
||||
return (
|
||||
<input
|
||||
id="datetime"
|
||||
className="c-w-full c-flex c-justify-center c-border c-px-3 c-py-2 c-rounded-lg c-cursor-pointer"
|
||||
type="datetime-local"
|
||||
onChange={(event) => {
|
||||
on_select(event.target.value);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,174 +0,0 @@
|
|||
import { Popover } from "@/comps/custom/Popover";
|
||||
import { Input } from "@/comps/ui/input";
|
||||
import { Skeleton } from "@/comps/ui/skeleton";
|
||||
import { useLocal } from "@/utils/use-local";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import { FC, useEffect } from "react";
|
||||
import { FieldListItem, FieldOptions } from "../type";
|
||||
import { FormHook } from "../utils/utils";
|
||||
|
||||
type DropdownProps = {
|
||||
value: string;
|
||||
options: FieldOptions;
|
||||
form?: FormHook;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export const Dropdown: FC<DropdownProps> = ({ value, options, form, name }) => {
|
||||
const local = useLocal({
|
||||
status: "loading" as "loading" | "ready",
|
||||
open: false,
|
||||
ref: { input: null as null | HTMLInputElement },
|
||||
list: [] as FieldListItem[],
|
||||
input: "",
|
||||
label: "",
|
||||
filter: "",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (form) {
|
||||
local.status = "loading";
|
||||
local.render();
|
||||
const callback = (result: any[]) => {
|
||||
local.list = result.map((e) => {
|
||||
if (typeof e === "string") {
|
||||
return {
|
||||
value: e,
|
||||
label: e,
|
||||
};
|
||||
}
|
||||
return e;
|
||||
});
|
||||
|
||||
const found = local.list.find((e) => e.value === value);
|
||||
if (found) {
|
||||
local.label = found.label;
|
||||
}
|
||||
|
||||
local.status = "ready";
|
||||
local.render();
|
||||
};
|
||||
|
||||
const res = options({ data: form.hook.getValues(), current_name: name });
|
||||
if (res instanceof Promise) {
|
||||
res.then(callback);
|
||||
} else {
|
||||
callback(res);
|
||||
}
|
||||
}
|
||||
}, [options]);
|
||||
|
||||
let filtered = local.list;
|
||||
|
||||
if (local.filter) {
|
||||
filtered = local.list.filter((e) => {
|
||||
if (e.label.toLowerCase().includes(local.filter)) return true;
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
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.ref.input?.clientWidth || 100}px;
|
||||
`
|
||||
)}
|
||||
>
|
||||
{local.status === "loading" && (
|
||||
<>
|
||||
<div className="c-flex c-flex-col c-space-y-1 c-px-3 c-py-2">
|
||||
<Skeleton className="c-h-[10px] c-w-[90px]" />
|
||||
<Skeleton className="c-h-[10px] c-w-[60px]" />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{local.status === "ready" && (
|
||||
<>
|
||||
{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 === local.list.length - 1 && "c-rounded-b-sm"
|
||||
)}
|
||||
onClick={() => {
|
||||
if (form) {
|
||||
local.open = false;
|
||||
form.hook.setValue(name, item.value);
|
||||
form.render();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={cx(
|
||||
"c-relative",
|
||||
css`
|
||||
cursor: pointer !important;
|
||||
`
|
||||
)}
|
||||
tabIndex={0}
|
||||
onFocus={() => {
|
||||
local.open = true;
|
||||
local.input = local.label;
|
||||
local.filter = "";
|
||||
local.render();
|
||||
setTimeout(() => {
|
||||
local.ref.input?.focus();
|
||||
});
|
||||
}}
|
||||
>
|
||||
<div className="c-absolute c-pointer-events-none c-inset-0 c-left-auto c-flex c-items-center c-pr-4">
|
||||
<ChevronDown size={14} />
|
||||
</div>
|
||||
<Input
|
||||
spellCheck={false}
|
||||
value={local.open ? local.input : ""}
|
||||
className={cx(
|
||||
local.open ? "c-cursor-pointer" : "c-pointer-events-none"
|
||||
)}
|
||||
onChange={(e) => {
|
||||
local.input = e.currentTarget.value;
|
||||
local.filter = local.input.toLowerCase();
|
||||
local.render();
|
||||
}}
|
||||
ref={(el) => {
|
||||
local.ref.input = el;
|
||||
}}
|
||||
type="text"
|
||||
/>
|
||||
{!local.open && (
|
||||
<div className="c-absolute c-text-sm c-inset-0 c-px-3 c-flex c-items-center">
|
||||
{local.label}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,232 +0,0 @@
|
|||
import { Popover } from "@/comps/custom/Popover";
|
||||
import { Input } from "@/comps/ui/input";
|
||||
import { Skeleton } from "@/comps/ui/skeleton";
|
||||
import { useLocal } from "@/utils/use-local";
|
||||
import { ChevronDown, Loader2 } from "lucide-react";
|
||||
import { FC, useEffect } from "react";
|
||||
import { FieldListItem, FieldOptions } from "../type";
|
||||
import { FormHook } from "../utils/utils";
|
||||
|
||||
type RelationProps = {
|
||||
value: string;
|
||||
relation: {
|
||||
table: string;
|
||||
fields: string[];
|
||||
query: () => Promise<any>;
|
||||
};
|
||||
form?: FormHook;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export const Relation: FC<RelationProps> = ({
|
||||
relation,
|
||||
value,
|
||||
form,
|
||||
name,
|
||||
}) => {
|
||||
const local = useLocal({
|
||||
status: "init" as "init" | "loading" | "ready",
|
||||
open: false,
|
||||
ref: { input: null as null | HTMLInputElement },
|
||||
list: [] as FieldListItem[],
|
||||
input: "",
|
||||
label: "",
|
||||
filter: "",
|
||||
pk_field: "",
|
||||
timeout: null as any,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
clearTimeout(local.timeout);
|
||||
local.timeout = setTimeout(async () => {
|
||||
if (form) {
|
||||
local.status = "loading";
|
||||
local.render();
|
||||
|
||||
if (form.cache[name]) {
|
||||
local.pk_field = form.cache[name].pk_field;
|
||||
local.list = form.cache[name].list;
|
||||
} else {
|
||||
const table_fn = (db as any)[relation.table];
|
||||
const select = {} as any;
|
||||
local.pk_field = "";
|
||||
for (const f of relation.fields) {
|
||||
if (typeof f === "string") {
|
||||
if (f.startsWith("::")) {
|
||||
select[f.substring(2)] = true;
|
||||
local.pk_field = f.substring(2);
|
||||
} else {
|
||||
select[f] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
let q = {};
|
||||
|
||||
if (typeof relation.query === "function") {
|
||||
q = await relation.query();
|
||||
}
|
||||
|
||||
const list = await table_fn.findMany({ select, ...q });
|
||||
if (Array.isArray(list)) {
|
||||
local.list = list.map((item: any) => {
|
||||
let label = [];
|
||||
for (const [k, v] of Object.entries(item)) {
|
||||
if (k !== local.pk_field) label.push(v);
|
||||
}
|
||||
return { value: item[local.pk_field], label: label.join(" - ") };
|
||||
});
|
||||
}
|
||||
form.cache[name] = { list: local.list, pk_field: local.pk_field };
|
||||
}
|
||||
|
||||
const found = local.list.find((e) => {
|
||||
if (typeof value === "object") {
|
||||
if (value["connect"]) {
|
||||
return e.value === value["connect"][local.pk_field];
|
||||
}
|
||||
return e.value === value[local.pk_field];
|
||||
} else {
|
||||
return e.value === value;
|
||||
}
|
||||
});
|
||||
if (found) {
|
||||
local.label = found.label;
|
||||
}
|
||||
|
||||
local.status = "ready";
|
||||
local.render();
|
||||
}
|
||||
}, 100);
|
||||
}, [relation, location.hash, location.pathname, value]);
|
||||
|
||||
let filtered = local.list;
|
||||
|
||||
if (local.filter) {
|
||||
filtered = local.list.filter((e) => {
|
||||
if (e.label.toLowerCase().includes(local.filter)) return true;
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
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 c-relative c-overflow-auto",
|
||||
css`
|
||||
width: ${local.ref.input?.clientWidth || 100}px;
|
||||
max-height: 300px;
|
||||
`
|
||||
)}
|
||||
>
|
||||
{local.status === "loading" && (
|
||||
<>
|
||||
<div className="c-flex c-flex-col c-space-y-1 c-px-3 c-py-2">
|
||||
<Skeleton className="c-h-[10px] c-w-[90px]" />
|
||||
<Skeleton className="c-h-[10px] c-w-[60px]" />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{local.status === "ready" && (
|
||||
<div className="c-flex c-flex-1 c-flex-col">
|
||||
{filtered.map((item, idx) => {
|
||||
let is_active = false;
|
||||
if (typeof value === "object") {
|
||||
const c = (value as any)?.connect;
|
||||
if (c) {
|
||||
is_active = item.value === c[local.pk_field];
|
||||
}
|
||||
} else {
|
||||
is_active = item.value === value;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
tabIndex={0}
|
||||
key={item.value + "_" + idx}
|
||||
className={cx(
|
||||
"c-px-3 c-py-1 cursor-pointer option-item",
|
||||
is_active
|
||||
? "c-bg-blue-600 c-text-white"
|
||||
: "hover:c-bg-blue-50",
|
||||
idx > 0 && "c-border-t",
|
||||
idx === 0 && "c-rounded-t-sm",
|
||||
idx === local.list.length - 1 && "c-rounded-b-sm"
|
||||
)}
|
||||
onClick={() => {
|
||||
if (form) {
|
||||
local.open = false;
|
||||
form.hook.setValue(name, {
|
||||
connect: { [local.pk_field]: item.value },
|
||||
});
|
||||
form.render();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{item.label || "-"}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={cx(
|
||||
"c-relative",
|
||||
css`
|
||||
cursor: pointer !important;
|
||||
`
|
||||
)}
|
||||
tabIndex={0}
|
||||
onFocus={() => {
|
||||
local.open = true;
|
||||
local.input = local.label;
|
||||
local.filter = "";
|
||||
local.render();
|
||||
setTimeout(() => {
|
||||
local.ref.input?.focus();
|
||||
});
|
||||
}}
|
||||
>
|
||||
<div className="c-absolute c-pointer-events-none c-inset-0 c-left-auto c-flex c-items-center c-pr-4">
|
||||
<ChevronDown size={14} />
|
||||
</div>
|
||||
<Input
|
||||
spellCheck={false}
|
||||
value={local.open ? local.input : ""}
|
||||
className={cx(
|
||||
local.open ? "c-cursor-pointer" : "c-pointer-events-none"
|
||||
)}
|
||||
onChange={(e) => {
|
||||
local.input = e.currentTarget.value;
|
||||
local.filter = local.input.toLowerCase();
|
||||
local.render();
|
||||
}}
|
||||
ref={(el) => {
|
||||
local.ref.input = el;
|
||||
}}
|
||||
type="text"
|
||||
/>
|
||||
{!local.open && (
|
||||
<div className="c-absolute c-text-sm c-inset-0 c-px-3 c-flex c-items-center">
|
||||
{local.status !== "ready" ? (
|
||||
<Loader2 className="c-h-4 c-w-4 c-animate-spin" />
|
||||
) : (
|
||||
<> {local.label || "-"}</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,357 +0,0 @@
|
|||
import {
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/comps/ui/form";
|
||||
import { useLocal } from "@/utils/use-local";
|
||||
import autosize from "autosize";
|
||||
import { FC, ReactNode, useEffect, useRef } from "react";
|
||||
import { Input } from "../ui/input";
|
||||
import { Textarea } from "../ui/textarea";
|
||||
import { Date } from "./Date";
|
||||
import { Datetime } from "./Datetime";
|
||||
import { Dropdown } from "./Dropdown";
|
||||
import { Relation } from "./Dropdown/relation";
|
||||
import { InputMoney } from "./InputMoney";
|
||||
import { Radio } from "./Radio";
|
||||
import { SliderOptions } from "./Slider/types";
|
||||
import { FieldOptions } from "./type";
|
||||
import { FormHook, modify } from "./utils/utils";
|
||||
|
||||
export const Field: FC<{
|
||||
name: string;
|
||||
label: string;
|
||||
desc?: string;
|
||||
form?: FormHook;
|
||||
type:
|
||||
| "text"
|
||||
| "number"
|
||||
| "textarea"
|
||||
| "dropdown"
|
||||
| "relation"
|
||||
| "password"
|
||||
| "radio"
|
||||
| "date"
|
||||
| "datetime"
|
||||
| "money"
|
||||
| "slider"
|
||||
| "master-link"
|
||||
| "custom";
|
||||
required: "y" | "n";
|
||||
options: FieldOptions;
|
||||
slider: () => Promise<SliderOptions>;
|
||||
on_change: (arg: { value: any }) => void | Promise<void>;
|
||||
PassProp: any;
|
||||
custom: "y" | "n";
|
||||
child: any;
|
||||
selection: "single" | "multi";
|
||||
suffix: any;
|
||||
placeholder?: any;
|
||||
label_alt:
|
||||
| ReactNode
|
||||
| FC<{ modify: typeof modify; data: any; current_name: string }>;
|
||||
rel_table: string;
|
||||
rel_fields: string[];
|
||||
rel_query: () => any;
|
||||
}> = ({
|
||||
name,
|
||||
form,
|
||||
desc,
|
||||
label,
|
||||
type,
|
||||
required,
|
||||
options,
|
||||
slider,
|
||||
on_change,
|
||||
PassProp,
|
||||
custom,
|
||||
selection,
|
||||
suffix,
|
||||
child,
|
||||
placeholder,
|
||||
label_alt,
|
||||
rel_fields,
|
||||
rel_table,
|
||||
rel_query,
|
||||
}) => {
|
||||
const values = form?.hook.getValues();
|
||||
const value = values[name];
|
||||
const local = useLocal({
|
||||
date: {
|
||||
// label: "",
|
||||
popup: false,
|
||||
},
|
||||
slider: {
|
||||
value: 0,
|
||||
opt: {
|
||||
step: 1,
|
||||
min: { value: 0, label: "Start" },
|
||||
max: { value: 100, label: "End" },
|
||||
} as SliderOptions,
|
||||
status: "init" as "init" | "loading" | "ready",
|
||||
},
|
||||
modify: null as any,
|
||||
});
|
||||
const textAreaRef = useRef<any>();
|
||||
useEffect(() => {
|
||||
autosize(textAreaRef.current);
|
||||
return () => {
|
||||
autosize.destroy(textAreaRef.current);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (type === "slider") {
|
||||
local.slider.value = parseSliderValue(value, local.slider.opt);
|
||||
if (typeof slider === "function") {
|
||||
if (local.slider.status === "init") {
|
||||
local.slider.status = "ready";
|
||||
local.render();
|
||||
(async () => {
|
||||
const res = await slider();
|
||||
|
||||
local.slider.opt = res;
|
||||
local.render();
|
||||
})();
|
||||
}
|
||||
} else {
|
||||
local.slider.status = "ready";
|
||||
local.render();
|
||||
}
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
const inputKeyDown = (e: any) => {
|
||||
if (e.key === "Enter" && form?.ref) {
|
||||
form.submit();
|
||||
}
|
||||
};
|
||||
|
||||
const onChange = (e: any) => {
|
||||
form?.hook.setValue(name, e.currentTarget.value);
|
||||
form?.render();
|
||||
};
|
||||
|
||||
if (form) {
|
||||
form.label[name] = label;
|
||||
if (required === "y") {
|
||||
form.validation[name] = "required";
|
||||
|
||||
if (value) {
|
||||
delete form.hook.formState.errors[name];
|
||||
}
|
||||
} else {
|
||||
delete form.validation[name];
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormField
|
||||
control={form?.hook.control || ({} as any)}
|
||||
name={name}
|
||||
render={({ field }) => (
|
||||
<FormItem className="c-flex c-flex-1 c-flex-col">
|
||||
<FormLabel className="c-flex c-justify-between">
|
||||
<div className="c-flex c-items-center">
|
||||
{label}
|
||||
{required === "y" && (
|
||||
<h1 className="c-ml-1 c-text-red-500">*</h1>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
{typeof label_alt === "function" &&
|
||||
form &&
|
||||
label_alt({
|
||||
modify: local.modify
|
||||
? local.modify
|
||||
: modify.bind({
|
||||
form,
|
||||
}),
|
||||
current_name: name,
|
||||
data: form.hook.getValues(),
|
||||
})}
|
||||
{typeof label_alt !== "function" && label_alt}
|
||||
</div>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<>
|
||||
{type === "custom" && (
|
||||
<div
|
||||
className={cx(
|
||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
)}
|
||||
>
|
||||
{custom}
|
||||
</div>
|
||||
)}
|
||||
{type === "slider" && (
|
||||
<div className="c-flex-1 c-min-h-[40px] c-flex">
|
||||
<div className="c-flex c-flex-col c-items-center">
|
||||
<div>{local.slider.opt.min.value}</div>
|
||||
<div>{local.slider.opt.min.label}</div>
|
||||
</div>
|
||||
<div className="c-flex-1 c-flex-col c-items-stretch">
|
||||
<input
|
||||
type="range"
|
||||
className="c-flex-1 c-w-full"
|
||||
onInput={(e) => {
|
||||
const value = e.currentTarget.value;
|
||||
|
||||
local.slider.value = parseSliderValue(
|
||||
value,
|
||||
local.slider.opt
|
||||
);
|
||||
form?.hook.setValue(name, value);
|
||||
local.render();
|
||||
}}
|
||||
value={local.slider.value}
|
||||
min={local.slider.opt.min.value}
|
||||
max={local.slider.opt.max.value}
|
||||
/>
|
||||
<div className="c-w-full c-mx-auto c-text-center">
|
||||
{local.slider.value}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="c-flex c-flex-col c-items-center">
|
||||
<div>{local.slider.opt.max.value}</div>
|
||||
<div>{local.slider.opt.max.label}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{["text", "number", "password"].includes(type) &&
|
||||
(suffix !== "" ? (
|
||||
<div className="c-flex c-items-stretch">
|
||||
<Input
|
||||
{...field}
|
||||
type={type}
|
||||
spellCheck={false}
|
||||
className="c-flex-1 c-rounded-r-none focus:c-rounded-r-none c-pr-1 c-border-r-0"
|
||||
placeholder={placeholder}
|
||||
onChange={onChange}
|
||||
onKeyDown={inputKeyDown}
|
||||
></Input>
|
||||
<span className="c-p-[7px] c-rounded-r c-bg-[#D3D3D5]">
|
||||
{suffix || "-"}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<Input
|
||||
{...field}
|
||||
type={type}
|
||||
spellCheck={false}
|
||||
placeholder={placeholder}
|
||||
onKeyDown={inputKeyDown}
|
||||
onChange={onChange}
|
||||
/>
|
||||
))}
|
||||
|
||||
{type === "textarea" && (
|
||||
<Textarea
|
||||
{...field}
|
||||
ref={textAreaRef}
|
||||
spellCheck={false}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
{type === "dropdown" && (
|
||||
<Dropdown
|
||||
options={options}
|
||||
form={form}
|
||||
name={name}
|
||||
value={field.value}
|
||||
/>
|
||||
)}
|
||||
|
||||
{type === "relation" && (
|
||||
<Relation
|
||||
relation={{
|
||||
fields: rel_fields,
|
||||
table: rel_table,
|
||||
query: rel_query,
|
||||
}}
|
||||
form={form}
|
||||
name={name}
|
||||
value={field.value}
|
||||
/>
|
||||
)}
|
||||
|
||||
{type === "date" && (
|
||||
<Date
|
||||
on_select={(value: any) => {
|
||||
form?.hook.setValue(name, value);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{type === "datetime" && (
|
||||
<Datetime
|
||||
on_select={(value: any) => {
|
||||
form?.hook.setValue(name, value);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{type === "radio" && (
|
||||
<Radio
|
||||
name={name}
|
||||
options={options}
|
||||
PassProp={PassProp}
|
||||
child={child}
|
||||
value={field.value}
|
||||
custom={custom}
|
||||
form={form}
|
||||
selection={selection}
|
||||
on_select={(value: any) => {
|
||||
form?.hook.setValue(name, value);
|
||||
}}
|
||||
init_modify={(mod) => {
|
||||
local.modify = mod;
|
||||
local.render();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{type === "money" && (
|
||||
<InputMoney
|
||||
value={field.value}
|
||||
on_select={(value: any) => {
|
||||
form?.hook.setValue(name, value);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</FormControl>
|
||||
{/* <FormDescription>{desc}</FormDescription> */}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const parseSliderValue = (value: any, opt: SliderOptions) => {
|
||||
let val = value;
|
||||
if (typeof value !== "number") {
|
||||
try {
|
||||
val = parseInt(val);
|
||||
} catch (e) {
|
||||
val = opt.min.value;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof val !== "number" || isNaN(val)) {
|
||||
val = opt.min.value;
|
||||
}
|
||||
|
||||
if (val >= opt.max.value) return opt.max.value;
|
||||
else if (val <= opt.min.value) return opt.min.value;
|
||||
return val;
|
||||
};
|
||||
|
|
@ -1,286 +0,0 @@
|
|||
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} />;
|
||||
};
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
import { FC } from "react";
|
||||
import { Input } from "../../ui/input";
|
||||
import { useLocal } from "@/utils/use-local";
|
||||
|
||||
export const InputMoney: FC<{
|
||||
value: string;
|
||||
on_select: (val: any) => void;
|
||||
}> = ({ value, on_select }) => {
|
||||
const local = useLocal({
|
||||
numberWithComma: "",
|
||||
number: 0,
|
||||
});
|
||||
|
||||
const removeNonNumeric = (num: any) => {
|
||||
// replace non numeric
|
||||
return num.replace(/[^0-9]/g, "");
|
||||
};
|
||||
|
||||
const handleChange = (event: any) => {
|
||||
const val = event.target.value;
|
||||
|
||||
local.number = parseInt(val.replace(/\W/g, ""));
|
||||
local.numberWithComma = formatMoney(removeNonNumeric(val));
|
||||
local.render();
|
||||
|
||||
on_select(local.number);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="c-relative">
|
||||
<Input
|
||||
type="text"
|
||||
className="c-pl-10"
|
||||
value={local.numberWithComma}
|
||||
onChange={(event) => {
|
||||
handleChange(event);
|
||||
}}
|
||||
/>
|
||||
<span className="c-absolute c-top-1/2 c-left-4 c-transform -c-translate-y-1/2 absolute top-1/2 left-1/2 c-text-base">
|
||||
Rp.
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const formatMoney = (num: any) => {
|
||||
if (!!num) {
|
||||
let str = num;
|
||||
if (typeof num === "number") str = num.toString();
|
||||
if (typeof str === "string")
|
||||
return str.replace(/\B(?=(\d{3})+(?!\d))/g, ".");
|
||||
}
|
||||
return "-";
|
||||
};
|
||||
|
|
@ -1,135 +0,0 @@
|
|||
import { useLocal } from "@/utils/use-local";
|
||||
import { FC, useEffect } from "react";
|
||||
import { Button } from "../../ui/button";
|
||||
import { FormHook, modify } from "../utils/utils";
|
||||
import { FieldOptions } from "../type";
|
||||
|
||||
export const Radio: FC<{
|
||||
name: string;
|
||||
on_select: (val: any) => void;
|
||||
options: FieldOptions;
|
||||
value: string | string[];
|
||||
PassProp: any;
|
||||
custom: "y" | "n";
|
||||
child: any;
|
||||
form?: FormHook;
|
||||
selection: "single" | "multi";
|
||||
init_modify: (modify: any) => void;
|
||||
}> = ({
|
||||
options,
|
||||
on_select,
|
||||
form,
|
||||
value,
|
||||
custom,
|
||||
child,
|
||||
PassProp,
|
||||
init_modify,
|
||||
selection,
|
||||
name,
|
||||
}) => {
|
||||
const local = useLocal({
|
||||
list: [] as { value: string; label: string }[],
|
||||
status: "init" as "init" | "loading" | "ready",
|
||||
mod: null as any,
|
||||
option_modified: false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!local.option_modified && form) {
|
||||
local.status = "loading";
|
||||
local.render();
|
||||
const callback = (result: any[]) => {
|
||||
local.list = result.map((e) => {
|
||||
if (typeof e === "string") {
|
||||
return {
|
||||
value: e,
|
||||
label: e,
|
||||
};
|
||||
}
|
||||
return e;
|
||||
});
|
||||
|
||||
local.status = "ready";
|
||||
local.render();
|
||||
};
|
||||
|
||||
const res = options({ data: form.hook.getValues(), current_name: name });
|
||||
if (res instanceof Promise) {
|
||||
res.then(callback);
|
||||
} else {
|
||||
callback(res);
|
||||
}
|
||||
}
|
||||
}, [options]);
|
||||
|
||||
if (form) {
|
||||
if (!local.mod) {
|
||||
local.mod = modify.bind({
|
||||
form,
|
||||
change_hook(opt) {
|
||||
const result = opt.options;
|
||||
if (result) {
|
||||
local.option_modified = true;
|
||||
local.list = result.map((e) => {
|
||||
if (typeof e === "string") {
|
||||
return {
|
||||
value: e,
|
||||
label: e,
|
||||
};
|
||||
}
|
||||
return e;
|
||||
});
|
||||
}
|
||||
form.render();
|
||||
},
|
||||
});
|
||||
init_modify(local.mod);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="c-flex c-flex-1 c-flex-wrap">
|
||||
{!!local.list &&
|
||||
local.list
|
||||
.filter((e) => e)
|
||||
.map((item, index: number) => {
|
||||
if (custom === "y" && form)
|
||||
return (
|
||||
<PassProp
|
||||
data={form.hook.getValues()}
|
||||
modify={local.mod}
|
||||
is_active={item.value === value}
|
||||
option_item={item}
|
||||
current_name={name}
|
||||
item_click={() => {
|
||||
if (selection === "single") {
|
||||
local.mod(name, { value: item.value });
|
||||
local.render();
|
||||
} else if (selection === "multi") {
|
||||
local.mod(name, { value: item.value });
|
||||
local.render();
|
||||
} else {
|
||||
null;
|
||||
}
|
||||
}}
|
||||
>
|
||||
{child}
|
||||
</PassProp>
|
||||
);
|
||||
return (
|
||||
<Button
|
||||
key={index}
|
||||
onClick={() => {
|
||||
on_select(item.value);
|
||||
local.render();
|
||||
}}
|
||||
className={cx("c-mr-2")}
|
||||
variant={item.value === value ? "default" : "outline"}
|
||||
>
|
||||
<span>{item.label}</span>
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
export type SliderOptions = {
|
||||
step: number;
|
||||
min: { value: number; label: string };
|
||||
max: { value: number; label: string };
|
||||
};
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
export type FieldOptions = (opt: {
|
||||
data: any;
|
||||
current_name: string;
|
||||
where?: { values: string[] }
|
||||
}) => Promise<(string | FieldListItem)[]>
|
||||
|
||||
export type FieldListItem = { value: string; label: string }
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
import { UseFormReturn } from "react-hook-form";
|
||||
|
||||
type ModifyOpt = {
|
||||
value?: any;
|
||||
options?: (string | { value: any; label: string })[];
|
||||
};
|
||||
|
||||
export const modify = function (
|
||||
this: { form: FormHook; change_hook?: (opt: ModifyOpt) => void },
|
||||
field_name: string,
|
||||
opt: ModifyOpt
|
||||
) {
|
||||
const f = this.form;
|
||||
|
||||
const keys = Object.keys(opt);
|
||||
if (keys.includes("value")) {
|
||||
f.hook.setValue(field_name, opt.value);
|
||||
}
|
||||
if (this.change_hook) this.change_hook(opt);
|
||||
};
|
||||
|
||||
export type FormHook = {
|
||||
hook: UseFormReturn<any, any, undefined>;
|
||||
ref: HTMLFormElement;
|
||||
submit: any;
|
||||
label: Record<string, string>;
|
||||
cache: any;
|
||||
validation: Record<string, "required">;
|
||||
render: () => void;
|
||||
unload?: () => void;
|
||||
};
|
||||
Loading…
Reference in New Issue