render the workflow-launch buttons configured on rv_screens
The "Create New Invoice" button was configured all along, on the rv_screen layout rather than the screen layout, and was being dropped twice over: RecordViewTable fetched the rv_screen but kept only rv_template_uid and threw away `layout`, and ScreenRenderer matched only the legacy params.activity_id_init while current studio writes activity_uid_init. collectStartButtons deep-walks either shape — the button nested under a node's `config` as screen layouts do, or carrying type/params/job_template at its own top level as rv_screens do — and accepts activity_uid_init, activity_id_init, activity_uid and activity_id, newest key first. RecordViewTable renders any start_state_machine buttons from its rv_screen in the toolbar alongside the injected toolbarAction; ScreenRenderer routes through the same helper and now skips buttons with no resolvable activity instead of rendering a dead control. Verified against app 169's live rv_screen config: yields exactly one button, "Create New Invoice" -> Submit Invoice, ignoring the show_detailview_popup entry. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
db8233fa45
commit
dc2e281347
@ -3,12 +3,13 @@ import { useNavigate } from "react-router-dom";
|
||||
import {
|
||||
ChevronLeft, ChevronRight, Search, ArrowUp, ArrowDown,
|
||||
X, CheckCircle2, XCircle, PauseCircle, MessageSquare,
|
||||
Inbox, MoreHorizontal, ArrowRight, RefreshCw,
|
||||
Inbox, MoreHorizontal, ArrowRight, RefreshCw, Plus,
|
||||
} from "lucide-react";
|
||||
import { getRVScreen, getRecordView, type RecordViewField, type SearchQuery } from "../api/viewService";
|
||||
import { TableSkeleton } from "./Skeleton";
|
||||
import FormModal from "./FormModal";
|
||||
import { STATE_IDS, ACTIVITY_IDS } from "../config";
|
||||
import { startWorkflowButtons, type StartButton } from "../lib/startButtons";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Row actions by state
|
||||
@ -46,13 +47,21 @@ export default function RecordViewTable({ viewId, onRowClick, toolbarAction }: P
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [dataLoading, setDataLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [formModal, setFormModal] = useState<{ activityId: string; instanceId: string; title: string } | null>(null);
|
||||
// instanceId is absent for start-workflow buttons (they create the instance).
|
||||
const [formModal, setFormModal] = useState<{ activityId: string; instanceId?: string; title: string } | null>(null);
|
||||
// Workflow-launching buttons configured on the rv_screen ("Create New
|
||||
// Invoice" and friends). They live in the rv_screen layout, not the screen
|
||||
// layout, so this is the only place they can be picked up.
|
||||
const [startButtons, setStartButtons] = useState<StartButton[]>([]);
|
||||
const searchRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
getRVScreen(viewId)
|
||||
.then((rv) => setRvTemplateId(rv.rv_template_uid))
|
||||
.then((rv) => {
|
||||
setRvTemplateId(rv.rv_template_uid);
|
||||
setStartButtons(startWorkflowButtons(rv.layout));
|
||||
})
|
||||
.catch((err) => { setError(err.message); setLoading(false); });
|
||||
}, [viewId]);
|
||||
|
||||
@ -145,8 +154,18 @@ export default function RecordViewTable({ viewId, onRowClick, toolbarAction }: P
|
||||
</div>
|
||||
)}
|
||||
|
||||
{toolbarAction && (
|
||||
<div className="pl-3 border-l border-slate-200 dark:border-zinc-700">
|
||||
{(startButtons.length > 0 || toolbarAction) && (
|
||||
<div className="pl-3 border-l border-slate-200 dark:border-zinc-700 flex items-center gap-2">
|
||||
{startButtons.map((b) => (
|
||||
<button
|
||||
key={b.key}
|
||||
onClick={() => setFormModal({ activityId: b.activityId, title: b.label })}
|
||||
className="h-8 px-3.5 text-[12px] font-semibold text-white rounded-xl inline-flex items-center gap-1.5 transition-all hover:scale-[1.02] active:scale-[0.98] shadow-md shadow-violet-500/20"
|
||||
style={{ background: "linear-gradient(135deg, #7c3aed, #a855f7)" }}
|
||||
>
|
||||
<Plus size={13} strokeWidth={2.5} />{b.label}
|
||||
</button>
|
||||
))}
|
||||
{toolbarAction}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -4,6 +4,7 @@ import type { LayoutElement } from "../api/viewService";
|
||||
import RecordViewTable from "./RecordViewTable";
|
||||
import DetailViewPanel from "./DetailViewPanel";
|
||||
import FormModal from "./FormModal";
|
||||
import { collectStartButtons } from "../lib/startButtons";
|
||||
|
||||
interface Props { layout: LayoutElement[]; instanceId?: string; onRefresh?: () => void; onRowClick?: (instanceId: string) => void; }
|
||||
|
||||
@ -25,7 +26,11 @@ export default function ScreenRenderer({ layout, instanceId, onRefresh, onRowCli
|
||||
const visit = (el: any) => {
|
||||
const cfg = el.config || {};
|
||||
if (cfg.type === "button" || cfg.job_template) {
|
||||
const aid = cfg.params?.activity_id_init || cfg.params?.activity_id;
|
||||
// Current studio writes params.activity_uid_init; older configs used
|
||||
// activity_id_init. collectStartButtons accepts both — see lib/startButtons.
|
||||
const [btn] = collectStartButtons(cfg);
|
||||
const aid = btn?.activityId;
|
||||
if (!aid) return; // a button with no activity to run is not renderable
|
||||
const label = (cfg.name || "").replace(/^\+\s*/, "");
|
||||
buttons.push(
|
||||
<button
|
||||
|
||||
86
src/lib/startButtons.ts
Normal file
86
src/lib/startButtons.ts
Normal file
@ -0,0 +1,86 @@
|
||||
// collectStartButtons — find the workflow-launching buttons in a studio layout.
|
||||
//
|
||||
// Studio puts these buttons in two different places, in two different shapes,
|
||||
// which is why they were previously missed entirely:
|
||||
//
|
||||
// * screen layout (tbl_appconfig_screens.layout) — the button lives under a
|
||||
// node's `config` key, nested anywhere in a `children` tree.
|
||||
// * rv_screen layout (tbl_appconfig_rv_screens.layout) — the button object
|
||||
// itself carries `type` / `params` / `job_template` at its top level.
|
||||
//
|
||||
// The activity identifier has also changed key over time. Current studio writes
|
||||
// `params.activity_uid_init`; older configs (and the dev-era app) used
|
||||
// `params.activity_id_init`. Both are accepted here, newest first.
|
||||
//
|
||||
// Rather than model either shape, this walks every object in the tree and picks
|
||||
// out anything that looks like a button carrying an activity id.
|
||||
|
||||
export interface StartButton {
|
||||
/** React key — the activity uid is unique per button in practice. */
|
||||
key: string;
|
||||
label: string;
|
||||
/** Activity uid to launch. For start_state_machine this is the INIT activity. */
|
||||
activityId: string;
|
||||
/** 'start_state_machine' (new instance) | 'perform_activity' (existing) | other. */
|
||||
jobTemplate?: string;
|
||||
workflowUuid?: string;
|
||||
versionUuid?: string;
|
||||
}
|
||||
|
||||
const pickActivityId = (params: any): string | undefined =>
|
||||
params?.activity_uid_init ||
|
||||
params?.activity_id_init ||
|
||||
params?.activity_uid ||
|
||||
params?.activity_id ||
|
||||
undefined;
|
||||
|
||||
// A node is button-ish if it says so, or if it carries a job_template (studio
|
||||
// omits type on some button variants).
|
||||
const isButtonish = (o: any): boolean =>
|
||||
o?.type === "button" || typeof o?.job_template === "string";
|
||||
|
||||
export function collectStartButtons(layout: unknown): StartButton[] {
|
||||
const out: StartButton[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
const visit = (node: any): void => {
|
||||
if (!node || typeof node !== "object") return;
|
||||
|
||||
if (Array.isArray(node)) {
|
||||
node.forEach(visit);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isButtonish(node)) {
|
||||
const activityId = pickActivityId(node.params);
|
||||
if (activityId) {
|
||||
const jobTemplate = node.job_template;
|
||||
const key = `${jobTemplate ?? "button"}:${activityId}`;
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key);
|
||||
out.push({
|
||||
key,
|
||||
// Studio labels sometimes carry a leading "+ ".
|
||||
label: String(node.name ?? "New").replace(/^\+\s*/, ""),
|
||||
activityId,
|
||||
jobTemplate,
|
||||
workflowUuid: node.params?.workflow_uuid,
|
||||
versionUuid: node.params?.version_uuid,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Recurse into everything — `config`, `children`, and any other nesting.
|
||||
for (const v of Object.values(node)) visit(v);
|
||||
};
|
||||
|
||||
visit(layout);
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Buttons that start a brand-new workflow instance. */
|
||||
export const startWorkflowButtons = (layout: unknown): StartButton[] =>
|
||||
collectStartButtons(layout).filter(
|
||||
(b) => b.jobTemplate === "start_state_machine" || b.jobTemplate === undefined,
|
||||
);
|
||||
Loading…
Reference in New Issue
Block a user