Render tiles/charts inside RecordViewTable (rv-screen layout, not parent screen)
This commit is contained in:
parent
50f0591b41
commit
dde6fdda9d
@ -6,6 +6,8 @@ import {
|
||||
} from "lucide-react";
|
||||
import { getRVScreen, getRecordView, type RecordViewField, type SearchQuery, type TileValue, type ChartDataResponse } from "../api/viewService";
|
||||
import FormModal from "./FormModal";
|
||||
import Tile from "./Tile";
|
||||
import BarChart from "./BarChart";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Action column (config-driven from rv-screen layout)
|
||||
@ -141,7 +143,26 @@ interface Props {
|
||||
viewId: number | string;
|
||||
onRowClick?: (instanceId: string) => void;
|
||||
toolbarAction?: React.ReactNode;
|
||||
onAnalyticsLoaded?: (tiles: TileValue[], charts: ChartDataResponse[]) => void;
|
||||
}
|
||||
|
||||
type AnalyticsSpec =
|
||||
| { kind: "tile"; uid: string; title?: string }
|
||||
| { kind: "chart"; uid: string; title?: string };
|
||||
|
||||
// Walk rv-screen layout to find rv_tile / rv_chart elements (the platform
|
||||
// places them as sibling blocks above the rv_table).
|
||||
function extractAnalyticsElements(layout: any): AnalyticsSpec[] {
|
||||
const out: AnalyticsSpec[] = [];
|
||||
const walk = (nodes: any[]): void => {
|
||||
for (const n of nodes ?? []) {
|
||||
const cfg = n?.config;
|
||||
if (cfg?.type === "rv_tile" && cfg.tile_uid) out.push({ kind: "tile", uid: cfg.tile_uid, title: cfg.title });
|
||||
if (cfg?.type === "rv_chart" && cfg.chart_uid) out.push({ kind: "chart", uid: cfg.chart_uid, title: cfg.title });
|
||||
if (Array.isArray(n?.children)) walk(n.children);
|
||||
}
|
||||
};
|
||||
walk(Array.isArray(layout) ? layout : []);
|
||||
return out;
|
||||
}
|
||||
|
||||
// Walks the rv-screen layout to find the rv_table element and returns a
|
||||
@ -164,11 +185,14 @@ function extractColumnLabels(layout: any): Record<string, string> {
|
||||
return out;
|
||||
}
|
||||
|
||||
export default function RecordViewTable({ viewId, onRowClick, toolbarAction, onAnalyticsLoaded }: Props) {
|
||||
export default function RecordViewTable({ viewId, onRowClick, toolbarAction }: Props) {
|
||||
const navigate = useNavigate();
|
||||
const [rvTemplateUid, setRvTemplateUid] = useState<string | null>(null);
|
||||
const [columnLabels, setColumnLabels] = useState<Record<string, string>>({});
|
||||
const [actionColumns, setActionColumns] = useState<ActionColumn[]>([]);
|
||||
const [analyticsSpecs, setAnalyticsSpecs] = useState<AnalyticsSpec[]>([]);
|
||||
const [tileValues, setTileValues] = useState<TileValue[]>([]);
|
||||
const [chartData, setChartData] = useState<ChartDataResponse[]>([]);
|
||||
const [fields, setFields] = useState<RecordViewField[]>([]);
|
||||
const [rows, setRows] = useState<Record<string, unknown>[]>([]);
|
||||
const [pagination, setPagination] = useState({ page: 1, limit: 10, total_count: 0, total_pages: 0 });
|
||||
@ -185,10 +209,14 @@ export default function RecordViewTable({ viewId, onRowClick, toolbarAction, onA
|
||||
setRvTemplateUid(null);
|
||||
setColumnLabels({});
|
||||
setActionColumns([]);
|
||||
setAnalyticsSpecs([]);
|
||||
setTileValues([]);
|
||||
setChartData([]);
|
||||
getRVScreen(viewId)
|
||||
.then((rv) => {
|
||||
setColumnLabels(extractColumnLabels(rv.layout));
|
||||
setActionColumns(extractActionColumns(rv.layout));
|
||||
setAnalyticsSpecs(extractAnalyticsElements(rv.layout));
|
||||
setRvTemplateUid(rv.rv_template_uid);
|
||||
})
|
||||
.catch((err) => { setError(err.message); setLoading(false); });
|
||||
@ -200,7 +228,8 @@ export default function RecordViewTable({ viewId, onRowClick, toolbarAction, onA
|
||||
setError(null);
|
||||
try {
|
||||
const res = await getRecordView(rvTemplateUid, viewId, query);
|
||||
onAnalyticsLoaded?.(res.tile_values ?? [], res.chart_data ?? []);
|
||||
setTileValues(res.tile_values ?? []);
|
||||
setChartData(res.chart_data ?? []);
|
||||
setFields(res.config.fields.filter((f) => f.field_key !== ""));
|
||||
const dataRows = Array.isArray(res.data) ? res.data : [];
|
||||
setRows(dataRows);
|
||||
@ -238,14 +267,30 @@ export default function RecordViewTable({ viewId, onRowClick, toolbarAction, onA
|
||||
onRowClick ? onRowClick(id) : navigate(`/detail/${id}`);
|
||||
};
|
||||
|
||||
if (loading) return <TableSkeleton rows={6} cols={fields.length || 6} />;
|
||||
const analyticsRow = analyticsSpecs.length > 0 ? (
|
||||
<div className="flex flex-wrap items-stretch gap-3 mb-4">
|
||||
{analyticsSpecs.map((a, i) => {
|
||||
if (a.kind === "tile") {
|
||||
const tv = tileValues.find(t => t.tile_uid === a.uid);
|
||||
return <Tile key={i} title={a.title} value={tv?.value as number | undefined} loading={loading || dataLoading} />;
|
||||
}
|
||||
const cd = chartData.find(c => c.chart_uid === a.uid);
|
||||
return <BarChart key={i} title={a.title} rows={cd?.rows ?? []} loading={loading || dataLoading} />;
|
||||
})}
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
if (loading) return <>{analyticsRow}<TableSkeleton rows={6} cols={fields.length || 6} /></>;
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<>
|
||||
{analyticsRow}
|
||||
<div className="rounded-2xl border border-red-100 dark:border-red-900/50 bg-red-50/50 dark:bg-red-950/20 p-10 text-center">
|
||||
<p className="text-sm text-red-600 dark:text-red-400 mb-2">{error}</p>
|
||||
<button onClick={fetchData} className="text-xs text-rose-600 dark:text-rose-400 hover:text-rose-800 font-semibold">Try again</button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -259,6 +304,8 @@ export default function RecordViewTable({ viewId, onRowClick, toolbarAction, onA
|
||||
const pages = buildPageRange(page, total_pages);
|
||||
|
||||
return (
|
||||
<>
|
||||
{analyticsRow}
|
||||
<div className="rounded-2xl border border-stone-200/80 dark:border-zinc-700/60 bg-white dark:bg-zinc-900 overflow-hidden shadow-sm dark:shadow-black/20">
|
||||
|
||||
{/* ── Toolbar ───────────────────────────────────────────────────────── */}
|
||||
@ -539,6 +586,7 @@ export default function RecordViewTable({ viewId, onRowClick, toolbarAction, onA
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -1,11 +1,9 @@
|
||||
import { useState, ReactNode } from "react";
|
||||
import { Plus } from "lucide-react";
|
||||
import type { LayoutElement, TileValue, ChartDataResponse } from "../api/viewService";
|
||||
import type { LayoutElement } from "../api/viewService";
|
||||
import RecordViewTable from "./RecordViewTable";
|
||||
import DetailViewPanel from "./DetailViewPanel";
|
||||
import FormModal from "./FormModal";
|
||||
import Tile from "./Tile";
|
||||
import BarChart from "./BarChart";
|
||||
|
||||
interface Props {
|
||||
layout: LayoutElement[];
|
||||
@ -21,9 +19,6 @@ interface AnyNode {
|
||||
children?: AnyNode[];
|
||||
}
|
||||
|
||||
interface TileSpec { kind: "tile"; uid: string; title?: string; style?: Record<string, string> }
|
||||
interface ChartSpec { kind: "chart"; uid: string; title?: string; style?: Record<string, string> }
|
||||
|
||||
function flatten(nodes: AnyNode[] | undefined): AnyNode[] {
|
||||
if (!nodes) return [];
|
||||
const out: AnyNode[] = [];
|
||||
@ -36,12 +31,9 @@ function flatten(nodes: AnyNode[] | undefined): AnyNode[] {
|
||||
|
||||
export default function ScreenRenderer({ layout, instanceId, onRefresh, onRowClick }: Props) {
|
||||
const [formModal, setFormModal] = useState<{ activityId: string; instanceId?: string; title?: string } | null>(null);
|
||||
const [tileValues, setTileValues] = useState<TileValue[]>([]);
|
||||
const [chartData, setChartData] = useState<ChartDataResponse[]>([]);
|
||||
|
||||
const buttons: ReactNode[] = [];
|
||||
const elements: { type: "rv" | "dv"; viewId: number | string; style?: Record<string, string> }[] = [];
|
||||
const analytics: (TileSpec | ChartSpec)[] = [];
|
||||
|
||||
for (const node of flatten(layout as AnyNode[])) {
|
||||
const cfg = node.config!;
|
||||
@ -69,31 +61,12 @@ export default function ScreenRenderer({ layout, instanceId, onRefresh, onRowCli
|
||||
} else if (cfg.type === "detailsview") {
|
||||
const id = cfg.view_uuid || cfg.view_id;
|
||||
elements.push({ type: "dv", viewId: id, style: node.style });
|
||||
} else if (cfg.type === "rv_tile" && cfg.tile_uid) {
|
||||
analytics.push({ kind: "tile", uid: cfg.tile_uid, title: cfg.title, style: node.style });
|
||||
} else if (cfg.type === "rv_chart" && cfg.chart_uid) {
|
||||
analytics.push({ kind: "chart", uid: cfg.chart_uid, title: cfg.title, style: node.style });
|
||||
}
|
||||
}
|
||||
|
||||
const tilesLoading = analytics.length > 0 && tileValues.length === 0 && chartData.length === 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-4">
|
||||
{analytics.length > 0 && (
|
||||
<div className="flex flex-wrap items-stretch gap-3">
|
||||
{analytics.map((a, i) => {
|
||||
if (a.kind === "tile") {
|
||||
const tv = tileValues.find(t => t.tile_uid === a.uid);
|
||||
return <Tile key={i} title={a.title} value={tv?.value as number | undefined} loading={tilesLoading} />;
|
||||
}
|
||||
const cd = chartData.find(c => c.chart_uid === a.uid);
|
||||
return <BarChart key={i} title={a.title} rows={cd?.rows ?? []} loading={tilesLoading} />;
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{elements.map((el, i) => {
|
||||
if (el.type === "rv") {
|
||||
return (
|
||||
@ -102,7 +75,6 @@ export default function ScreenRenderer({ layout, instanceId, onRefresh, onRowCli
|
||||
viewId={el.viewId}
|
||||
onRowClick={onRowClick}
|
||||
toolbarAction={buttons.length > 0 ? <div className="flex items-center gap-2">{buttons}</div> : undefined}
|
||||
onAnalyticsLoaded={(tiles, charts) => { setTileValues(tiles); setChartData(charts); }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user