Render tiles/charts inside RecordViewTable (rv-screen layout, not parent screen)

This commit is contained in:
Bhanu Prakash Sai Potteri 2026-05-23 11:13:25 +05:30
parent 50f0591b41
commit dde6fdda9d
2 changed files with 57 additions and 37 deletions

View File

@ -6,6 +6,8 @@ import {
} from "lucide-react"; } from "lucide-react";
import { getRVScreen, getRecordView, type RecordViewField, type SearchQuery, type TileValue, type ChartDataResponse } from "../api/viewService"; import { getRVScreen, getRecordView, type RecordViewField, type SearchQuery, type TileValue, type ChartDataResponse } from "../api/viewService";
import FormModal from "./FormModal"; import FormModal from "./FormModal";
import Tile from "./Tile";
import BarChart from "./BarChart";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Action column (config-driven from rv-screen layout) // Action column (config-driven from rv-screen layout)
@ -141,7 +143,26 @@ interface Props {
viewId: number | string; viewId: number | string;
onRowClick?: (instanceId: string) => void; onRowClick?: (instanceId: string) => void;
toolbarAction?: React.ReactNode; 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 // 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; return out;
} }
export default function RecordViewTable({ viewId, onRowClick, toolbarAction, onAnalyticsLoaded }: Props) { export default function RecordViewTable({ viewId, onRowClick, toolbarAction }: Props) {
const navigate = useNavigate(); const navigate = useNavigate();
const [rvTemplateUid, setRvTemplateUid] = useState<string | null>(null); const [rvTemplateUid, setRvTemplateUid] = useState<string | null>(null);
const [columnLabels, setColumnLabels] = useState<Record<string, string>>({}); const [columnLabels, setColumnLabels] = useState<Record<string, string>>({});
const [actionColumns, setActionColumns] = useState<ActionColumn[]>([]); 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 [fields, setFields] = useState<RecordViewField[]>([]);
const [rows, setRows] = useState<Record<string, unknown>[]>([]); const [rows, setRows] = useState<Record<string, unknown>[]>([]);
const [pagination, setPagination] = useState({ page: 1, limit: 10, total_count: 0, total_pages: 0 }); 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); setRvTemplateUid(null);
setColumnLabels({}); setColumnLabels({});
setActionColumns([]); setActionColumns([]);
setAnalyticsSpecs([]);
setTileValues([]);
setChartData([]);
getRVScreen(viewId) getRVScreen(viewId)
.then((rv) => { .then((rv) => {
setColumnLabels(extractColumnLabels(rv.layout)); setColumnLabels(extractColumnLabels(rv.layout));
setActionColumns(extractActionColumns(rv.layout)); setActionColumns(extractActionColumns(rv.layout));
setAnalyticsSpecs(extractAnalyticsElements(rv.layout));
setRvTemplateUid(rv.rv_template_uid); setRvTemplateUid(rv.rv_template_uid);
}) })
.catch((err) => { setError(err.message); setLoading(false); }); .catch((err) => { setError(err.message); setLoading(false); });
@ -200,7 +228,8 @@ export default function RecordViewTable({ viewId, onRowClick, toolbarAction, onA
setError(null); setError(null);
try { try {
const res = await getRecordView(rvTemplateUid, viewId, query); 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 !== "")); setFields(res.config.fields.filter((f) => f.field_key !== ""));
const dataRows = Array.isArray(res.data) ? res.data : []; const dataRows = Array.isArray(res.data) ? res.data : [];
setRows(dataRows); setRows(dataRows);
@ -238,14 +267,30 @@ export default function RecordViewTable({ viewId, onRowClick, toolbarAction, onA
onRowClick ? onRowClick(id) : navigate(`/detail/${id}`); 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) { if (error) {
return ( return (
<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> {analyticsRow}
<button onClick={fetchData} className="text-xs text-rose-600 dark:text-rose-400 hover:text-rose-800 font-semibold">Try again</button> <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">
</div> <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); const pages = buildPageRange(page, total_pages);
return ( 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"> <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 ───────────────────────────────────────────────────────── */} {/* ── Toolbar ───────────────────────────────────────────────────────── */}
@ -539,6 +586,7 @@ export default function RecordViewTable({ viewId, onRowClick, toolbarAction, onA
/> />
)} )}
</div> </div>
</>
); );
} }

View File

@ -1,11 +1,9 @@
import { useState, ReactNode } from "react"; import { useState, ReactNode } from "react";
import { Plus } from "lucide-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 RecordViewTable from "./RecordViewTable";
import DetailViewPanel from "./DetailViewPanel"; import DetailViewPanel from "./DetailViewPanel";
import FormModal from "./FormModal"; import FormModal from "./FormModal";
import Tile from "./Tile";
import BarChart from "./BarChart";
interface Props { interface Props {
layout: LayoutElement[]; layout: LayoutElement[];
@ -21,9 +19,6 @@ interface AnyNode {
children?: 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[] { function flatten(nodes: AnyNode[] | undefined): AnyNode[] {
if (!nodes) return []; if (!nodes) return [];
const out: AnyNode[] = []; const out: AnyNode[] = [];
@ -36,12 +31,9 @@ function flatten(nodes: AnyNode[] | undefined): AnyNode[] {
export default function ScreenRenderer({ layout, instanceId, onRefresh, onRowClick }: Props) { export default function ScreenRenderer({ layout, instanceId, onRefresh, onRowClick }: Props) {
const [formModal, setFormModal] = useState<{ activityId: string; instanceId?: string; title?: string } | null>(null); 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 buttons: ReactNode[] = [];
const elements: { type: "rv" | "dv"; viewId: number | string; style?: Record<string, string> }[] = []; const elements: { type: "rv" | "dv"; viewId: number | string; style?: Record<string, string> }[] = [];
const analytics: (TileSpec | ChartSpec)[] = [];
for (const node of flatten(layout as AnyNode[])) { for (const node of flatten(layout as AnyNode[])) {
const cfg = node.config!; const cfg = node.config!;
@ -69,31 +61,12 @@ export default function ScreenRenderer({ layout, instanceId, onRefresh, onRowCli
} else if (cfg.type === "detailsview") { } else if (cfg.type === "detailsview") {
const id = cfg.view_uuid || cfg.view_id; const id = cfg.view_uuid || cfg.view_id;
elements.push({ type: "dv", viewId: id, style: node.style }); 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 ( return (
<> <>
<div className="space-y-4"> <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) => { {elements.map((el, i) => {
if (el.type === "rv") { if (el.type === "rv") {
return ( return (
@ -102,7 +75,6 @@ export default function ScreenRenderer({ layout, instanceId, onRefresh, onRowCli
viewId={el.viewId} viewId={el.viewId}
onRowClick={onRowClick} onRowClick={onRowClick}
toolbarAction={buttons.length > 0 ? <div className="flex items-center gap-2">{buttons}</div> : undefined} toolbarAction={buttons.length > 0 ? <div className="flex items-center gap-2">{buttons}</div> : undefined}
onAnalyticsLoaded={(tiles, charts) => { setTileValues(tiles); setChartData(charts); }}
/> />
</div> </div>
); );