Skip to content

Commit 86aaff3

Browse files
fix(studio): an item just added to a dashboard list property is held until it is filled in, not autosaved and refused (#27459) (#27784)
<!-- claude-projects-attribution: {"github_login":"os-bill"} --> _Requested by **Bill** · [project thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wg4i9JiKjEjwtuVzd4gKy9hB)_ ## 维护者速读 - 改了什么:Studio 界面支柱里,仪表盘属性面板的列表(「全局筛选」、页眉「操作按钮」)点「Add item」后,新项在必填项填好之前不再自动保存草稿;画布上方出现一行中性提示,点名列表、第几项和还缺的输入(如 “Field on item #1 of “Global Filters” needs a value”),带「Show me」打开仪表盘属性并定位到该列表;填好后下一次自动保存把期间所有改动一起发出。 - 为什么改:以前空项立刻被 PUT 成 `globalFilters[N] = {}`,草稿门 422;之后每次保存都带着这个空项继续 422,所以在这之后改的 Label 等都丢了(重载后消失)。 - 风险与回滚:只改前端的拦截判定,判定问 spec 自己的 `DashboardSchema`,不另存必填清单;某项只在「所有报错都落在它还没填的输入上」时才拦,填了错值的照常发送并显示红色拒绝条。最坏情况是多拦一次(提示行 + Show me 可解)。回滚即 revert 本 PR。 - 先看:`ui/packages/app-shell/src/views/studio-design/metadataError.ts`(`listItemHeldInput` / `heldListItem` / `dashboardHeldEdit`)、`StudioDesignSurface.tsx`(`StudioHeldNotice` 泛型化与界面支柱的 Show me 分支)、`InterfacesPillar.heldListItem-27459.test.tsx`。 - 两分钟验证:`pnpm --dir ui exec vitest run packages/app-shell/src/views/studio-design/metadataError.heldListItem-27459.test.ts packages/app-shell/src/views/studio-design/InterfacesPillar.heldListItem-27459.test.tsx`,预期 2 个文件 19 条全部通过。 - 需要你做的:无,席位落地。 Fixes #27459 Clause-②: no Metadata-protocol: none ## What changes **The hold.** The Interfaces pillar's leaf autosave already holds a dashboard whose new widget still waits for its dataset or measures (`dashboardHeldEdit`, judged by the spec's `DashboardWidgetSchema`). It now also holds a dashboard with an item of one of its own list properties that is merely unfinished: - `listItemHeldInput(body, schema, skip)` asks the leaf's spec schema (`DashboardSchema` here) of the body the autosave would send. Each issue is placed on the item of the outermost list its path enters. An item is held only when every issue placed on it names an input directly below it that the item leaves unset (absent, or a text cleared to an empty string). An issue on the item itself, or on an input holding a value, makes it finished but wrong: it is sent, and the refusal it draws shows as before. The `widgets` list is skipped, so the widget rule keeps its own hold and is asked first. - No required-input list lives in the Studio: the spec decides what an item needs. - The held line names the input and the list the way the properties panel shows them (the form row's own label, translated, or its prettified key) and the item by the `#n` its row shows. One new clause, `engine.studio.held.listItemNeedsInput` (en, zh): the existing widget, step, action and field clauses name no list item. - `StudioHeld` gains a `Target` type parameter (default `MetadataSelection`, so the other pillars are unchanged) and an optional `input`. A list item's target is `null`, the leaf's own properties, which no selection opens. Its "Show me" is always offered and goes through the pillar's existing `showLeafRefusal` (clears the selection, opens the properties slot, expands a collapsed inspector, focuses the list by its label). - `prettify` moves from `SchemaForm.tsx` to `field-source-label.ts` as `prettifyFieldName` (no runtime imports there), so the held line can name a row exactly as `SchemaForm` does without importing the form component. `SchemaForm` imports it under its old local name; nothing else changes there. The Publish refusal and the surface's leave guard read the same reported clause (`useHeldEditReport`), unchanged. ## Tests - New `metadataError.heldListItem-27459.test.ts` (15 cases). Every held case is paired with the spec's verdict on the same dashboard (it refuses exactly the unset inputs): Global Filters' new item (the repeater's all-`undefined` row, and the stored `{}` the card saw), header Actions' new item (held for Label, then for Action URL), an Action URL cleared to blank, zh-CN naming, and the widget rule asked first. Controls: a Field filled is accepted and not held; a Field cleared to blank is one the spec takes, so not held; a header action refused for the URL it holds is sent; an item with an unset Field and a wrong `type` is sent; a list holding a non-item value is sent; a dashboard the spec accepts holds nothing. - New `InterfacesPillar.heldListItem-27459.test.tsx` (4 cases), the real `DashboardDefaultInspector` over a recording metadata client: the card's path (Filters → Global Filters → Add item, then a Label edit) sends no PUT past the debounce, shows no red strip, and names the Field; "Show me" focuses a control in the Global Filters row; filling the Field sends one save carrying the item and the Label edit kept meanwhile, which `DashboardSchema` accepts, and clears the line; control: a Label edit alone saves at once. Readings below are on the final head `7ff9cb5c` (this branch merged with `origin/main`) unless a line says otherwise. <details><summary>Ablation (ablation-replace, restored blob == HEAD)</summary> - `metadataError.ts`: in `dashboardHeldEdit`, `return item ? heldListItem(` replaced by `return item && !"ABLATED_27459" ? heldListItem(` (no list item is ever held, the behaviour on `main`), on pre-merge head `62dac23e` → 9 failed / 19 across the two new suites: the 6 held predicate cases and the 3 pillar cases that need a hold; the 9 predicate controls and the pillar's "Label edit alone saves at once" control stay green. Restored: blob after restore == blob at HEAD (`e52f543c2055`), `git diff HEAD` empty. - app-shell tests import `src` directly, so no dist build sits between the mutation and the run. </details> <details><summary>Affected suites (pre-merge head 62dac23): 220 files / 2807 tests passed</summary> Every ui test file naming `metadataError`, `SchemaForm`, `field-source-label`, `StudioHeldNotice`, `DashboardDefaultInspector`, `InterfacesPillar`, `engine.studio.held` or `metadata-form-i18n` (130 files across app-shell, core, data-objectstack and the console), plus the whole `packages/i18n/` directory, under the verification lock: `Test Files 220 passed (220) · Tests 2807 passed | 13 skipped (2820)`, lock VERDICT command-exit 0. The rest of the ui tree is left to CI `Test UI`. </details> <details><summary>After merging origin/main (head 7ff9cb5)</summary> The merge conflicted only beside a sibling card's new held clause (`engine.studio.held.fieldNeedsInput` in en / zh) and its new spec imports in `metadataError.ts`; both sides kept. ``` pnpm --filter '@object-ui/app-shell^...' build :: exit 0 pnpm --filter @object-ui/app-shell --filter @object-ui/i18n run type-check :: exit 0 vitest: every studio-design *held* suite (19 files, the two new ones included) + DashboardDefaultInspector.test :: Test Files 20 passed (20) · Tests 143 passed (143) ``` </details> ## Acceptance notes - **The report half of the card's step 7 is not in this PR.** The card names the same class on a report's Properties panel (Order and joined-blocks "Add item", a chart type chosen before its axes). This PR holds the dashboard path the card's title, reproduction and clause name (`studio-authoring.dashboard-widget-and-filter-types` acceptance[4]). `listItemHeldInput` takes the schema as an argument, so the report's list items need only `ReportSchema` and the report form at the pillar's leaf gate; a chart type set before its axes is not a list item and needs its own rule. Named in the report for the seat to carry as a follow-up card before this one closes. - **Header Actions: the spec judges an action's URL only once the action has its label.** An action with a bad URL and no label is therefore held for its label first, and its URL refusal shows after the label is given. That is the spec's issue order, not a second validator. - **"Show me" does not expand a collapsed form section.** The Filters section of the dashboard form is collapsed by default; when the author has collapsed it again, "Show me" opens the dashboard properties but finds no Global Filters label to focus. The line still names the list. - A Field cleared to an empty string is accepted by the installed spec (`GlobalFilterSchema.field` is a plain string), so it is sent, not held: the hold is never stricter than the contract. <details><summary>Derived gates (dispatch-gates --commands on this diff, head 7ff9cb5): 34 / 34 exit 0</summary> ``` node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0 node scripts/check-adr-0087-registration.mjs --self-test :: exit 0 node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0 node scripts/check-closing-keyword-parity.mjs :: exit 0 node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 node scripts/check-comment-mask-corpus.mjs :: exit 0 node scripts/check-empty-changeset.mjs --base origin/main :: exit 0 node scripts/check-empty-changeset.mjs --self-test :: exit 0 node scripts/check-sdui-manifest.mjs :: exit 0 node scripts/check-sdui-manifest.mjs --self-test :: exit 0 node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0 node scripts/release-pending-publish.mjs --self-test :: exit 0 pnpm --dir ui exec vitest run scripts/ :: exit 0 pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0 pnpm check:changeset-gate-self-tests :: exit 0 pnpm check:closed-ceremony-text :: exit 0 pnpm check:cross-package-test-inputs :: exit 0 pnpm check:dispatcher-error-vocabulary :: exit 0 pnpm check:driver-memory-census :: exit 0 pnpm check:engine-double-contract :: exit 0 pnpm check:gitlink-declared :: exit 0 pnpm check:nul-bytes :: exit 0 pnpm check:pm-changeset-deadline-census :: exit 0 pnpm check:published-files :: exit 0 pnpm check:query-options-erasure :: exit 0 pnpm check:refd-timer-probe :: exit 0 pnpm check:skill-identifier-liveness :: exit 0 pnpm check:test-source-alias :: exit 0 pnpm check:tier-file-adoption :: exit 0 pnpm check:type-check-coverage :: exit 0 pnpm check:type-check-debt :: exit 0 pnpm check:undeclared-dep-imports :: exit 0 pnpm check:watch-hint-literal :: exit 0 pnpm check:where-matcher :: exit 0 ``` `dispatch-gates --ran`: 34 derived families accounted for, 34 run, 0 NOT-MEASURED. `node scripts/check-sdui-manifest.mjs` (the ui card gate) is among them. CI runs the type-check lanes, the artifact-roster and wide-population families and `Test UI`. </details> --- _Generated by [Claude Code](https://claude.ai/code/session_01Utp6CZQGM7KwDbfPanMns3)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 5c083e8 commit 86aaff3

9 files changed

Lines changed: 565 additions & 24 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/app-shell': patch
3+
'@object-ui/i18n': patch
4+
---
5+
6+
fix(studio): an item just added to a dashboard's list property is held until it is filled in, instead of being autosaved and refused
7+
8+
Clause-②: no
9+
Metadata-protocol: none
10+
11+
In the Studio's Interfaces pillar, "Add item" under a dashboard's **Global Filters** or its header **Actions** used to autosave the new, empty item. The draft door refused it (422), and every later save of the dashboard carried the same item and was refused too, so an edit made afterwards, such as a new Label, was lost on reload.
12+
13+
Such an item is now held the way a widget waiting for its dataset is: nothing is sent while the dashboard's own schema refuses the item only for inputs it leaves unset, a neutral line names what it needs (for example "Field on item #1 of “Global Filters” needs a value") with a **Show me** that opens the dashboard's properties at that list, and Publish refuses while it is held. Once the item is filled in, the next autosave sends every edit kept meanwhile. An item refused for a value it holds is not held: it is sent, and the refusal it draws shows as before.

‎ui/packages/app-shell/src/views/metadata-admin/SchemaForm.tsx‎

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ import {
6969
type WidgetRenderer,
7070
} from './widgets.js';
7171
import { useMetadataLocale, t, tFormat, translateValidationMessage, translateEnumOption, translateSchemaFieldLabel, translateSchemaFieldHelp } from './i18n.js';
72-
import { untranslatedFieldLabel } from './field-source-label.js';
72+
import { prettifyFieldName as prettify, untranslatedFieldLabel } from './field-source-label.js';
7373

7474
/**
7575
* The form authoring surface — `FormFieldSpec` and the `VisibilityPredicate`
@@ -3437,13 +3437,6 @@ function safeStringify(v: unknown): string {
34373437
}
34383438
}
34393439

3440-
function prettify(key: string): string {
3441-
return key
3442-
.replace(/[_-]+/g, ' ')
3443-
.replace(/([a-z])([A-Z])/g, '$1 $2')
3444-
.replace(/\b\w/g, (c) => c.toUpperCase());
3445-
}
3446-
34473440
function orderKeys(keys: string[], preferred: string[]): string[] {
34483441
if (!preferred.length) return keys;
34493442
const set = new Set(keys);

‎ui/packages/app-shell/src/views/metadata-admin/field-source-label.ts‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,16 @@ export function untranslatedFieldLabel(
6161
}
6262
return typeof fieldSpec.label === 'string' ? fieldSpec.label : undefined;
6363
}
64+
65+
/**
66+
* The label a form row shows for `key` when neither the form nor the schema
67+
* names it: `globalFilters` reads "Global Filters". Kept here, beside the
68+
* source label, so a reader that names a row as `SchemaForm` does (the Studio's
69+
* held line) needs no import of the form itself.
70+
*/
71+
export function prettifyFieldName(key: string): string {
72+
return key
73+
.replace(/[_-]+/g, ' ')
74+
.replace(/([a-z])([A-Z])/g, '$1 $2')
75+
.replace(/\b\w/g, (c) => c.toUpperCase());
76+
}
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* objectstack#27459 — the Interfaces pillar holds an item just added to a
5+
* dashboard's own list property unsent until it is filled in, instead of
6+
* autosaving it for the draft door to refuse.
7+
*
8+
* The card: Properties → Global Filters → "Add item", then a Label edit. The
9+
* autosave PUT `globalFilters[N] = {}`, the door answered 422 on
10+
* `globalFilters.N.field`, the Label save carried the same item and was refused
11+
* too, and the Label edit was gone after a reload. The pillar now holds the
12+
* dashboard as it holds a widget waiting for its binding (objectui#11910):
13+
* nothing is sent, a neutral line names the input with a "Show me", and
14+
* filling the item in lets the next autosave send every edit kept meanwhile.
15+
*
16+
* Mounted with a metadata client double that records every save, a stub
17+
* canvas and the real `DashboardDefaultInspector` (its spec-driven form).
18+
*/
19+
20+
import '@testing-library/jest-dom/vitest';
21+
import * as React from 'react';
22+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
23+
import { act, cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
24+
import { MemoryRouter } from 'react-router-dom';
25+
import { DashboardSchema } from '@objectstack/spec/ui';
26+
27+
const NAV = [{ id: 'nav_sales', type: 'dashboard', label: 'Sales', dashboardName: 'sales_dash' }];
28+
29+
/** One bound widget and no list property yet: the dashboard parses with the spec as served. */
30+
const SALES_DASH = {
31+
name: 'sales_dash',
32+
label: 'Sales',
33+
widgets: [
34+
{
35+
id: 'pipeline',
36+
type: 'metric',
37+
title: 'Pipeline',
38+
dataset: 'sales_ds',
39+
values: ['revenue'],
40+
layout: { x: 0, y: 0, w: 3, h: 2 },
41+
},
42+
],
43+
};
44+
45+
const mockClient = {
46+
withPreviewDrafts: vi.fn((): unknown => mockClient),
47+
save: vi.fn(async (_type: string, _name: string, _body: Record<string, unknown>) => ({
48+
success: true,
49+
version: 'v1',
50+
state: 'draft',
51+
})),
52+
list: vi.fn(async (type: string) => {
53+
if (type === 'app') return [{ name: 'acme_app', label: 'Acme' }];
54+
if (type === 'dashboard') return [SALES_DASH];
55+
return [];
56+
}),
57+
listDrafts: vi.fn(async () => []),
58+
layered: vi.fn(async (type: string) => {
59+
if (type === 'app') return { effective: { name: 'acme_app', label: 'Acme', active: true, navigation: NAV } };
60+
if (type === 'dashboard') return { effective: JSON.parse(JSON.stringify(SALES_DASH)) };
61+
return { code: null, overlay: null, overlayScope: null, effective: null };
62+
}),
63+
getDraft: vi.fn(async () => null),
64+
get: vi.fn(async () => undefined),
65+
};
66+
67+
vi.mock('../metadata-admin/useMetadata', async (importOriginal) => {
68+
const mod = await importOriginal<typeof import('../metadata-admin/useMetadata')>();
69+
return { ...mod, useMetadataClient: () => mockClient, useMetadataTypes: () => ({ entries: [] }) };
70+
});
71+
72+
vi.mock('./packages-io', async (importOriginal) => {
73+
const mod = await importOriginal<typeof import('./packages-io')>();
74+
return { ...mod, fetchPackages: vi.fn(async () => []) };
75+
});
76+
77+
vi.mock('@object-ui/react', async (importOriginal) => {
78+
const mod = await importOriginal<typeof import('@object-ui/react')>();
79+
return { ...mod, useAdapter: () => ({}) };
80+
});
81+
82+
import { InterfacesPillar } from './StudioDesignSurface';
83+
import { registerMetadataPreview } from '../metadata-admin/preview-registry';
84+
import { registerMetadataDefaultInspector } from '../metadata-admin/default-inspector-registry';
85+
import { DashboardDefaultInspector } from '../metadata-admin/inspectors/DashboardDefaultInspector';
86+
import { t, tFormat } from '../metadata-admin/i18n';
87+
88+
// A stub canvas: the leaf needs a registered preview to be draft-editable,
89+
// and the canvas's own drawing is not what this file pins.
90+
registerMetadataPreview('dashboard', () => null);
91+
registerMetadataDefaultInspector('dashboard', DashboardDefaultInspector);
92+
93+
beforeEach(() => {
94+
for (const fn of Object.values(mockClient)) fn.mockClear();
95+
});
96+
afterEach(cleanup);
97+
98+
function mountPillar(onHeldEditChange?: (clause: string | null) => void) {
99+
return render(
100+
<MemoryRouter initialEntries={['/studio/com.acme.app/interfaces']}>
101+
<InterfacesPillar packageId="com.acme.app" onHeldEditChange={onHeldEditChange} />
102+
</MemoryRouter>,
103+
);
104+
}
105+
106+
function dashboardSaves(): Array<Record<string, unknown>> {
107+
return mockClient.save.mock.calls.filter((c) => c[0] === 'dashboard').map((c) => c[2]);
108+
}
109+
110+
/** Past the autosave's 1.5 s debounce. */
111+
async function outlastDebounce(): Promise<void> {
112+
await act(async () => {
113+
await new Promise((r) => setTimeout(r, 2300));
114+
});
115+
}
116+
117+
/** The dashboard's properties are open, with its Filters section expanded: its Global Filters row is drawn. */
118+
async function ready(): Promise<HTMLElement> {
119+
const inspector = await screen.findByTestId('if-inspector', undefined, { timeout: 8000 });
120+
fireEvent.click(await within(inspector).findByRole('button', { name: /^Filters/ }, { timeout: 8000 }));
121+
await within(inspector).findByText('Global Filters', { selector: 'label' }, { timeout: 8000 });
122+
return inspector;
123+
}
124+
125+
/** The form row of the list property labelled `label`: its label and its repeater. */
126+
function listRow(inspector: HTMLElement, label: string): HTMLElement {
127+
const row = within(inspector).getByText(label, { selector: 'label' }).parentElement?.parentElement;
128+
if (!row) throw new Error(`no form row for ${label}`);
129+
return row;
130+
}
131+
132+
/** The dashboard's own Label input commits on blur. */
133+
function editLabel(inspector: HTMLElement, value: string): void {
134+
const input = within(inspector).getByDisplayValue('Sales');
135+
fireEvent.change(input, { target: { value } });
136+
fireEvent.blur(input);
137+
}
138+
139+
const clause = tFormat('engine.studio.held.listItemNeedsInput', 'en', { input: 'Field', n: 1, list: 'Global Filters' });
140+
141+
describe('an item just added to a dashboard list property is held until it is filled in (objectstack#27459)', () => {
142+
it('Global Filters → Add item, then a Label edit: nothing is sent, no red strip, the line names the Field', async () => {
143+
const report = vi.fn();
144+
mountPillar(report);
145+
const inspector = await ready();
146+
fireEvent.click(within(listRow(inspector, 'Global Filters')).getByRole('button', { name: t('engine.form.addItem', 'en') }));
147+
editLabel(inspector, 'Sales v2');
148+
await outlastDebounce();
149+
150+
expect(dashboardSaves(), 'the unfinished item is not sent, nor the Label edit with it').toEqual([]);
151+
expect(screen.queryByTestId('studio-refusal'), 'no red strip on the normal path').toBeNull();
152+
expect(screen.getByTestId('studio-held-message')).toHaveTextContent(
153+
tFormat('engine.studio.held.line', 'en', { clause }),
154+
);
155+
// Publish's half: the surface is told what is held.
156+
expect(report).toHaveBeenLastCalledWith(clause);
157+
});
158+
159+
it('"Show me" focuses the list in the dashboard properties', async () => {
160+
mountPillar();
161+
const inspector = await ready();
162+
const row = listRow(inspector, 'Global Filters');
163+
fireEvent.click(within(row).getByRole('button', { name: t('engine.form.addItem', 'en') }));
164+
const show = await within(await screen.findByTestId('studio-held')).findByRole('button', {
165+
name: t('engine.studio.refusal.show', 'en'),
166+
});
167+
(document.activeElement as HTMLElement | null)?.blur();
168+
fireEvent.click(show);
169+
await waitFor(() => expect(row.contains(document.activeElement)).toBe(true));
170+
});
171+
172+
it('filling the Field in sends one save carrying the item and the Label edit kept meanwhile, and clears the line', async () => {
173+
const report = vi.fn();
174+
mountPillar(report);
175+
const inspector = await ready();
176+
const row = listRow(inspector, 'Global Filters');
177+
fireEvent.click(within(row).getByRole('button', { name: t('engine.form.addItem', 'en') }));
178+
editLabel(inspector, 'Sales v2');
179+
await outlastDebounce();
180+
expect(dashboardSaves()).toEqual([]);
181+
182+
const field = within(row).getByLabelText('Field');
183+
fireEvent.change(field, { target: { value: 'stage' } });
184+
fireEvent.blur(field);
185+
await waitFor(() => expect(dashboardSaves()).toHaveLength(1), { timeout: 4000 });
186+
const [sent] = dashboardSaves();
187+
expect(sent.label).toBe('Sales v2');
188+
expect(sent.globalFilters).toEqual([expect.objectContaining({ field: 'stage' })]);
189+
expect(
190+
DashboardSchema.safeParse(JSON.parse(JSON.stringify(sent))).success,
191+
'the body sent is one the door accepts',
192+
).toBe(true);
193+
await waitFor(() => expect(screen.queryByTestId('studio-held')).toBeNull());
194+
expect(screen.queryByTestId('studio-refusal')).toBeNull();
195+
expect(report).toHaveBeenLastCalledWith(null);
196+
});
197+
198+
// CONTROL: an edit to a dashboard with no unfinished item saves at once.
199+
it('a Label edit alone saves at once, with no line', async () => {
200+
mountPillar();
201+
const inspector = await ready();
202+
editLabel(inspector, 'Sales v2');
203+
await waitFor(() => expect(dashboardSaves()).toHaveLength(1), { timeout: 4000 });
204+
expect(dashboardSaves()[0].label).toBe('Sales v2');
205+
expect(screen.queryByTestId('studio-held')).toBeNull();
206+
});
207+
});

‎ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx‎

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -556,16 +556,16 @@ function CreateTakenNotice({
556556
*
557557
* Exported for its pins; `index.ts` does not re-export it.
558558
*/
559-
export function StudioHeldNotice({
559+
export function StudioHeldNotice<Target extends MetadataSelection | null = MetadataSelection>({
560560
held,
561561
locale,
562562
onShow,
563563
className,
564564
}: {
565-
held: StudioHeld;
565+
held: StudioHeld<Target>;
566566
locale: string;
567567
/** Opens the held input; omitted while that input is the one open. */
568-
onShow?: (target: MetadataSelection) => void;
568+
onShow?: (target: Target) => void;
569569
/** Spacing and type size, which differ by pillar. */
570570
className?: string;
571571
}): React.ReactElement {
@@ -4832,6 +4832,9 @@ export function InterfacesPillar({
48324832
// widget's inspector hints the input, and the line on the canvas names it.
48334833
// Asked of the spec's own widget schema (`dashboardHeldEdit`); a widget that
48344834
// is finished but wrong is sent, and its refusal shows as before.
4835+
// objectstack#27459 — so is an item just added to one of the dashboard's
4836+
// own list properties (Global Filters, the header's Actions), asked of the
4837+
// spec's dashboard schema, until its required inputs are filled in.
48354838
const leafType = current?.type;
48364839
const leafIncomplete = React.useMemo(
48374840
() => (ifDirty && leafType === 'dashboard' ? dashboardHeldEdit(draft, locale) : null),
@@ -5572,10 +5575,15 @@ export function InterfacesPillar({
55725575
held={leafHeld}
55735576
locale={locale}
55745577
// objectui#11910 — no "Show me" while that widget is the one open.
5578+
// objectstack#27459 — an item of the leaf's own list property
5579+
// (`target: null`) always offers it: the leaf's properties may
5580+
// be collapsed, or scrolled past the list it opens and focuses.
55755581
onShow={
5576-
selection?.kind === leafHeld.target.kind && selection.id === leafHeld.target.id
5577-
? undefined
5578-
: (target) => setSelection(target)
5582+
leafHeld.target === null
5583+
? () => showLeafRefusal(null, leafHeld.input)
5584+
: selection?.kind === leafHeld.target.kind && selection.id === leafHeld.target.id
5585+
? undefined
5586+
: (target) => setSelection(target)
55795587
}
55805588
className="px-3 py-2 text-xs"
55815589
/>

0 commit comments

Comments
 (0)