diff --git a/comps/form/Form.tsx b/comps/form/Form.tsx index b4ad389..8b87467 100755 --- a/comps/form/Form.tsx +++ b/comps/form/Form.tsx @@ -13,7 +13,6 @@ const editorFormWidth = {} as Record; export { FMLocal } from "./typings"; export const Form: FC = (props) => { - const { PassProp, body } = props; const fm = useLocal({ data: editorFormData[props.item.id] @@ -113,7 +112,7 @@ export const Form: FC = (props) => { document.body.appendChild(elemDiv); } const toaster_el = document.getElementsByClassName("prasi-toaster")[0]; - + if (fm.status === "resizing") { setTimeout(() => { fm.status = "ready"; @@ -121,6 +120,7 @@ export const Form: FC = (props) => { }, 100); return null; } + return (
{ diff --git a/comps/form/base/BaseField.tsx b/comps/form/base/BaseField.tsx index 5fd092a..566a3b3 100755 --- a/comps/form/base/BaseField.tsx +++ b/comps/form/base/BaseField.tsx @@ -1,7 +1,7 @@ import { ReactNode } from "react"; import { FMLocal, FieldLocal, FieldProp } from "../typings"; import { Label } from "../field/Label"; -import { FieldLoading } from "../field/raw/FieldLoading"; +import { FieldLoading } from "../../ui/field-loading"; export const BaseField = (prop: { field: FieldLocal; diff --git a/comps/form/field/FieldInput.tsx b/comps/form/field/FieldInput.tsx index 97c5936..c988e52 100755 --- a/comps/form/field/FieldInput.tsx +++ b/comps/form/field/FieldInput.tsx @@ -1,6 +1,6 @@ import { FC, isValidElement } from "react"; import { FMLocal, FieldLocal, FieldProp } from "../typings"; -import { FieldLoading } from "./raw/FieldLoading"; +import { FieldLoading } from "../../ui/field-loading"; import { MultiOption } from "./type/TypeMultiOption"; import { SingleOption } from "./type/TypeSingleOption"; import { FieldTypeText, PropTypeText } from "./type/TypeText"; diff --git a/comps/form/field/raw/Dropdown.tsx b/comps/form/field/raw/Dropdown.tsx deleted file mode 100755 index 50a6856..0000000 --- a/comps/form/field/raw/Dropdown.tsx +++ /dev/null @@ -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 ( - { - local.open = false; - local.render(); - }} - arrow={false} - className={cx("c-rounded-sm c-bg-white")} - content={ -
- <> - {filtered.map((item, idx) => { - return ( -
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} -
- ); - })} - -
- } - > -
{ - 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(); - } - } - }} - > -
- {!isEditor && ( - { - 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 && ( -
- {local.selected.el || local.selected.label} -
- )} -
-
- -
-
-
- ); -}; diff --git a/comps/form/field/type/TypeButton.tsx b/comps/form/field/type/TypeButton.tsx index 992efd1..964286e 100755 --- a/comps/form/field/type/TypeButton.tsx +++ b/comps/form/field/type/TypeButton.tsx @@ -10,21 +10,22 @@ export const FieldButton: FC<{ }> = ({ field, fm, arg }) => { const local = useLocal({ list: [] as any[], - value: [] as any[], }); useEffect(() => { const callback = (res: any[]) => { local.list = res; - if (Array.isArray(res)) { - local.value = res.map((e) => get(e, arg.pk)); - } local.render(); }; const res = arg.on_load(); if (res instanceof Promise) res.then(callback); 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") { value = fm.data[field.name] || []; @@ -42,32 +43,45 @@ export const FieldButton: FC<{ {local.list.map((item) => { let isChecked = false; try { - isChecked = value.some((e: any) => e[arg.pk] === item[arg.pk]); + isChecked = value.some((e: any) => e === item[arg.pk]); } catch (ex) {} + return (
{ - if (!Array.isArray(fm.data[field.name])) - fm.data[field.name] = []; + let selected = Array.isArray(value) + ? value.map((row) => { + return local.list.find((e) => e.value === row); + }) + : []; if (isChecked) { - fm.data[field.name] = fm.data[field.name].filter( - (e: any) => e[arg.pk] !== item[arg.pk] + selected = selected.filter( + (e: any) => e.value !== item.value ); } 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" role="button" title="Hover chip" className={cx( - isChecked ? "c-bg-gray-200" : "c-border c-border-gray-500", - " 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 " + isChecked + ? "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 " )} > - {arg.on_row(item)} + {arg.opt_get_label(item)}
); @@ -84,7 +98,7 @@ export const FieldButton: FC<{
); diff --git a/comps/form/field/type/TypeCheckbox.tsx b/comps/form/field/type/TypeCheckbox.tsx index 24a506e..0530c2d 100755 --- a/comps/form/field/type/TypeCheckbox.tsx +++ b/comps/form/field/type/TypeCheckbox.tsx @@ -10,22 +10,24 @@ export const FieldCheckbox: FC<{ }> = ({ field, fm, arg }) => { const local = useLocal({ list: [] as any[], - value: [] as any[], }); useEffect(() => { const callback = (res: any[]) => { local.list = res; - if (Array.isArray(res)) { - local.value = res.map((e) => get(e, arg.pk)); - } local.render(); }; const res = arg.on_load(); if (res instanceof Promise) res.then(callback); 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 ( <>
@@ -33,25 +35,32 @@ export const FieldCheckbox: FC<{ {local.list.map((item) => { let isChecked = false; try { - isChecked = value.some((e: any) => e[arg.pk] === item[arg.pk]); + isChecked = value.some((e: any) => e === item[arg.pk]); } catch (ex) {} - console.log(item[arg.pk], isChecked) + return (
{ - console.log(item); - if (!Array.isArray(fm.data[field.name])) - fm.data[field.name] = []; - console.log(isChecked); + let selected = Array.isArray(value) + ? value.map((row) => { + return local.list.find((e) => e.value === row); + }) + : []; if (isChecked) { - fm.data[field.name] = fm.data[field.name].filter( - (e: any) => e[arg.pk] !== item[arg.pk] + selected = selected.filter( + (e: any) => e.value !== item.value ); } 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" > @@ -81,7 +90,7 @@ export const FieldCheckbox: FC<{ /> )} -
{arg.on_row(item)}
+
{arg.opt_get_label(item)}
); })} diff --git a/comps/form/field/type/TypeDropdown.tsx b/comps/form/field/type/TypeDropdown.tsx index 4cf9758..d07b4c7 100755 --- a/comps/form/field/type/TypeDropdown.tsx +++ b/comps/form/field/type/TypeDropdown.tsx @@ -1,96 +1,97 @@ -import { FC, useEffect } from "react"; -import { FMLocal, FieldLocal, FieldProp } from "../../typings"; import { useLocal } from "@/utils/use-local"; -import { OptionItem, RawDropdown } from "../raw/Dropdown"; -import { FieldLoading } from "../raw/FieldLoading"; -import get from "lodash.get"; +import { FC, useEffect } from "react"; +import { Typeahead } from "../../../../.."; +import { FMLocal, FieldLocal, FieldProp } from "../../typings"; +import { FieldLoading } from "lib/comps/ui/field-loading"; export const TypeDropdown: FC<{ field: FieldLocal; fm: FMLocal; arg: FieldProp; }> = ({ field, fm, arg }) => { - const input = useLocal({ - list: null as null | any[], - pk: "", + const local = useLocal({ + loaded: false, + options: [], }); - const value = fm.data[field.name]; - field.input = input; + let value = arg.opt_get_value({ + fm, + name: field.name, + options: local.options, + type: field.type, + }); + console.log({ value }); useEffect(() => { - if (!isEditor && input.list === null) { - field.status = "loading"; - input.pk = arg.pk; - field.render(); - const callback = (arg: any[]) => { - input.list = arg; - field.status = "ready"; - input.render(); - }; - const res = arg.on_load(); - if (res instanceof Promise) res.then(callback); - else callback(res); + if (typeof arg.on_load === "function") { + const options = arg.on_load({ mode: "query" }); + console.log("Masuk"); + // console.log(options) + if (options instanceof Promise) { + options.then((res) => { + console.log({ res }); + local.options = res; + local.loaded = true; + local.render(); + }); + } else { + local.options = options; + local.render(); + } } }, []); - let list: OptionItem[] = []; - if (input.list && input.list.length) { - input.list.map((e: any) => { - let id = null; - try { - id = e[arg.pk]; - } catch (ex: any) { - console.error( - "Error: PK Invalid atau tidak ditemukan, cek lagi keys yang ingin dijadikan value" - ); - } - list.push({ - value: id, - label: arg.on_row(e), - }); - }); - } - let selected = null; - if (value && typeof value === "object") { - if (input.pk) selected = value[input.pk]; - } else { - selected = value; - } + if (!local.loaded) return ; + + if (field.type === "single-option") + return ( + { + if (item) { + arg.opt_set_value({ + fm, + name: field.name, + type: field.type, + options: local.options, + selected: [item.value], + }); + } + + return item?.value || search; + }} + allowNew={false} + autoPopupWidth={true} + focusOpen={true} + mode={"single"} + placeholder={arg.placeholder} + options={() => { + return local.options; + }} + /> + ); + return ( - <> - {field.status === "loading" ? ( - - ) : ( - { - if (val === null) { - fm.data[field.name] = null; - fm.render(); - return; - } - if (input.list && input.pk) { - for (const item of input.list) { - if (item[input.pk] === val) { - fm.data[field.name] = val; - fm.render(); - break; - } - } - } - }} - 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(); - }} - /> - )} - + { + return item?.value || search; + }} + onChange={(values) => { + arg.opt_set_value({ + fm, + name: field.name, + type: field.type, + options: local.options, + selected: values, + }); + }} + allowNew={false} + autoPopupWidth={true} + focusOpen={true} + mode={"multi"} + placeholder={arg.placeholder} + options={() => { + return local.options; + }} + /> ); }; diff --git a/comps/form/field/type/TypeMultiOption.tsx b/comps/form/field/type/TypeMultiOption.tsx index 361b20c..a8ed60b 100755 --- a/comps/form/field/type/TypeMultiOption.tsx +++ b/comps/form/field/type/TypeMultiOption.tsx @@ -1,12 +1,8 @@ -import { FC, useEffect } from "react"; +import { FC } from "react"; 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 { FieldRadio } from "./TypeRadio"; import { FieldCheckbox } from "./TypeCheckbox"; +import { TypeDropdown } from "./TypeDropdown"; import { FieldTag } from "./TypeTag"; export const MultiOption: FC<{ @@ -16,7 +12,9 @@ export const MultiOption: FC<{ }> = ({ field, fm, arg }) => { return ( <> - {arg.sub_type === "checkbox" ? ( + {arg.sub_type === "typeahead" ? ( + + ):arg.sub_type === "checkbox" ? ( ): arg.sub_type === "button" ? ( diff --git a/comps/form/field/type/TypeRadio.tsx b/comps/form/field/type/TypeRadio.tsx index c2e2f55..231cc71 100755 --- a/comps/form/field/type/TypeRadio.tsx +++ b/comps/form/field/type/TypeRadio.tsx @@ -1,7 +1,7 @@ -import { FC, useEffect } from "react"; -import { FMLocal, FieldLocal, FieldProp } from "../../typings"; import { useLocal } from "@/utils/use-local"; import get from "lodash.get"; +import { FC, useEffect } from "react"; +import { FMLocal, FieldLocal, FieldProp } from "../../typings"; export const FieldRadio: FC<{ field: FieldLocal; @@ -24,9 +24,13 @@ export const FieldRadio: FC<{ if (res instanceof Promise) res.then(callback); else callback(res); }, []); - let listValue = []; - let value: any = fm.data[field.name]; - let checked = local.value.indexOf(value) > 0 ? true : false; + + let value = arg.opt_get_value({ + fm, + name: field.name, + options: local.list, + type: field.type, + }); return ( <>
@@ -36,38 +40,24 @@ export const FieldRadio: FC<{
{ - fm.data[field.name] = get(e, arg.pk); - fm.render(); + arg.opt_set_value({ + fm, + name: field.name, + selected: [e.value], + options: local.list, + type: field.type, + }); }} > -
- ); - return ( -
-
); diff --git a/comps/form/field/type/TypeRelation.tsx b/comps/form/field/type/TypeRelation.tsx index ce9fc7e..ef2342f 100755 --- a/comps/form/field/type/TypeRelation.tsx +++ b/comps/form/field/type/TypeRelation.tsx @@ -3,7 +3,7 @@ import { useLocal } from "@/utils/use-local"; import { FC, useEffect } from "react"; import { FMLocal, FieldLocal } from "../../typings"; import { OptionItem, RawDropdown } from "../raw/Dropdown"; -import { FieldLoading } from "../raw/FieldLoading"; +import { FieldLoading } from "../../../ui/field-loading"; export type PropTypeRelation = { type: "has-one" | "has-many"; diff --git a/comps/form/field/type/TypeTag.tsx b/comps/form/field/type/TypeTag.tsx index af72b61..9c3e53c 100755 --- a/comps/form/field/type/TypeTag.tsx +++ b/comps/form/field/type/TypeTag.tsx @@ -1,6 +1,7 @@ import { useLocal } from "@/utils/use-local"; import { FC } from "react"; import { FMLocal, FieldLocal, FieldProp } from "../../typings"; +import { Typeahead } from "../../../../.."; export const FieldTag: FC<{ field: FieldLocal; @@ -13,95 +14,20 @@ export const FieldTag: FC<{ value: null as any, }); let value: any = fm.data[field.name]; - let tags: Array = typeof value === "string" ? value.split(",") : []; - if(isEditor){ - tags = ["sample","sample"] - } return ( -
-
{ - if (local.ref) { - local.ref.focus(); - } - }} - > - {tags.map((item) => { - return ( -
- {item} - { - // delete tag, pakai filter - let tag: Array = tags.filter((e) => e !== item) || []; - // jadiin value string - let value = tags.join(","); - fm.data[field.name] = value; - fm.render(); - }} - > - - -
- ); - })} - (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 = 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?"); - }} - /> -
-
+ { + return item?.value || search; + }} + allowNew + focusOpen={false} + placeholder={arg.placeholder} + options={async () => { + if (typeof arg.on_load === "function") return await arg.on_load(); + return []; + }} + /> ); }; diff --git a/comps/form/field/type/TypeToggle.tsx b/comps/form/field/type/TypeToggle.tsx index a8fffe6..2097360 100755 --- a/comps/form/field/type/TypeToggle.tsx +++ b/comps/form/field/type/TypeToggle.tsx @@ -1,11 +1,7 @@ +import { useLocal } from "@/utils/use-local"; +import get from "lodash.get"; import { FC, useEffect } from "react"; 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<{ field: FieldLocal; @@ -14,13 +10,13 @@ export const FieldToggle: FC<{ }> = ({ field, fm, arg }) => { const local = useLocal({ list: [] as any[], - value:[] as any[] + value: [] as any[], }); useEffect(() => { const callback = (res: any[]) => { local.list = res; - if(Array.isArray(res)){ - local.value = res.map((e) => get(e, arg.pk)) + if (Array.isArray(res)) { + local.value = res.map((e) => get(e, arg.pk)); } local.render(); }; @@ -28,54 +24,71 @@ export const FieldToggle: FC<{ if (res instanceof Promise) res.then(callback); else callback(res); }, []); - let listValue = [] - let value: any = fm.data[field.name]; - let checked = local.value.indexOf(value) > 0 ? true: false; - - if(local.list.length < 2){ - return <>Minimum dan maksimal 2 Data - } + let value = arg.opt_get_value({ + fm, + name: field.name, + options: local.list, + type: field.type, + }); + let checked = local.value.indexOf(value) > 0 ? true : false; + return ( <> -
+
diff --git a/comps/md/gen/form/fields.ts b/comps/form/gen/fields.ts similarity index 74% rename from comps/md/gen/form/fields.ts rename to comps/form/gen/fields.ts index 7f140d5..5eca809 100755 --- a/comps/md/gen/form/fields.ts +++ b/comps/form/gen/fields.ts @@ -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 { ArrowBigDown } from "lucide-react"; +import { on_load_rel } from "./on_load_rel"; export type GFCol = { name: string; type: string; @@ -16,7 +19,6 @@ export const newField = ( arg: GFCol, opt: { parent_table: string; value: Array } ) => { - console.log({ arg, opt }); let type = "input"; if (["int", "string", "text"].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) { 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({ component: { id: "32550d01-42a3-4b15-a04a-2c2d5c3c8e67", @@ -76,15 +87,10 @@ export const newField = ( type: "single-option", sub_type: "dropdown", rel__gen_table: arg.name, - rel__gen_fields: [`[${opt.value.join(",")}]`], + // rel__gen_fields: [`[${opt.value.join(",")}]`], opt__on_load: [ `\ - () => { - console.log("halo"); - return { - label: "halo", value: "value" - } - } + ${load} `, ], child: { @@ -127,6 +133,38 @@ export const newField = ( // }, // }; } 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({ component: { id: "32550d01-42a3-4b15-a04a-2c2d5c3c8e67", diff --git a/comps/md/gen/gen-form.ts b/comps/form/gen/gen-form.ts similarity index 65% rename from comps/md/gen/gen-form.ts rename to comps/form/gen/gen-form.ts index faecd97..ffc0374 100755 --- a/comps/md/gen/gen-form.ts +++ b/comps/form/gen/gen-form.ts @@ -1,30 +1,30 @@ import { createItem, parseGenField } from "lib/gen/utils"; import get from "lodash.get"; -import { generateTableList } from "./gen-table-list"; -import { generateSelect } from "./md-select"; -import { on_load } from "./tbl-list/on_load"; -import { on_submit } from "./tbl-list/on_submit"; -import { newField } from "./form/fields"; -import { createId } from "@paralleldrive/cuid2"; +import { newField } from "./fields"; +import { generateSelect } from "../../md/gen/md-select"; +import { on_load } from "../../md/gen/tbl-list/on_load"; +import { on_submit } from "../../md/gen/tbl-list/on_submit"; export const generateForm = async ( modify: (data: any) => void, - data: any, + data: { + gen__table: any; + gen__fields: any; + on_load: any; + on_submit: any; + body: any; + }, item: PrasiItem, commit: boolean ) => { - const table = JSON.parse(data.gen_table.value); - console.log("halo"); - console.log(table); - const raw_fields = JSON.parse(data.gen_fields.value) as ( + const table = JSON.parse(data.gen__table.value); + const raw_fields = JSON.parse(data.gen__fields.value) as ( | string | { value: string; checked: string[] } )[]; let pk = ""; - console.log({ raw_fields }); let pks: Record = {}; const fields = parseGenField(raw_fields); - // convert ke bahasa prisma untuk select const res = generateSelect(fields); pk = res.pk; const select = res.select as any; @@ -33,8 +33,8 @@ export const generateForm = async ( alert("Failed to generate! Primary Key not found. "); return; } + console.log({ pk, table, select, pks }) if (pk) { - console.log("masuk"); if (data["on_load"]) { result.on_load = { mode: "raw", @@ -49,18 +49,17 @@ export const generateForm = async ( } result.body = data["body"]; - console.log({ fields, result }); const childs = []; + console.log({fields}) for (const item of fields.filter((e) => !e.is_pk)) { let value = [] as Array; - if(["has-one", "has-many"].includes(item.type)){ + if (["has-one", "has-many"].includes(item.type)) { value = get(item, "value.checked") as any; } const field = newField(item, { parent_table: table, value }); childs.push(field); } if (commit) { - const body = item.edit.childs[0] as PrasiItem; item.edit.setProp("body", { mode: "jsx", value: createItem({ @@ -68,9 +67,7 @@ export const generateForm = async ( }), }); await item.edit.commit(); - // console.log("done") } else { - } } }; diff --git a/comps/form/gen/gen-rel.ts b/comps/form/gen/gen-rel.ts new file mode 100755 index 0000000..de45f9a --- /dev/null +++ b/comps/form/gen/gen-rel.ts @@ -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); +}; diff --git a/comps/md/gen/form/on_load.ts b/comps/form/gen/on_load.ts similarity index 100% rename from comps/md/gen/form/on_load.ts rename to comps/form/gen/on_load.ts diff --git a/comps/form/gen/on_load_rel.ts b/comps/form/gen/on_load_rel.ts new file mode 100755 index 0000000..731afcd --- /dev/null +++ b/comps/form/gen/on_load_rel.ts @@ -0,0 +1,77 @@ +export const on_load_rel = ({ + pk, + table, + select, + pks, + }: { + pk: string; + table: string; + select: any; + pks: Record; + }) => { + 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; + orderBy?: Record; + 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([]) + } + }) + } + `; + }; + \ No newline at end of file diff --git a/comps/form/typings.ts b/comps/form/typings.ts index d92b71e..f43cc0c 100755 --- a/comps/form/typings.ts +++ b/comps/form/typings.ts @@ -20,15 +20,17 @@ export type FMProps = { on_load_deps?: any[]; }; -export type GenField = { - name: string, - is_pk: boolean, - type: string, - optional: boolean, -} | { - checked: GenField[], - value: GFCol -}; +export type GenField = + | { + name: string; + is_pk: boolean; + type: string; + optional: boolean; + } + | { + checked: GenField[]; + value: GFCol; + }; type FieldType = | "-" @@ -56,8 +58,26 @@ export type FieldProp = { width: "auto" | "full" | "¾" | "½" | "⅓" | "¼"; _item: PrasiItem; custom?: () => CustomField; - on_load: () => any | Promise; - on_row: (row: any) => string; + on_load: (arg?: any) => any | Promise; + 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; sub_type: string; placeholder: string; @@ -120,6 +140,9 @@ export type FieldInternal = { input: Record & { render: () => void; }; + options: { + on_load?: () => Promise<{ value: string; label: string }[]>; + }; prop?: any; }; export type FieldLocal = FieldInternal & { diff --git a/comps/form/utils/use-field.tsx b/comps/form/utils/use-field.tsx index 79119d0..dc5636d 100755 --- a/comps/form/utils/use-field.tsx +++ b/comps/form/utils/use-field.tsx @@ -2,10 +2,12 @@ import { useLocal } from "@/utils/use-local"; import { useEffect } from "react"; import { FieldInternal, FieldProp } from "../typings"; -export const useField = (arg: Omit & { - name: string | (() => string); - label: string | (() => string) -}) => { +export const useField = ( + arg: Omit & { + name: string | (() => string); + label: string | (() => string); + } +) => { const field = useLocal>({ status: "init", Child: () => { @@ -14,9 +16,10 @@ export const useField = (arg: Omit & { input: {}, } as any); - const name = typeof arg.name === 'string' ? arg.name : arg.name(); - const label = typeof arg.label === 'string' ? arg.label : arg.label(); - const required = typeof arg.required === 'string' ? arg.required : arg.required(); + const name = typeof arg.name === "string" ? arg.name : arg.name(); + const label = typeof arg.label === "string" ? arg.label : arg.label(); + const required = + typeof arg.required === "string" ? arg.required : arg.required(); const update_field = { name: name.replace(/\s*/gi, ""), @@ -31,6 +34,7 @@ export const useField = (arg: Omit & { required_msg: arg.required_msg, disabled: arg.disabled === "y", }; + if (field.status === "init" || isEditor) { for (const [k, v] of Object.entries(update_field)) { (field as any)[k] = v; diff --git a/comps/md/gen/gen-table-list.ts b/comps/md/gen/gen-table-list.ts index 8349821..ac36d05 100755 --- a/comps/md/gen/gen-table-list.ts +++ b/comps/md/gen/gen-table-list.ts @@ -1,10 +1,10 @@ +import { set } from "lib/utils/set"; 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 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 ( modify: (data: any) => void, diff --git a/comps/form/field/raw/FieldLoading.tsx b/comps/ui/field-loading.tsx similarity index 100% rename from comps/form/field/raw/FieldLoading.tsx rename to comps/ui/field-loading.tsx diff --git a/comps/ui/typeahead-opt.tsx b/comps/ui/typeahead-opt.tsx new file mode 100755 index 0000000..66230b0 --- /dev/null +++ b/comps/ui/typeahead-opt.tsx @@ -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 ( + + {options.map((item, idx) => { + const is_selected = selected?.({ item, options, idx }); + + if (is_selected) { + local.selectedIdx = idx; + } + + return ( +
0 && "c-border-t" + )} + onClick={() => { + onSelect?.(item.value); + }} + > + {item.label} +
+ ); + })} + + {searching ? ( +
+ Loading... +
+ ) : ( + <> + {options.length === 0 && ( +
+ — Empty — +
+ )} + + )} +
+ } + > + {children} + + ); +}; diff --git a/comps/ui/typeahead.tsx b/comps/ui/typeahead.tsx index fec5fa2..a0d3221 100755 --- a/comps/ui/typeahead.tsx +++ b/comps/ui/typeahead.tsx @@ -1,11 +1,12 @@ 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 { Popover } from "../custom/Popover"; import { Badge } from "./badge"; +import { TypeaheadOptions } from "./typeahead-opt"; export const Typeahead: FC<{ value?: string[]; + placeholder?: string; options?: (arg: { search: string; existing: { value: string; label: string }[]; @@ -16,10 +17,14 @@ export const Typeahead: FC<{ search: string; item?: null | { value: string; label: string }; }) => string | false; + onChange?: (selected: string[]) => void; unique?: boolean; allowNew?: boolean; localSearch?: boolean; + autoPopupWidth?: boolean; focusOpen?: boolean; + disabled?: boolean; + mode?: "multi" | "single"; }> = ({ value, options: options_fn, @@ -28,6 +33,11 @@ export const Typeahead: FC<{ allowNew: allow_new, focusOpen: on_focus_open, localSearch: local_search, + autoPopupWidth: auto_popup_width, + placeholder, + mode, + disabled, + onChange, }) => { const local = useLocal({ value: [] as string[], @@ -42,9 +52,12 @@ export const Typeahead: FC<{ result: null as null | { value: string; label: string }[], }, unique: typeof unique === "undefined" ? true : unique, - allow_new: typeof allow_new === "undefined" ? true : allow_new, - on_focus_open: typeof on_focus_open === "undefined" ? false : on_focus_open, + allow_new: typeof allow_new === "undefined" ? false : allow_new, + on_focus_open: typeof on_focus_open === "undefined" ? true : on_focus_open, 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 }, }); const input = useRef(null); @@ -55,26 +68,30 @@ export const Typeahead: FC<{ options.push({ value: local.search.input, label: local.search.input }); } const added = new Set(); - options = options.filter((e) => { - if (!added.has(e.value)) added.add(e.value); - else return false; - if (local.select && local.select.value === e.value) select_found = true; - if (local.unique) { - if (local.value.includes(e.value)) { - return false; + if (local.mode === "multi") { + options = options.filter((e) => { + if (!added.has(e.value)) added.add(e.value); + else return false; + if (local.select && local.select.value === e.value) select_found = true; + if (local.unique) { + if (local.value.includes(e.value)) { + return false; + } } - } - return true; - }); + return true; + }); - if (!select_found) { - local.select = options[0]; + if (!select_found) { + local.select = options[0]; + } } useEffect(() => { - if (typeof value === "object" && value) { - local.value = value; - local.render(); + if (!isEditor) { + if (typeof value === "object" && value) { + local.value = value; + local.render(); + } } }, [value]); @@ -110,17 +127,26 @@ export const Typeahead: FC<{ if (result) { local.value.push(result); local.render(); + return result; } else { return false; } } else { + let val = false as any; if (arg.item) { local.value.push(arg.item.value); + val = arg.item.value; } else { if (!arg.search) return false; local.value.push(arg.search); + val = arg.search; + } + + if (typeof onChange === "function") { + onChange(local.value); } local.render(); + return val; } return true; }, @@ -129,6 +155,14 @@ export const Typeahead: FC<{ const keydown = useCallback( (e: KeyboardEvent) => { + if (!local.open) { + e.preventDefault(); + e.stopPropagation(); + local.open = true; + local.render(); + return; + } + if (e.key === "Backspace") { if (local.value.length > 0 && e.currentTarget.selectionStart === 0) { local.value.pop(); @@ -136,17 +170,32 @@ export const Typeahead: FC<{ } } if (e.key === "Enter") { + e.preventDefault(); + e.stopPropagation(); + const selected = select({ search: local.search.input, item: local.select, }); - if (selected) { - resetSearch(); - local.render(); + if (local.mode === "single") { + local.open = false; } + 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) { + local.open = true; if (e.key === "ArrowDown") { e.preventDefault(); const idx = options.findIndex((item) => { @@ -170,10 +219,10 @@ export const Typeahead: FC<{ if (item.value === local.select?.value) return true; }); if (idx >= 0) { - if (idx + 1 < options.length) { - local.select = options[idx + 1]; + if (idx - 1 >= 0) { + local.select = options[idx - 1]; } else { - local.select = options[0]; + local.select = options[options.length - 1]; } } else { local.select = options[0]; @@ -220,40 +269,59 @@ export const Typeahead: FC<{ 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 (
{ input.current?.focus(); }} > - {local.value.map((e, idx) => { - return ( - { - ev.stopPropagation(); - ev.preventDefault(); - local.value = local.value.filter((val) => e !== val); - local.render(); - input.current?.focus(); - }} - > -
{e}
- -
- ); - })} + {local.mode === "multi" ? ( + <> + {valueLabel.map((e, idx) => { + return ( + { + ev.stopPropagation(); + ev.preventDefault(); + local.value = local.value.filter((val) => e?.value !== val); + local.render(); + input.current?.focus(); + }} + > +
{e?.label}
+ +
+ ); + })} + + ) : ( + <> + )} - { if (!open) { local.select = null; @@ -267,19 +335,32 @@ export const Typeahead: FC<{ onSelect={(value) => { local.open = false; local.value.push(value); + resetSearch(); + if (local.mode === "single") { + const item = local.options.find((item) => item.value === value); + if (item) { + local.search.input = item.label; + } + } 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; + }} > { e.stopPropagation(); - }} - onFocus={(e) => { + if (!local.open) { if (local.on_focus_open) { openOptions(); @@ -313,6 +394,15 @@ export const Typeahead: FC<{ local.search.result = local.options.filter((e) => 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 { local.search.result = null; } @@ -335,7 +425,6 @@ export const Typeahead: FC<{ }); local.search.searching = false; local.search.promise = null; - local.render(); } else { local.search.result = result.map((item) => { if (typeof item === "string") @@ -343,95 +432,43 @@ export const Typeahead: FC<{ return item; }); 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); } } }} 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} /> - + + + {local.mode === "single" && ( +
+ +
+ )}
); }; - -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 ( - - {options.map((item, idx) => { - return ( -
0 && "c-border-t" - )} - onClick={() => { - onSelect(item.value); - }} - > - {item.label} -
- ); - })} - - {searching ? ( -
- Loading... -
- ) : ( - <> - {options.length === 0 && ( -
- — Empty — -
- )} - - )} -
- } - > - {children} - - ); -}; diff --git a/exports.ts b/exports.ts index 0a62c11..8246ca9 100755 --- a/exports.ts +++ b/exports.ts @@ -42,7 +42,7 @@ export const FilterField = lazify( ); /** Generator */ -export { generateMasterDetail } from "lib/comps/md/gen/md-gen"; +export { generateMasterDetail } from "@/comps/md/gen/md-gen"; /** ETC */ export { filterWhere } from "@/comps/filter/utils/filter-where"; @@ -55,12 +55,12 @@ export { export { MasterDetailType } from "@/comps/md/utils/typings"; export { FormatValue } from "@/utils/format-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 { prasi_gen } from "@/gen/prasi_gen"; export { password } from "@/utils/password"; 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 */ export { @@ -97,7 +97,7 @@ export { Profile } from "@/preset/profile/Profile"; export { generateProfile } from "@/preset/profile/utils/generate"; export { ButtonUpload } from "@/preset/profile/ButtonUpload"; export { longDate, shortDate, timeAgo, formatTime } from "@/utils/date"; - - +export { getPathname } from "./utils/pathname"; + export * from '@/comps/ui/typeahead' -export * from '@/comps/ui/input' \ No newline at end of file +export * from '@/comps/ui/input' \ No newline at end of file diff --git a/gen/prop/gen_object_rel.ts b/gen/prop/gen_object_rel.ts index a0f3906..403033b 100755 --- a/gen/prop/gen_object_rel.ts +++ b/gen/prop/gen_object_rel.ts @@ -1,4 +1,4 @@ -import set from "lodash.set"; +import { set } from "lib/utils/set"; const cache: any = []; diff --git a/preset/login/utils/logout.ts b/preset/login/utils/logout.ts index deb48ae..2607e97 100755 --- a/preset/login/utils/logout.ts +++ b/preset/login/utils/logout.ts @@ -8,7 +8,6 @@ export type RGSession = { }; export const logout = (url_login?: string) => { - console.log("halo") if (typeof get(w, "user") === "object") { w.user = null; } diff --git a/preset/login/utils/select.ts b/preset/login/utils/select.ts index aee19bf..baf450e 100755 --- a/preset/login/utils/select.ts +++ b/preset/login/utils/select.ts @@ -1,4 +1,4 @@ -import set from "lodash.set"; +import { set } from "lib/utils/set"; export const select = (rel: any) => { const result = {}; diff --git a/preset/profile/utils/generate.ts b/preset/profile/utils/generate.ts index e87385d..e4d7a8b 100755 --- a/preset/profile/utils/generate.ts +++ b/preset/profile/utils/generate.ts @@ -1,6 +1,6 @@ import { select } from "@/preset/login/utils/select"; +import { set } from "lib/utils/set"; import get from "lodash.get"; -import set from "lodash.set"; type typeFieldLogin = { upload: string; @@ -29,7 +29,7 @@ export const generateProfile = async ( `, }); } - console.log({btn}) + console.log({ btn }); if (btn) { const upload = btn.edit.childs.find( (e) => get(e, "component.id") === "296825f3-dac7-4a13-8871-9743718bc411" diff --git a/utils/set.ts b/utils/set.ts new file mode 100755 index 0000000..c8b3fa6 --- /dev/null +++ b/utils/set.ts @@ -0,0 +1,30 @@ +export function set( + obj: T, + keys: string | ArrayLike, + 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(".") + ? {} + : []; + } + } +} diff --git a/utils/use-local.ts b/utils/use-local.ts index d96bcd0..757bf75 100755 --- a/utils/use-local.ts +++ b/utils/use-local.ts @@ -8,14 +8,17 @@ export const useLocal = ( deps?: any[] ): { [K in keyof T]: T[K] extends Promise ? null | Awaited : T[K]; -} & { render: (force?: boolean) => void } => { +} & { render: () => void } => { const [, _render] = useState({}); const _ = useRef({ data: data as unknown as T & { - render: (force?: boolean) => void; + render: () => void; }, deps: (deps || []) as any[], ready: false, + _loading: {} as any, + lastRender: 0, + lastRenderCount: 0, }); const local = _.current; @@ -25,9 +28,23 @@ export const useLocal = ( }, []); if (local.ready === false) { - local.data.render = (force) => { - if (force) _render({}); - else if (local.ready) _render({}); + local._loading = {}; + + 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 { if (local.deps.length > 0 && deps) { @@ -47,4 +64,4 @@ export const useLocal = ( } return local.data as any; -}; \ No newline at end of file +};