Skip to content

Commit f0f7e57

Browse files
committed
Merge origin/main into the 2i PR B branch (#26137)
#26277 re-recorded scripts/sdui-manifest.sources/plugin-detail.json while the manifest's bytes stayed the same: one modify/delete conflict against this PR's deletion of the records, resolved by deleting it. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A6SEitNngAMugGbuJHWsDS
2 parents bb61df2 + f02e644 commit f0f7e57

4 files changed

Lines changed: 95 additions & 12 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@object-ui/plugin-detail': minor
3+
---
4+
5+
fix(plugin-detail): `record:related_list` with `actions: []` no longer draws the built-in New
6+
7+
Clause-②: no
8+
Metadata-protocol: semantic
9+
10+
- **Before.** `actions: []` on a `record:related_list` hid the list's authored and default actions, but the built-in **New** stayed whenever the host offered create. The only way to drop it was the child object's `userActions.create`, which removes New from every list of that object. A page could not say that one list's children must be created through a process (a renewal through its flow, a version through upload), and the generic create form, with the relationship prefilled, bypassed that process.
11+
- **Now.** An authored `actions: []` is the author's choice of no actions on that list, New included. It holds per list and does not depend on the child object's `userActions.create`. Every list that does not author `[]` keeps the host's verdict.
12+
- **Unchanged.** A list with no `actions`, or with a non-empty `actions` list, draws New exactly as before. The row affordances (View, Edit, Delete) and the `add` (link existing) button are not governed by this key. No key is added and none is removed.
13+
- **What to do.** Nothing for a list that should keep New. A page that authored `actions: []` and still wants New on that list deletes the key.

‎ui/packages/plugin-detail/README.md‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -737,6 +737,22 @@ the whole section renders nothing — no header, no empty grid, no "New"
737737
button that would be rejected server-side. With no `PermissionProvider`
738738
mounted (Studio designer, standalone embeds) the gate stays open.
739739
740+
A page author withdraws **New** from one related list by authoring
741+
`actions: []` on its `record:related_list` node — the author's choice of no
742+
actions on that list, the generic create form included. Use it where the
743+
children must be created through a process (a renewal through its flow, a
744+
version through upload). It holds per list, whatever the child object's
745+
`userActions.create` says; every list that does not author `[]` keeps the
746+
host's verdict. A non-empty `actions` list leaves New as it was, and the row
747+
affordances (View, Edit, Delete) are not governed by this key.
748+
749+
```json
750+
{
751+
"type": "record:related_list",
752+
"properties": { "objectName": "contract", "relationshipField": "renewed_from", "actions": [] }
753+
}
754+
```
755+
740756
### InlineEditSaveBar & InlineFieldInput
741757
742758
The record-level inline-edit session (#2407): double-clicking a field (or its

‎ui/packages/plugin-detail/src/__tests__/RecordRelatedListRenderer.authoredActions-11163.test.tsx‎

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,11 @@
2727
* its own `locations`, with the bridge's rule: `list_toolbar` → header
2828
* button, `list_item` → row menu. Running it stays the host's (the
2929
* bridge's `onToolbarAction` / `onRowAction`).
30-
* - `actions: []` → the author's choice of NO actions. The built-in
31-
* New / Edit / Delete / View affordances are not action ids (the runtime
32-
* ships no built-in action names) and are untouched by this key.
30+
* - `actions: []` → the author's choice of NO actions, the built-in New
31+
* included (#26182): the list offers no generic create form, whatever the
32+
* child object's `userActions.create` says. A NON-empty list leaves New
33+
* the host's, and Edit / Delete / View are row affordances this key never
34+
* touches (the runtime ships no built-in action names).
3335
* - An id the registry cannot resolve, or one whose action declares no
3436
* location this list renders, is REFUSED where the lookup answers: an
3537
* inline `role="status"` notice on the list names it and says why. Never a
@@ -251,18 +253,55 @@ describe('record:related_list.actions — AUTHORED: the authored list is what re
251253
expect(onToolbarAction.mock.calls[0][0]).toMatchObject({ name: 'invite', label: 'Invite', type: 'api' });
252254
});
253255

254-
it('`actions: []` is the author’s choice of NO actions — while New stays, because it is not an action id', async () => {
256+
it('`actions: []` is the author’s choice of NO actions — New included, though the host offers it (#26182)', async () => {
255257
mount([]);
256258
await waitForRow();
257-
// Positive first, so the empty reads below are taken on a list that
258-
// really drew its header: the host's create affordance is still there.
259-
expect(await screen.findByTestId('related-list-new')).toBeInTheDocument();
259+
expect(screen.queryByTestId('related-list-new')).toBeNull();
260260
expect(toolbarNames()).toEqual([]);
261261
expect(rowTriggers()).toHaveLength(0);
262262
expect(refusal()).toBeNull();
263263
});
264264
});
265265

266+
describe('record:related_list.actions — the built-in New: only `[]` withholds it (#26182)', () => {
267+
// Each case reads the SAME host, which offers create for `contact`: what
268+
// differs is only the authored key, so the verdict is this list's own.
269+
it('CONTROL: with no `actions` the host’s New is drawn and runs the host’s create', async () => {
270+
mount(undefined);
271+
await waitForRow();
272+
const button = await screen.findByTestId('related-list-new');
273+
fireEvent.click(button);
274+
expect(onCreate).toHaveBeenCalledTimes(1);
275+
});
276+
277+
it('a NON-empty authored list leaves New the host’s: naming actions is not withholding create', async () => {
278+
mount(['invite']);
279+
await waitForRow();
280+
await waitFor(() => expect(toolbarNames()).toEqual(['invite']));
281+
expect(screen.getByTestId('related-list-new')).toBeInTheDocument();
282+
});
283+
284+
it('`actions: []` withholds New on THIS list only — a sibling list of the same object keeps it', async () => {
285+
render(
286+
<MetadataCtx.Provider value={METADATA}>
287+
<RecordContextProvider objectName="account" recordId="ACC-1" dataSource={makeDataSource() as never}>
288+
<RelatedRecordActionsProvider value={HOST}>
289+
<div data-testid="list-empty">
290+
<SchemaRenderer schema={doc([]) as never} />
291+
</div>
292+
<div data-testid="list-default">
293+
<SchemaRenderer schema={doc(undefined) as never} />
294+
</div>
295+
</RelatedRecordActionsProvider>
296+
</RecordContextProvider>
297+
</MetadataCtx.Provider>,
298+
);
299+
await waitFor(() => expect(screen.getAllByText('Alice')).toHaveLength(2));
300+
expect(await within(screen.getByTestId('list-default')).findByTestId('related-list-new')).toBeInTheDocument();
301+
expect(within(screen.getByTestId('list-empty')).queryByTestId('related-list-new')).toBeNull();
302+
});
303+
});
304+
266305
describe('record:related_list.actions — an id that cannot be placed is REFUSED where it resolves (objectui#11163)', () => {
267306
it('an id the registry cannot resolve is named in a `role="status"` notice; the resolvable ones still render', async () => {
268307
mount(['invite', 'no_such_action']);

‎ui/packages/plugin-detail/src/renderers/record-related-list.tsx‎

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -312,14 +312,16 @@ const RecordRelatedListBody: React.FC<RecordRelatedListRendererProps> = ({
312312
* - AUTHORED → the authored list is what renders, in authored order:
313313
* each id resolves against the RELATED object's registered `actions`
314314
* and is placed by its own `locations` (see `relatedListActions.ts`).
315-
* `[]` is the author's choice of no actions.
315+
* - AUTHORED `[]` → the author's choice of NO actions on this list, the
316+
* built-in New included (#26182): see `createWithheldByAuthor` below.
316317
*
317318
* The bridge offers no per-list channel of its own — `resolve` is keyed on
318319
* the child OBJECT (`objectName`, `relationshipField`, `parentId`), so two
319320
* lists of one object get one set — which is why this key is the per-list
320321
* one rather than a duplicate of it. Built-in New / Edit / Delete / View are
321-
* NOT action ids (the runtime ships no built-in action names) and stay the
322-
* host's either way. Running an authored action stays the host's too: it is
322+
* NOT action ids (the runtime ships no built-in action names): a non-empty
323+
* list leaves all four the host's, and `[]` withdraws New alone. Running an
324+
* authored action stays the host's too: it is
323325
* handed to the bridge's `onToolbarAction` / `onRowAction`, the executor
324326
* that already runs the child object's actions against the clicked row, so
325327
* with no host the list stays read-only exactly as it always has.
@@ -332,6 +334,18 @@ const RecordRelatedListBody: React.FC<RecordRelatedListRendererProps> = ({
332334
*/
333335
const authoredActions: unknown = schema.actions;
334336
const actionsAuthored = authoredActions !== undefined;
337+
/**
338+
* `actions: []` withholds the built-in New on THIS list (#26182). A related
339+
* list often shows children that may only be created through a process (a
340+
* renewal through its flow, a version through upload), and the empty list is
341+
* how the page author says the list offers nothing — so the generic create
342+
* form, which would bypass that process, is not offered either. Per list,
343+
* and independent of the child object's `userActions.create`: the host's
344+
* object-wide verdict still decides every list that does not say `[]`.
345+
* Edit / Delete / View are row affordances on records that already exist and
346+
* stay the host's. Absent, or a non-empty list, leaves New exactly as before.
347+
*/
348+
const createWithheldByAuthor = Array.isArray(authoredActions) && authoredActions.length === 0;
335349
const needsActionLookup = !!objectName && relatedListActionsNeedLookup(authoredActions);
336350
/**
337351
* The FALLBACK remove (below, `onRowDelete`): offered with an `add` config
@@ -661,8 +675,9 @@ const RecordRelatedListBody: React.FC<RecordRelatedListRendererProps> = ({
661675
toolbarActions={toolbarActions}
662676
onToolbarAction={handlers?.onToolbarAction}
663677
// Create a new child, pre-linked to this parent (增). Host omits when
664-
// create is denied by lifecycle/permissions, hiding the "New" button.
665-
onNew={handlers?.onCreate}
678+
// create is denied by lifecycle/permissions, hiding the "New" button;
679+
// an authored `actions: []` omits it on this list alone (#26182).
680+
onNew={createWithheldByAuthor ? undefined : handlers?.onCreate}
666681
// [#4646] …and greys it — without hiding it — when the child object's
667682
// `userActions.create.disabledWhen` holds for THIS parent record. The
668683
// host owns both verdicts (it is the side that has the parent record

0 commit comments

Comments
 (0)