From ed70ab32961f12573e78191d028038a7f905b18c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 09:39:41 +0000 Subject: [PATCH 1/4] Initial plan From 9fd3b5a5ea9c820e627883f3eaa1ae3cac29828a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 09:46:01 +0000 Subject: [PATCH 2/4] fix: deduplicate action buttons in action-bar and RecordDetailView Actions with duplicate `name` entries are now filtered (keep-first) in the action:bar renderer (platform-level fix) and in RecordDetailView (consumer-level defense). This prevents duplicate buttons appearing inline and in overflow menus. Fixes #duplicate-action-buttons Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- .../src/components/RecordDetailView.tsx | 4 +-- .../src/__tests__/action-bar.test.tsx | 34 +++++++++++++++++++ .../src/renderers/action/action-bar.tsx | 19 ++++++++--- 3 files changed, 50 insertions(+), 7 deletions(-) diff --git a/apps/console/src/components/RecordDetailView.tsx b/apps/console/src/components/RecordDetailView.tsx index b361b50d26..d282eb7fb7 100644 --- a/apps/console/src/components/RecordDetailView.tsx +++ b/apps/console/src/components/RecordDetailView.tsx @@ -386,10 +386,10 @@ export function RecordDetailView({ dataSource, objects, onEdit }: RecordDetailVi }, ]; - // Filter actions for record_header location + // Filter actions for record_header location and deduplicate by name const recordHeaderActions = (objectDef.actions || []).filter( (a: any) => a.locations?.includes('record_header'), - ); + ).filter((a: any, i: number, arr: any[]) => arr.findIndex((b: any) => b.name === a.name) === i); // Build highlightFields: prefer explicit config, fallback to auto-detect key fields const explicitHighlight: HighlightField[] | undefined = objectDef.views?.detail?.highlightFields; diff --git a/packages/components/src/__tests__/action-bar.test.tsx b/packages/components/src/__tests__/action-bar.test.tsx index a8e2b0d775..c432a9f13e 100644 --- a/packages/components/src/__tests__/action-bar.test.tsx +++ b/packages/components/src/__tests__/action-bar.test.tsx @@ -94,6 +94,40 @@ describe('ActionBar (action:bar)', () => { expect(container.textContent).toContain('Action 1'); expect(container.textContent).toContain('Action 2'); }); + + it('deduplicates actions by name', () => { + const { container } = renderComponent({ + type: 'action:bar', + actions: [ + { name: 'change_status', label: 'Change Status', type: 'script', component: 'action:button' }, + { name: 'assign_user', label: 'Assign User', type: 'script', component: 'action:button' }, + { name: 'change_status', label: 'Change Status', type: 'script', component: 'action:button' }, + ], + }); + const toolbar = container.querySelector('[role="toolbar"]'); + expect(toolbar).toBeTruthy(); + // Should only render 2 actions (duplicates removed) + expect(toolbar!.children.length).toBe(2); + expect(container.textContent).toContain('Change Status'); + expect(container.textContent).toContain('Assign User'); + }); + + it('deduplicates actions after location filtering', () => { + const { container } = renderComponent({ + type: 'action:bar', + location: 'record_header', + actions: [ + { name: 'change_status', label: 'Change Status', type: 'script', locations: ['record_header'] }, + { name: 'assign_user', label: 'Assign User', type: 'script', locations: ['record_header'] }, + { name: 'change_status', label: 'Change Status', type: 'script', locations: ['record_header', 'record_more'] }, + { name: 'assign_user', label: 'Assign User', type: 'script', locations: ['record_header'] }, + ], + }); + const toolbar = container.querySelector('[role="toolbar"]'); + expect(toolbar).toBeTruthy(); + // Should only render 2 unique actions + expect(toolbar!.children.length).toBe(2); + }); }); describe('overflow', () => { diff --git a/packages/components/src/renderers/action/action-bar.tsx b/packages/components/src/renderers/action/action-bar.tsx index 4d647d0e92..f11a618d2d 100644 --- a/packages/components/src/renderers/action/action-bar.tsx +++ b/packages/components/src/renderers/action/action-bar.tsx @@ -76,13 +76,22 @@ const ActionBarRenderer = forwardRef { const actions = schema.actions || []; - if (!schema.location) return actions; - return actions.filter( - a => !a.locations || a.locations.length === 0 || a.locations.includes(schema.location!), - ); + const located = !schema.location + ? actions + : actions.filter( + a => !a.locations || a.locations.length === 0 || a.locations.includes(schema.location!), + ); + // Deduplicate by action name — keep first occurrence + const seen = new Set(); + return located.filter(a => { + if (!a.name) return true; + if (seen.has(a.name)) return false; + seen.add(a.name); + return true; + }); }, [schema.actions, schema.location]); // Split into visible inline actions and overflow From f41721266a8b43565bf6a30fad54b7997aff0bc5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 09:49:42 +0000 Subject: [PATCH 3/4] refactor: use Set-based deduplication in RecordDetailView for consistency Address code review: use the same O(n) Set-based dedup pattern as action-bar.tsx, wrapped in useMemo for React performance. Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- apps/console/src/components/RecordDetailView.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/apps/console/src/components/RecordDetailView.tsx b/apps/console/src/components/RecordDetailView.tsx index d282eb7fb7..5c701013ac 100644 --- a/apps/console/src/components/RecordDetailView.tsx +++ b/apps/console/src/components/RecordDetailView.tsx @@ -387,9 +387,16 @@ export function RecordDetailView({ dataSource, objects, onEdit }: RecordDetailVi ]; // Filter actions for record_header location and deduplicate by name - const recordHeaderActions = (objectDef.actions || []).filter( - (a: any) => a.locations?.includes('record_header'), - ).filter((a: any, i: number, arr: any[]) => arr.findIndex((b: any) => b.name === a.name) === i); + const recordHeaderActions = useMemo(() => { + const seen = new Set(); + return (objectDef.actions || []).filter((a: any) => { + if (!a.locations?.includes('record_header')) return false; + if (!a.name) return true; + if (seen.has(a.name)) return false; + seen.add(a.name); + return true; + }); + }, [objectDef.actions]); // Build highlightFields: prefer explicit config, fallback to auto-detect key fields const explicitHighlight: HighlightField[] | undefined = objectDef.views?.detail?.highlightFields; From 97d667865bf35d8857ad683ad8ea8c0b41bc38f7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 10:23:18 +0000 Subject: [PATCH 4/4] fix: remove useMemo after early returns to fix React error #310 The useMemo hook was placed after conditional early returns in RecordDetailView, violating React's Rules of Hooks and causing "Rendered fewer hooks than expected" (React error #310). Replaced with an IIFE to keep the same Set-based dedup logic without hooks. Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- apps/console/src/components/RecordDetailView.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/console/src/components/RecordDetailView.tsx b/apps/console/src/components/RecordDetailView.tsx index 5c701013ac..318b098dcf 100644 --- a/apps/console/src/components/RecordDetailView.tsx +++ b/apps/console/src/components/RecordDetailView.tsx @@ -387,7 +387,7 @@ export function RecordDetailView({ dataSource, objects, onEdit }: RecordDetailVi ]; // Filter actions for record_header location and deduplicate by name - const recordHeaderActions = useMemo(() => { + const recordHeaderActions = (() => { const seen = new Set(); return (objectDef.actions || []).filter((a: any) => { if (!a.locations?.includes('record_header')) return false; @@ -396,7 +396,7 @@ export function RecordDetailView({ dataSource, objects, onEdit }: RecordDetailVi seen.add(a.name); return true; }); - }, [objectDef.actions]); + })(); // Build highlightFields: prefer explicit config, fallback to auto-detect key fields const explicitHighlight: HighlightField[] | undefined = objectDef.views?.detail?.highlightFields;