This commit is contained in:
rizky 2024-05-16 08:52:07 -07:00
parent 1747269a04
commit cf6b8b0728
47 changed files with 2274 additions and 183 deletions

View File

@ -1,6 +1,7 @@
import { useLocal } from "@/utils/use-local";
import { FC, ReactNode, useEffect } from "react";
import { Skeleton } from "../ui/skeleton";
import { MDLocal } from "../md/utils/typings";
export type BreadItem = {
label: React.ReactNode;
@ -15,33 +16,37 @@ type BreadcrumbProps = {
className?: string;
props?: any;
value?: BreadItem[];
item?: any;
item?: any
};
export const Breadcrumb: FC<BreadcrumbProps> = (_arg) => {
const { on_load, item } = _arg;
const local = useLocal({
list: _arg.value || ([] as BreadItem[]),
status: "init" as "init" | "loading" | "ready",
params: {},
});
// code review: md.breadcrumb yang di set di props value dipindahkan di on_load
// dan ketika panjang _arg.value bernilai null maka status berubah menjadi init
// untuk menjalankan on_load
// case: ketika refreshBread dijalankan pada MasterDetail
// md.breadcrum yang awalnya array kosong akan berisi satu array namun
// md.breadcrumb yang diterima di komponen ini tidak berubah tetap seperti
// value default yakni array kosong
if (_arg.value) {
local.list = _arg.value;
local.status = "ready";
if(!_arg.value.length) local.status = "init"
}
if (local.status === "init") {
let should_load = true;
local.status = "loading";
if (isEditor && item && breadcrumbData[item.id]) {
local.list = breadcrumbData[item.id];
local.status = "ready";
should_load = false;
if(!local.list.length) should_load = true;
}
if (should_load && typeof on_load === "function") {
const callback = (res: any) => {
local.list = res;

26
comps/filter/Filter.tsx Normal file
View File

@ -0,0 +1,26 @@
import { useLocal } from "@/utils/use-local";
import get from "lodash.get";
import { FC, useEffect, useRef } from "react";
export const MasterFilter: FC<{
name: string;
value: any;
child: any;
}> = ({name, value, child }) => {
useEffect(() => {
if (!isEditor) {
const w = window as any;
if (typeof w["prasi_filter"] !== "object") w["prasi_filter"] = {};
if (typeof w["prasi_filter"][name] !== "object")
w["prasi_filter"][name] = {};
const val = value();
w["prasi_filter"][name] = {
...w["prasi_filter"][name],
...val
};
w.prasiContext.render();
}
},[])
return <div>{child}</div>;
};

View File

@ -10,7 +10,10 @@ import { formReload } from "./utils/reload";
const editorFormWidth = {} as Record<string, { w: number; f: any }>;
export { FMLocal } from "./typings";
export const Form: FC<FMProps> = (props) => {
const { PassProp, body } = props;
const fm = useLocal<FMInternal>({
data: editorFormData[props.item.id]
@ -104,7 +107,6 @@ export const Form: FC<FMProps> = (props) => {
formInit(fm, props);
fm.reload();
}
if (document.getElementsByClassName("prasi-toaster").length === 0) {
const elemDiv = document.createElement("div");
elemDiv.className = "prasi-toaster";
@ -119,7 +121,6 @@ export const Form: FC<FMProps> = (props) => {
}, 100);
return null;
}
return (
<form
onSubmit={(e) => {

View File

@ -25,10 +25,8 @@ export const Field: FC<FieldProp> = (arg) => {
if (field.status === "init" && !isEditor) return null;
const errors = fm.error.get(field.name);
const props = { ...arg.props };
delete props.className;
return (
<label
className={cx(
@ -59,6 +57,7 @@ export const Field: FC<FieldProp> = (arg) => {
_meta={arg._meta}
_item={arg._item}
_sync={arg._sync}
arg={arg}
/>
{field.desc && (
<div className={cx("c-p-2 c-text-xs", errors.length > 0 && "c-pb-1")}>

View File

@ -1,11 +1,16 @@
import { createItem } from "@/gen/utils";
import get from "lodash.get";
import { FC, useEffect } from "react";
import { FMLocal, FieldLocal } from "../typings";
import { FMLocal, FieldLocal, FieldProp } from "../typings";
import { genFieldMitem, updateFieldMItem } from "../utils/gen-mitem";
import { fieldMapping } from "./mapping";
import { FieldLoading } from "./raw/FieldLoading";
import { TypeCustom } from "./type/TypeCustom";
import { FieldTypeText, PropTypeText } from "./type/TypeText";
import { TypeDropdown } from "./type/TypeDropdown";
import { FieldToggle } from "./type/TypeToggle";
import { SingleOption } from "./type/TypeSingleOption";
import { MultiOption } from "./type/TypeMultiOption";
const modify = {
timeout: null as any,
@ -19,69 +24,69 @@ export const FieldInput: FC<{
_item: any;
_meta: any;
_sync: (mitem: any, item: any) => void;
}> = ({ field, fm, PassProp, child, _meta, _item, _sync }) => {
const prefix = typeof field.prefix === "function" ? field.prefix() : null;
const suffix = typeof field.suffix === "function" ? field.suffix() : null;
arg: FieldProp;
}> = ({ field, fm, PassProp, child, _meta, _item, _sync, arg }) => {
// return <></>
const prefix = typeof field.prefix === "function" ? field.prefix() : typeof field.prefix === "string" ? field.prefix : null;
const suffix = typeof field.suffix === "function" ? field.suffix() : typeof field.suffix === "string" ? field.prefix : null;
const errors = fm.error.get(field.name);
const childs = get(
child,
"props.meta.item.component.props.child.content.childs"
);
const type_field = arg.type; // tipe field
const sub_type_field = arg.sub_type;
// sudah gk pake children
// const childs = get(
// child,
// "props.meta.item.component.props.child.content.childs"
// );
let found = null as any;
// let found = null as any;
if (childs && childs.length > 0 && field.type !== "custom") {
for (const child of childs) {
let mp = (fieldMapping as any)[field.type];
if (!mp) {
mp = (fieldMapping as any)["text"];
}
if (child.component?.id === mp.id) {
found = child;
if (mp.props) {
const item = createItem({
component: {
id: "--",
props:
typeof mp.props === "function" ? mp.props(fm, field) : mp.props,
},
});
const props = found.component.props;
let should_update = false;
for (const [k, v] of Object.entries(item.component.props) as any) {
if (props[k] && props[k].valueBuilt === v.valueBuilt) {
continue;
} else {
if (field.prop && !field.prop[k]) {
props[k] = v;
should_update = true;
}
}
}
if (should_update) {
updateFieldMItem(_meta, found, _sync);
}
}
}
}
}
useEffect(() => {
if (isEditor && !found && field.type !== "custom") {
genFieldMitem({ _meta, _item, _sync, field, fm });
}
}, []);
// if (childs && childs.length > 0 && field.type !== "custom") {
// for (const child of childs) {
// let mp = (fieldMapping as any)[field.type];
// if (!mp) {
// mp = (fieldMapping as any)["text"];
// }
// if (child.component?.id === mp.id) {
// found = child;
// if (mp.props) {
// const item = createItem({
// component: {
// id: "--",
// props:
// typeof mp.props === "function" ? mp.props(fm, field) : mp.props,
// },
// });
// const props = found.component.props;
// let should_update = false;
// for (const [k, v] of Object.entries(item.component.props) as any) {
// if (props[k] && props[k].valueBuilt === v.valueBuilt) {
// continue;
// } else {
// if (field.prop && !field.prop[k]) {
// props[k] = v;
// should_update = true;
// }
// }
// }
// if (should_update) {
// updateFieldMItem(_meta, found, _sync);
// }
// }
// }
// }
// }
// useEffect(() => {
// if (isEditor && !found && field.type !== "custom") {
// genFieldMitem({ _meta, _item, _sync, field, fm });
// }
// }, []);
return (
<div
className={cx(
"field-outer c-flex c-flex-1 c-flex-row c-rounded c-border c-text-sm",
!["toogle", "button", "radio","checkbox"].includes(arg.sub_type)
? "field-outer c-flex c-flex-1 c-flex-row c-rounded c-border c-text-sm"
: "",
fm.status === "loading"
? css`
border-color: transparent;
@ -100,17 +105,44 @@ export const FieldInput: FC<{
`
)}
>
{prefix && <></>}
{prefix && prefix !== "" ? (
<div
className="
c-px-2 c-bg-gray-200 c-flex c-flex-row c-items-center"
>
{prefix}
</div>
) : (
<></>
)}
{fm.status === "loading" ? (
<FieldLoading />
) : (
<div
className={cx(
"field-inner c-flex-1 c-flex c-items-center",
field.disabled && "c-pointer-events-none"
field.disabled && "c-pointer-events-none",
)}
>
{field.type === "custom" ? (
{/* <FieldToggle arg={arg} field={field} fm={fm} /> */}
{[
"date",
"input"
].includes(type_field) ? (
<FieldTypeText
field={field}
fm={fm}
prop={{ type: arg.type, sub_type: arg.sub_type, prefix, suffix} as PropTypeText}
/>
) : ["single-option"].includes(type_field) ? (
<SingleOption arg={arg} field={field} fm={fm} />
) : ["multi-option"].includes(type_field) ? (
<MultiOption arg={arg} field={field} fm={fm}/>
) : (
<>OTW</>
)}
{/* Komentar nanti diaktifkan setelah input */}
{/* {field.type === "custom" ? (
<TypeCustom fm={fm} field={field} />
) : (
<>
@ -121,10 +153,19 @@ export const FieldInput: FC<{
</PassProp>
)}
</>
)}
)} */}
</div>
)}
{suffix && <></>}
{suffix && suffix !== "" ? (
<div
className="
c-px-2 c-bg-gray-200 c-flex c-flex-row c-items-center"
>
{suffix}
</div>
) : (
<></>
)}
</div>
);
};

View File

@ -26,4 +26,5 @@ export const fieldMapping: {
return {};
},
},
switch: { id: ""},
};

View File

@ -0,0 +1,119 @@
import { FC, useEffect } from "react";
import { FMLocal, FieldLocal, FieldProp } from "../../typings";
import { useLocal } from "@/utils/use-local";
import get from "lodash.get";
export const FieldButton: FC<{
field: FieldLocal;
fm: FMLocal;
arg: FieldProp;
}> = ({ 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];
if (arg.type === "multi-option") {
value = fm.data[field.name] || [];
return (
<>
<div className={cx("c-flex c-items-center c-w-full c-flex-row")}>
<div
className={cx(
`c-flex`,
css`
gap: 0.5rem;
`
)}
>
{local.list.map((item) => {
let isChecked = false;
try {
isChecked = value.some((e: any) => e[arg.pk] === item[arg.pk]);
} catch (ex) {}
return (
<div
onClick={() => {
if (!Array.isArray(fm.data[field.name]))
fm.data[field.name] = [];
if (isChecked) {
fm.data[field.name] = fm.data[field.name].filter(
(e: any) => e[arg.pk] !== item[arg.pk]
);
} else {
fm.data[field.name].push(item);
}
fm.render();
}}
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 "
)}
>
<span className="block text-sm font-medium">
{arg.on_row(item)}
</span>
</div>
);
})}
</div>
</div>
</>
);
} else {
if (Array.isArray(value)) value = null;
}
return (
<>
<div className={cx("c-flex c-items-center c-w-full c-flex-row")}>
<div
className={cx(
`c-grid c-grid-cols- c-flex-grow c-gap-2 c-rounded-md c-bg-gray-200 c-p-0.5`,
css`
grid-template-columns: repeat(
${local.list.length},
minmax(0, 1fr)
);
`
)}
>
{local.list.map((e) => {
let checked = get(e, arg.pk) === value;
return (
<div>
<label
onClick={() => {
fm.data[field.name] = get(e, arg.pk);
fm.render();
}}
className={cx(
`${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"
)}
>
{arg.on_row(e)}
</label>
</div>
);
})}
</div>
</div>
</>
);
};

View File

@ -0,0 +1,93 @@
import { FC, useEffect } from "react";
import { FMLocal, FieldLocal, FieldProp } from "../../typings";
import { useLocal } from "@/utils/use-local";
import get from "lodash.get";
import { DMMF } from "../../../../../typings/runtime/library";
export const FieldCheckbox: FC<{
field: FieldLocal;
fm: FMLocal;
arg: FieldProp;
}> = ({ 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})
return (
<>
<div className={cx("c-flex c-items-center c-w-full c-flex-row")}>
<div className={cx(`c-flex c-flex-col c-space-y-1 c-p-0.5`)}>
{local.list.map((item) => {
let isChecked = false;
try {
isChecked = value.some((e: any) => e[arg.pk] === item[arg.pk]);
} catch (ex) {}
console.log(item[arg.pk], isChecked)
return (
<div
onClick={() => {
console.log(item);
if (!Array.isArray(fm.data[field.name]))
fm.data[field.name] = [];
console.log(isChecked);
if (isChecked) {
fm.data[field.name] = fm.data[field.name].filter(
(e: any) => e[arg.pk] !== item[arg.pk]
);
} else {
fm.data[field.name].push(item);
}
fm.render();
console.log({data: fm.data})
}}
className="c-flex c-flex-row c-space-x-1 cursor-pointer c-items-center rounded-full p-0.5"
>
{isChecked ? (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
className="c-fill-sky-500"
>
<path
fill="currentColor"
d="m10.6 14.092l-2.496-2.496q-.14-.14-.344-.15q-.204-.01-.364.15t-.16.354q0 .194.16.354l2.639 2.638q.242.243.565.243q.323 0 .565-.243l5.477-5.477q.14-.14.15-.344q.01-.204-.15-.363q-.16-.16-.354-.16q-.194 0-.353.16L10.6 14.092ZM5.615 20q-.69 0-1.152-.462Q4 19.075 4 18.385V5.615q0-.69.463-1.152Q4.925 4 5.615 4h12.77q.69 0 1.152.463q.463.462.463 1.152v12.77q0 .69-.462 1.152q-.463.463-1.153.463H5.615Z"
/>
</svg>
) : (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M5.615 20q-.69 0-1.152-.462Q4 19.075 4 18.385V5.615q0-.69.463-1.152Q4.925 4 5.615 4h12.77q.69 0 1.152.463q.463.462.463 1.152v12.77q0 .69-.462 1.152q-.463.463-1.153.463H5.615Zm0-1h12.77q.23 0 .423-.192q.192-.193.192-.423V5.615q0-.23-.192-.423Q18.615 5 18.385 5H5.615q-.23 0-.423.192Q5 5.385 5 5.615v12.77q0 .23.192.423q.193.192.423.192Z"
/>
</svg>
)}
<div className="">{arg.on_row(item)}</div>
</div>
);
})}
</div>
</div>
</>
);
};

View File

@ -0,0 +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";
export const TypeDropdown: FC<{
field: FieldLocal;
fm: FMLocal;
arg: FieldProp;
}> = ({ field, fm, arg }) => {
const input = useLocal({
list: null as null | any[],
pk: "",
});
const value = fm.data[field.name];
field.input = input;
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);
}
}, []);
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;
}
console.log({list})
return (
<>
{field.status === "loading" ? (
<FieldLoading />
) : (
<RawDropdown
options={list}
value={selected}
onChange={(val) => {
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] = item;
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();
}}
/>
)}
</>
);
};

View File

@ -0,0 +1,75 @@
import { FC } from "react";
import { FMLocal, FieldLocal } from "../../typings";
import { useLocal } from "@/utils/use-local";
import parser from "any-date-parser";
import { format } from "date-fns";
import get from "lodash.get";
import { argv } from "bun";
import { PropTypeText } from "./TypeText";
export const FieldMoney: FC<{
field: FieldLocal;
fm: FMLocal;
prop: PropTypeText;
}> = ({ field, fm, prop }) => {
let type_field = prop.sub_type;
let value: any = fm.data[field.name];
const input = useLocal({
value: 0 as any,
display: false as any,
ref: null as any,
});
let display: any = null;
// console.log({ prop });
return (
<div className="c-flex-grow c-flex-row c-flex c-w-full c-h-full">
<div
className={cx(
input.display ? "c-hidden" : "",
"c-flex-grow c-px-2 c-flex c-flex-row c-items-center"
)}
onClick={() => {
if (input.ref) {
input.display = !input.display;
input.ref.focus();
input.render();
}
}}
>
{formatMoney(Number(value) || 0)}
</div>
<input
ref={(el) => (input.ref = el)}
type={"number"}
onClick={() => {}}
onChange={(ev) => {
fm.data[field.name] = ev.currentTarget.value;
fm.render();
}}
value={value}
disabled={field.disabled}
className={cx(
!input.display ? "c-hidden" : "",
"c-flex-1 c-bg-transparent c-outline-none c-px-2 c-text-sm c-w-full"
)}
spellCheck={false}
onFocus={() => {
field.focused = true;
field.render();
}}
onBlur={() => {
console.log("blur");
field.focused = false;
input.display = !input.display;
input.render();
field.render();
}}
/>
</div>
);
};
const formatMoney = (res: number) => {
const formattedAmount = new Intl.NumberFormat("id-ID", {
minimumFractionDigits: 0,
}).format(res);
return formattedAmount;
};

View File

@ -0,0 +1,29 @@
import { FC, useEffect } 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";
export const MultiOption: FC<{
field: FieldLocal;
fm: FMLocal;
arg: FieldProp;
}> = ({ field, fm, arg }) => {
return (
<>
{arg.sub_type === "checkbox" ? (
<FieldCheckbox field={field} fm={fm} arg={arg}/>
) : arg.sub_type === "tag" ? (
<>{arg.sub_type}</>
) : arg.sub_type === "button" ? (
<FieldButton arg={arg} field={field} fm={fm} />
) : (
<></>
)}
</>
);
};

View File

@ -0,0 +1,79 @@
import { FC, useEffect } from "react";
import { FMLocal, FieldLocal, FieldProp } from "../../typings";
import { useLocal } from "@/utils/use-local";
import get from "lodash.get";
export const FieldRadio: FC<{
field: FieldLocal;
fm: FMLocal;
arg: FieldProp;
}> = ({ 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 listValue = [];
let value: any = fm.data[field.name];
let checked = local.value.indexOf(value) > 0 ? true : false;
return (
<>
<div className={cx("c-flex c-items-center c-w-full c-flex-row")}>
<div className={cx(`c-flex c-flex-col c-space-y-1 c-p-0.5`)}>
{local.list.map((e) => {
return (
<div
className="flex items-center mb-4"
onClick={() => {
fm.data[field.name] = get(e, arg.pk);
fm.render();
}}
>
<input
id="country-option-1"
type="radio"
name="countries"
value="USA"
className="h-4 w-4 border-gray-300 focus:ring-2 focus:ring-blue-300"
aria-labelledby="country-option-1"
aria-describedby="country-option-1"
checked={get(e, arg.pk) === value}
/>
<label className="text-sm font-medium text-gray-900 ml-2 block">
{arg.on_row(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>
</div>
);
})}
</div>
</div>
</>
);
};

View File

@ -0,0 +1,57 @@
import { FC, useEffect } from "react";
import { FMLocal, FieldLocal, FieldProp } from "../../typings";
import { useLocal } from "@/utils/use-local";
import Quill from "quill";
import "quill/dist/quill.snow.css"; // Import CSS untuk tema Quill
import get from "lodash.get";
export const FieldRichText: FC<{
field: FieldLocal;
fm: FMLocal;
arg: FieldProp;
}> = ({ field, fm, arg }) => {
const local = useLocal({
ref: null as any,
});
useEffect(() => {
if (local.ref) {
console.log(local.ref);
console.log(local.ref.current);
const q = new Quill(local.ref, {
theme: "snow",
modules: {
toolbar: [
["bold", "italic", "underline", "strike"], // toggled buttons
["blockquote", "code-block"],
["link", "image", "video", "formula"],
[{ header: 1 }, { header: 2 }], // custom button values
[{ list: "ordered" }, { list: "bullet" }, { list: "check" }],
[{ script: "sub" }, { script: "super" }], // superscript/subscript
[{ indent: "-1" }, { indent: "+1" }], // outdent/indent
[{ direction: "rtl" }], // text direction
[{ size: ["small", false, "large", "huge"] }], // custom dropdown
[{ header: [1, 2, 3, 4, 5, 6, false] }],
[{ color: [] }, { background: [] }], // dropdown with defaults from theme
[{ font: [] }],
[{ align: [] }],
["clean"], // remove formatting button
],
},
});
}
}, []);
let value: any = fm.data[field.name];
return (
<div className="c-flex c-flex-col c-w-full">
<div
ref={(e) => (local.ref = e)}
className={cx(css`
height: 20rem !important;
`)}
></div>
</div>
);
};

View File

@ -0,0 +1,30 @@
import { FC, useEffect } 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";
export const SingleOption: FC<{
field: FieldLocal;
fm: FMLocal;
arg: FieldProp;
}> = ({ field, fm, arg }) => {
return (
<>
{arg.sub_type === "dropdown" ? (
<TypeDropdown arg={arg} field={field} fm={fm} />
) : arg.sub_type === "toogle" ? (
<FieldToggle arg={arg} field={field} fm={fm} />
) : arg.sub_type === "button" ? (
<FieldButton arg={arg} field={field} fm={fm} />
) : arg.sub_type === "radio" ? (
<FieldRadio arg={arg} field={field} fm={fm} />
) : (
<></>
)}
</>
);
};

View File

@ -0,0 +1,81 @@
import { Label } from "@/comps/ui/label";
import { Switch } from "@/comps/ui/switch";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { Button } from "@/comps/ui/button";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
} from "@/comps/ui/form";
import { FC } from "react";
import { useLocal } from "@/utils/use-local";
export type PropTypeSwitch = {};
export const FieldTypeSwitch: FC<{
valueName: string;
description?: string;
label?: string;
}> = ({ valueName, description, label }) => {
const local = useLocal({ checked: false });
// const FormSchema = z.object({
// valueName: z.boolean().default(false).optional(),
// });
// const form = useForm<z.infer<typeof FormSchema>>({
// resolver: zodResolver(FormSchema),
// });
// function onSubmit(data: z.infer<typeof FormSchema>) {
// console.log({ data });
// }
return (
<div
onClick={() => {
local.checked = !local.checked;
local.render();
}}
>
{JSON.stringify(local.checked)}
</div>
// <Form {...form}>
// <form onSubmit={form.handleSubmit(onSubmit)} className="w-full space-y-6">
// <div>
// <h3 className="mb-4 text-lg font-medium">
// {label != undefined ? label : "-"}
// </h3>
// <div className="space-y-4">
// <FormField
// control={form.control}
// name="first_value"
// render={({ field }) => (
// <FormItem className="flex items-center space-x-2">
// <div>
// <FormLabel className="text-base">
// {label != undefined ? label : "-"}
// </FormLabel>
// <FormDescription>
// {description != undefined ? description : "-"}
// </FormDescription>
// </div>
// <FormControl>
// <Switch
// checked={field.value}
// onCheckedChange={field.onChange}
// />
// </FormControl>
// </FormItem>
// )}
// />
// </div>
// </div>
// <Button type="submit">Submit</Button>
// </form>
// </Form>
);
};

View File

@ -3,9 +3,31 @@ import { FMLocal, FieldLocal } 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";
import { FieldMoney } from "./TypeMoney";
import { FieldUpload } from "./TypeUpload";
import { FieldRichText } from "./TypeRichText";
export type PropTypeText = {
type: "text" | "password" | "number" | "date" | "datetime" | "textarea";
type: "text" | "date";
sub_type:
| "text"
| "password"
| "number"
| "date"
| "datetime"
| "textarea"
| "datetime-local"
| "time"
| "money"
| "rich-text"
| "upload"
| "file"
| "password";
suffix: string;
prefix: string;
};
const parse = parser.exportAsFunctionAny("en-US");
@ -15,24 +37,46 @@ export const FieldTypeText: FC<{
fm: FMLocal;
prop: PropTypeText;
}> = ({ field, fm, prop }) => {
let type_field = prop.sub_type;
switch (type_field) {
case "datetime":
type_field = "datetime-local";
break;
default:
}
const input = useLocal({});
let display: any = null;
let value: any = fm.data[field.name];
// let value: any = "2024-05-14T05:58:01.376Z" // case untuk date time
field.input = input;
field.prop = prop;
if (["date", "datetime"].includes(prop.type)) {
if (typeof value === "string") {
if (["date", "datetime", "datetime-local", "time"].includes(type_field)) {
if (typeof value === "string" || value instanceof Date) {
let date = parse(value);
if (typeof date === "object" && date instanceof Date) {
if (prop.type === "date") value = date.toISOString().substring(0, 10);
else if (prop.type === "datetime") value = date.toISOString();
if (type_field === "date") value = format(date, "yyyy-MM-dd");
else if (type_field === "datetime-local")
value = format(date, "yyyy-MM-dd HH:mm");
else if (type_field === "time") value = format(date, "HH:mm");
} else if (type_field === "time") {
if (value && !isTimeString(value)) value = null;
} else {
value = null;
}
} else {
try {
let date = parse(value);
if (typeof date === "object" && date instanceof Date) {
value = date.toISOString();
}
} catch (e) {}
}
} else if (["number"].includes(type_field)) {
value = Number(value) || null;
}
return (
<>
{prop.type === "textarea" ? (
{type_field === "textarea" ? (
<AutoHeightTextarea
onChange={(ev) => {
fm.data[field.name] = ev.currentTarget.value;
@ -51,19 +95,81 @@ export const FieldTypeText: FC<{
field.render();
}}
/>
) : type_field === "upload" ? (
<>
<FieldUpload field={field} fm={fm} prop={prop} />
{/* <input
type="file"
id="avatar"
name="avatar"
accept="image/png, image/jpeg"
onChange={async (event: any) => {
let file = null;
try {
file = event.target.files[0];
} catch (ex) {}
const formData = new FormData();
formData.append("file", file);
const response = await fetch(
"https://prasi.avolut.com/_proxy/https%3A%2F%2Feam.avolut.com%2F_upload",
{
method: "POST",
body: formData,
}
);
if (response.ok) {
const contentType: any = response.headers.get("content-type");
let result;
if (contentType.includes("application/json")) {
result = await response.json();
} else if (contentType.includes("text/plain")) {
result = await response.text();
} else {
result = await response.blob();
}
if (Array.isArray(result)) {
fm.data[field.name] = get(result, "[0]");
fm.render();
} else {
alert("Error upload");
}
} else {
}
}}
/> */}
</>
) : type_field === "money" ? (
<>
<FieldMoney field={field} fm={fm} prop={prop} />
</>
) : type_field === "rich-text" ? (
<>
<FieldRichText field={field} fm={fm} prop={prop} />
</>
) : (
<input
type={prop.type}
type={type_field}
onChange={(ev) => {
console.log("onchange");
if (["date", "datetime", "datetime-local"].includes(type_field)) {
let result = null;
try {
result = new Date(ev.currentTarget.value);
} catch (ex) {}
fm.data[field.name] = result;
} else {
fm.data[field.name] = ev.currentTarget.value;
}
fm.render();
}}
value={value || ""}
value={value}
disabled={field.disabled}
className="c-flex-1 c-bg-transparent c-outline-none c-px-2 c-text-sm c-w-full"
spellCheck={false}
onFocus={() => {
field.focused = true;
display = "halo dek";
field.render();
}}
onBlur={() => {
@ -75,3 +181,8 @@ export const FieldTypeText: FC<{
</>
);
};
const isTimeString = (time: any) => {
// Regex untuk deteksi string ini tipenya time
const timePattern = /^([01]\d|2[0-3]):([0-5]\d)(:[0-5]\d)?$/;
return timePattern.test(time);
};

View File

@ -0,0 +1,83 @@
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;
fm: FMLocal;
arg: FieldProp;
}> = ({ 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 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</>
}
return (
<>
<div
className={cx(
"c-flex c-items-center c-justify-start c-w-full"
)}
>
<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={cx(
"c-relative",
css`
input:checked ~ .dot {
transform: translateX(100%);
background-color: #48bb78;
}
`
)}
>
<input type="checkbox" id="toggleB" checked={checked} className="c-sr-only" onChange={(e) => {
const check = e.target.checked;
if(check){
fm.data[field.name] = local.value[1];
}else{
fm.data[field.name] = local.value[0];
}
fm.render();
console.log({data: fm.data})
// if(val) value = local.list[0];
// value = local.list[1]
}}/>
<div className="c-block c-bg-gray-600 c-w-8 c-h-5 c-rounded-full"></div>
<div
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"
)}
></div>
</div>
<div className="c-ml-3 c-text-gray-700 c-font-medium">{get(local,"list[1].label")}</div>
</label>
</div>
</>
);
};

View File

@ -0,0 +1,164 @@
import { FC } from "react";
import { FMLocal, FieldLocal } from "../../typings";
import { useLocal } from "@/utils/use-local";
import parser from "any-date-parser";
import { format } from "date-fns";
import get from "lodash.get";
import { argv } from "bun";
import { PropTypeText } from "./TypeText";
export const FieldUpload: FC<{
field: FieldLocal;
fm: FMLocal;
prop: PropTypeText;
}> = ({ field, fm, prop }) => {
let type_field = prop.sub_type;
let value: any = fm.data[field.name];
const input = useLocal({
value: 0 as any,
display: false as any,
ref: null as any,
drop: false as boolean
});
let display: any = null;
return (
<div className="c-flex-grow c-flex-row c-flex c-w-full c-h-full">
<div
onDrop={(e: any) => {
console.log({ e });
e.preventDefault();
input.drop = false;
input.render();
}}
onDragOver={(e:any) => {
console.log("File(s) in drop zone");
// Prevent default behavior (Prevent file from being opened)
e.preventDefault();
input.drop = true;
input.render();
}}
className={cx(input.drop ? "c-bg-gray-100": "", "hover:c-bg-gray-100 c-m-1 c-relative c-flex-grow c-p-4 c-items-center c-flex c-flex-row c-text-gray-400 c-border c-border-gray-200 c-border-dashed c-rounded c-cursor-pointer")}
>
<div className="c-flex-row c-flex c-flex-grow c-space-x-2">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 14 14"
>
<g
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M3.5 13.5h-2a1 1 0 0 1-1-1v-8h13v8a1 1 0 0 1-1 1h-2" />
<path d="M4.5 10L7 7.5L9.5 10M7 7.5v6M11.29 1a1 1 0 0 0-.84-.5h-6.9a1 1 0 0 0-.84.5L.5 4.5h13zM7 .5v4" />
</g>
</svg>
<div className="c-flex c-flex-col">
<span className="c-font-medium">
Drop Your File or{" "}
<span className="c-underline c-text-blue-500">Browse</span>
</span>
</div>
</div>
<input
ref={(ref) => (input.ref = ref)}
type="file"
multiple
onChange={async (event: any) => {
let file = null;
try {
file = event.target.files[0];
} catch (ex) {}
const formData = new FormData();
formData.append("file", file);
const response = await fetch(
"https://prasi.avolut.com/_proxy/https%3A%2F%2Feam.avolut.com%2F_upload",
{
method: "POST",
body: formData,
}
);
if (response.ok) {
const contentType: any = response.headers.get("content-type");
let result;
if (contentType.includes("application/json")) {
result = await response.json();
} else if (contentType.includes("text/plain")) {
result = await response.text();
} else {
result = await response.blob();
}
if (Array.isArray(result)) {
fm.data[field.name] = get(result, "[0]");
fm.render();
} else {
alert("Error upload");
}
} else {
}
}}
className={
"c-absolute c-w-full c-h-full c-cursor-pointer c-top-0 c-left-0 c-hidden"
}
/>
</div>
</div>
);
// console.log({ prop });
return (
<div className="c-flex-grow c-flex-row c-flex c-w-full c-h-full">
<div
className={cx(
input.display ? "c-hidden" : "",
"c-flex-grow c-px-2 c-flex c-flex-row c-items-center"
)}
onClick={() => {
if (input.ref) {
input.display = !input.display;
input.ref.focus();
input.render();
}
}}
>
{formatMoney(Number(value) || 0)}
</div>
<input
ref={(el) => (input.ref = el)}
type={"number"}
onClick={() => {}}
onChange={(ev) => {
fm.data[field.name] = ev.currentTarget.value;
fm.render();
}}
value={value}
disabled={field.disabled}
className={cx(
!input.display ? "c-hidden" : "",
"c-flex-1 c-bg-transparent c-outline-none c-px-2 c-text-sm c-w-full"
)}
spellCheck={false}
onFocus={() => {
field.focused = true;
field.render();
}}
onBlur={() => {
console.log("blur");
field.focused = false;
input.display = !input.display;
input.render();
field.render();
}}
/>
</div>
);
};
const formatMoney = (res: number) => {
const formattedAmount = new Intl.NumberFormat("id-ID", {
minimumFractionDigits: 0,
}).format(res);
return formattedAmount;
};

View File

@ -6,6 +6,7 @@ import { editorFormData } from "./utils/ed-data";
import { PropTypeText } from "./field/type/TypeText";
import { PropTypeRelation } from "./field/type/TypeRelation";
import { getProp } from "../../..";
import { PropTypeSwitch } from "./field/type/TypeSwitch";
export type FMProps = {
on_init: (arg: { fm: FMLocal; submit: any; reload: any }) => any;
@ -32,7 +33,7 @@ export type FieldProp = {
desc?: string;
props?: any;
fm: FMLocal;
type: "text" | "relation";
type: "text" | "relation" | "switch" | "input" | "single-option"| "multi-option";
// | "number"
// | "textarea"
// | "dropdown"
@ -59,6 +60,10 @@ export type FieldProp = {
_item: any;
_sync: any;
custom?: () => CustomField;
on_load: () => any | Promise<any>;
on_row: (row: any) => string;
pk: string;
sub_type: string;
};
export type FMInternal = {
@ -103,6 +108,7 @@ type FieldInternalProp = {
text: PropTypeText;
number: PropTypeText;
relation: PropTypeRelation;
switch: PropTypeSwitch;
};
export type FieldInternal<T extends FieldProp["type"]> = {

View File

@ -22,7 +22,6 @@ export const genFieldMitem = (arg: {
?.get("child")
?.get("content")
?.get("childs");
let component = fieldMapping[field.type as "text"];
if (!component) {

View File

@ -0,0 +1,59 @@
import { useLocal } from "@/utils/use-local";
import { FC, MouseEvent } from "react";
import ExcelJS from "exceljs";
import * as FileSaver from "file-saver";
export const ExportExcel: FC<{
data: any[],
fileName?: string
}> = ({
data, fileName = "exported_data.xlsx"
}): JSX.Element => {
const local = useLocal({
data: [] as any[]
});
local.data = data;
local.render();
const getAllKeys = (arr: Array<Record<string, any>>): string[] => {
const keysSet = new Set<string>();
arr.forEach(obj => {
Object.keys(obj).forEach(key => keysSet.add(key));
});
return Array.from(keysSet);
};
const handleExport = async (e: MouseEvent<HTMLButtonElement>) => {
try {
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet("Sheet 1");
const columns = getAllKeys(local.data);
worksheet.addRow(columns);
local.data.forEach((row) => {
const values = columns.map((col) => row[col]);
worksheet.addRow(values);
});
const buffer = await workbook.xlsx.writeBuffer();
const blob = new Blob([buffer], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
});
FileSaver.saveAs(blob, fileName);
console.log("Data exported");
} catch (error) {
console.error("Error exporting data:", error);
}
};
return (
<div>
<button onClick={handleExport} style={{background: '#00ffff'}}>Export</button>
</div>
);
};

229
comps/list/ImportExcel.tsx Normal file
View File

@ -0,0 +1,229 @@
import { GFCol } from "@/gen/utils";
import { useLocal } from "@/utils/use-local";
import { ChangeEvent, FC, MouseEvent } from "react";
import * as XLSX from "xlsx";
type ImportExcelProps = {
gen_fields: string[];
gen_table: string;
};
export const ImportExcel: FC<ImportExcelProps> = ({
gen_fields,
gen_table,
}) => {
const local = useLocal({
data: [] as any[],
columns: [] as string[],
fields: [] as string[],
tableName: "",
selectedRows: [] as {
pk: string | number;
rows: any;
}[],
pk: null as null | GFCol,
columnMappings: [] as Array<Record<string, string>>,
});
const pk = local.pk?.name || "id";
const getAllKeys = (arr: Array<Record<string, any>>): string[] => {
const keysSet = new Set<string>();
arr.forEach((obj) => {
Object.keys(obj).forEach((key) => keysSet.add(key));
});
return Array.from(keysSet);
};
const handleFileUpload = (e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (event: ProgressEvent<FileReader>) => {
if (!event.target?.result) return;
const workbook = XLSX.read(event.target.result, { type: "binary" });
const sheetName = workbook.SheetNames[0];
const sheet = workbook.Sheets[sheetName];
const sheetData = XLSX.utils.sheet_to_json(sheet);
local.data = sheetData;
local.columns = getAllKeys(local.data);
gen_fields.forEach((data: any) => {
local.fields.push(JSON.parse(data).name);
});
local.tableName = gen_table;
local.render();
};
reader.readAsBinaryString(file);
};
const executeImport = async (
columnMappings: string[],
data: {
pk: string | number;
rows: any;
}[]
) => {
const table = (db as any)[local.tableName];
if (table) {
// execute
}
};
const headerCheckboxClick = (e: ChangeEvent<HTMLInputElement>) => {
if (e.target.checked) {
local.data.forEach((data) => {
local.selectedRows.push({
pk: data[pk],
rows: data,
});
});
local.render();
console.log("Select All", local.selectedRows);
} else {
local.selectedRows = [];
local.render();
console.log("Deselect all", local.selectedRows);
}
};
const checkboxClick = (rowId: any) => (e: MouseEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
const checked = !!local.selectedRows.find((data) => data.pk === rowId);
console.log(checked);
if (!checked) {
const checkedRowData = local.data.filter((row) => row[pk] === rowId);
local.selectedRows.push({
pk: rowId,
rows: checkedRowData,
});
local.render();
console.log("selected", local.selectedRows);
} else {
local.selectedRows = local.selectedRows.filter(
(data) => data.pk !== rowId
);
local.render();
console.log("deselected", local.selectedRows);
}
};
const columnMappingChange =
(col: string) => (e: ChangeEvent<HTMLSelectElement>) => {
const selectedValue = e.target.value;
local.columnMappings.push({
column: col,
selectedColumn: selectedValue,
});
local.render();
console.log("column mappings", local.columnMappings);
};
const isRowChecked = (id: any) => {
return local.selectedRows.some((checked) => checked.pk === id);
};
const isConfirmed = (e: MouseEvent<HTMLButtonElement>) => {
console.log("selected rows", local.selectedRows);
console.log("column mappings", local.columnMappings);
};
return (
<div>
<input type="file" onChange={handleFileUpload} />
{local.data.length > 0 && (
<div>
<button
onClick={isConfirmed}
style={{
backgroundColor: "#4CAF50",
border: "none",
color: "white",
padding: "15px 32px",
textAlign: "center",
textDecoration: "none",
display: "inline-block",
fontSize: "16px",
margin: "4px 2px",
cursor: "pointer",
borderRadius: "10px",
}}
>
Confirm Import
</button>
<h2>Imported Data:</h2>
<table style={{ borderCollapse: "collapse", width: "100%" }}>
<thead>
<tr>
<th style={{ border: "1px solid black", padding: "8px" }}>
<input type="checkbox" onChange={headerCheckboxClick} />
</th>
{local.columns.map((col) => (
<th
key={col}
style={{ border: "1px solid black", padding: "8px" }}
>
{col}{" "}
<select
onChange={columnMappingChange(col)}
style={{
marginLeft: "8px",
padding: "4px",
borderRadius: "4px",
border: "1px solid #ccc",
}}
>
<option value="">Select Field</option>
{local.fields.map((field) => (
<option key={field} value={field}>
{field}
</option>
))}
</select>
</th>
))}
</tr>
</thead>
<tbody>
{local.data.map((row, index) => (
<tr key={index}>
<td style={{ border: "1px solid black", padding: "8px" }}>
<div
onClick={checkboxClick(row[pk])}
className={cx(
css`
width: 100%;
height: 100%;
`,
"c-flex c-items-center c-justify-center"
)}
>
<input
className="c-pointer-events-none"
type="checkbox"
checked={isRowChecked(row[pk])}
/>
</div>
</td>
{local.columns.map((col) => (
<td
key={col}
style={{ border: "1px solid black", padding: "8px" }}
>
{row[col]}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
);
};

View File

@ -3,11 +3,15 @@ import { fields_map } from "@/utils/format-value";
import { useLocal } from "@/utils/use-local";
import get from "lodash.get";
import { Loader2 } from "lucide-react";
import { FC, useEffect } from "react";
import { ChangeEvent, FC, MouseEvent, useEffect, useState } from "react";
import DataGrid, {
ColumnOrColumnGroup,
Row,
SortColumn,
SelectColumn,
textEditor,
RenderCheckboxProps,
SELECT_COLUMN_KEY,
} from "react-data-grid";
import "react-data-grid/lib/styles.css";
import { createPortal } from "react-dom";
@ -21,9 +25,9 @@ type OnRowClick = (arg: {
row: any;
rows: any[];
idx: any;
event: React.MouseEvent<HTMLDivElement, MouseEvent>;
event: React.MouseEvent<HTMLDivElement>;
}) => void;
type SelectedRow = (arg: { row: any; rows: any[]; idx: any }) => boolean;
type TableListProp = {
child: any;
PassProp: any;
@ -34,28 +38,60 @@ type TableListProp = {
paging: { take: number; skip: number };
mode: "count" | "query";
}) => Promise<any[]>;
mode: "table" | "list" | "grid";
on_init: (arg?: any) => any;
mode: "table" | "list" | "grid" | "auto";
_meta: Record<string, any>;
gen_fields: string[];
row_click: OnRowClick;
selected: SelectedRow;
id_parent?: string;
feature?: Array<any>;
filter_name: string;
};
const w = window as any;
const selectCellClassname = css`
display: flex;
align-items: center;
justify-content: center;
> input {
margin: 0;
}
`;
export const TableList: FC<TableListProp> = ({
name,
on_load,
child,
PassProp,
mode,
on_init,
_meta,
gen_fields,
row_click,
selected,
id_parent,
feature,
filter_name,
}) => {
console.log( `prasi_filter.${filter_name}`)
const where = get(w, `prasi_filter.${filter_name}`);
console.log(w.prasi_filter)
if (mode === "auto") {
if (w.isMobile) {
mode = "list";
} else {
mode = "table";
}
}
const local = useLocal({
selectedRows: [] as {
pk: string | number;
rows: any;
}[],
el: null as null | HTMLDivElement,
width: 0,
height: 0,
selectedRowIds: [] as (string | number)[],
rob: new ResizeObserver(([e]) => {
local.height = e.contentRect.height;
local.width = e.contentRect.width;
@ -66,6 +102,7 @@ export const TableList: FC<TableListProp> = ({
scrolled: false,
data: [] as any[],
status: "init" as "loading" | "ready" | "resizing" | "reload" | "init",
where: null as any,
paging: {
take: 0,
skip: 0,
@ -134,10 +171,12 @@ export const TableList: FC<TableListProp> = ({
>,
},
});
// code ini digunakan untuk mengambil nama dari pk yang akan digunakan sebagai key untuk id
const pk = local.pk?.name || "id";
useEffect(() => {
if (isEditor) return;
(async () => {
on_init(local);
if (local.status === "reload" && typeof on_load === "function") {
local.status = "loading";
local.render();
@ -145,13 +184,13 @@ export const TableList: FC<TableListProp> = ({
const orderBy = local.sort.orderBy || undefined;
const load_args: any = {
async reload() {},
where,
orderBy,
paging: { take: local.paging.take, skip: local.paging.skip },
};
if (id_parent) {
load_args.paging = {};
}
const result = on_load({ ...load_args, mode: "query" });
const callback = (data: any[]) => {
if (local.paging.skip === 0) {
@ -172,11 +211,67 @@ export const TableList: FC<TableListProp> = ({
child,
"props.meta.item.component.props.child.content.childs"
);
let childs: any[] = [];
let checkedBox: any[] = [];
let sub_name = "fields";
if (mode === "table") sub_name = "columns";
// if (mode === "table") sub_name = "columns";
switch (mode) {
case "table":
sub_name = "tbl-col";
break;
case "list":
sub_name = "md-list";
break;
}
// passing local data ke variable baru (biar gak panjang nulisnya hehe)
let rowData = local.data;
// function untuk menghandle checkbox di header (digunakan untuk check all)
const headerCheckboxClick = (e: ChangeEvent<HTMLInputElement>) => {
if (e.target.checked) {
// jika checbox checked, maka semua rowData akan dimasukkan ke dalam local selected rows
rowData.forEach((data) => {
local.selectedRows.push({
pk: data[pk],
rows: data,
});
});
local.render();
} else {
// jika tidak, maka local selected rows akan dikosongkan
local.selectedRows = [];
local.render();
}
};
// function untuk menghandle checkbox pada setiap row (digunakan untuk check setiap rowData)
const checkboxClick = (rowId: any) => (e: MouseEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
const checked = !!local.selectedRows.find((data) => data.pk === rowId);
console.log(checked);
if (!checked) {
// jika checkbox tercheck, maka rowData akan diambil jika memiliki id yang sama dengan rowId yang dikirim
const checkedRowData = rowData.filter((row) => row[pk] === rowId);
local.selectedRows.push({
pk: rowId,
rows: checkedRowData,
});
local.render();
console.log("selected", local.selectedRows);
} else {
// jika tidak, maka akan dihapus
local.selectedRows = local.selectedRows.filter(
(data) => data.pk !== rowId
);
local.render();
console.log("deselected", local.selectedRows);
}
};
const mode_child = raw_childs.find(
(e: any) => e.name === sub_name || e.name === mode
@ -187,12 +282,57 @@ export const TableList: FC<TableListProp> = ({
childs = meta.item.childs;
}
}
const columns: ColumnOrColumnGroup<any>[] = [];
let columns: ColumnOrColumnGroup<any>[] = [];
let isCheckbox = false;
try {
if (feature?.find((e) => e === "checkbox")) isCheckbox = true;
} catch (e) {}
if (childs.length && isCheckbox) {
columns.push({
key: SELECT_COLUMN_KEY,
name: "",
width: 35,
minWidth: 35,
maxWidth: 35,
resizable: false,
sortable: false,
frozen: true,
renderHeaderCell(props) {
return <input type="checkbox" onChange={headerCheckboxClick} />;
},
renderCell(props) {
// digunakan untuk mengecek apakah local selected rows memiliki pk dari props.row.id
const isChecked = local.selectedRows.some(
(checked) => checked.pk === props.row.id
);
return (
<div
onClick={checkboxClick(props.row.id)}
className={cx(
css`
width: 100%;
height: 100%;
`,
"c-flex c-items-center c-justify-center"
)}
>
<input
className="c-pointer-events-none"
type="checkbox"
checked={isChecked}
/>
</div>
);
},
headerCellClass: selectCellClassname,
cellClass: selectCellClassname,
});
}
for (const child of childs) {
const key = getProp(child, "name", {});
const name = getProp(child, "title", {});
const width = parseInt(getProp(child, "width", {}));
columns.push({
key,
name,
@ -217,6 +357,10 @@ export const TableList: FC<TableListProp> = ({
},
});
}
if (mode === "list") {
// ambil satu index saja;
if (columns.length > 1) columns = columns.slice(0, 0 + 1);
}
if (local.status === "resizing" && !isEditor) {
local.status = "ready";
@ -264,6 +408,7 @@ export const TableList: FC<TableListProp> = ({
if (local.data.length === 0) {
const load_args: any = {
async reload() {},
where,
paging: { take: local.paging.take, skip: local.paging.skip },
};
@ -281,7 +426,6 @@ export const TableList: FC<TableListProp> = ({
if (id_parent && local.pk && local.sort.columns.length === 0) {
data = sortTree(local.data, id_parent, local.pk.name);
}
if (mode === "table") {
return (
<div
@ -342,7 +486,12 @@ export const TableList: FC<TableListProp> = ({
: {
renderRow(key, props) {
const is_selected = selected_idx === props.rowIdx;
const isSelect = selected({
idx: props.rowIdx,
row: props.row,
rows: local.data,
});
// return "halo"
return (
<Row
key={key}
@ -360,10 +509,10 @@ export const TableList: FC<TableListProp> = ({
});
}
}}
isRowSelected={is_selected}
isRowSelected={true}
className={cx(
props.className,
is_selected && "row-selected"
isSelect && "row-selected"
)}
/>
);
@ -390,6 +539,78 @@ export const TableList: FC<TableListProp> = ({
</div>
</div>
);
} else if (mode === "list") {
return (
<div
className={cx(
"c-w-full c-h-full c-flex-1 c-relative c-overflow-hidden",
dataGridStyle(local)
)}
ref={(el) => {
if (!local.el && el) {
local.el = el;
local.rob.observe(el);
}
}}
>
<div className="c-absolute c-inset-0">
{toaster_el && createPortal(<Toaster cn={cn} />, toaster_el)}
{local.status === "init" ? (
<DataGrid
style={{ opacity: 0 }}
columns={[
{
key: "_",
name: "",
renderCell({ rowIdx }) {
if (local.paging.take < rowIdx) {
local.paging.take = rowIdx;
}
clearTimeout(local.paging.timeout);
local.paging.timeout = setTimeout(() => {
local.status = "reload";
local.paging.take = local.paging.take * 5;
local.render();
}, 100);
return <></>;
},
},
]}
rows={genRows(200)}
/>
) : (
<>
<div
className="w-full h-full overflow-y-auto"
onScroll={local.paging.scroll}
>
{data.map((e, idx) => {
return (
<div
className="flex-grow hover:c-bg-[#e2f1ff]"
onClick={(ev) => {
if (!isEditor && typeof row_click === "function") {
row_click({
event: ev,
idx: idx,
row: e,
rows: local.data,
});
}
}}
>
<PassProp idx={idx} row={e} col={{}} rows={local.data}>
{child}
</PassProp>
</div>
);
})}
</div>
</>
)}
</div>
</div>
);
} else {
}
};

View File

@ -26,21 +26,21 @@ export const MDAction: FC<{ md: MDLocal; PassProp: any; child: any }> = ({
});
}
}
return (
<div
className={cx(
"c-flex c-flex-row c-space-x-1 c-items-stretch c-self-stretch c-h-full"
)}
>
{md.actions.map((e, idx) => {
{child}
{/* {md.actions.map((e, idx) => {
if (isValidElement(e)) {
return <Fragment key={idx}>{e}</Fragment>;
}
if (typeof e === "object" && (e.action || e.label)) {
return <PassProp item={e}>{child}</PassProp>;
}
})}
})} */}
</div>
);
};
@ -62,7 +62,6 @@ const refreshActionInternal = (md: MDLocal, callback: () => void) => {
mode = md.tab.active;
}
}
if (mode === "master") {
const master_bread = getProp(md.master.internal, "actions", { md });
if (master_bread instanceof Promise) {

View File

@ -4,7 +4,7 @@ import { MDLocal } from "./utils/typings";
import { BreadItem } from "../custom/Breadcrumb";
export const refreshBread = (md: MDLocal) => {
const bread = useLocal({
const local = useLocal({
crumb: null as any,
last_render: Date.now(),
selected: null as any,
@ -12,16 +12,16 @@ export const refreshBread = (md: MDLocal) => {
if (isEditor) {
refreshBreadInternal(md, () => {
if (Date.now() - bread.last_render > 1000) {
if (Date.now() - local.last_render > 1000) {
md.render();
}
bread.last_render = Date.now();
local.last_render = Date.now();
});
} else {
if (bread.crumb !== md.breadcrumb || md.selected !== bread.selected) {
if (local.crumb !== md.breadcrumb || md.selected !== local.selected) {
refreshBreadInternal(md, () => {
bread.crumb = md.breadcrumb;
bread.selected = md.selected;
local.crumb = md.breadcrumb;
local.selected = md.selected;
md.render();
});
}
@ -29,8 +29,8 @@ export const refreshBread = (md: MDLocal) => {
};
const refreshBreadInternal = (md: MDLocal, callback: () => void) => {
let mode = "master";
let mode = "master";
if (isEditor) {
mode = md.props.editor_tab;
} else {
@ -41,11 +41,12 @@ const refreshBreadInternal = (md: MDLocal, callback: () => void) => {
mode = md.tab.active;
}
}
if (mode === "master") {
const master_bread = getProp(md.master.internal, "breadcrumb", { md });
if (master_bread instanceof Promise) {
master_bread.then((e) => done(md, e, callback));
master_bread.then((e) => {
done(md, e, callback);
});
} else {
done(md, master_bread, callback);
}

View File

@ -5,6 +5,5 @@ import { MDLocal, MDRef } from "./utils/typings";
export const MDHeader: FC<{ md: MDLocal; mdr: MDRef }> = ({ md, mdr }) => {
const head = get(mdr.child, "props.meta.item.component.props.header.content");
const PassProp = mdr.PassProp;
return <PassProp md={md}>{head}</PassProp>;
};

32
comps/md/MDMaster.tsx Executable file
View File

@ -0,0 +1,32 @@
import { useLocal } from "@/utils/use-local";
import { FC, Fragment, isValidElement } from "react";
import { getProp } from "./utils/get-prop";
import { MDActions, MDLocal } from "./utils/typings";
const w = window as unknown as {
md_panel_master: any;
};
export const MDMaster: FC<{ size: any; min_size: any; md: MDLocal;child: any; on_init: () => void }> = ({child,on_init, min_size,size, md
}) => {
let result = on_init();
let width = 0;
let min_width = 0;
try {
width = Number(size) || 0;
min_width = Number(min_size) || 0;
} catch (e: any) {
}
w.md_panel_master = JSON.stringify({
size: width,
min_size: min_width
})
md.panel.min_size = min_width;
md.panel.size = width;
return (
<>
{child}
</>
);
};

View File

@ -1,5 +1,5 @@
import { FC } from "react";
import { MDLocal, MDRef } from "./utils/typings";
import { MDLocal, MDRef, w } from "./utils/typings";
import { MDHeader } from "./MDHeader";
export const should_show_tab = (md: MDLocal) => {
@ -15,7 +15,6 @@ export const MDTab: FC<{ md: MDLocal; mdr: MDRef }> = ({ md, mdr }) => {
if (!detail) {
return null;
}
return (
<>
{md.props.show_head === "only-child" && <MDHeader md={md} mdr={mdr} />}
@ -68,7 +67,9 @@ export const MDNavTab: FC<{ md: MDLocal; mdr: MDRef }> = ({ md, mdr }) => {
)}
key={tab_name}
onClick={() => {
if (isEditor) return;
if (isEditor) {
return;
}
md.tab.active = tab_name;
md.params.apply();
md.render();

View File

@ -1,4 +1,5 @@
import { useLocal } from "@/utils/use-local";
import get from "lodash.get";
import { FC, useEffect, useRef } from "react";
import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";
import { refreshBread } from "./MDBread";
@ -9,11 +10,7 @@ import {
masterDetailParseHash as masterDetailParseParams,
} from "./utils/md-hash";
import { masterDetailInit, masterDetailSelected } from "./utils/md-init";
import { MDLocal, MDLocalInternal, MDRef } from "./utils/typings";
const w = window as unknown as {
generating_prasi_md: Record<string, true>;
};
import { MDLocal, MDLocalInternal, MDRef, w } from "./utils/typings";
export const MasterDetail: FC<{
child: any;
@ -26,7 +23,11 @@ export const MasterDetail: FC<{
gen_fields: any;
gen_table: string;
on_init: (md: MDLocal) => void;
_sync: any;
_item: any;
// _meta: any;
}> = ({
_item,
PassProp,
child,
name,
@ -39,13 +40,17 @@ export const MasterDetail: FC<{
on_init,
}) => {
let isGenerate = false as Boolean;
try {
if (!get(w.md_internal, _item.id)) w.md_internal = {
...w.md_internal,
[_item.id]: _item
};
isGenerate = false;
if (w.generating_prasi_md["master_detail"]) {
isGenerate = true;
}
} catch (ex) {}
const _ref = useRef({ PassProp, child });
const md = useLocal<MDLocalInternal>({
name,
@ -78,7 +83,13 @@ export const MasterDetail: FC<{
},
},
master: { internal: null, render() {} },
panel: {
size: 25,
min_size: 0,
},
});
const local = useLocal({ init: false });
if (isEditor) {
md.props.mode = mode;
@ -98,7 +109,6 @@ export const MasterDetail: FC<{
}, [editor_tab]);
refreshBread(md);
if (!local.init || isEditor) {
local.init = true;
masterDetailInit(md, child, editor_tab);
@ -116,6 +126,7 @@ export const MasterDetail: FC<{
)}
{md.props.mode === "full" && <ModeFull md={md} mdr={_ref.current} />}
{md.props.mode === "v-split" && <ModeVSplit md={md} mdr={_ref.current} />}
{md.props.mode === "h-split" && <ModeHSplit md={md} mdr={_ref.current} />}
</div>
);
};
@ -124,6 +135,7 @@ const ModeFull: FC<{ md: MDLocal; mdr: MDRef }> = ({ md, mdr }) => {
if (should_show_tab(md)) {
return <MDTab md={md} mdr={mdr} />;
}
return (
<>
{!md.selected && <Master md={md} mdr={mdr} />}
@ -136,7 +148,11 @@ const ModeVSplit: FC<{ md: MDLocal; mdr: MDRef }> = ({ md, mdr }) => {
return (
<div className={cx("c-flex-1")}>
<PanelGroup direction="vertical">
<Panel className="c-border-b">
<Panel
className="c-border-b"
defaultSize={md.panel.size}
minSize={md.panel.min_size}
>
<Master md={md} mdr={mdr} />
</Panel>
<>
@ -160,10 +176,40 @@ const ModeVSplit: FC<{ md: MDLocal; mdr: MDRef }> = ({ md, mdr }) => {
</div>
);
};
const ModeHSplit: FC<{ md: MDLocal; mdr: MDRef }> = ({ md, mdr }) => {
return (
<div className={cx("c-flex-1")}>
<PanelGroup direction="horizontal">
<Panel
className="c-border-r"
defaultSize={md.panel.size}
minSize={md.panel.min_size}
>
<Master md={md} mdr={mdr} />
</Panel>
<>
<PanelResizeHandle />
<Panel
className="c-flex c-flex-col c-items-stretch c-w-10"
defaultSize={
parseInt(localStorage.getItem(`prasi-md-h-${md.name}`) || "") ||
undefined
}
onResize={(e) => {
if (e < 80) {
localStorage.setItem(`prasi-md-h-${md.name}`, e.toString());
}
}}
>
<MDTab md={md} mdr={mdr} />
</Panel>
</>
</PanelGroup>
</div>
);
};
const Master: FC<{ md: MDLocal; mdr: MDRef }> = ({ md, mdr }) => {
const PassProp = mdr.PassProp;
return (
<>
{md.props.show_head === "only-master" && <MDHeader md={md} mdr={mdr} />}

View File

@ -1,4 +1,4 @@
import { MDLocal } from "./typings";
import { MDLocal, w } from "./typings";
import { getProp } from "./get-prop";
import get from "lodash.get";
import { parseGenField } from "@/gen/utils";
@ -12,6 +12,7 @@ export const masterDetailInit = (
child,
"props.meta.item.component.props.child.content.childs"
);
if (Array.isArray(childs)) {
md.master.internal = null;
md.childs = {};
@ -51,7 +52,6 @@ export const masterDetailInit = (
hide() {},
show() {},
render() {},
data: {},
};
}
}

View File

@ -1,7 +1,16 @@
import { BreadItem } from "@/comps/custom/Breadcrumb";
import { FMLocal } from "@/comps/form/typings";
import { GFCol } from "@/gen/utils";
import { ReactNode } from "react";
type ID_MASTER_DETAIL = string;
export const w = window as unknown as {
generating_prasi_md: Record<string, true>;
md_panel_master: any;
md_active_tab: Record<ID_MASTER_DETAIL, string>;
md_internal: Record<string, any>;
};
export type MDActions = {
action?: string;
label: ReactNode;
@ -44,9 +53,15 @@ export type MDLocalInternal = {
show: () => void;
render: () => void;
internal: any;
data: any;
fm?: FMLocal;
md?: MDLocal;
list?: any;
}
>;
panel: {
size: number;
min_size: number;
};
};
export type MDRef = { PassProp: any; child: any };
export type MDLocal = MDLocalInternal & { render: (force?: boolean) => void };
@ -108,6 +123,8 @@ export const MasterDetailType = `const md = {
render: () => void;
internal: any;
data: any;
fm?: fm;
md?: md;
}
>;
};`;

27
comps/ui/switch.tsx Executable file
View File

@ -0,0 +1,27 @@
import * as React from "react"
import * as SwitchPrimitives from "@radix-ui/react-switch"
import { cn } from "@/utils"
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"c-peer c-inline-flex c-h-6 c-w-11 c-shrink-0 c-cursor-pointer c-items-center c-rounded-full c-border-2 c-border-transparent c-transition-colors focus-visible:c-outline-none focus-visible:c-ring-2 focus-visible:c-ring-ring focus-visible:c-ring-offset-2 focus-visible:c-ring-offset-background disabled:c-cursor-not-allowed disabled:c-opacity-50 data-[state=checked]:c-bg-primary data-[state=unchecked]:c-bg-input",
className
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"c-pointer-events-none c-block c-h-5 c-w-5 c-rounded-full c-bg-background c-shadow-lg c-ring-0 c-transition-transform data-[state=checked]:c-translate-x-5 data-[state=unchecked]:c-translate-x-0"
)}
/>
</SwitchPrimitives.Root>
))
Switch.displayName = SwitchPrimitives.Root.displayName
export { Switch }

18
data.ts
View File

@ -1,10 +1,11 @@
export { FieldTypeCustom } from "@/comps/form/typings";
export { FieldTypeText } from "./comps/form/field/type/TypeText";
export { FieldTypeRelation } from "./comps/form/field/type/TypeRelation";
export { Form } from "@/comps/form/Form";
export { Field } from "@/comps/form/field/Field";
export { formType } from "@/comps/form/typings";
export { fieldType } from "@/comps/form/typings";
export {
FMLocal,
FieldTypeCustom,
fieldType,
formType,
} from "@/comps/form/typings";
export { TableList } from "@/comps/list/TableList";
export { TableListType } from "@/comps/list/typings";
export { MasterDetail } from "@/comps/md/MasterDetail";
@ -12,4 +13,11 @@ export { getProp } from "@/comps/md/utils/get-prop";
export { MasterDetailType } from "@/comps/md/utils/typings";
export { prasi_gen } from "@/gen/prasi_gen";
export { FormatValue } from "@/utils/format-value";
export { GetValue } from "@/utils/get-value";
export { FieldTypeRelation } from "./comps/form/field/type/TypeRelation";
export { FieldTypeSwitch } from "./comps/form/field/type/TypeSwitch";
export { FieldTypeText } from "./comps/form/field/type/TypeText";
export { MDAction } from "./comps/md/MDAction";
export { MDMaster } from "./comps/md/MDMaster";
export { ImportExcel } from "./comps/list/ImportExcel";
export { ExportExcel } from "./comps/list/ExportExcel";

View File

@ -12,3 +12,5 @@ export { Breadcrumb } from "./comps/custom/Breadcrumb";
export { Header } from "./comps/custom/Header";
export { Carousel } from "./comps/custom/Carousel";
export { Tree } from "./comps/list/Tree";
export {MasterFilter} from "./comps/filter/Filter"
export * from "./data";

View File

@ -0,0 +1,23 @@
import { MDLocal } from "@/comps/md/utils/typings";
import { codeBuild } from "../master_detail/utils";
import { GenFn } from "../utils";
export const gen_action: GenFn<{ name: string; on_load: () => MDLocal }> = async (
modify,
data,
arg: { name: string, on_load: () => MDLocal }
) => {
const md = arg.on_load
console.log("halo gen")
console.log({modify, data, arg, md: md()})
// const res = await codeBuild({
// div: `\
// <>
// {md.tab.active === "${arg.name}" && (
// <div {...props} className={cx(props.className, "")}>
// {children}
// </div>
// )}
// </>
// `,
// });
};

View File

@ -0,0 +1,81 @@
import capitalize from "lodash.capitalize";
import { GFCol, createItem, parseGenField } from "../utils";
import { on_load } from "./on_load";
import { codeBuild, codeBuildTest } from "../master_detail/utils";
// import * as Excel from "exceljs";
import ExcelJS from "exceljs";
import * as FileSaver from "file-saver";
export const gen_export = async (
modify: (data: any) => void,
data: any,
arg: { mode: "table" | "list" | "grid" | "auto"; id_parent?: string }
) => {
const table = JSON.parse(data.gen_table.value) as string;
const raw_fields = JSON.parse(data.gen_fields.value) as (
| string
| { value: string; checked: string[] }
)[];
const select = {} as any;
let pk = "";
let pks: Record<string, string> = {};
const fields = parseGenField(raw_fields);
for (const f of fields) {
select[f.name] = true;
if (f.relation) {
select[f.name] = {
select: {},
};
for (const r of f.relation.fields) {
select[f.name].select[r.name] = true;
}
}
if (f.is_pk) {
pk = f.name;
}
}
if (arg.id_parent) {
select[arg.id_parent] = true;
}
if (!pk) {
alert("Failed to generate! Primary Key not found. ");
return;
}
const keys = Object.keys(select);
const tableName = data.gen_table.value;
const selectFields = keys.join(", ");
try {
const result: any[] = await db.$queryRawUnsafe(
`SELECT ${selectFields} FROM ${tableName};`
);
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet("Sheet 1");
const columns = Object.keys(result[0]);
worksheet.addRow(columns);
result.forEach((row) => {
const values = columns.map((col) => row[col]);
worksheet.addRow(values);
});
const buffer = await workbook.xlsx.writeBuffer();
const blob = new Blob([buffer], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
});
FileSaver.saveAs(blob, "exported_data.xlsx");
console.log("Data exported");
} catch (error) {
console.error("Error exporting data:", error);
} finally {
await db.$disconnect();
}
};

View File

@ -1,12 +1,12 @@
import capitalize from "lodash.capitalize";
import { createItem, parseGenField } from "../utils";
import { GFCol, createItem, parseGenField } from "../utils";
import { on_load } from "./on_load";
import { codeBuild } from "../master_detail/utils";
export const gen_table_list = async (
modify: (data: any) => void,
data: any,
arg: { mode: "table" | "list" | "grid"; id_parent: string }
arg: { mode: "table" | "list" | "grid" | "auto"; id_parent?: string }
) => {
const table = JSON.parse(data.gen_table.value) as string;
const raw_fields = JSON.parse(data.gen_fields.value) as (
@ -29,7 +29,6 @@ export const gen_table_list = async (
select[f.name].select[r.name] = true;
}
}
if (f.is_pk) {
pk = f.name;
}
@ -56,7 +55,14 @@ export const gen_table_list = async (
result["child"] = data["child"];
let sub_name = "fields";
if (arg.mode === "table") sub_name = "columns";
switch (arg.mode) {
case "table":
sub_name = "tbl-col";
break;
case "list":
sub_name = "md-list";
break;
}
result["child"].content.childs = result["child"].content.childs.filter(
(e: any) => {
@ -68,8 +74,11 @@ export const gen_table_list = async (
const child = createItem({
name: sub_name,
childs: fields
.map((e) => {
if (e.is_pk) return;
.map((e, idx) => {
if (idx >= 1 && arg.mode === "list") {
return;
}
if (e.is_pk && arg.mode === "table") return;
let tree_depth = "";
let tree_depth_built = "";
if (first) {
@ -116,11 +125,28 @@ render(React.createElement("div", Object.assign({}, props, { className: cx(props
...result["child"].content.childs,
];
}
// detect row yang aktif
if (data["selected"]) {
result["selected"] = data["selected"];
result["selected"].value = `\
({ row, rows, idx }: SelectedRow) => {
try {
if (typeof md === "object") {
if (Array.isArray(md.selected)) {
if (md.selected.length) {
let select = md.selected.find((e) => e === row)
if(select) return true
}
} else {
if (md.selected === row) {
return true;
}
}
}
} catch (e) {
}
return false;
};
type SelectedRow = {

66
gen/master_detail/gen-action.ts Executable file
View File

@ -0,0 +1,66 @@
import get from "lodash.get";
import { GenMasterDetailArg, codeBuild } from "./utils";
import { createItem, formatName } from "../utils";
import { gen_form } from "../gen_form/gen_form";
export const genAtions = async (arg: GenMasterDetailArg, data: any) => {
console.log({ arg, data });
const header = get(data, "header");
const action_right = header.content.childs.find(
(e: any) => e.name === "right"
);
const list_action_right =
typeof action_right === "object" ? action_right.childs : [];
for (const c of get(data, "child.content.childs") || []) {
if (c.component?.id === "cb52075a-14ab-455a-9847-6f1d929a2a73") {
const name_tab = c.component.props.name.value.replaceAll('"', "");
const is_already = list_action_right.length
? list_action_right.some(
(action: any) =>
action.component.props.name.value.replaceAll(/`/g, "") ===
name_tab
)
: false;
if (!is_already) {
const res = await codeBuild({
show: `\
() => {
if (typeof md === "object") {
const tab_active = md.tab.active;
if (name === "master") {
if (tab_active === "" || tab_active === "master") return true;
} else {
if (tab_active === name) return true;
}
}
return false;
}
`,
});
// const childs = get(action_right, "childs") || [];
// childs.push(
// createItem({
// name: "action"
// })
// );
// const childs = get(data, "child.content.childs[3].childs") || [];
// console.log(childs)
// childs.push(
// createItem({
// name: "action",
// // component: {
// // id: "83a2859d-2f72-4e7d-a0c6-9d3368e1ed85",
// // props: {
// // show: res.show,
// // name: name_tab,
// // },
// // },
// })
// );
// console.log({action_right})
}
console.log(is_already);
}
}
};

View File

@ -4,8 +4,21 @@ import { createItem, formatName } from "../utils";
import { gen_form } from "../gen_form/gen_form";
export const genForm = async (arg: GenMasterDetailArg, data: any) => {
// tambahi kondisi, jika gk nemu component tab detail maka update tab detail atau bikin baru
const childs_form = data.child.content.childs || [];
const tab_form = childs_form.filter((c: any) => c.component?.id === "cb52075a-14ab-455a-9847-6f1d929a2a73" && c.component.props.name.value.replaceAll("\"","") === "detail")
for (const c of get(data, "child.content.childs") || []) {
if (c.component?.id === "cb52075a-14ab-455a-9847-6f1d929a2a73") {
// passing khusus tab detail, biar tab lain gk di rewrite.
let passing = false;
if(!tab_form.length){
passing = true;
}else{
const name_tab = c.component.props.name.value.replaceAll("\"","");
passing = name_tab === "detail"
}
if(passing){
const res = await codeBuild({
breadcrumb: `\
async () => {
@ -14,6 +27,7 @@ async () => {
label: "List ${formatName(arg.gen_table)}",
onClick: () => {
md.selected = null;
md.tab.active = "";
md.internal.action_should_refresh = true;
md.params.apply();
md.render();
@ -65,27 +79,36 @@ type ActionItem =
onClick?: (e: any) => Promise<void>;
}
| React.ReactNode`,
on_init: `\
async ({ submit, reload }: Init) => {
const tab = md.childs[md.tab.active];
if (tab) {
const actions = await getProp(tab.internal, "actions", { md });
if (Array.isArray(actions)) {
const save_btn = actions
.filter((e) => e)
.find((e) => e.action === "save");
if (save_btn) {
save_btn.onClick = async () => {
await submit();
};
md.actions = actions;
md.render();
}
}
}
};
// on_init: `\
// async ({ submit, reload }: Init) => {
// const tab = md.childs[md.tab.active];
// if (tab) {
// const actions = await getProp(tab.internal, "actions", { md });
// if (Array.isArray(actions)) {
// const save_btn = actions
// .filter((e) => e)
// .find((e) => e.action === "save");
// if (save_btn) {
// save_btn.onClick = async () => {
// await submit();
// };
// md.actions = actions;
// md.render();
// }
// }
// }
// };
type Init = { submit: () => Promise<boolean>; reload: () => void }
// type Init = { submit: () => Promise<boolean>; reload: () => void }
// `,
on_init: `\
async ({ submit, reload, fm }: Init) => {
if (typeof md === "object") {
md.childs["form"] = {} as any;
md.childs["form"]["fm"] = fm;
}
}
type Init = { submit: () => Promise<boolean>; reload: () => void; fm:any }
`,
});
const comp = createItem({
@ -102,7 +125,6 @@ type Init = { submit: () => Promise<boolean>; reload: () => void }
for (const [k, v] of Object.entries(comp.component.props)) {
c.component.props[k] = v;
}
const childs = get(c, "component.props.child.content.childs") || [];
childs.length = 0;
childs.push(
@ -131,4 +153,5 @@ type Init = { submit: () => Promise<boolean>; reload: () => void }
await gen_form(modify, data);
}
}
}
};

View File

@ -5,6 +5,7 @@ import { gen_table_list } from "../gen_table_list/gen_table_list";
export const genList = async (arg: GenMasterDetailArg, data: any) => {
for (const c of get(data, "child.content.childs") || []) {
if (c.component?.id === "c68415ca-dac5-44fe-aeb6-936caf8cc491") {
const res = await codeBuild({
row_click: `\
({ row, rows, idx, event }: OnRowClick) => {
@ -21,6 +22,32 @@ type OnRowClick = {
event: React.MouseEvent<HTMLDivElement, MouseEvent>;
}
`,
selected: `\
({ row, rows, idx }: SelectedRow) => {
try {
if (typeof md === "object") {
if (Array.isArray(md.selected)) {
if (md.selected.length) {
let select = md.selected.find((e) => e === row)
if(select) return true
}
} else {
if (md.selected === row) {
return true;
}
}
}
} catch (e) {
}
return false;
};
type SelectedRow = {
row: any;
rows: any[];
idx: any;
}`,
breadcrumb: `\
async () => {
return [{ label: "List ${formatName(arg.gen_table)}" }] as BreadItem[];
@ -75,9 +102,9 @@ type ActionItem =
name: arg.gen_table,
gen_table: arg.gen_table,
generate: "y",
selected: "",
on_load: "",
row_click: res.row_click,
selected: res.selected,
gen_fields: [JSON.stringify(arg.gen_fields)],
child: {
childs: [],

View File

@ -1,4 +1,5 @@
import { PropOptRaw, GenFn, parseGenField, parseOpt } from "../utils";
import { genAtions } from "./gen-action";
import { genForm } from "./gen-form";
import { genList } from "./gen-list";
import { GenMasterDetailArg } from "./utils";
@ -35,6 +36,4 @@ export const gen_master_detail: GenFn<GenMasterDetailArg> = async (
delete w.generating_prasi_md["master_detail"];
modify(result);
console.log("LAGI GENERATE");
};

View File

@ -7,6 +7,7 @@ export type GenMasterDetailArg = {
gen_feature: PropOptRaw;
gen_table: string;
gen_fields: PropOptRaw;
child:any
};
export const codeBuild = async <K extends Record<string, string>>(input: K) => {
@ -22,3 +23,18 @@ export const codeBuild = async <K extends Record<string, string>>(input: K) => {
return result as Record<keyof K, [string, string]>;
};
export const codeBuildTest = async (input: string) => {
const result = {} as any;
//@ts-ignore
const res = await _api.code_build(input);
return res;
// if (res) {
// for (const [k, v] of Object.entries(res) as any) {
// result[k] = [input[k], v];
// }
// }
// return result as Record<keyof K, [string, string]>;
};

View File

@ -1,13 +1,17 @@
import { gen_action } from "./gen_action/gen_action";
import { gen_form } from "./gen_form/gen_form";
import { gen_relation } from "./gen_relation/gen_relation";
import { gen_table_list } from "./gen_table_list/gen_table_list";
import { gen_export } from "./gen_table_list/gen_export";
import { gen_master_detail } from "./master_detail/gen";
import { gen_prop_fields } from "./prop/gen_prop_fields";
import { gen_props_table } from "./prop/gen_prop_table";
export const prasi_gen = {
actions_tab: gen_action,
master_detail: gen_master_detail,
table_list: gen_table_list,
export_excel: gen_export,
form: gen_form,
relation: gen_relation,
prop: {

View File

@ -48,7 +48,6 @@ export const gen_prop_fields = async (gen_table: string) => {
id_site = window.location.hostname;
}
const schema = getSchemaOnStorage(id_site, gen_table);
console.log({schema})
if (!schema) {
const result: {
label: string;
@ -155,13 +154,12 @@ export const gen_prop_fields = async (gen_table: string) => {
const saveSchemaOnStorage = (res: any, id_site: string, table: string) => {
let schemaSite = null;
let schema_master_detail: Record<string, any> = {}
const keys = `schema-md-${id_site}`
let schema_master_detail: Record<string, any> = {};
const keys = `schema-md-${id_site}`;
try {
let smd = localStorage.getItem(keys) as string;
schemaSite = JSON.parse(smd);
} catch (error) {
}
} catch (error) {}
try {
schema_master_detail = {
...schemaSite,
@ -171,16 +169,15 @@ const saveSchemaOnStorage = (res: any, id_site: string, table: string) => {
} catch (e: any) {
console.error(e.message);
}
}
};
const getSchemaOnStorage = (id_site: string, table: string) => {
const keys = `schema-md-${id_site}`
const keys = `schema-md-${id_site}`;
let schemaSite = null;
try {
let smd = localStorage.getItem(keys) as string;
schemaSite = JSON.parse(smd);
} catch (error) {
}
} catch (error) {}
const schema = get(schemaSite, `${table}`);
if (schema) return JSON.parse(schema);
return null
}
return null;
};

View File

@ -2,13 +2,11 @@ const cache: any = [];
export const gen_props_table = async () => {
if (cache.length > 0) return cache;
const tables = await db._schema.tables();
if (!Array.isArray(tables)) {
alert("WARNING: failed to get tables from app server");
return [];
}
const result = [{ value: "", label: "" }];
const final = [
...result,

View File

@ -10,7 +10,6 @@ export const FormatValue: FC<{
tree_depth?: number;
}> = (prop) => {
const { value, gen_fields, name, tree_depth } = prop;
if (gen_fields) {
const gf = JSON.stringify(gen_fields);
if (!fields_map.has(gf)) {
@ -22,7 +21,16 @@ export const FormatValue: FC<{
} else {
return {
...JSON.parse(e.value),
checked: e.checked.map(JSON.parse),
checked: e.checked.map((ex: any) => {
if(typeof ex === "string"){
return JSON.parse(e.value)
}
try{
return JSON.parse(ex["value"]);
}catch(em){
return null
}
}),
};
}
})

86
utils/get-value.tsx Executable file
View File

@ -0,0 +1,86 @@
import { GFCol } from "@/gen/utils";
import get from "lodash.get";
import { FC } from "react";
export const fields_map = new Map<string, (GFCol & { checked?: GFCol[] })[]>();
export const GetValue: FC<{
value: any;
name: string;
}> = (prop) => {
const { value, name } = prop;
return get(value, name)
return (
<div className="c-flex c-space-x-2 c-items-center">
{get(value, name)}
</div>
);
// if (gen_fields) {
// const gf = JSON.stringify(gen_fields);
// if (!fields_map.has(gf)) {
// fields_map.set(
// gf,
// gen_fields.map((e: any) => {
// if (typeof e === "string") {
// return JSON.parse(e);
// } else {
// return {
// ...JSON.parse(e.value),
// checked: e.checked.map(JSON.parse),
// };
// }
// })
// );
// }
// const fields = fields_map.get(gf);
// if (typeof value === "object" && value) {
// const rel = fields?.find((e) => e.name === name);
// if (rel && rel.checked) {
// const result = rel.checked
// .filter((e) => !e.is_pk)
// .map((e) => {
// return value[e.name];
// })
// .join(" - ");
// if (Array.isArray(value)) {
// const len = value.length;
// if (len === 0) return ` - `;
// return `${len} item${len > 1 ? "s" : ""}`;
// }
// return result;
// }
// return JSON.stringify(value);
// }
// }
// let prefix = <></>;
// if (typeof tree_depth === "number" && tree_depth > 0) {
// prefix = (
// <div
// className={css`
// padding-left: ${tree_depth * 5}px;
// `}
// >
// <div
// className={cx(
// " c-border-l c-border-b c-border-black c-w-[10px] c-h-[15px]",
// css`
// margin-top: -10px;
// `
// )}
// ></div>
// </div>
// );
// }
// return (
// <div className="c-flex c-space-x-2 c-items-center">
// {prefix}
// <div>{value}</div>
// </div>
// );
};