This commit is contained in:
rizky 2024-04-15 12:25:34 -07:00
parent 3bc6330379
commit c98e77afa6
24 changed files with 372 additions and 105 deletions

View File

@ -3,7 +3,7 @@ import { useLocal } from "@/utils/use-local";
import { ChevronDown } from "lucide-react";
import { FC, ReactNode } from "react";
export type OptionItem = { value: string; label: string; el?: ReactNode };
export type OptionItem = { value: any; label: string; el?: ReactNode };
export const RawDropdown: FC<{
options: OptionItem[];
@ -28,8 +28,18 @@ export const RawDropdown: FC<{
let filtered = options;
if (local.filter) {
filtered = options.filter((e) => {
if (e.label.toLowerCase().includes(local.filter)) return true;
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;
});
}
@ -118,7 +128,7 @@ export const RawDropdown: FC<{
{!isEditor && (
<input
spellCheck={false}
value={local.open ? local.input.value : "Halo"}
value={local.open ? local.input.value : ""}
className={cx(
"c-absolute c-inset-0 c-w-full c-h-full c-outline-none c-p-0",
disabled

View File

@ -17,13 +17,26 @@ export const TypeCustom: FC<{ field: FieldLocal; fm: FMLocal }> = ({
local.custom = field.custom;
}
if (!local.exec) {
local.exec = true;
const callback = (value: any, should_render: boolean) => {
local.result = value;
if (should_render) {
local.render();
setTimeout(() => {
local.exec = false;
}, 100);
}
};
if (field.custom) {
const res = local.custom();
if (res instanceof Promise) {
console.error("Custom Function cannot be async");
return null;
res.then((value) => {
callback(value, true);
});
} else {
local.result = res;
callback(res, false);
}
}
}

View File

@ -3,11 +3,13 @@ import { FC, useEffect } from "react";
import { FMLocal, FieldLocal } from "../../typings";
import { OptionItem, RawDropdown } from "../raw/Dropdown";
import { FieldLoading } from "../raw/FieldLoading";
import { sortTree } from "@/comps/list/sort-tree";
export type PropTypeRelation = {
type: "has-one" | "has-many";
on_load: (opt: { value?: any }) => Promise<{ items: any[]; pk: string }>;
label: (item: any, pk: string) => string;
id_parent: string;
};
export const FieldTypeRelation: FC<{
field: FieldLocal;
@ -35,7 +37,7 @@ export const FieldTypeRelation: FC<{
field.status = "ready";
input.render();
};
const res = prop.on_load({});
const res = prop.on_load({ value });
if (res instanceof Promise) res.then(callback);
else callback(res);
}
@ -43,12 +45,33 @@ export const FieldTypeRelation: FC<{
let list: OptionItem[] = [];
if (input.list && input.pk && input.list.length) {
for (const item of input.list) {
if (fm.field_def[field.name]?.optional) {
list.push({
value: null,
label: "-",
});
}
let sorted = input.list;
if (prop.id_parent && input.pk) {
sorted = sortTree(sorted, prop.id_parent, input.pk);
}
for (const item of sorted) {
if (typeof item !== "object") continue;
let label = "";
if (typeof prop.label === "function") {
label = prop.label(item, input.pk);
if (!label) {
const label_arr: string[] = [];
for (const [k, v] of Object.entries(item)) {
if (k !== input.pk) label_arr.push(v as any);
}
label = label_arr.join(" ");
}
} else {
const label_arr: string[] = [];
@ -67,7 +90,7 @@ export const FieldTypeRelation: FC<{
}
let selected = null;
if (typeof value === "object") {
if (value && typeof value === "object") {
if (input.pk) selected = value[input.pk];
} else {
selected = value;
@ -82,6 +105,11 @@ export const FieldTypeRelation: FC<{
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) {

View File

@ -151,6 +151,7 @@ export const formType = (active: { item_id: string }, meta: any) => {
data: typeof ___data;
reload: () => Promise<void>;
submit: () => Promise<void>;
render: () => void;
error: {
list: { name: string; error: string }[];
set: (name: string, error: string) => void;

View File

@ -14,13 +14,11 @@ export const formInit = (fm: FMLocal, props: FMProps) => {
const { on_load, sonar } = fm.props;
fm.error = formError(fm);
if (isEditor) {
fm.field_def = {};
const defs = parseGenField(fm.props.gen_fields);
for (const d of defs) {
fm.field_def[d.name] = d;
}
}
fm.reload = () => {
fm.status = isEditor ? "ready" : "loading";

View File

@ -14,6 +14,8 @@ import { createPortal } from "react-dom";
import { Toaster, toast } from "sonner";
import { getProp } from "../md/utils/get-prop";
import { Skeleton } from "../ui/skeleton";
import { sortTree } from "./sort-tree";
import { GFCol, parseGenField } from "@/gen/utils";
type OnRowClick = (arg: {
row: any;
@ -36,6 +38,7 @@ type TableListProp = {
_meta: Record<string, any>;
gen_fields: string[];
row_click: OnRowClick;
id_parent?: string;
};
export const TableList: FC<TableListProp> = ({
@ -47,6 +50,7 @@ export const TableList: FC<TableListProp> = ({
_meta,
gen_fields,
row_click,
id_parent,
}) => {
const local = useLocal({
el: null as null | HTMLDivElement,
@ -58,6 +62,7 @@ export const TableList: FC<TableListProp> = ({
if (local.status === "ready") local.status = "resizing";
local.render();
}),
pk: null as null | GFCol,
scrolled: false,
data: [] as any[],
status: "init" as "loading" | "ready" | "resizing" | "reload" | "init",
@ -91,9 +96,17 @@ export const TableList: FC<TableListProp> = ({
if (fields) {
const rel = fields?.find((e) => e.name === columnKey);
if (rel && rel.checked) {
should_set = false;
if (rel.type === "has-many") {
local.sort.orderBy = {
[columnKey]: {
_count: direction === "ASC" ? "asc" : "desc",
},
};
} else {
const field = rel.checked.find((e) => !e.is_pk);
if (field) {
should_set = false;
local.sort.orderBy = {
[columnKey]: {
[field.name]: direction === "ASC" ? "asc" : "desc",
@ -102,6 +115,7 @@ export const TableList: FC<TableListProp> = ({
}
}
}
}
if (should_set) {
local.sort.orderBy = {
@ -129,11 +143,14 @@ export const TableList: FC<TableListProp> = ({
local.render();
const orderBy = local.sort.orderBy || undefined;
const load_args = {
const load_args: any = {
async reload() {},
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[]) => {
@ -190,6 +207,7 @@ export const TableList: FC<TableListProp> = ({
col={{
name: props.column.key,
value: props.row[props.column.key],
depth: props.row.__depth || 0,
}}
rows={local.data}
>
@ -233,12 +251,23 @@ export const TableList: FC<TableListProp> = ({
}
const toaster_el = document.getElementsByClassName("prasi-toaster")[0];
if (local.status === "init") {
const fields = parseGenField(gen_fields);
for (const field of fields) {
if (field.is_pk) {
local.pk = field;
}
}
}
if (isEditor && local.status !== "ready") {
if (local.data.length === 0) {
const load_args = {
const load_args: any = {
async reload() {},
paging: { take: local.paging.take, skip: local.paging.skip },
};
if (id_parent) load_args.paging = {};
if (typeof on_load === "function") {
local.data = on_load({ ...load_args, mode: "query" }) as any;
}
@ -248,6 +277,11 @@ export const TableList: FC<TableListProp> = ({
let selected_idx = -1;
let data = local.data || [];
if (id_parent && local.pk && local.sort.columns.length === 0) {
data = sortTree(local.data, id_parent, local.pk.name);
}
if (mode === "table") {
return (
<div
@ -300,7 +334,7 @@ export const TableList: FC<TableListProp> = ({
sortColumns={local.sort.columns}
onSortColumnsChange={local.sort.on_change}
columns={columns}
rows={local.data || []}
rows={data}
onScroll={local.paging.scroll}
renderers={
local.status !== "ready"

83
comps/list/sort-tree.ts Executable file
View File

@ -0,0 +1,83 @@
export const sortTree = (list: any, parent_key: string, pk: string) => {
let meta = {} as Record<
string,
{ item: any; idx: string; depth: number; id_parent: any }
>;
if (list.length > 0 && !isEditor) {
const new_list = [];
const unlisted = {} as Record<string, any>;
for (const item of list) {
if (item[parent_key] === null) {
if (!meta[item[pk]]) {
meta[item[pk]] = {
item,
idx: new_list.length + "",
depth: 0,
id_parent: null,
};
item.__depth = 0;
new_list.push(item);
}
} else {
unlisted[item[pk]] = item;
}
}
let cyclic = {} as Record<string, number>;
while (Object.values(unlisted).length > 0) {
for (const item of Object.values(unlisted)) {
const parent = meta[item[parent_key]];
if (!cyclic[item[pk]]) {
cyclic[item[pk]] = 1;
} else {
cyclic[item[pk]]++;
}
if (cyclic[item[pk]] > 5) {
item.__depth = 0;
meta[item[pk]] = {
item,
depth: 0,
idx: new_list.length + "",
id_parent: null,
};
new_list.push(item);
delete unlisted[item[pk]];
continue;
}
if (item[parent_key] === item[pk]) {
item.__depth = 0;
meta[item[pk]] = {
item,
depth: 0,
idx: new_list.length + "",
id_parent: null,
};
new_list.push(item);
delete unlisted[item[pk]];
continue;
}
if (parent) {
item.__depth = parent.depth + 1;
meta[item[pk]] = {
item,
depth: parent.depth + 1,
idx: parent.idx + ".",
id_parent: item[parent_key],
};
delete unlisted[item[pk]];
}
}
}
const sorted = Object.values(meta)
.sort((a, b) => a.idx.localeCompare(b.idx))
.map((e) => e.item);
return sorted;
}
return list;
};

View File

@ -28,12 +28,16 @@ export const MDAction: FC<{ md: MDLocal; PassProp: any; child: any }> = ({
}
return (
<div className={cx("c-flex c-flex-row c-space-x-1")}>
<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) => {
if (isValidElement(e)) {
return <Fragment key={idx}>{e}</Fragment>;
}
if (typeof e === "object" && e.label) {
if (typeof e === "object" && (e.action || e.label)) {
return <PassProp item={e}>{child}</PassProp>;
}
})}

View File

@ -16,7 +16,6 @@ export const MDTab: FC<{ md: MDLocal; mdr: MDRef }> = ({ md, mdr }) => {
return null;
}
return (
<>
{md.props.show_head === "only-child" && <MDHeader md={md} mdr={mdr} />}
@ -41,7 +40,7 @@ export const MDNavTab: FC<{ md: MDLocal; mdr: MDRef }> = ({ md, mdr }) => {
<div
className={cx(
"tab-list c-flex c-text-sm",
mode === "v-tab" && "c-flex-row c-border-b c-pl-2",
mode === "v-tab" && "c-flex-row c-border-b c-pl-2 c-pt-2",
mode === "h-tab" && "c-flex-col c-border-r c-pl-2 c-min-w-[100px]"
)}
>

View File

@ -44,7 +44,7 @@ export const MasterDetail: FC<{
active: "",
list: [],
},
internal: { action_should_refresh: false },
internal: { action_should_refresh: true },
childs: {},
props: {
mode,
@ -65,7 +65,7 @@ export const MasterDetail: FC<{
masterDetailApplyParams(md);
},
},
master: { internal: null, render() {}, pk: null },
master: { internal: null, render() {} },
});
const local = useLocal({ init: false });
if (isEditor) {

View File

@ -36,7 +36,7 @@ export const masterDetailApplyParams = (md: MDLocal) => {
}
}
const pk = md.master.pk;
const pk = md.pk;
if (pk && row[pk.name]) {
md.params.hash[md.name] = row[pk.name];
}

View File

@ -30,7 +30,7 @@ export const masterDetailInit = (
md.master.internal = child;
const pk = parseGenField(md.props.gen_fields).find((e) => e.is_pk);
if (pk) {
md.master.pk = pk;
md.pk = pk;
}
}
if (cid === "cb52075a-14ab-455a-9847-6f1d929a2a73") {
@ -62,7 +62,7 @@ export const masterDetailInit = (
export const masterDetailSelected = (md: MDLocal) => {
md.params.parse();
const pk = md.master.pk;
const pk = md.pk;
if (pk) {
const value = md.params.hash[md.name];
if (value) {

View File

@ -18,13 +18,14 @@ export type MDLocalInternal = {
list: string[];
};
internal: { action_should_refresh: boolean };
master: { internal: any; render: () => void; pk: null | GFCol };
master: { internal: any; render: () => void };
params: {
hash: any;
tabs: any;
parse: () => void;
apply: () => void;
};
pk?: GFCol;
props: {
mode: "full" | "h-split" | "v-split";
show_head: "always" | "only-master" | "only-child" | "hidden";
@ -75,9 +76,19 @@ export const MasterDetailType = `const md = {
parse: () => void;
apply: () => void;
};
props: {
mode: "full" | "h-split" | "v-split";
show_head: "always" | "only-master" | "only-child" | "hidden";
tab_mode: "h-tab" | "v-tab" | "hidden";
editor_tab: string;
gen_fields: any;
gen_table: string;
on_init: (md: any) => void;
};
internal: { action_should_refresh: boolean };
render: () => void;
master: { internal: any; render: () => void; pk: null | {
master: { internal: any; render: () => void; };
pk?: {
name: string;
type: string;
is_pk: boolean;
@ -87,7 +98,6 @@ export const MasterDetailType = `const md = {
to: { table: string; fields: string[] };
fields: GFCol[];
};
}
};
childs: Record<
string,

View File

@ -60,7 +60,6 @@ export const gen_form = async (modify: (data: any) => void, data: any) => {
result["body"] = data["body"];
result.body.content.childs = [];
console.log(fields);
for (const item of fields.filter((e) => !e.is_pk)) {
result.body.content.childs.push(await newField(item));
}

View File

@ -34,8 +34,8 @@ async (opt) => {
if (isEditor) return ${JSON.stringify(sample)};
let raw_id = params.id;
if (typeof md === 'object' && md.selected && md.master?.pk) {
const pk = md.master?.pk?.name;
if (typeof md === 'object' && md.selected && md.pk) {
const pk = md.pk?.name;
if (md.selected[pk]) {
raw_id = md.selected[pk];
}

View File

@ -34,6 +34,11 @@ async ({ form, error }: IForm) => {
const pks = ${JSON.stringify(pks)};
for (const [k, v] of Object.entries(pks)) {
if (typeof data[k] === 'object') {
if (data[k] === null) {
data[k] = {
disconnect: true
}
}
if (data[k][v]) {
data[k] = {
connect: {

View File

@ -3,7 +3,11 @@ import { GFCol, parseGenField } from "../utils";
import { newField } from "./new_field";
import { on_load } from "./on_load";
export const gen_relation = async (modify: (data: any) => void, data: any) => {
export const gen_relation = async (
modify: (data: any) => void,
data: any,
arg: { id_parent: string }
) => {
const table = JSON.parse(data.gen_table.value);
const raw_fields = JSON.parse(data.gen_fields.value) as (
| string
@ -31,6 +35,10 @@ export const gen_relation = async (modify: (data: any) => void, data: any) => {
}
}
if (arg.id_parent) {
select[arg.id_parent] = true;
}
if (!pk) {
alert("Failed to generate! Primary Key not found. ");
return;
@ -40,7 +48,13 @@ export const gen_relation = async (modify: (data: any) => void, data: any) => {
const code = {} as any;
if (data["on_load"]) {
result["on_load"] = data["on_load"];
result["on_load"].value = on_load({ pk, pks, select, table });
result["on_load"].value = on_load({
pk,
pks,
select,
table,
id_parent: arg.id_parent,
});
code.on_load = result["on_load"].value;
}
@ -50,7 +64,7 @@ export const gen_relation = async (modify: (data: any) => void, data: any) => {
(item:any, pk:string) => {
return \`${Object.entries(select)
.filter(([k, v]) => {
if (typeof v !== "object" && k !== pk?.name) {
if (typeof v !== "object" && k !== pk?.name && k !== arg.id_parent) {
return true;
}
})

View File

@ -28,15 +28,22 @@ export type NewFieldArg = {
};
};
export const newField = (arg: GFCol) => {
export const newField = (arg: GFCol, idx: number) => {
let result = `{item["${arg.name}"]}`;
let result_built = `render(React.createElement("div", Object.assign({}, props, { className: cx(props.className, "") }), item["${arg.name}"]));`;
if (idx === 0) {
result = `<FormatValue value={item["${arg.name}"]} tree_depth={item.__depth} />`;
result_built = `render(React.createElement("div", Object.assign({}, props, { className: cx(props.className, "") }),
React.createElement(FormatValue, { value: item["${arg.name}"], tree_depth: item.__depth })));`;
}
return createItem({
name: arg.name,
adv: {
js: `\
<div {...props} className={cx(props.className, "")}>
{item["${arg.name}"]}
${result}
</div>`,
jsBuilt: `render(React.createElement("div", Object.assign({}, props, { className: cx(props.className, "") }), item["${arg.name}"]));`,
jsBuilt: result_built,
},
dim: {
h: "full",

View File

@ -6,6 +6,7 @@ export const on_load = ({
select,
pks,
opt,
id_parent,
}: {
pk: GFCol;
table: string;
@ -15,6 +16,7 @@ export const on_load = ({
before_load: string;
after_load: string;
};
id_parent: string;
}) => {
const sample: any = {};
for (const [k, v] of Object.entries(select) as any) {
@ -41,11 +43,6 @@ async (opt: { value: any }) => {
}
let items = await db.${table}.findMany({
where: !!id
? {
${pk.name}: id,
}
: undefined,
select: ${JSON.stringify(select, null, 2).split("\n").join("\n ")},
});

View File

@ -6,7 +6,7 @@ import { codeBuild } from "../master_detail/utils";
export const gen_table_list = async (
modify: (data: any) => void,
data: any,
arg: { mode: "table" | "list" | "grid" }
arg: { mode: "table" | "list" | "grid"; id_parent: string }
) => {
const table = JSON.parse(data.gen_table.value) as string;
const raw_fields = JSON.parse(data.gen_fields.value) as (
@ -35,6 +35,10 @@ export const gen_table_list = async (
}
}
if (arg.id_parent) {
select[arg.id_parent] = true;
}
if (!pk) {
alert("Failed to generate! Primary Key not found. ");
return;
@ -60,11 +64,19 @@ export const gen_table_list = async (
}
);
let first = true;
const child = createItem({
name: sub_name,
childs: fields
.map((e) => {
if (e.is_pk) return;
let tree_depth = "";
let tree_depth_built = "";
if (first) {
tree_depth = `tree_depth={col.depth}`;
tree_depth_built = `tree_depth:col.depth`;
first = false;
}
return {
component: {
id: "297023a4-d552-464a-971d-f40dcd940b77",
@ -84,10 +96,10 @@ export const gen_table_list = async (
adv: {
js: `\
<div {...props} className={cx(props.className, "")}>
<FormatValue value={col.value} name={col.name} gen_fields={gen_fields} />
<FormatValue value={col.value} name={col.name} gen_fields={gen_fields} ${tree_depth} />
</div>`,
jsBuilt: `\
render(React.createElement("div", Object.assign({}, props, { className: cx(props.className, "") }),React.createElement(FormatValue, { value: col.value, name: col.name, gen_fields: gen_fields })));
render(React.createElement("div", Object.assign({}, props, { className: cx(props.className, "") }),React.createElement(FormatValue, { value: col.value, name: col.name, gen_fields: gen_fields, ${tree_depth_built} })));
`,
},
}),

View File

@ -48,6 +48,9 @@ type BreadItem = {
actions: `\
async () => {
return [
{
action: "delete",
},
{
label: "Save",
action: "save",
@ -68,7 +71,9 @@ async ({ submit, reload }: Init) => {
if (tab) {
const actions = await getProp(tab.internal, "actions", { md });
if (Array.isArray(actions)) {
const save_btn = actions.find((e) => e.action === "save");
const save_btn = actions
.filter((e) => e)
.find((e) => e.action === "save");
if (save_btn) {
save_btn.onClick = async () => {
await submit();

View File

@ -1,4 +1,8 @@
const cache = {} as Record<string, any>;
export const gen_prop_fields = async (gen_table: string) => {
if (cache[gen_table]) return cache[gen_table];
const result: {
label: string;
value: string;
@ -48,5 +52,10 @@ export const gen_prop_fields = async (gen_table: string) => {
options,
});
}
if (!cache[gen_table]) {
cache[gen_table] = result;
}
return result;
};

View File

@ -1,6 +1,10 @@
const cache: any = [];
export const gen_props_table = async () => {
if (cache.length > 0) return cache;
const result = [{ value: "", label: "" }];
return [
const final = [
...result,
...(await db._schema.tables()).map((e) => ({
value: e,
@ -8,4 +12,10 @@ export const gen_props_table = async () => {
reload: ["gen_fields", "gen_label"],
})),
];
for (const f of final) {
cache.push(f);
}
return cache;
};

View File

@ -7,9 +7,11 @@ export const FormatValue: FC<{
value: any;
name: string;
gen_fields: string[];
tree_depth?: number;
}> = (prop) => {
const { value, gen_fields, name } = prop;
const { value, gen_fields, name, tree_depth } = prop;
if (gen_fields) {
const gf = JSON.stringify(gen_fields);
if (!fields_map.has(gf)) {
fields_map.set(
@ -49,5 +51,32 @@ export const FormatValue: FC<{
return JSON.stringify(value);
}
return <>{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>
);
};