0 && `c-text-red-600`)}>
{field.label}
diff --git a/comps/form/field/type/TypeLink.tsx b/comps/form/field/type/TypeLink.tsx
new file mode 100755
index 0000000..86997e5
--- /dev/null
+++ b/comps/form/field/type/TypeLink.tsx
@@ -0,0 +1,196 @@
+import { FieldLoading, Spinner } from "lib/comps/ui/field-loading";
+import { hashSum } from "lib/utils/hash-sum";
+import { getPathname } from "lib/utils/pathname";
+import { useLocal } from "lib/utils/use-local";
+import { ArrowUpRight } from "lucide-react";
+import { FC, ReactNode, useEffect } from "react";
+import { FMLocal, FieldLocal, FieldProp } from "../../typings";
+
+export const FieldLink: FC<{
+ field: FieldLocal;
+ fm: FMLocal;
+ arg: FieldProp;
+}> = ({ field, fm, arg }) => {
+ const local = useLocal({
+ text: "",
+ init: false,
+ navigating: false,
+ custom: false,
+ });
+
+ const Link = ({
+ children,
+ }: {
+ children: (arg: { icon: any }) => ReactNode;
+ }) => {
+ return (
+
{
+ if (!isEditor) {
+ local.navigating = true;
+ local.render();
+ if (!(await navigateLink(arg.link, field))) {
+ local.navigating = false;
+ local.render();
+ }
+ }
+ }}
+ >
+ {children({
+ icon: local.navigating ? (
+
+ ) : (
+
+ ),
+ })}
+
+ );
+ };
+
+ useEffect(() => {
+ if (arg.link && !local.init) {
+ if (typeof arg.link.text === "string") {
+ local.text = arg.link.text;
+ local.init = true;
+ } else if (typeof arg.link.text === "function") {
+ const res = arg.link.text({
+ field,
+ Link,
+ });
+ if (res instanceof Promise) {
+ res.then((text) => {
+ local.text = text;
+ local.init = true;
+ local.render();
+ });
+ } else {
+ local.text = res;
+ local.init = true;
+ }
+ }
+ }
+ local.render();
+ }, []);
+
+ return (
+
+ {!local.init ? (
+
+ ) : (
+
+ {({ icon }) => {
+ return (
+ <>
+
{local.text}
+ {icon}
+ >
+ );
+ }}
+
+ )}
+
+ );
+};
+
+export type LinkParam = {
+ url: string;
+ where: any;
+ hash: any;
+ prefix: {
+ label: any;
+ url: string;
+ md?: { name: string; value: any };
+ }[];
+};
+
+const navigateLink = async (link: FieldProp["link"], field: FieldLocal) => {
+ let params = link.params(field);
+
+ if (typeof params === "object") {
+ if (params instanceof Promise) {
+ params = await params;
+ }
+ }
+
+ const md = params.md;
+ const where = params.where;
+
+ const prefix: LinkParam["prefix"] = [];
+
+ if (md) {
+ md.header.render();
+ if (md.header.breadcrumb.length > 0) {
+ const path = getPathname({ hash: false });
+ let i = 0;
+ for (const b of md.header.breadcrumb) {
+ prefix.push({
+ label: b.label,
+ url: `${path}`,
+ md:
+ i > 0
+ ? { name: md.name, value: md.selected[md.pk?.name || ""] }
+ : undefined,
+ });
+ i++;
+ }
+ }
+ }
+
+ const values: LinkParam = {
+ url: getPathname({ hash: false }),
+ where,
+ prefix,
+ hash: "",
+ };
+ const vhash = hashSum(values);
+ values.hash = vhash;
+
+ if (!link.url) {
+ alert("No URL defined!");
+ return false;
+ }
+ await api._kv("set", vhash, values);
+ const lnk = location.hash.split("#").find((e) => e.startsWith("lnk="));
+ let prev_link = "";
+ if (lnk) {
+ prev_link = lnk.split("=").pop() || "";
+ if (prev_link) prev_link = prev_link + "+";
+ }
+
+ navigate(`${link.url}#lnk=${prev_link + vhash}`);
+ return true;
+};
+
+export const parseLink = () => {
+ const lnk = location.hash.split("#").find((e) => e.startsWith("lnk="));
+
+ if (lnk) {
+ const res = lnk.split("=").pop() || "";
+ if (res) {
+ return res.split("+");
+ }
+ }
+ return [];
+};
+export const fetchLinkParams = async (
+ parsed_link?: ReturnType
+) => {
+ const parsed = parsed_link || parseLink();
+
+ return await Promise.all(parsed.map((e) => api._kv("get", e)));
+};
diff --git a/comps/form/gen/gen-form.ts b/comps/form/gen/gen-form.ts
index a549fbc..1c63727 100755
--- a/comps/form/gen/gen-form.ts
+++ b/comps/form/gen/gen-form.ts
@@ -60,11 +60,7 @@ export const generateForm = async (
after_load: `
if (typeof md === "object") {
opt.fm.status = "ready";
- if (item) {
- for (const [k,v] of Object.entries(item)) {
- md.selected[k] = v;
- }
- }
+ md.selected = opt.fm.data;
md.header.render();
md.render();
}
diff --git a/comps/form/typings.ts b/comps/form/typings.ts
index 94d92e3..ae4b09c 100755
--- a/comps/form/typings.ts
+++ b/comps/form/typings.ts
@@ -1,6 +1,7 @@
import { GFCol } from "@/gen/utils";
-import { MutableRefObject, ReactElement, ReactNode } from "react";
+import { FC, MutableRefObject, ReactElement, ReactNode } from "react";
import { editorFormData } from "./utils/ed-data";
+import { MDLocal } from "../md/utils/typings";
export type FMProps = {
on_init: (arg: { fm: FMLocal; submit: any; reload: any }) => any;
@@ -46,6 +47,18 @@ export type FieldProp = {
label: string;
desc?: string;
props?: any;
+ link: {
+ text:
+ | string
+ | ((arg: {
+ field: FieldLocal;
+ Link: FC<{ children: any; }>;
+ }) => Promise | string);
+ url: string;
+ params: (
+ field: FieldLocal
+ ) => { md: MDLocal; where: any } | Promise<{ md: MDLocal; where: any }>;
+ };
fm: FMLocal;
type: FieldType | (() => FieldType);
required: ("y" | "n") | (() => "y" | "n");
@@ -142,7 +155,7 @@ export type FMInternal = {
soft_delete: {
field: any;
};
- has_fields_container: boolean,
+ has_fields_container: boolean;
};
export type FMLocal = FMInternal & { render: () => void };
diff --git a/comps/md/MasterDetail.tsx b/comps/md/MasterDetail.tsx
index e2a4107..0b4b14b 100755
--- a/comps/md/MasterDetail.tsx
+++ b/comps/md/MasterDetail.tsx
@@ -12,7 +12,6 @@ import {
} from "./utils/md-hash";
import { mdRenderLoop } from "./utils/md-render-loop";
import { MDLocalInternal, MDProps } from "./utils/typings";
-import { any } from "zod";
export const MasterDetail: FC = (arg) => {
const {
@@ -36,8 +35,8 @@ export const MasterDetail: FC = (arg) => {
status: isEditor ? "init" : "ready",
actions: [],
header: {
+ loading: false,
breadcrumb: [],
- internalRender() {},
render: () => {},
master: { prefix: null, suffix: null },
child: { prefix: null, suffix: null },
@@ -60,6 +59,7 @@ export const MasterDetail: FC = (arg) => {
item: _item,
},
params: {
+ links: [],
hash: {},
tabs: {},
parse: () => {
@@ -93,7 +93,9 @@ export const MasterDetail: FC = (arg) => {
if (pk) {
const value = md.params.hash[md.name];
if (value) {
- md.selected = { [pk.name]: value };
+ if (!md.selected) {
+ md.selected = { [pk.name]: value };
+ }
const tab = md.params.tabs[md.name];
if (tab && md.tab.list.includes(tab)) {
md.tab.active = tab;
diff --git a/comps/md/gen/md-form.ts b/comps/md/gen/md-form.ts
index 79e21f8..ab982c2 100755
--- a/comps/md/gen/md-form.ts
+++ b/comps/md/gen/md-form.ts
@@ -78,7 +78,7 @@ export const generateMDForm = async (
if (Object.keys(md.selected).length === 0){
breads.push({ label: "Add New" });
} else {
- breads.push({ label: "Edit" });
+ breads.push({ label: guessLabel(md.selected) || "Detail" });
}
}
}
diff --git a/comps/md/gen/md-gen.ts b/comps/md/gen/md-gen.ts
index f12d2f2..caf073e 100755
--- a/comps/md/gen/md-gen.ts
+++ b/comps/md/gen/md-gen.ts
@@ -1,7 +1,7 @@
import { GenFn } from "lib/gen/utils";
import { generateMDForm } from "./md-form";
import { generateMDList } from "./md-list";
-
+import capitalize from "lodash.capitalize";
const w = window as any;
export const generateMasterDetail: GenFn<{
item: PrasiItem;
@@ -16,7 +16,9 @@ export const generateMasterDetail: GenFn<{
);
const title = fn_title();
if (!title && item.edit.props?.gen_table) {
- item.edit.setProp('title', item.edit.props?.gen_table)
+ const table = { ...item.edit.props?.gen_table };
+ table.value = `${capitalize(table.value as string)}`;
+ item.edit.setProp("title", table);
}
} catch (e) {}
diff --git a/comps/md/parts/MDDetail.tsx b/comps/md/parts/MDDetail.tsx
index 57bd285..210cefc 100755
--- a/comps/md/parts/MDDetail.tsx
+++ b/comps/md/parts/MDDetail.tsx
@@ -1,7 +1,8 @@
+import { useLocal } from "lib/utils/use-local";
import { FC, useEffect } from "react";
+import { breadcrumbPrefix } from "../utils/md-hash";
import { MDLocal, MDRef } from "../utils/typings";
import { MDHeader } from "./MDHeader";
-import { useLocal } from "lib/utils/use-local";
export const should_show_tab = (md: MDLocal) => {
if (isEditor) {
@@ -114,19 +115,8 @@ export const MDRenderTab: FC<{
on_init: () => MDLocal;
breadcrumb: () => Array;
}> = ({ child, on_init, breadcrumb }) => {
- const local = useLocal({ md: null as null | MDLocal });
- if (!local.md) {
- local.md = on_init();
- }
- const md = local.md;
-
- md.header.render = () => {
- md.header.breadcrumb = breadcrumb();
- md.header.internalRender();
- };
- useEffect(() => {
- md.header.render();
- }, Object.values(md.deps || {}) || []);
+ const md = on_init();
+ md.header.child.breadcrumb = breadcrumb;
return <>{child}>;
};
diff --git a/comps/md/parts/MDHeader.tsx b/comps/md/parts/MDHeader.tsx
index 4fcfa71..9d482ec 100755
--- a/comps/md/parts/MDHeader.tsx
+++ b/comps/md/parts/MDHeader.tsx
@@ -1,11 +1,19 @@
import { FC, useState } from "react";
+import { breadcrumbPrefix } from "../utils/md-hash";
import { MDLocal, MDRef } from "../utils/typings";
export const MDHeader: FC<{ md: MDLocal; mdr: MDRef }> = ({ md, mdr }) => {
const [_, set] = useState({});
const head = mdr.item.edit.props?.header.value;
const PassProp = mdr.PassProp;
- md.header.internalRender = () => set({});
md.header.render = () => set({});
+
+ const prefix = breadcrumbPrefix(md);
+ if (md.selected && md.header.child.breadcrumb) {
+ md.header.breadcrumb = [...prefix, ...md.header.child.breadcrumb()];
+ } else if (!md.selected && md.header.master.breadcrumb) {
+ md.header.breadcrumb = [...prefix, ...md.header.master.breadcrumb()];
+ }
+
return {head};
};
diff --git a/comps/md/parts/MDMaster.tsx b/comps/md/parts/MDMaster.tsx
index 4fdacf2..8cc1737 100755
--- a/comps/md/parts/MDMaster.tsx
+++ b/comps/md/parts/MDMaster.tsx
@@ -1,7 +1,7 @@
+import { useLocal } from "lib/utils/use-local";
import { FC, useEffect } from "react";
import { MDLocal, MDRef } from "../utils/typings";
import { MDHeader } from "./MDHeader";
-import { useLocal } from "lib/utils/use-local";
const w = window as unknown as {
md_panel_master: any;
@@ -18,14 +18,9 @@ export const MDRenderMaster: FC<{
local.md = on_init();
}
const md = local.md;
-
- md.header.render = () => {
- md.header.breadcrumb = breadcrumb();
- md.header.internalRender();
- };
+ md.header.master.breadcrumb = breadcrumb;
useEffect(() => {
- md.header.render();
if (md) {
let width = 0;
let min_width = 0;
diff --git a/comps/md/utils/editor-init.tsx b/comps/md/utils/editor-init.tsx
index 8ec7523..d355954 100755
--- a/comps/md/utils/editor-init.tsx
+++ b/comps/md/utils/editor-init.tsx
@@ -40,7 +40,6 @@ export const editorMDInit = (md: MDLocal, mdr: MDRef, arg: MDProps) => {
];
md.status = "unready";
} else {
- // md.header.breadcrumb = [];
md.status = "ready";
}
};
diff --git a/comps/md/utils/md-hash.ts b/comps/md/utils/md-hash.ts
index 345c86b..d9ca93b 100755
--- a/comps/md/utils/md-hash.ts
+++ b/comps/md/utils/md-hash.ts
@@ -1,4 +1,6 @@
+import { fetchLinkParams, parseLink } from "lib/comps/form/field/type/TypeLink";
import { MDLocal } from "./typings";
+import { BreadItem } from "lib/comps/custom/Breadcrumb";
export const masterDetailParseHash = (md: MDLocal) => {
let raw_hash = decodeURIComponent(location.hash);
@@ -21,6 +23,27 @@ export const masterDetailParseHash = (md: MDLocal) => {
}
}
}
+
+ const parsed_link = parseLink();
+ let changed = parsed_link.length !== md.params.links.length;
+
+ if (!changed) {
+ for (let i = 0; i < parsed_link.length; i++) {
+ if (parsed_link[i] !== md.params.links[i].hash) {
+ changed = true;
+ }
+ }
+ }
+
+ if (changed) {
+ md.params.links = [];
+ md.header.loading = true;
+ fetchLinkParams(parsed_link).then((links) => {
+ md.params.links = links;
+ md.header.loading = false;
+ md.header.render();
+ });
+ }
};
export const masterDetailApplyParams = (md: MDLocal) => {
@@ -56,3 +79,40 @@ export const masterDetailApplyParams = (md: MDLocal) => {
location.hash = hash;
}
};
+
+export const breadcrumbPrefix = (md: MDLocal) => {
+ const prefix: BreadItem[] = [];
+ if (md.params.links && md.params.links.length > 0) {
+ const hashes: string[] = [];
+ for (const link of md.params.links) {
+ if (!hashes.includes(link.hash)) {
+ hashes.push(link.hash);
+ }
+ }
+ for (const link of md.params.links) {
+ for (const p of link.prefix) {
+ prefix.push({
+ label: p.label,
+ onClick(ev) {
+ let url = "";
+
+ const hashIndex = hashes.indexOf(link.hash);
+ const link_hashes = hashes.slice(0, hashIndex).join("+");
+ const lnk = link_hashes ? `#lnk=${link_hashes}` : ``;
+
+ if (p.md) {
+ url = `${link.url}#${p.md.name}=${p.md.value}${lnk}`;
+ } else {
+ url = `${link.url}${lnk}`;
+ }
+
+ if (url) {
+ navigate(url);
+ }
+ },
+ });
+ }
+ }
+ }
+ return prefix;
+};
diff --git a/comps/md/utils/typings.ts b/comps/md/utils/typings.ts
index 2126c6d..d11cbef 100755
--- a/comps/md/utils/typings.ts
+++ b/comps/md/utils/typings.ts
@@ -1,6 +1,7 @@
import { BreadItem } from "@/comps/custom/Breadcrumb";
import { FMLocal } from "@/comps/form/typings";
import { GFCol } from "@/gen/utils";
+import { LinkParam } from "lib/comps/form/field/type/TypeLink";
import { ReactNode } from "react";
type ID_MASTER_DETAIL = string;
@@ -38,11 +39,11 @@ export type MDLocalInternal = {
title: string;
status: "init" | "unready" | "ready";
header: {
+ loading: boolean;
breadcrumb: BreadItem[];
- internalRender: () => void;
render: () => void;
- master: { prefix: any; suffix: any };
- child: { prefix: any; suffix: any };
+ master: { prefix: any; suffix: any; breadcrumb?: () => BreadItem[] };
+ child: { prefix: any; suffix: any; breadcrumb?: () => BreadItem[] };
};
actions: MDActions;
selected: any;
@@ -53,7 +54,8 @@ export type MDLocalInternal = {
internal: { action_should_refresh: boolean };
master: { render: () => void };
params: {
- hash: any;
+ links: LinkParam[];
+ hash: Record;
tabs: any;
parse: () => void;
apply: () => void;
diff --git a/comps/ui/field-loading.tsx b/comps/ui/field-loading.tsx
index e842e98..2617c27 100755
--- a/comps/ui/field-loading.tsx
+++ b/comps/ui/field-loading.tsx
@@ -1,28 +1,41 @@
import { Skeleton } from "@/comps/ui/skeleton";
+import { cn } from "lib/utils";
+import { Loader2 } from "lucide-react";
+import { FC } from "react";
-export const FieldLoading = () => {
+export const FieldLoading: FC<{ height?: "normal" | "short" }> = (prop) => {
+ let height = "10px";
+ if (prop.height === "short") height = "6px";
return (
);
};
+
+export const Spinner = ({ className }: { className?: string }) => {
+ return (
+
+
+
+ );
+};
diff --git a/exports.tsx b/exports.tsx
index cdda649..a42afb9 100755
--- a/exports.tsx
+++ b/exports.tsx
@@ -1,5 +1,7 @@
export { FieldLoading } from "@/comps/ui/field-loading";
import { lazify, lazifyMany } from "@/utils/lazify";
+export { guessLabel } from "./utils/guess-label";
+export { fetchLinkParams } from "./comps/form/field/type/TypeLink";
export { prasi_gen } from "./gen/prasi_gen";
export const Popover = lazify(
diff --git a/utils/guess-label.ts b/utils/guess-label.ts
new file mode 100755
index 0000000..d1c766d
--- /dev/null
+++ b/utils/guess-label.ts
@@ -0,0 +1,27 @@
+import { validate } from "uuid";
+
+export const guessLabel = (_obj: Record, key?: string) => {
+ let label = "";
+ let obj = _obj;
+ if (key) obj = _obj[key];
+
+ const label_key =
+ Object.keys(obj)
+ .map((e) => e.toLowerCase())
+ .find((e) => e.includes("name") || e.includes("nama")) || "";
+
+ label = obj[label_key];
+ if (obj.length > 1) {
+ for (const v of Object.values(obj)) {
+ if (typeof v === "string" && v.length >= 2 && !validate(v)) {
+ label = v;
+ }
+ }
+ }
+
+ if (typeof label === "string" && label.length > 10) {
+ label = label.substring(0, 10) + "…";
+ }
+
+ return label;
+};
diff --git a/utils/pathname.ts b/utils/pathname.ts
index ed4b360..ba2f060 100755
--- a/utils/pathname.ts
+++ b/utils/pathname.ts
@@ -1,4 +1,4 @@
-export const getPathname = (url?: string) => {
+export const getPathname = (opt?: { hash?: boolean }) => {
if (
["prasi.avolut.com"].includes(location.hostname) ||
location.host === "localhost:4550"
@@ -9,11 +9,8 @@ export const getPathname = (url?: string) => {
location.pathname.startsWith("/deploy")
) {
const hash = location.hash;
- if (url?.startsWith("/prod")) {
- return "/" + url.split("/").slice(3).join("/");
- }
- if (hash !== "") {
+ if (hash !== "" && opt?.hash !== false) {
return "/" + location.pathname.split("/").slice(3).join("/") + hash;
} else {
return "/" + location.pathname.split("/").slice(3).join("/");