This commit is contained in:
rizrmd 2024-06-02 18:16:11 -07:00
parent c7965e81b8
commit 3d470e3f3e
30 changed files with 776 additions and 658 deletions

View File

@ -13,7 +13,6 @@ const editorFormWidth = {} as Record<string, { w: number; f: any }>;
export { FMLocal } from "./typings"; export { FMLocal } from "./typings";
export const Form: FC<FMProps> = (props) => { export const Form: FC<FMProps> = (props) => {
const { PassProp, body } = props; const { PassProp, body } = props;
const fm = useLocal<FMInternal>({ const fm = useLocal<FMInternal>({
data: editorFormData[props.item.id] data: editorFormData[props.item.id]
@ -113,7 +112,7 @@ export const Form: FC<FMProps> = (props) => {
document.body.appendChild(elemDiv); document.body.appendChild(elemDiv);
} }
const toaster_el = document.getElementsByClassName("prasi-toaster")[0]; const toaster_el = document.getElementsByClassName("prasi-toaster")[0];
if (fm.status === "resizing") { if (fm.status === "resizing") {
setTimeout(() => { setTimeout(() => {
fm.status = "ready"; fm.status = "ready";
@ -121,6 +120,7 @@ export const Form: FC<FMProps> = (props) => {
}, 100); }, 100);
return null; return null;
} }
return ( return (
<form <form
onSubmit={(e) => { onSubmit={(e) => {

View File

@ -1,7 +1,7 @@
import { ReactNode } from "react"; import { ReactNode } from "react";
import { FMLocal, FieldLocal, FieldProp } from "../typings"; import { FMLocal, FieldLocal, FieldProp } from "../typings";
import { Label } from "../field/Label"; import { Label } from "../field/Label";
import { FieldLoading } from "../field/raw/FieldLoading"; import { FieldLoading } from "../../ui/field-loading";
export const BaseField = (prop: { export const BaseField = (prop: {
field: FieldLocal; field: FieldLocal;

View File

@ -1,6 +1,6 @@
import { FC, isValidElement } from "react"; import { FC, isValidElement } from "react";
import { FMLocal, FieldLocal, FieldProp } from "../typings"; import { FMLocal, FieldLocal, FieldProp } from "../typings";
import { FieldLoading } from "./raw/FieldLoading"; import { FieldLoading } from "../../ui/field-loading";
import { MultiOption } from "./type/TypeMultiOption"; import { MultiOption } from "./type/TypeMultiOption";
import { SingleOption } from "./type/TypeSingleOption"; import { SingleOption } from "./type/TypeSingleOption";
import { FieldTypeText, PropTypeText } from "./type/TypeText"; import { FieldTypeText, PropTypeText } from "./type/TypeText";

View File

@ -1,172 +0,0 @@
import { Popover } from "@/comps/custom/Popover";
import { useLocal } from "@/utils/use-local";
import { ChevronDown } from "lucide-react";
import { FC, ReactNode } from "react";
export type OptionItem = { value: any; label: string; el?: ReactNode };
export const RawDropdown: FC<{
options: OptionItem[];
className?: string;
value: string;
onFocus?: () => void;
onBlur?: () => void;
onChange?: (value: string) => void;
disabled?: boolean;
}> = ({ value, options, className, onFocus, onBlur, onChange, disabled }) => {
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: any) => {
if (typeof e === "string") {
if (e.toLowerCase().includes(local.filter)) {
return true;
}
return false;
}
if (
typeof e.label === "string" &&
e.label.toLowerCase().includes(local.filter)
)
return true;
return false;
});
}
local.selected = options.find((e) => e.value === value);
if (disabled || isEditor) {
local.open = false;
}
return (
<Popover
open={disabled ? false : 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={() => {
local.open = false;
local.render();
if (onChange) onChange(item.value);
}}
>
{item.el || 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();
local.input.el?.select();
});
}}
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">
{!isEditor && (
<input
spellCheck={false}
value={local.open ? local.input.value : ""}
className={cx(
"c-absolute c-inset-0 c-w-full c-h-full c-outline-none c-p-0",
disabled
? "c-invisible"
: 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",
disabled && "c-hidden"
)}
>
<ChevronDown size={14} />
</div>
</div>
</Popover>
);
};

View File

@ -10,21 +10,22 @@ export const FieldButton: FC<{
}> = ({ field, fm, arg }) => { }> = ({ field, fm, arg }) => {
const local = useLocal({ const local = useLocal({
list: [] as any[], list: [] as any[],
value: [] as any[],
}); });
useEffect(() => { useEffect(() => {
const callback = (res: any[]) => { const callback = (res: any[]) => {
local.list = res; local.list = res;
if (Array.isArray(res)) {
local.value = res.map((e) => get(e, arg.pk));
}
local.render(); local.render();
}; };
const res = arg.on_load(); const res = arg.on_load();
if (res instanceof Promise) res.then(callback); if (res instanceof Promise) res.then(callback);
else callback(res); else callback(res);
}, []); }, []);
let value: any = fm.data[field.name]; let value = arg.opt_get_value({
fm,
name: field.name,
options: local.list,
type: field.type,
});
if (arg.type === "multi-option") { if (arg.type === "multi-option") {
value = fm.data[field.name] || []; value = fm.data[field.name] || [];
@ -42,32 +43,45 @@ export const FieldButton: FC<{
{local.list.map((item) => { {local.list.map((item) => {
let isChecked = false; let isChecked = false;
try { try {
isChecked = value.some((e: any) => e[arg.pk] === item[arg.pk]); isChecked = value.some((e: any) => e === item[arg.pk]);
} catch (ex) {} } catch (ex) {}
return ( return (
<div <div
onClick={() => { onClick={() => {
if (!Array.isArray(fm.data[field.name])) let selected = Array.isArray(value)
fm.data[field.name] = []; ? value.map((row) => {
return local.list.find((e) => e.value === row);
})
: [];
if (isChecked) { if (isChecked) {
fm.data[field.name] = fm.data[field.name].filter( selected = selected.filter(
(e: any) => e[arg.pk] !== item[arg.pk] (e: any) => e.value !== item.value
); );
} else { } else {
fm.data[field.name].push(item); selected.push(item);
} }
fm.render();
arg.opt_set_value({
fm,
name: field.name,
selected: selected.map((e) => e.value),
options: local.list,
type: field.type,
});
}} }}
draggable="true" draggable="true"
role="button" role="button"
title="Hover chip" title="Hover chip"
className={cx( className={cx(
isChecked ? "c-bg-gray-200" : "c-border c-border-gray-500", isChecked
" c-text-gray-700 c-h-8 c-px-3 c-w-max c-flex c-gap-2 c-items-center c-rounded-full hover:c-bg-gray-300 hover:c-bg-opacity-75 " ? "c-border c-border-blue-500 c-bg-blue-500 c-text-white"
: "c-border c-border-gray-500 hover:c-bg-gray-300 ",
"c-text-sm c-text-gray-700 c-h-8 c-px-3 c-w-max c-flex c-gap-2 c-items-center c-rounded-full hover:c-bg-opacity-75 "
)} )}
> >
<span className="block text-sm font-medium"> <span className="block text-sm font-medium">
{arg.on_row(item)} {arg.opt_get_label(item)}
</span> </span>
</div> </div>
); );
@ -84,7 +98,7 @@ export const FieldButton: FC<{
<div className={cx("c-flex c-items-center c-w-full c-flex-row")}> <div className={cx("c-flex c-items-center c-w-full c-flex-row")}>
<div <div
className={cx( className={cx(
`c-grid c-grid-cols- c-flex-grow c-gap-2 c-rounded-md c-bg-gray-200 c-p-0.5`, `c-grid c-grid-cols- c-flex-grow c-gap-2 c-rounded-md c-bg-gray-200 c-p-1`,
css` css`
grid-template-columns: repeat( grid-template-columns: repeat(
${local.list.length}, ${local.list.length},
@ -99,15 +113,20 @@ export const FieldButton: FC<{
<div> <div>
<label <label
onClick={() => { onClick={() => {
fm.data[field.name] = get(e, arg.pk); arg.opt_set_value({
fm.render(); fm,
name: field.name,
selected: [e.value],
options: local.list,
type: field.type,
});
}} }}
className={cx( className={cx(
`${checked ? "c-bg-blue-500 c-text-white" : ""} `, `${checked ? "c-bg-blue-500 c-text-white" : ""} `,
"c-block c-cursor-pointer c-select-none c-rounded-md c-p-1 c-text-center peer-checked: peer-checked:c-font-bold" "c-text-sm c-block c-cursor-pointer c-select-none c-rounded-md c-p-1 c-text-center peer-checked: peer-checked:c-font-bold"
)} )}
> >
{arg.on_row(e)} {arg.opt_get_label(e)}
</label> </label>
</div> </div>
); );

View File

@ -10,22 +10,24 @@ export const FieldCheckbox: FC<{
}> = ({ field, fm, arg }) => { }> = ({ field, fm, arg }) => {
const local = useLocal({ const local = useLocal({
list: [] as any[], list: [] as any[],
value: [] as any[],
}); });
useEffect(() => { useEffect(() => {
const callback = (res: any[]) => { const callback = (res: any[]) => {
local.list = res; local.list = res;
if (Array.isArray(res)) {
local.value = res.map((e) => get(e, arg.pk));
}
local.render(); local.render();
}; };
const res = arg.on_load(); const res = arg.on_load();
if (res instanceof Promise) res.then(callback); if (res instanceof Promise) res.then(callback);
else callback(res); else callback(res);
}, []); }, []);
let value: any = Array.isArray(fm.data[field.name]) ?fm.data[field.name] : [];
console.log({value}) let value = arg.opt_get_value({
fm,
name: field.name,
options: local.list,
type: field.type,
});
return ( return (
<> <>
<div className={cx("c-flex c-items-center c-w-full c-flex-row")}> <div className={cx("c-flex c-items-center c-w-full c-flex-row")}>
@ -33,25 +35,32 @@ export const FieldCheckbox: FC<{
{local.list.map((item) => { {local.list.map((item) => {
let isChecked = false; let isChecked = false;
try { try {
isChecked = value.some((e: any) => e[arg.pk] === item[arg.pk]); isChecked = value.some((e: any) => e === item[arg.pk]);
} catch (ex) {} } catch (ex) {}
console.log(item[arg.pk], isChecked)
return ( return (
<div <div
onClick={() => { onClick={() => {
console.log(item); let selected = Array.isArray(value)
if (!Array.isArray(fm.data[field.name])) ? value.map((row) => {
fm.data[field.name] = []; return local.list.find((e) => e.value === row);
console.log(isChecked); })
: [];
if (isChecked) { if (isChecked) {
fm.data[field.name] = fm.data[field.name].filter( selected = selected.filter(
(e: any) => e[arg.pk] !== item[arg.pk] (e: any) => e.value !== item.value
); );
} else { } else {
fm.data[field.name].push(item); selected.push(item);
} }
fm.render();
console.log({data: fm.data}) arg.opt_set_value({
fm,
name: field.name,
selected: selected.map((e) => e.value),
options: local.list,
type: field.type,
});
}} }}
className="c-flex c-flex-row c-space-x-1 cursor-pointer c-items-center rounded-full p-0.5" className="c-flex c-flex-row c-space-x-1 cursor-pointer c-items-center rounded-full p-0.5"
> >
@ -81,7 +90,7 @@ export const FieldCheckbox: FC<{
/> />
</svg> </svg>
)} )}
<div className="">{arg.on_row(item)}</div> <div className="">{arg.opt_get_label(item)}</div>
</div> </div>
); );
})} })}

View File

@ -1,96 +1,97 @@
import { FC, useEffect } from "react";
import { FMLocal, FieldLocal, FieldProp } from "../../typings";
import { useLocal } from "@/utils/use-local"; import { useLocal } from "@/utils/use-local";
import { OptionItem, RawDropdown } from "../raw/Dropdown"; import { FC, useEffect } from "react";
import { FieldLoading } from "../raw/FieldLoading"; import { Typeahead } from "../../../../..";
import get from "lodash.get"; import { FMLocal, FieldLocal, FieldProp } from "../../typings";
import { FieldLoading } from "lib/comps/ui/field-loading";
export const TypeDropdown: FC<{ export const TypeDropdown: FC<{
field: FieldLocal; field: FieldLocal;
fm: FMLocal; fm: FMLocal;
arg: FieldProp; arg: FieldProp;
}> = ({ field, fm, arg }) => { }> = ({ field, fm, arg }) => {
const input = useLocal({ const local = useLocal({
list: null as null | any[], loaded: false,
pk: "", options: [],
}); });
const value = fm.data[field.name]; let value = arg.opt_get_value({
field.input = input; fm,
name: field.name,
options: local.options,
type: field.type,
});
console.log({ value });
useEffect(() => { useEffect(() => {
if (!isEditor && input.list === null) { if (typeof arg.on_load === "function") {
field.status = "loading"; const options = arg.on_load({ mode: "query" });
input.pk = arg.pk; console.log("Masuk");
field.render(); // console.log(options)
const callback = (arg: any[]) => { if (options instanceof Promise) {
input.list = arg; options.then((res) => {
field.status = "ready"; console.log({ res });
input.render(); local.options = res;
}; local.loaded = true;
const res = arg.on_load(); local.render();
if (res instanceof Promise) res.then(callback); });
else callback(res); } else {
local.options = options;
local.render();
}
} }
}, []); }, []);
let list: OptionItem[] = []; if (!local.loaded) return <FieldLoading />;
if (input.list && input.list.length) {
input.list.map((e: any) => { if (field.type === "single-option")
let id = null; return (
try { <Typeahead
id = e[arg.pk]; value={value}
} catch (ex: any) { onSelect={({ search, item }) => {
console.error( if (item) {
"Error: PK Invalid atau tidak ditemukan, cek lagi keys yang ingin dijadikan value" arg.opt_set_value({
); fm,
} name: field.name,
list.push({ type: field.type,
value: id, options: local.options,
label: arg.on_row(e), selected: [item.value],
}); });
}); }
}
let selected = null; return item?.value || search;
if (value && typeof value === "object") { }}
if (input.pk) selected = value[input.pk]; allowNew={false}
} else { autoPopupWidth={true}
selected = value; focusOpen={true}
} mode={"single"}
placeholder={arg.placeholder}
options={() => {
return local.options;
}}
/>
);
return ( return (
<> <Typeahead
{field.status === "loading" ? ( value={value}
<FieldLoading /> onSelect={({ search, item }) => {
) : ( return item?.value || search;
<RawDropdown }}
options={list} onChange={(values) => {
value={selected} arg.opt_set_value({
onChange={(val) => { fm,
if (val === null) { name: field.name,
fm.data[field.name] = null; type: field.type,
fm.render(); options: local.options,
return; selected: values,
} });
if (input.list && input.pk) { }}
for (const item of input.list) { allowNew={false}
if (item[input.pk] === val) { autoPopupWidth={true}
fm.data[field.name] = val; focusOpen={true}
fm.render(); mode={"multi"}
break; placeholder={arg.placeholder}
} options={() => {
} return local.options;
} }}
}} />
className="c-flex-1 c-bg-transparent c-outline-none c-px-2 c-text-sm c-w-full c-h-full"
disabled={field.disabled}
onFocus={() => {
field.focused = true;
field.render();
}}
onBlur={() => {
field.focused = false;
field.render();
}}
/>
)}
</>
); );
}; };

View File

@ -1,12 +1,8 @@
import { FC, useEffect } from "react"; import { FC } from "react";
import { FMLocal, FieldLocal, FieldProp } from "../../typings"; import { FMLocal, FieldLocal, FieldProp } from "../../typings";
import { useLocal } from "@/utils/use-local";
import get from "lodash.get";
import { TypeDropdown } from "./TypeDropdown";
import { FieldToggle } from "./TypeToggle";
import { FieldButton } from "./TypeButton"; import { FieldButton } from "./TypeButton";
import { FieldRadio } from "./TypeRadio";
import { FieldCheckbox } from "./TypeCheckbox"; import { FieldCheckbox } from "./TypeCheckbox";
import { TypeDropdown } from "./TypeDropdown";
import { FieldTag } from "./TypeTag"; import { FieldTag } from "./TypeTag";
export const MultiOption: FC<{ export const MultiOption: FC<{
@ -16,7 +12,9 @@ export const MultiOption: FC<{
}> = ({ field, fm, arg }) => { }> = ({ field, fm, arg }) => {
return ( return (
<> <>
{arg.sub_type === "checkbox" ? ( {arg.sub_type === "typeahead" ? (
<TypeDropdown field={field} fm={fm} arg={arg}/>
):arg.sub_type === "checkbox" ? (
<FieldCheckbox field={field} fm={fm} arg={arg}/> <FieldCheckbox field={field} fm={fm} arg={arg}/>
): arg.sub_type === "button" ? ( ): arg.sub_type === "button" ? (
<FieldButton arg={arg} field={field} fm={fm} /> <FieldButton arg={arg} field={field} fm={fm} />

View File

@ -1,7 +1,7 @@
import { FC, useEffect } from "react";
import { FMLocal, FieldLocal, FieldProp } from "../../typings";
import { useLocal } from "@/utils/use-local"; import { useLocal } from "@/utils/use-local";
import get from "lodash.get"; import get from "lodash.get";
import { FC, useEffect } from "react";
import { FMLocal, FieldLocal, FieldProp } from "../../typings";
export const FieldRadio: FC<{ export const FieldRadio: FC<{
field: FieldLocal; field: FieldLocal;
@ -24,9 +24,13 @@ export const FieldRadio: FC<{
if (res instanceof Promise) res.then(callback); if (res instanceof Promise) res.then(callback);
else callback(res); else callback(res);
}, []); }, []);
let listValue = [];
let value: any = fm.data[field.name]; let value = arg.opt_get_value({
let checked = local.value.indexOf(value) > 0 ? true : false; fm,
name: field.name,
options: local.list,
type: field.type,
});
return ( return (
<> <>
<div className={cx("c-flex c-items-center c-w-full c-flex-row")}> <div className={cx("c-flex c-items-center c-w-full c-flex-row")}>
@ -36,38 +40,24 @@ export const FieldRadio: FC<{
<div <div
className="flex items-center mb-4" className="flex items-center mb-4"
onClick={() => { onClick={() => {
fm.data[field.name] = get(e, arg.pk); arg.opt_set_value({
fm.render(); fm,
name: field.name,
selected: [e.value],
options: local.list,
type: field.type,
});
}} }}
> >
<input <input
id="country-option-1"
type="radio" type="radio"
name="countries"
value="USA"
className="h-4 w-4 border-gray-300 focus:ring-2 focus:ring-blue-300" className="h-4 w-4 border-gray-300 focus:ring-2 focus:ring-blue-300"
aria-labelledby="country-option-1" aria-labelledby="country-option-1"
aria-describedby="country-option-1" aria-describedby="country-option-1"
checked={get(e, arg.pk) === value} checked={get(e, arg.pk) === value}
/> />
<label className="text-sm font-medium text-gray-900 ml-2 block"> <label className="text-sm font-medium text-gray-900 ml-2 block">
{arg.on_row(e)} {arg.opt_get_label(e)}
</label>
</div>
);
return (
<div>
<label
className={cx(
`${
get(e, arg.pk) === value
? "c-bg-blue-500 c-text-white"
: ""
} `,
"c-block c-cursor-pointer c-select-none c-rounded-md c-p-1 c-text-center peer-checked: peer-checked:c-font-bold"
)}
>
{arg.on_row(e)}
</label> </label>
</div> </div>
); );

View File

@ -3,7 +3,7 @@ import { useLocal } from "@/utils/use-local";
import { FC, useEffect } from "react"; import { FC, useEffect } from "react";
import { FMLocal, FieldLocal } from "../../typings"; import { FMLocal, FieldLocal } from "../../typings";
import { OptionItem, RawDropdown } from "../raw/Dropdown"; import { OptionItem, RawDropdown } from "../raw/Dropdown";
import { FieldLoading } from "../raw/FieldLoading"; import { FieldLoading } from "../../../ui/field-loading";
export type PropTypeRelation = { export type PropTypeRelation = {
type: "has-one" | "has-many"; type: "has-one" | "has-many";

View File

@ -1,6 +1,7 @@
import { useLocal } from "@/utils/use-local"; import { useLocal } from "@/utils/use-local";
import { FC } from "react"; import { FC } from "react";
import { FMLocal, FieldLocal, FieldProp } from "../../typings"; import { FMLocal, FieldLocal, FieldProp } from "../../typings";
import { Typeahead } from "../../../../..";
export const FieldTag: FC<{ export const FieldTag: FC<{
field: FieldLocal; field: FieldLocal;
@ -13,95 +14,20 @@ export const FieldTag: FC<{
value: null as any, value: null as any,
}); });
let value: any = fm.data[field.name]; let value: any = fm.data[field.name];
let tags: Array<string> = typeof value === "string" ? value.split(",") : [];
if(isEditor){
tags = ["sample","sample"]
}
return ( return (
<div className="c-flex-grow c-flex-row c-flex c-w-full c-h-full"> <Typeahead
<div value={value}
className={cx( onSelect={({ search, item }) => {
"c-px-2 c-flex c-flex-row c-items-center c-flex-wrap c-flex-grow c-gap-1 c-m-1" return item?.value || search;
)} }}
onClick={() => { allowNew
if (local.ref) { focusOpen={false}
local.ref.focus(); placeholder={arg.placeholder}
} options={async () => {
}} if (typeof arg.on_load === "function") return await arg.on_load();
> return [];
{tags.map((item) => { }}
return ( />
<div className="c-cursor-text c-flex-row c-flex c-items-center c-text-xs c-font-medium c-rounded c-border c-border-black">
<span className="c-flex-grow c-px-2.5 c-py-0.5">{item}</span>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
className="c-px-1 c-border-l c-border-black c-cursor-pointer "
viewBox="0 0 40 40"
onClick={() => {
// delete tag, pakai filter
let tag: Array<string> = tags.filter((e) => e !== item) || [];
// jadiin value string
let value = tags.join(",");
fm.data[field.name] = value;
fm.render();
}}
>
<path
fill="currentColor"
d="M21.499 19.994L32.755 8.727a1.064 1.064 0 0 0-.001-1.502c-.398-.396-1.099-.398-1.501.002L20 18.494L8.743 7.224c-.4-.395-1.101-.393-1.499.002a1.05 1.05 0 0 0-.309.751c0 .284.11.55.309.747L18.5 19.993L7.245 31.263a1.064 1.064 0 0 0 .003 1.503c.193.191.466.301.748.301h.006c.283-.001.556-.112.745-.305L20 21.495l11.257 11.27c.199.198.465.308.747.308a1.058 1.058 0 0 0 1.061-1.061c0-.283-.11-.55-.31-.747z"
/>
</svg>
</div>
);
})}
<input
ref={(el) => (local.ref = el)}
type={"text"}
value={local.value}
onClick={() => {}}
onChange={(ev) => {
local.value = ev.currentTarget.value;
local.render();
}}
onKeyDown={(event) => {
if (event.key === "Enter") {
event.preventDefault();
// detect string kosong
if (local.value !== "" && local.value) {
// jadiin array atau split
let tag: Array<string> = local.value.split(",") || [];
// filter tag dari value gk boleh sama
tag = tag.filter((e) => !tags.includes(e));
// concat
tags = tags.concat(tag);
// jadiin value string
let value = tags.join(",");
local.value = "";
local.render();
fm.data[field.name] = value;
fm.render();
}
event.preventDefault();
event.stopPropagation();
}
}}
disabled={field.disabled}
className={cx(
"c-flex-grow c-flex-1 c-items-center c-bg-transparent c-outline-none c-px-2 c-text-sm",
"c-max-w-full"
)}
spellCheck={false}
onFocus={() => {
console.log("focus?");
}}
onBlur={() => {
console.log("blur?");
}}
/>
</div>
</div>
); );
}; };

View File

@ -1,11 +1,7 @@
import { useLocal } from "@/utils/use-local";
import get from "lodash.get";
import { FC, useEffect } from "react"; import { FC, useEffect } from "react";
import { FMLocal, FieldLocal, FieldProp } from "../../typings"; import { FMLocal, FieldLocal, FieldProp } from "../../typings";
import { useLocal } from "@/utils/use-local";
import parser from "any-date-parser";
import { AutoHeightTextarea } from "@/comps/custom/AutoHeightTextarea";
import { M } from "src/data/unitShortcuts";
import { format } from "date-fns";
import get from "lodash.get";
export const FieldToggle: FC<{ export const FieldToggle: FC<{
field: FieldLocal; field: FieldLocal;
@ -14,13 +10,13 @@ export const FieldToggle: FC<{
}> = ({ field, fm, arg }) => { }> = ({ field, fm, arg }) => {
const local = useLocal({ const local = useLocal({
list: [] as any[], list: [] as any[],
value:[] as any[] value: [] as any[],
}); });
useEffect(() => { useEffect(() => {
const callback = (res: any[]) => { const callback = (res: any[]) => {
local.list = res; local.list = res;
if(Array.isArray(res)){ if (Array.isArray(res)) {
local.value = res.map((e) => get(e, arg.pk)) local.value = res.map((e) => get(e, arg.pk));
} }
local.render(); local.render();
}; };
@ -28,54 +24,71 @@ export const FieldToggle: FC<{
if (res instanceof Promise) res.then(callback); if (res instanceof Promise) res.then(callback);
else callback(res); else callback(res);
}, []); }, []);
let listValue = [] let value = arg.opt_get_value({
let value: any = fm.data[field.name]; fm,
let checked = local.value.indexOf(value) > 0 ? true: false; name: field.name,
options: local.list,
if(local.list.length < 2){ type: field.type,
return <>Minimum dan maksimal 2 Data</> });
} let checked = local.value.indexOf(value) > 0 ? true : false;
return ( return (
<> <>
<div <div className={cx("c-flex c-items-center c-justify-start c-w-full")}>
className={cx(
"c-flex c-items-center c-justify-start c-w-full"
)}
>
<label className="c-flex c-items-center c-cursor-pointer"> <label className="c-flex c-items-center c-cursor-pointer">
<div className="c-mr-3 c-text-gray-700 c-font-medium">{get(local,"list[0].label")}</div> <div className="c-mr-3 c-text-gray-700 c-font-medium">
{get(local, "list[0].label")}
</div>
<div <div
className={cx( className={cx(
"c-relative", "c-relative",
css` css`
input:checked ~ .dot { input:checked ~ .dot {
transform: translateX(100%); transform: translateX(100%);
background-color: #48bb78; }
input:checked ~ .dot-wrap {
background-color: #125ad6;
} }
` `
)} )}
> >
<input type="checkbox" id="toggleB" checked={checked} className="c-sr-only" onChange={(e) => { <input
const check = e.target.checked; type="checkbox"
id="toggleB"
if(check){ checked={checked}
fm.data[field.name] = local.value[1]; className="c-sr-only"
}else{ onChange={(e) => {
fm.data[field.name] = local.value[0]; const check = e.target.checked;
}
fm.render(); if (check) {
console.log({data: fm.data}) arg.opt_set_value({
// if(val) value = local.list[0]; fm,
// value = local.list[1] name: field.name,
}}/> selected: [local.list[1]?.value],
<div className="c-block c-bg-gray-600 c-w-8 c-h-5 c-rounded-full"></div> options: local.list,
type: field.type,
});
} else {
arg.opt_set_value({
fm,
name: field.name,
selected: [local.list[0]?.value],
options: local.list,
type: field.type,
});
}
}}
/>
<div className="dot-wrap c-block c-bg-gray-600 c-w-8 c-h-5 c-rounded-full"></div>
<div <div
className={cx( className={cx(
"dot c-absolute c-left-1 c-top-1 c-bg-white c-w-3 c-h-3 c-rounded-full c-transition" "dot c-absolute c-left-1 c-top-1 c-bg-white c-w-3 c-h-3 c-rounded-full c-transition"
)} )}
></div> ></div>
</div> </div>
<div className="c-ml-3 c-text-gray-700 c-font-medium">{get(local,"list[1].label")}</div> <div className="c-ml-3 c-text-gray-700 c-font-medium">
{get(local, "list[1].label")}
</div>
</label> </label>
</div> </div>
</> </>

View File

@ -1,6 +1,9 @@
import { createItem } from "lib/gen/utils"; import { generateSelect } from "lib/comps/md/gen/md-select";
import { on_load } from "lib/comps/md/gen/tbl-list/on_load";
import { createItem, parseGenField } from "lib/gen/utils";
import capitalize from "lodash.capitalize"; import capitalize from "lodash.capitalize";
import { ArrowBigDown } from "lucide-react"; import { ArrowBigDown } from "lucide-react";
import { on_load_rel } from "./on_load_rel";
export type GFCol = { export type GFCol = {
name: string; name: string;
type: string; type: string;
@ -16,7 +19,6 @@ export const newField = (
arg: GFCol, arg: GFCol,
opt: { parent_table: string; value: Array<string> } opt: { parent_table: string; value: Array<string> }
) => { ) => {
console.log({ arg, opt });
let type = "input"; let type = "input";
if (["int", "string", "text"].includes(arg.type)) { if (["int", "string", "text"].includes(arg.type)) {
if (["int"].includes(arg.type)) { if (["int"].includes(arg.type)) {
@ -67,6 +69,15 @@ export const newField = (
}); });
} else if (["has-many", "has-one"].includes(arg.type) && arg.relation) { } else if (["has-many", "has-one"].includes(arg.type) && arg.relation) {
if (["has-one"].includes(arg.type)) { if (["has-one"].includes(arg.type)) {
console.log(opt.value);
const fields = parseGenField(opt.value);
const res = generateSelect(fields);
const load = on_load_rel({
pk: res.pk,
table: arg.name,
select: res.select,
pks: {},
});
return createItem({ return createItem({
component: { component: {
id: "32550d01-42a3-4b15-a04a-2c2d5c3c8e67", id: "32550d01-42a3-4b15-a04a-2c2d5c3c8e67",
@ -76,15 +87,10 @@ export const newField = (
type: "single-option", type: "single-option",
sub_type: "dropdown", sub_type: "dropdown",
rel__gen_table: arg.name, rel__gen_table: arg.name,
rel__gen_fields: [`[${opt.value.join(",")}]`], // rel__gen_fields: [`[${opt.value.join(",")}]`],
opt__on_load: [ opt__on_load: [
`\ `\
() => { ${load}
console.log("halo");
return {
label: "halo", value: "value"
}
}
`, `,
], ],
child: { child: {
@ -127,6 +133,38 @@ export const newField = (
// }, // },
// }; // };
} else { } else {
const fields = parseGenField(opt.value);
const res = generateSelect(fields);
const load = on_load_rel({
pk: res.pk,
table: arg.name,
select: res.select,
pks: {},
});
console.log(load)
return createItem({
component: {
id: "32550d01-42a3-4b15-a04a-2c2d5c3c8e67",
props: {
name: arg.name,
label: formatName(arg.name),
type: "single-option",
sub_type: "dropdown",
rel__gen_table: arg.name,
// rel__gen_fields: [`[${opt.value.join(",")}]`],
opt__on_load: [
`\
${load}
`,
],
child: {
childs: [],
},
},
},
});
return createItem({ return createItem({
component: { component: {
id: "32550d01-42a3-4b15-a04a-2c2d5c3c8e67", id: "32550d01-42a3-4b15-a04a-2c2d5c3c8e67",

View File

@ -1,30 +1,30 @@
import { createItem, parseGenField } from "lib/gen/utils"; import { createItem, parseGenField } from "lib/gen/utils";
import get from "lodash.get"; import get from "lodash.get";
import { generateTableList } from "./gen-table-list"; import { newField } from "./fields";
import { generateSelect } from "./md-select"; import { generateSelect } from "../../md/gen/md-select";
import { on_load } from "./tbl-list/on_load"; import { on_load } from "../../md/gen/tbl-list/on_load";
import { on_submit } from "./tbl-list/on_submit"; import { on_submit } from "../../md/gen/tbl-list/on_submit";
import { newField } from "./form/fields";
import { createId } from "@paralleldrive/cuid2";
export const generateForm = async ( export const generateForm = async (
modify: (data: any) => void, modify: (data: any) => void,
data: any, data: {
gen__table: any;
gen__fields: any;
on_load: any;
on_submit: any;
body: any;
},
item: PrasiItem, item: PrasiItem,
commit: boolean commit: boolean
) => { ) => {
const table = JSON.parse(data.gen_table.value); const table = JSON.parse(data.gen__table.value);
console.log("halo"); const raw_fields = JSON.parse(data.gen__fields.value) as (
console.log(table);
const raw_fields = JSON.parse(data.gen_fields.value) as (
| string | string
| { value: string; checked: string[] } | { value: string; checked: string[] }
)[]; )[];
let pk = ""; let pk = "";
console.log({ raw_fields });
let pks: Record<string, string> = {}; let pks: Record<string, string> = {};
const fields = parseGenField(raw_fields); const fields = parseGenField(raw_fields);
// convert ke bahasa prisma untuk select
const res = generateSelect(fields); const res = generateSelect(fields);
pk = res.pk; pk = res.pk;
const select = res.select as any; const select = res.select as any;
@ -33,8 +33,8 @@ export const generateForm = async (
alert("Failed to generate! Primary Key not found. "); alert("Failed to generate! Primary Key not found. ");
return; return;
} }
console.log({ pk, table, select, pks })
if (pk) { if (pk) {
console.log("masuk");
if (data["on_load"]) { if (data["on_load"]) {
result.on_load = { result.on_load = {
mode: "raw", mode: "raw",
@ -49,18 +49,17 @@ export const generateForm = async (
} }
result.body = data["body"]; result.body = data["body"];
console.log({ fields, result });
const childs = []; const childs = [];
console.log({fields})
for (const item of fields.filter((e) => !e.is_pk)) { for (const item of fields.filter((e) => !e.is_pk)) {
let value = [] as Array<string>; let value = [] as Array<string>;
if(["has-one", "has-many"].includes(item.type)){ if (["has-one", "has-many"].includes(item.type)) {
value = get(item, "value.checked") as any; value = get(item, "value.checked") as any;
} }
const field = newField(item, { parent_table: table, value }); const field = newField(item, { parent_table: table, value });
childs.push(field); childs.push(field);
} }
if (commit) { if (commit) {
const body = item.edit.childs[0] as PrasiItem;
item.edit.setProp("body", { item.edit.setProp("body", {
mode: "jsx", mode: "jsx",
value: createItem({ value: createItem({
@ -68,9 +67,7 @@ export const generateForm = async (
}), }),
}); });
await item.edit.commit(); await item.edit.commit();
// console.log("done")
} else { } else {
} }
} }
}; };

10
comps/form/gen/gen-rel.ts Executable file
View File

@ -0,0 +1,10 @@
export const generateRelation = (
data: {
rel__gen_table: any;
rel__gen_field: any;
},
item: PrasiItem,
commit: boolean
) => {
console.log(data, item, commit);
};

77
comps/form/gen/on_load_rel.ts Executable file
View File

@ -0,0 +1,77 @@
export const on_load_rel = ({
pk,
table,
select,
pks,
}: {
pk: string;
table: string;
select: any;
pks: Record<string, string>;
}) => {
const sample = {} as any;
const cols = [];
for (const [k, v] of Object.entries(select) as any) {
if(k !== pk && typeof v !== "object"){
cols.push(k);
}
if (typeof v === "object") {
sample[k] = {};
Object.keys(v.select)
.filter((e) => e !== pks[k])
.map((e) => {
sample[k][e] = "sample";
});
} else {
sample[k] = "sample";
}
}
console.log({cols})
return `\
(arg: {
reload: () => Promise<void>;
orderBy?: Record<string, "asc" | "desc">;
paging: { take: number; skip: number };
mode: 'count' | 'query'
}) => {
if (isEditor) return [${JSON.stringify(sample)}];
return new Promise(async (done) => {
if (arg.mode === 'count') {
return await db.${table}.count();
}
const items = await db.${table}.findMany({
select: ${JSON.stringify(select)},
orderBy: arg.orderBy || {
${pk}: "desc"
},
...arg.paging,
});
if(items.length){
const cols = ${JSON.stringify(cols)};
const getLabel = (data: any) => {
const result = [];
cols.map((e) => {
if(data[e]){
result.push(data[e]);
}
})
return result.join(" - ");
}
done(items.map((e) => {
return {
value: e.${pk},
label: getLabel(e),
}
}))
} else {
done([])
}
})
}
`;
};

View File

@ -20,15 +20,17 @@ export type FMProps = {
on_load_deps?: any[]; on_load_deps?: any[];
}; };
export type GenField = { export type GenField =
name: string, | {
is_pk: boolean, name: string;
type: string, is_pk: boolean;
optional: boolean, type: string;
} | { optional: boolean;
checked: GenField[], }
value: GFCol | {
}; checked: GenField[];
value: GFCol;
};
type FieldType = type FieldType =
| "-" | "-"
@ -56,8 +58,26 @@ export type FieldProp = {
width: "auto" | "full" | "¾" | "½" | "⅓" | "¼"; width: "auto" | "full" | "¾" | "½" | "⅓" | "¼";
_item: PrasiItem; _item: PrasiItem;
custom?: () => CustomField; custom?: () => CustomField;
on_load: () => any | Promise<any>; on_load: (arg?: any) => any | Promise<any>;
on_row: (row: any) => string; opt_get_label: (row: any) => string;
opt_get_value: (arg: {
options: { label: string; value: string; item?: string }[];
fm: FMLocal;
name: string;
type: string;
}) => any;
opt_set_value: (arg: {
selected: string[];
options: { label: string; value: string; item?: string }[];
fm: FMLocal;
name: string;
type: string;
}) => any;
opt_selected: (arg: {
item: { value: string; label: string; item?: any };
current: any;
options: { value: string; label: string; item?: any }[];
}) => boolean;
pk: string; pk: string;
sub_type: string; sub_type: string;
placeholder: string; placeholder: string;
@ -120,6 +140,9 @@ export type FieldInternal<T extends FieldProp["type"]> = {
input: Record<string, any> & { input: Record<string, any> & {
render: () => void; render: () => void;
}; };
options: {
on_load?: () => Promise<{ value: string; label: string }[]>;
};
prop?: any; prop?: any;
}; };
export type FieldLocal = FieldInternal<any> & { export type FieldLocal = FieldInternal<any> & {

View File

@ -2,10 +2,12 @@ import { useLocal } from "@/utils/use-local";
import { useEffect } from "react"; import { useEffect } from "react";
import { FieldInternal, FieldProp } from "../typings"; import { FieldInternal, FieldProp } from "../typings";
export const useField = (arg: Omit<FieldProp, 'name' | 'label'> & { export const useField = (
name: string | (() => string); arg: Omit<FieldProp, "name" | "label"> & {
label: string | (() => string) name: string | (() => string);
}) => { label: string | (() => string);
}
) => {
const field = useLocal<FieldInternal<typeof arg.type>>({ const field = useLocal<FieldInternal<typeof arg.type>>({
status: "init", status: "init",
Child: () => { Child: () => {
@ -14,9 +16,10 @@ export const useField = (arg: Omit<FieldProp, 'name' | 'label'> & {
input: {}, input: {},
} as any); } as any);
const name = typeof arg.name === 'string' ? arg.name : arg.name(); const name = typeof arg.name === "string" ? arg.name : arg.name();
const label = typeof arg.label === 'string' ? arg.label : arg.label(); const label = typeof arg.label === "string" ? arg.label : arg.label();
const required = typeof arg.required === 'string' ? arg.required : arg.required(); const required =
typeof arg.required === "string" ? arg.required : arg.required();
const update_field = { const update_field = {
name: name.replace(/\s*/gi, ""), name: name.replace(/\s*/gi, ""),
@ -31,6 +34,7 @@ export const useField = (arg: Omit<FieldProp, 'name' | 'label'> & {
required_msg: arg.required_msg, required_msg: arg.required_msg,
disabled: arg.disabled === "y", disabled: arg.disabled === "y",
}; };
if (field.status === "init" || isEditor) { if (field.status === "init" || isEditor) {
for (const [k, v] of Object.entries(update_field)) { for (const [k, v] of Object.entries(update_field)) {
(field as any)[k] = v; (field as any)[k] = v;

View File

@ -1,10 +1,10 @@
import { set } from "lib/utils/set";
import capitalize from "lodash.capitalize"; import capitalize from "lodash.capitalize";
import { GFCol, createItem, parseGenField } from "../../../gen/utils";
import { generateSelect } from "./md-select";
import { on_load } from "./tbl-list/on_load";
import { modeTableList } from "./mode-table-list";
import get from "lodash.get"; import get from "lodash.get";
import set from "lodash.set"; import { createItem, parseGenField } from "../../../gen/utils";
import { generateSelect } from "./md-select";
import { modeTableList } from "./mode-table-list";
import { on_load } from "./tbl-list/on_load";
export const generateTableList = async ( export const generateTableList = async (
modify: (data: any) => void, modify: (data: any) => void,

102
comps/ui/typeahead-opt.tsx Executable file
View File

@ -0,0 +1,102 @@
import { FC } from "react";
import { Popover } from "../custom/Popover";
import { useLocal } from "lib/utils/use-local";
export type OptionItem = { value: string; label: string };
export const TypeaheadOptions: FC<{
popup?: boolean;
open?: boolean;
children: any;
onOpenChange?: (open: boolean) => void;
options: OptionItem[];
selected?: (arg: {
item: OptionItem;
options: OptionItem[];
idx: number;
}) => boolean;
onSelect?: (value: string) => void;
searching?: boolean;
width?: number;
}> = ({
popup,
children,
open,
onOpenChange,
options,
selected,
onSelect,
searching,
width,
}) => {
if (!popup) return children;
const local = useLocal({
selectedIdx: 0,
});
return (
<Popover
open={open}
arrow={false}
onOpenChange={onOpenChange}
backdrop={false}
placement="bottom-start"
className="c-flex-1"
content={
<div
className={cx(
width
? css`
min-width: ${width}px;
`
: css`
min-width: 150px;
`
)}
>
{options.map((item, idx) => {
const is_selected = selected?.({ item, options, idx });
if (is_selected) {
local.selectedIdx = idx;
}
return (
<div
tabIndex={0}
key={item.value + "_" + idx}
className={cx(
"c-px-3 c-py-1 cursor-pointer option-item text-sm",
is_selected
? "c-bg-blue-600 c-text-white"
: "hover:c-bg-blue-50",
idx > 0 && "c-border-t"
)}
onClick={() => {
onSelect?.(item.value);
}}
>
{item.label}
</div>
);
})}
{searching ? (
<div className="c-px-4 c-w-full c-text-xs c-text-slate-400">
Loading...
</div>
) : (
<>
{options.length === 0 && (
<div className="c-p-4 c-w-full c-text-center c-text-sm c-text-slate-400">
&mdash; Empty &mdash;
</div>
)}
</>
)}
</div>
}
>
{children}
</Popover>
);
};

View File

@ -1,11 +1,12 @@
import { useLocal } from "lib/utils/use-local"; import { useLocal } from "lib/utils/use-local";
import { X } from "lucide-react"; import { ChevronDown, X } from "lucide-react";
import { FC, KeyboardEvent, useCallback, useEffect, useRef } from "react"; import { FC, KeyboardEvent, useCallback, useEffect, useRef } from "react";
import { Popover } from "../custom/Popover";
import { Badge } from "./badge"; import { Badge } from "./badge";
import { TypeaheadOptions } from "./typeahead-opt";
export const Typeahead: FC<{ export const Typeahead: FC<{
value?: string[]; value?: string[];
placeholder?: string;
options?: (arg: { options?: (arg: {
search: string; search: string;
existing: { value: string; label: string }[]; existing: { value: string; label: string }[];
@ -16,10 +17,14 @@ export const Typeahead: FC<{
search: string; search: string;
item?: null | { value: string; label: string }; item?: null | { value: string; label: string };
}) => string | false; }) => string | false;
onChange?: (selected: string[]) => void;
unique?: boolean; unique?: boolean;
allowNew?: boolean; allowNew?: boolean;
localSearch?: boolean; localSearch?: boolean;
autoPopupWidth?: boolean;
focusOpen?: boolean; focusOpen?: boolean;
disabled?: boolean;
mode?: "multi" | "single";
}> = ({ }> = ({
value, value,
options: options_fn, options: options_fn,
@ -28,6 +33,11 @@ export const Typeahead: FC<{
allowNew: allow_new, allowNew: allow_new,
focusOpen: on_focus_open, focusOpen: on_focus_open,
localSearch: local_search, localSearch: local_search,
autoPopupWidth: auto_popup_width,
placeholder,
mode,
disabled,
onChange,
}) => { }) => {
const local = useLocal({ const local = useLocal({
value: [] as string[], value: [] as string[],
@ -42,9 +52,12 @@ export const Typeahead: FC<{
result: null as null | { value: string; label: string }[], result: null as null | { value: string; label: string }[],
}, },
unique: typeof unique === "undefined" ? true : unique, unique: typeof unique === "undefined" ? true : unique,
allow_new: typeof allow_new === "undefined" ? true : allow_new, allow_new: typeof allow_new === "undefined" ? false : allow_new,
on_focus_open: typeof on_focus_open === "undefined" ? false : on_focus_open, on_focus_open: typeof on_focus_open === "undefined" ? true : on_focus_open,
local_search: typeof local_search === "undefined" ? true : local_search, local_search: typeof local_search === "undefined" ? true : local_search,
mode: typeof mode === "undefined" ? "multi" : mode,
auto_popup_width:
typeof auto_popup_width === "undefined" ? false : auto_popup_width,
select: null as null | { value: string; label: string }, select: null as null | { value: string; label: string },
}); });
const input = useRef<HTMLInputElement>(null); const input = useRef<HTMLInputElement>(null);
@ -55,26 +68,30 @@ export const Typeahead: FC<{
options.push({ value: local.search.input, label: local.search.input }); options.push({ value: local.search.input, label: local.search.input });
} }
const added = new Set<string>(); const added = new Set<string>();
options = options.filter((e) => { if (local.mode === "multi") {
if (!added.has(e.value)) added.add(e.value); options = options.filter((e) => {
else return false; if (!added.has(e.value)) added.add(e.value);
if (local.select && local.select.value === e.value) select_found = true; else return false;
if (local.unique) { if (local.select && local.select.value === e.value) select_found = true;
if (local.value.includes(e.value)) { if (local.unique) {
return false; if (local.value.includes(e.value)) {
return false;
}
} }
} return true;
return true; });
});
if (!select_found) { if (!select_found) {
local.select = options[0]; local.select = options[0];
}
} }
useEffect(() => { useEffect(() => {
if (typeof value === "object" && value) { if (!isEditor) {
local.value = value; if (typeof value === "object" && value) {
local.render(); local.value = value;
local.render();
}
} }
}, [value]); }, [value]);
@ -110,17 +127,26 @@ export const Typeahead: FC<{
if (result) { if (result) {
local.value.push(result); local.value.push(result);
local.render(); local.render();
return result;
} else { } else {
return false; return false;
} }
} else { } else {
let val = false as any;
if (arg.item) { if (arg.item) {
local.value.push(arg.item.value); local.value.push(arg.item.value);
val = arg.item.value;
} else { } else {
if (!arg.search) return false; if (!arg.search) return false;
local.value.push(arg.search); local.value.push(arg.search);
val = arg.search;
}
if (typeof onChange === "function") {
onChange(local.value);
} }
local.render(); local.render();
return val;
} }
return true; return true;
}, },
@ -129,6 +155,14 @@ export const Typeahead: FC<{
const keydown = useCallback( const keydown = useCallback(
(e: KeyboardEvent<HTMLInputElement>) => { (e: KeyboardEvent<HTMLInputElement>) => {
if (!local.open) {
e.preventDefault();
e.stopPropagation();
local.open = true;
local.render();
return;
}
if (e.key === "Backspace") { if (e.key === "Backspace") {
if (local.value.length > 0 && e.currentTarget.selectionStart === 0) { if (local.value.length > 0 && e.currentTarget.selectionStart === 0) {
local.value.pop(); local.value.pop();
@ -136,17 +170,32 @@ export const Typeahead: FC<{
} }
} }
if (e.key === "Enter") { if (e.key === "Enter") {
e.preventDefault();
e.stopPropagation();
const selected = select({ const selected = select({
search: local.search.input, search: local.search.input,
item: local.select, item: local.select,
}); });
if (selected) { if (local.mode === "single") {
resetSearch(); local.open = false;
local.render();
} }
if (typeof selected === "string") {
resetSearch();
if (local.mode === "single") {
const item = local.options.find((item) => item.value === selected);
if (item) {
local.search.input = item.label;
}
}
}
local.render();
return;
} }
if (options.length > 0) { if (options.length > 0) {
local.open = true;
if (e.key === "ArrowDown") { if (e.key === "ArrowDown") {
e.preventDefault(); e.preventDefault();
const idx = options.findIndex((item) => { const idx = options.findIndex((item) => {
@ -170,10 +219,10 @@ export const Typeahead: FC<{
if (item.value === local.select?.value) return true; if (item.value === local.select?.value) return true;
}); });
if (idx >= 0) { if (idx >= 0) {
if (idx + 1 < options.length) { if (idx - 1 >= 0) {
local.select = options[idx + 1]; local.select = options[idx - 1];
} else { } else {
local.select = options[0]; local.select = options[options.length - 1];
} }
} else { } else {
local.select = options[0]; local.select = options[0];
@ -220,40 +269,59 @@ export const Typeahead: FC<{
clearTimeout(local.search.timeout); clearTimeout(local.search.timeout);
}; };
if (local.mode === "single" && local.value.length > 1) {
local.value = [local.value.pop() || ""];
}
const valueLabel = local.value.map((value) => {
const item = local.options.find((item) => item.value === value);
if (local.mode === "single") {
if (!local.open) {
local.select = item || null;
local.search.input = item?.label || "";
}
}
return item;
});
return ( return (
<div <div
className={cx( className={cx(
"c-flex c-cursor-text c-space-x-2 c-flex-wrap c-p-2 c-pb-0 c-items-center c-w-full c-h-full c-flex-1", local.mode === "single" ? "c-cursor-pointer" : "c-cursor-text",
css` "c-flex c-relative c-space-x-2 c-flex-wrap c-pt-2 c-px-2 c-pb-0 c-items-center c-w-full c-h-full c-flex-1"
min-height: 40px;
`
)} )}
onClick={() => { onClick={() => {
input.current?.focus(); input.current?.focus();
}} }}
> >
{local.value.map((e, idx) => { {local.mode === "multi" ? (
return ( <>
<Badge {valueLabel.map((e, idx) => {
key={idx} return (
variant={"outline"} <Badge
className="c-space-x-1 c-mb-2 c-cursor-pointer hover:c-bg-red-100" key={idx}
onClick={(ev) => { variant={"outline"}
ev.stopPropagation(); className="c-space-x-1 c-mb-2 c-cursor-pointer hover:c-bg-red-100"
ev.preventDefault(); onClick={(ev) => {
local.value = local.value.filter((val) => e !== val); ev.stopPropagation();
local.render(); ev.preventDefault();
input.current?.focus(); local.value = local.value.filter((val) => e?.value !== val);
}} local.render();
> input.current?.focus();
<div>{e}</div> }}
<X size={12} /> >
</Badge> <div>{e?.label}</div>
); <X size={12} />
})} </Badge>
);
})}
</>
) : (
<></>
)}
<WrapOptions <TypeaheadOptions
wrap={true} popup={true}
onOpenChange={(open) => { onOpenChange={(open) => {
if (!open) { if (!open) {
local.select = null; local.select = null;
@ -267,19 +335,32 @@ export const Typeahead: FC<{
onSelect={(value) => { onSelect={(value) => {
local.open = false; local.open = false;
local.value.push(value); local.value.push(value);
resetSearch(); resetSearch();
if (local.mode === "single") {
const item = local.options.find((item) => item.value === value);
if (item) {
local.search.input = item.label;
}
}
local.render(); local.render();
}} }}
selected={local.select?.value} width={local.auto_popup_width ? input.current?.offsetWidth : undefined}
selected={({ item, options, idx }) => {
if (item.value === local.select?.value) {
return true;
}
return false;
}}
> >
<input <input
placeholder={local.mode === "multi" ? placeholder : ""}
type="text" type="text"
ref={input} ref={input}
value={local.search.input} value={local.search.input}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
}}
onFocus={(e) => {
if (!local.open) { if (!local.open) {
if (local.on_focus_open) { if (local.on_focus_open) {
openOptions(); openOptions();
@ -313,6 +394,15 @@ export const Typeahead: FC<{
local.search.result = local.options.filter((e) => local.search.result = local.options.filter((e) =>
e.label.toLowerCase().includes(search) e.label.toLowerCase().includes(search)
); );
if (
local.search.result.length > 0 &&
!local.search.result.find(
(e) => e.value === local.select?.value
)
) {
local.select = local.search.result[0];
}
} else { } else {
local.search.result = null; local.search.result = null;
} }
@ -335,7 +425,6 @@ export const Typeahead: FC<{
}); });
local.search.searching = false; local.search.searching = false;
local.search.promise = null; local.search.promise = null;
local.render();
} else { } else {
local.search.result = result.map((item) => { local.search.result = result.map((item) => {
if (typeof item === "string") if (typeof item === "string")
@ -343,95 +432,43 @@ export const Typeahead: FC<{
return item; return item;
}); });
local.search.searching = false; local.search.searching = false;
local.render();
} }
if (
local.search.result.length > 0 &&
!local.search.result.find(
(e) => e.value === local.select?.value
)
) {
local.select = local.search.result[0];
}
local.render();
} }
}, 100); }, 100);
} }
} }
}} }}
spellCheck={false} spellCheck={false}
className={cx("c-flex-1 c-mb-2 c-text-sm c-outline-none")} className={cx(
"c-flex-1 c-mb-2 c-text-sm c-outline-none",
local.mode === "single" ? "c-cursor-pointer" : ""
)}
onKeyDown={keydown} onKeyDown={keydown}
/> />
</WrapOptions> </TypeaheadOptions>
{local.mode === "single" && (
<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",
disabled && "c-hidden"
)}
>
<ChevronDown size={14} />
</div>
)}
</div> </div>
); );
}; };
const WrapOptions: FC<{
wrap: boolean;
children: any;
open: boolean;
onOpenChange: (open: boolean) => void;
options: { value: string; label: string }[];
selected?: string;
onSelect: (value: string) => void;
searching?: boolean;
}> = ({
wrap,
children,
open,
onOpenChange,
options,
selected,
onSelect,
searching,
}) => {
if (!wrap) return children;
return (
<Popover
open={open}
arrow={false}
onOpenChange={onOpenChange}
placement="bottom-start"
content={
<div
className={cx(
css`
min-width: 150px;
`
)}
>
{options.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 === selected
? "c-bg-blue-600 c-text-white"
: "hover:c-bg-blue-50",
idx > 0 && "c-border-t"
)}
onClick={() => {
onSelect(item.value);
}}
>
{item.label}
</div>
);
})}
{searching ? (
<div className="c-px-4 c-w-full c-text-xs c-text-slate-400">
Loading...
</div>
) : (
<>
{options.length === 0 && (
<div className="c-p-4 c-w-full c-text-center c-text-sm c-text-slate-400">
&mdash; Empty &mdash;
</div>
)}
</>
)}
</div>
}
>
{children}
</Popover>
);
};

View File

@ -42,7 +42,7 @@ export const FilterField = lazify(
); );
/** Generator */ /** Generator */
export { generateMasterDetail } from "lib/comps/md/gen/md-gen"; export { generateMasterDetail } from "@/comps/md/gen/md-gen";
/** ETC */ /** ETC */
export { filterWhere } from "@/comps/filter/utils/filter-where"; export { filterWhere } from "@/comps/filter/utils/filter-where";
@ -55,12 +55,12 @@ export {
export { MasterDetailType } from "@/comps/md/utils/typings"; export { MasterDetailType } from "@/comps/md/utils/typings";
export { FormatValue } from "@/utils/format-value"; export { FormatValue } from "@/utils/format-value";
export { GetValue } from "@/utils/get-value"; export { GetValue } from "@/utils/get-value";
export { TableListType } from "lib/comps/list/utils/typings"; export { TableListType } from "@/comps/list/utils/typings";
export { Button, FloatButton } from "@/comps/ui/button"; export { Button, FloatButton } from "@/comps/ui/button";
export { prasi_gen } from "@/gen/prasi_gen"; export { prasi_gen } from "@/gen/prasi_gen";
export { password } from "@/utils/password"; export { password } from "@/utils/password";
export { generateTableList } from "@/comps/md/gen/gen-table-list"; export { generateTableList } from "@/comps/md/gen/gen-table-list";
export { generateForm } from "@/comps/md/gen/gen-form"; export { generateForm } from "@/comps/form/gen/gen-form";
/** Session */ /** Session */
export { export {
@ -97,7 +97,7 @@ export { Profile } from "@/preset/profile/Profile";
export { generateProfile } from "@/preset/profile/utils/generate"; export { generateProfile } from "@/preset/profile/utils/generate";
export { ButtonUpload } from "@/preset/profile/ButtonUpload"; export { ButtonUpload } from "@/preset/profile/ButtonUpload";
export { longDate, shortDate, timeAgo, formatTime } from "@/utils/date"; export { longDate, shortDate, timeAgo, formatTime } from "@/utils/date";
export { getPathname } from "./utils/pathname";
export * from '@/comps/ui/typeahead' export * from '@/comps/ui/typeahead'
export * from '@/comps/ui/input' export * from '@/comps/ui/input'

View File

@ -1,4 +1,4 @@
import set from "lodash.set"; import { set } from "lib/utils/set";
const cache: any = []; const cache: any = [];

View File

@ -8,7 +8,6 @@ export type RGSession = {
}; };
export const logout = (url_login?: string) => { export const logout = (url_login?: string) => {
console.log("halo")
if (typeof get(w, "user") === "object") { if (typeof get(w, "user") === "object") {
w.user = null; w.user = null;
} }

View File

@ -1,4 +1,4 @@
import set from "lodash.set"; import { set } from "lib/utils/set";
export const select = (rel: any) => { export const select = (rel: any) => {
const result = {}; const result = {};

View File

@ -1,6 +1,6 @@
import { select } from "@/preset/login/utils/select"; import { select } from "@/preset/login/utils/select";
import { set } from "lib/utils/set";
import get from "lodash.get"; import get from "lodash.get";
import set from "lodash.set";
type typeFieldLogin = { type typeFieldLogin = {
upload: string; upload: string;
@ -29,7 +29,7 @@ export const generateProfile = async (
`, `,
}); });
} }
console.log({btn}) console.log({ btn });
if (btn) { if (btn) {
const upload = btn.edit.childs.find( const upload = btn.edit.childs.find(
(e) => get(e, "component.id") === "296825f3-dac7-4a13-8871-9743718bc411" (e) => get(e, "component.id") === "296825f3-dac7-4a13-8871-9743718bc411"

30
utils/set.ts Executable file
View File

@ -0,0 +1,30 @@
export function set<T extends object, V>(
obj: T,
keys: string | ArrayLike<string | number>,
value: V
): void {
if (typeof keys === "string") {
keys = keys.split(".");
}
let i = 0,
l = keys.length,
t = obj as any,
x,
k;
if (Array.isArray(keys)) {
while (i < l) {
k = keys[i++];
if (k === "__proto__" || k === "constructor" || k === "prototype") break;
t = t[k] =
i === l
? value
: typeof (x = t[k]) === typeof keys
? x
: keys[i] * 0 !== 0 || !!~("" + keys[i]).indexOf(".")
? {}
: [];
}
}
}

View File

@ -8,14 +8,17 @@ export const useLocal = <T extends object>(
deps?: any[] deps?: any[]
): { ): {
[K in keyof T]: T[K] extends Promise<any> ? null | Awaited<T[K]> : T[K]; [K in keyof T]: T[K] extends Promise<any> ? null | Awaited<T[K]> : T[K];
} & { render: (force?: boolean) => void } => { } & { render: () => void } => {
const [, _render] = useState({}); const [, _render] = useState({});
const _ = useRef({ const _ = useRef({
data: data as unknown as T & { data: data as unknown as T & {
render: (force?: boolean) => void; render: () => void;
}, },
deps: (deps || []) as any[], deps: (deps || []) as any[],
ready: false, ready: false,
_loading: {} as any,
lastRender: 0,
lastRenderCount: 0,
}); });
const local = _.current; const local = _.current;
@ -25,9 +28,23 @@ export const useLocal = <T extends object>(
}, []); }, []);
if (local.ready === false) { if (local.ready === false) {
local.data.render = (force) => { local._loading = {};
if (force) _render({});
else if (local.ready) _render({}); local.data.render = () => {
if (local.ready) {
if (Date.now() - local.lastRender < 200) {
local.lastRenderCount++;
} else {
local.lastRenderCount = 0;
}
if (local.lastRenderCount > 20) {
throw new Error("local.render more than 20 times in less than 200ms");
}
local.lastRender = Date.now();
_render({});
}
}; };
} else { } else {
if (local.deps.length > 0 && deps) { if (local.deps.length > 0 && deps) {
@ -47,4 +64,4 @@ export const useLocal = <T extends object>(
} }
return local.data as any; return local.data as any;
}; };