From 5dfdab0d693c98e4a74bd2a37291f25bed4efd5f Mon Sep 17 00:00:00 2001 From: Bhanu Prakash Sai Potteri Date: Fri, 22 May 2026 23:56:13 +0530 Subject: [PATCH] Render rv_table custom action column inline (config-driven) --- src/components/RecordViewTable.tsx | 229 +++++++++++++++++------------ 1 file changed, 131 insertions(+), 98 deletions(-) diff --git a/src/components/RecordViewTable.tsx b/src/components/RecordViewTable.tsx index cafc97e..45938ea 100644 --- a/src/components/RecordViewTable.tsx +++ b/src/components/RecordViewTable.tsx @@ -2,39 +2,97 @@ import { useEffect, useState, useCallback, useRef } from "react"; import { useNavigate } from "react-router-dom"; import { ChevronLeft, ChevronRight, Search, ArrowUp, ArrowDown, - X, Inbox, MoreHorizontal, ArrowRight, RefreshCw, + X, Inbox, ArrowRight, RefreshCw, CheckCircle2, } from "lucide-react"; import { getRVScreen, getRecordView, type RecordViewField, type SearchQuery } from "../api/viewService"; import FormModal from "./FormModal"; -import { STATE_IDS, ACTIVITY_IDS } from "../config"; -import { CheckCircle2, RefreshCw as RefreshIcon } from "lucide-react"; // --------------------------------------------------------------------------- -// Row actions by state +// Action column (config-driven from rv-screen layout) // --------------------------------------------------------------------------- -const ROW_ACTIONS: Record> = { - [STATE_IDS.SCHEDULED]: [ - { activityId: ACTIVITY_IDS.MARK_TEST_DRIVE_DONE, label: "Mark Test Drive Done", icon: }, - ], - [STATE_IDS.SCHEDULING_CALL_FAILED]: [ - { activityId: ACTIVITY_IDS.RETRY_SCHEDULING_CALL, label: "Retry Scheduling Call", icon: }, - ], - [STATE_IDS.FEEDBACK_CALL_FAILED]: [ - { activityId: ACTIVITY_IDS.RETRY_FEEDBACK_CALL, label: "Retry Feedback Call", icon: }, - ], +interface ActionCondition { + field_key: string; + operator: string; + value: unknown; + data_type?: string; +} +interface ActionConditions { + logic?: "AND" | "OR"; + conditions: ActionCondition[]; +} +interface ActionButton { + name: string; + icon?: string; + classes?: string[]; + job_template?: string; + params?: { activity_uid?: string; workflow_uuid?: string; [k: string]: unknown }; + show_conditions?: ActionConditions | null; + action_end?: Array<{ type: string }>; +} +interface ActionColumn { + id: string; + display: string; + buttons: ActionButton[]; +} + +const ICONS: Record = { + refresh: , + task_alt: , }; -// State name (as returned by record view) → state UID. Used to resolve row actions. -const STATE_BY_NAME: Record = { - "Awaiting Scheduler Call": STATE_IDS.AWAITING_SCHEDULER_CALL, - "Scheduled": STATE_IDS.SCHEDULED, - "Awaiting Feedback Call": STATE_IDS.AWAITING_FEEDBACK_CALL, - "Feedback Collected": STATE_IDS.FEEDBACK_COLLECTED, - "Scheduling Call Failed": STATE_IDS.SCHEDULING_CALL_FAILED, - "Feedback Call Failed": STATE_IDS.FEEDBACK_CALL_FAILED, - "End State": STATE_IDS.END_STATE, -}; +function iconFor(name?: string): React.ReactNode { + return (name && ICONS[name]) || ; +} + +// Walks the rv-screen layout to find columns with `type: "custom"` and their +// inner button configs. +function extractActionColumns(layout: any): ActionColumn[] { + const out: ActionColumn[] = []; + const walk = (nodes: any[]): void => { + for (const n of nodes ?? []) { + const cfg = n?.config; + if (cfg?.type === "rv_table" && Array.isArray(cfg.columns)) { + for (const col of cfg.columns) { + if (col?.type === "custom" && Array.isArray(col.config)) { + const buttons = col.config.filter((b: any) => b?.type === "button"); + if (buttons.length > 0) { + out.push({ id: col.id, display: col.display || "Actions", buttons }); + } + } + } + } + if (Array.isArray(n?.children)) walk(n.children); + } + }; + walk(Array.isArray(layout) ? layout : []); + return out; +} + +function evalShowConditions(c: ActionConditions | null | undefined, row: Record): boolean { + if (!c || !Array.isArray(c.conditions) || c.conditions.length === 0) return true; + const results = c.conditions.map((cond) => { + const v = row[cond.field_key]; + switch (cond.operator) { + case "equals": return v === cond.value; + case "not_equals": return v !== cond.value; + case "is_empty": return v == null || v === ""; + case "is_not_empty": return v != null && v !== ""; + case "in": return Array.isArray(cond.value) && cond.value.includes(v as never); + case "not_in": return Array.isArray(cond.value) && !cond.value.includes(v as never); + default: return false; + } + }); + return (c.logic === "OR") ? results.some(Boolean) : results.every(Boolean); +} + +function buttonClass(classes?: string[]): string { + const ghost = classes?.includes("z-btn-secondary"); + const base = "h-7 px-2.5 text-[11px] font-semibold rounded-lg inline-flex items-center gap-1 transition-all"; + return ghost + ? `${base} text-stone-600 dark:text-zinc-300 bg-white dark:bg-zinc-800 border border-stone-200 dark:border-zinc-700 hover:bg-stone-50 dark:hover:bg-zinc-700` + : `${base} text-white mh-glow hover:scale-[1.02]`; +} // --------------------------------------------------------------------------- // Skeleton @@ -109,6 +167,7 @@ export default function RecordViewTable({ viewId, onRowClick, toolbarAction }: P const navigate = useNavigate(); const [rvTemplateUid, setRvTemplateUid] = useState(null); const [columnLabels, setColumnLabels] = useState>({}); + const [actionColumns, setActionColumns] = useState([]); const [fields, setFields] = useState([]); const [rows, setRows] = useState[]>([]); const [pagination, setPagination] = useState({ page: 1, limit: 10, total_count: 0, total_pages: 0 }); @@ -124,9 +183,11 @@ export default function RecordViewTable({ viewId, onRowClick, toolbarAction }: P setLoading(true); setRvTemplateUid(null); setColumnLabels({}); + setActionColumns([]); getRVScreen(viewId) .then((rv) => { setColumnLabels(extractColumnLabels(rv.layout)); + setActionColumns(extractActionColumns(rv.layout)); setRvTemplateUid(rv.rv_template_uid); }) .catch((err) => { setError(err.message); setLoading(false); }); @@ -298,6 +359,11 @@ export default function RecordViewTable({ viewId, onRowClick, toolbarAction }: P ); })} + {actionColumns.map((ac) => ( + + {ac.display} + + ))} @@ -305,7 +371,7 @@ export default function RecordViewTable({ viewId, onRowClick, toolbarAction }: P {rows.length === 0 ? ( - +
@@ -325,12 +391,7 @@ export default function RecordViewTable({ viewId, onRowClick, toolbarAction }: P
- ) : rows.map((row, i) => { - const stateName = String(row.current_state_name ?? ""); - const stateId = STATE_BY_NAME[stateName]; - const actions = stateId ? ROW_ACTIONS[stateId] : undefined; - - return ( + ) : rows.map((row, i) => ( handleRowClick(row)} @@ -358,27 +419,48 @@ export default function RecordViewTable({ viewId, onRowClick, toolbarAction }: P ); })} + {actionColumns.map((ac) => { + const visible = ac.buttons.filter((b) => evalShowConditions(b.show_conditions, row)); + return ( + e.stopPropagation()}> + {visible.length === 0 ? ( + + ) : ( +
+ {visible.map((b, bi) => { + const aid = b.params?.activity_uid; + if (!aid) return null; + return ( + + ); + })} +
+ )} + + ); + })} + e.stopPropagation()}> -
- {actions && actions.length > 0 && ( - - setFormModal({ activityId, instanceId: String(row.instance_id), title: label }) - } - /> - )} - -
+ - ); - })} + ))}
@@ -458,55 +540,6 @@ export default function RecordViewTable({ viewId, onRowClick, toolbarAction }: P ); } -// --------------------------------------------------------------------------- -// Row menu -// --------------------------------------------------------------------------- - -interface RowMenuProps { - actions: Array<{ activityId: string; label: string; icon: React.ReactNode }>; - onAction: (activityId: string, label: string) => void; -} - -function RowMenu({ actions, onAction }: RowMenuProps) { - const [open, setOpen] = useState(false); - const ref = useRef(null); - - useEffect(() => { - if (!open) return; - const close = (e: MouseEvent) => { - if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false); - }; - document.addEventListener("mousedown", close); - return () => document.removeEventListener("mousedown", close); - }, [open]); - - return ( -
- - - {open && ( -
- {actions.map((a) => ( - - ))} -
- )} -
- ); -} - // --------------------------------------------------------------------------- // Helpers // ---------------------------------------------------------------------------