Skip to content
This repository was archived by the owner on Oct 26, 2026. It is now read-only.

Commit 2cc4ab9

Browse files
committed
Merge origin/main into claude/issue-6152-unmirrored-round4
Carries main through PR #11275. No conflicts. Claude-Session: https://claude.ai/code/session_01TdiauJaVCHuj45EzZGUxHh Co-authored-by: Claude <noreply@anthropic.com>
2 parents 556373c + e420df3 commit 2cc4ab9

3 files changed

Lines changed: 75 additions & 1 deletion

File tree

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
'@object-ui/plugin-detail': patch
3+
---
4+
5+
fix(plugin-detail): a related list's `list_toolbar` action authored `visible: false` is hidden
6+
7+
`RelatedToolbarButton` decided whether an action declared a `visible` gate by
8+
truthiness. A literal `visible: false`, the most explicit way an author can say
9+
"never show this", is falsy, so the gate was skipped and the header button
10+
rendered for everyone. It now asks the question with `hasDeclaredVisibilityGate`,
11+
the one definition the rest of the action family already uses (objectui#3812):
12+
13+
- `visible: false` hides the button.
14+
- A blank predicate (`''`, or a whitespace-only string) is no gate, so the button
15+
shows. A whitespace-only `visible` used to hide the button; it now shows, as it
16+
does on every other action surface.
17+
- A CEL string is evaluated, as before.
18+
- A predicate that faults still hides the button and is reported once
19+
(objectui#11212), unchanged.
20+
21+
The server still judges the action's permission when it runs; this is the
22+
presentation gate.

‎packages/plugin-detail/src/RelatedList.tsx‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
AlertDialogTitle,
2626
cn,
2727
EmptyValue,
28+
hasDeclaredVisibilityGate,
2829
resolveIcon,
2930
useIsMobile,
3031
} from '@object-ui/components';
@@ -435,6 +436,11 @@ function dropRedactedColumns<T>(cols: T[], redacted: ReadonlySet<string>): T[] {
435436
* permissions payload has loaded, an unbound root — hides the button and is
436437
* reported once, instead of showing an action whose guard is broken. This leg
437438
* used to fail SOFT to `true`.
439+
*
440+
* Whether a gate is DECLARED at all is asked with `hasDeclaredVisibilityGate`,
441+
* as every other member of the action family asks it (objectui#3812,
442+
* objectui#11244), never by truthiness: `visible: false` is a declared gate and
443+
* hides the button, while a blank predicate (`''`, whitespace) is no gate.
438444
*/
439445
export const RelatedToolbarButton: React.FC<{
440446
action: RelatedRowActionDef;
@@ -447,7 +453,7 @@ export const RelatedToolbarButton: React.FC<{
447453
throwOnError: true,
448454
label: `related-list toolbar action "${action.name}" (visible)`,
449455
});
450-
if (visiblePred && !isVisible) return null;
456+
if (hasDeclaredVisibilityGate(visiblePred) && !isVisible) return null;
451457
const ActionIcon = action.icon ? resolveIconComponent(action.icon) : null;
452458
return (
453459
<Button

‎packages/plugin-detail/src/__tests__/related-toolbar-visible.test.tsx‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,3 +115,49 @@ describe('RelatedList list_toolbar button — a faulting `visible` hides (object
115115
}
116116
});
117117
});
118+
119+
/**
120+
* objectui#11244 — whether a `visible` gate is DECLARED is asked with
121+
* `hasDeclaredVisibilityGate`, as the rest of the action family asks it
122+
* (objectui#3812), never by truthiness. A literal `visible: false` — the most
123+
* explicit way an author can say "never show this" — is falsy, so a truthiness
124+
* test read it as "no gate" and the button rendered for everyone. A blank
125+
* predicate is still no gate (the action shows), and a CEL string is evaluated.
126+
* The fail-closed leg on a FAULT is the objectui#11212 block above, unchanged.
127+
*/
128+
describe('RelatedList list_toolbar button — a declared `visible` is asked by declaredness, not truthiness (objectui#11244)', () => {
129+
it('hides a toolbar action authored `visible: false` (DETECTOR)', () => {
130+
renderButton({ name: 'archive_11244', label: 'Archive', visible: false });
131+
renderButton({ name: 'export', label: 'Export' });
132+
expect(screen.getByTestId('related-toolbar-action-export')).toBeInTheDocument();
133+
expect(screen.queryByTestId('related-toolbar-action-archive_11244')).toBeNull();
134+
});
135+
136+
it('shows a toolbar action authored `visible: true`', () => {
137+
renderButton({ name: 'archive_11244', label: 'Archive', visible: true });
138+
expect(screen.getByTestId('related-toolbar-action-archive_11244')).toBeInTheDocument();
139+
});
140+
141+
it.each([
142+
['an empty string', ''],
143+
['a whitespace-only string', ' '],
144+
])('reads a blank `visible` (%s) as no gate, so the action shows', (_label, blank) => {
145+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
146+
try {
147+
renderButton({ name: 'archive_11244', label: 'Archive', visible: blank });
148+
expect(screen.getByTestId('related-toolbar-action-archive_11244')).toBeInTheDocument();
149+
} finally {
150+
warn.mockRestore();
151+
}
152+
});
153+
154+
it('evaluates a CEL string `visible` that answers true: the action shows', () => {
155+
renderButton({ name: 'archive_11244', label: 'Archive', visible: '1 == 1' });
156+
expect(screen.getByTestId('related-toolbar-action-archive_11244')).toBeInTheDocument();
157+
});
158+
159+
it('evaluates a CEL string `visible` that answers false: the action hides', () => {
160+
renderButton({ name: 'archive_11244', label: 'Archive', visible: '1 == 2' });
161+
expect(screen.queryByTestId('related-toolbar-action-archive_11244')).toBeNull();
162+
});
163+
});

0 commit comments

Comments
 (0)