prasi-lib/comps/mst/MasterDetail.tsx

319 lines
8.2 KiB
TypeScript
Executable File

import { GFCol } from "@/gen/utils";
import { useLocal } from "@/utils/use-local";
import get from "lodash.get";
import { FC, useEffect } from "react";
import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";
import { Tabs, TabsList, TabsTrigger } from "../ui/tabs";
import {
MasterDetailConfig,
MasterDetailLocal,
MasterDetailProp,
} from "./type";
import { master_detail_gen_hash, master_detail_params } from "./utils";
export const MasterDetail: FC<MasterDetailProp> = (props) => {
const { header, name, mode, title, actions, gen_fields, md_parent } = props;
const md = useLocal<MasterDetailLocal & { cache_internal: any }>(
{
name,
mode,
selected: null,
active_tab: "",
ui: {
back: false,
title: title,
breadcrumb: [],
default_actions: null as any,
actions: null as any,
},
cache_internal: {},
cache: null as any,
pk: null as null | GFCol,
parent: md_parent,
},
() => {
const params = master_detail_params(md);
const hash = params.hash;
if (params.tabs && params.tabs[md.name]) {
md.active_tab = params.tabs[md.name];
}
if (md.mode === "breadcrumb") {
if (hash && hash[name] && md.pk) {
if (md.pk.type === "int") {
md.selected = { [md.pk.name]: parseInt(hash[name]) };
} else {
md.selected = { [md.pk.name]: hash[name] };
}
}
}
md.render();
}
);
const local = useLocal({ init: false }, () => {
local.init = true;
local.render();
});
if (local.init) {
const params = master_detail_params(md);
const hash = params.hash;
delete hash.parent_id;
if (!md.selected) {
delete hash[md.name];
master_detail_gen_hash(params);
} else if (md.pk) {
hash[md.name] = md.selected[md.pk.name];
master_detail_gen_hash(params);
}
}
if (!md.pk && gen_fields) {
for (const str of gen_fields) {
try {
const f = JSON.parse(str) as GFCol;
if (f.is_pk) md.pk = f;
} catch (e) {}
}
}
if (!md.ui.actions) {
md.ui.actions = actions(md);
md.ui.default_actions = actions(md);
}
if (!md.cache) {
md.cache = (name: string, opt?: { reset: boolean }) => {
if (!md.cache_internal[name] || opt?.reset) md.cache_internal[name] = {};
return md.cache_internal[name];
};
}
if (isEditor) {
useEffect(() => {
md.ui.title = title;
md.render();
}, [title]);
useEffect(() => {
md.mode = mode;
md.render();
}, [mode]);
}
useEffect(() => {
let back = false;
if (md.selected && md.mode === "breadcrumb") {
back = true;
}
if (back !== md.ui.back) {
md.ui.back = back;
md.render();
}
}, [md.selected]);
return (
<div
className={cx(
"c-flex-1 c-flex-col c-flex c-w-full c-h-full c-overflow-hidden"
)}
>
{md.mode === "breadcrumb" && (
<BreadcrumbMode props={props} md={md} header={header} />
)}
{md.mode === "vertical" && (
<VerticalMode props={props} md={md} header={header} />
)}
{md.mode === "horizontal" && (
<HorizontalMode props={props} md={md} header={header} />
)}
{isEditor && (
<div className="c-hidden">
<props.PassProp md={md}>{props.detail}</props.PassProp>
</div>
)}
</div>
);
};
const VerticalMode: FC<{
props: MasterDetailProp;
md: MasterDetailConfig;
header: any;
}> = ({ props, md, header }) => {
return (
<div className={cx("c-flex-1")}>
<PanelGroup direction="vertical">
<Panel className="c-border-b">
<props.PassProp md={md}>{props.master}</props.PassProp>
</Panel>
<>
<PanelResizeHandle />
<Panel
className="c-flex c-flex-col c-items-stretch"
defaultSize={
parseInt(localStorage.getItem(`prasi-md-h-${md.name}`) || "") ||
undefined
}
onResize={(e) => {
if (e < 80) {
localStorage.setItem(`prasi-md-h-${md.name}`, e.toString());
}
}}
>
<props.PassProp md={md}>{header}</props.PassProp>
<Detail props={props} md={md} />
</Panel>
</>
</PanelGroup>
</div>
);
};
const HorizontalMode: FC<{
props: MasterDetailProp;
md: MasterDetailConfig;
header: any;
}> = ({ props, md, header }) => {
return (
<div className={cx("c-flex-1")}>
<PanelGroup direction="horizontal">
<Panel className="c-border-r">
<props.PassProp md={md}>{props.master}</props.PassProp>
</Panel>
<>
<PanelResizeHandle />
<Panel
className="c-flex c-flex-col c-items-stretch"
defaultSize={
parseInt(localStorage.getItem(`prasi-md-h-${md.name}`) || "") ||
undefined
}
onResize={(e) => {
if (e < 80) {
localStorage.setItem(`prasi-md-h-${md.name}`, e.toString());
}
}}
>
<props.PassProp md={md}>{header}</props.PassProp>
<Detail props={props} md={md} />
</Panel>
</>
</PanelGroup>
</div>
);
};
const BreadcrumbMode: FC<{
props: MasterDetailProp;
md: MasterDetailConfig;
header: any;
}> = ({ props, md, header }) => {
return (
<>
<props.PassProp md={md}>{header}</props.PassProp>
<div className={cx("c-flex-1 c-flex-col c-flex")}>
<div
className={cx(
md.selected && "c-hidden",
"c-flex c-flex-1 c-flex-col"
)}
>
<props.PassProp md={md}>{props.master}</props.PassProp>
</div>
{md.selected && <Detail props={props} md={md} />}
</div>
</>
);
};
const Detail: FC<{
props: MasterDetailProp;
md: MasterDetailConfig;
}> = ({ props, md }) => {
const childs = get(
props.detail,
"props.meta.item.component.props.detail.content.childs"
);
let idx = childs.findIndex(
(e: any) => e.name.toLowerCase() === md.active_tab.toLowerCase()
);
if (idx < 0) {
idx = 0;
if (childs[idx]) {
md.active_tab = childs[idx].name;
}
setTimeout(md.render);
}
const content = childs[idx];
return (
<>
{childs.length > 1 && (
<Tabs
value={content.name}
className={cx(
css`
padding: 0;
button {
border-radius: 0;
}
`
)}
>
<TabsList
overrideClassName
className={cx(
"c-flex c-w-full c-rounded-none c-border-b c-border-gray-300 c-justify-start",
css`
padding: 0 !important;
padding-left: 20px !important;
height: auto !important;
`
)}
>
{childs.map((e: { name: string }) => {
return (
<TabsTrigger
value={e.name}
onClick={() => {
const params = master_detail_params(md);
const hash = params.hash;
delete hash.parent_id;
params.tabs[md.name] = e.name;
master_detail_gen_hash(params);
md.active_tab = e.name;
md.render();
}}
overrideClassName
>
<div
className={cx(
"c-p-1 c-h-10 c-flex c-items-center",
md.active_tab === e.name
? css`
border-bottom: 2px solid #3c82f6;
`
: "border-b-transparent"
)}
>
<div className={cx("c-mr-1 c-flex-1 c-px-1 c-flex")}>
{e.name}
</div>
</div>
</TabsTrigger>
);
})}
</TabsList>
</Tabs>
)}
<props.PassProp md={md}>{content}</props.PassProp>
</>
);
};