Skip to content

Commit 1013ef1

Browse files
fix(app-shell): a draft-only object's nav entry opens its drafted default list view in Studio Interfaces (#30315)
<!-- claude-projects-attribution: {"github_login":"os-support-ai"} --> _Requested by **Yibo Wang** · [project thread](https://claude.ai/code/project/chan_01BVUyyPie8gHwoPM18iABwB?thread=cmsg_01BVUyyPie8gHwoPM18iABwB7gJBqvTiMpqqBp2Jkh3F7S)_ Fixes #30177 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:Studio 界面里,一个还只是草稿的对象,从导航入口打开时现在会打开作者草拟的那个默认列表视图(例如「All deals」),视图下拉里不再多出一个并不存在的「Default view」,第一次编辑也写进这个视图本身。 - 为什么改:之前入口只按已发布的元数据找默认视图,草稿对象不在其中,于是打开不存在的 `OBJECT.default`;作者一编辑,包里就多出第二个默认列表视图,而且它的列是空的。 - 风险与回滚:只改 Studio 面板选哪个视图,不动接口、数据和规格。已发布对象的行为不变;包的草稿列表读回来之前,面板对「尚未标记默认的对象」多转一下加载圈。回滚即还原本 PR。 - 先看:`ui/packages/app-shell/src/views/studio-design/ObjectListViewInspector.tsx` 里 `useObjectListViewDraft` 的 `entryTarget` / `holding` 两段,以及新函数 `draftedListViewsOf`。 - 两分钟验证:`pnpm --dir ui exec vitest run packages/app-shell/src/views/studio-design/ObjectListViewInspector.draftOnlyObject-12508.test.tsx`,预期 8 个测试全部通过,其中 objectstack#30177 一组断言入口打开 `acmetrial_deal.all`、切换器只列这一项、第一次编辑只写这一个视图。 - 需要你做的:无,席位落地。 ## What changes In Studio › Interfaces, the list-view panel of an `object` nav entry with no `viewName` edits the object's default list view. `useObjectListViewDraft` resolved that view (`listViewTargetOf`) from the shell's merged `listViews` only. A draft-only object is absent from the shell's metadata, so the entry opened `OBJECT.default`, which does not exist; the header's view switcher listed that "Default view" beside the view the author had drafted with `isDefault: true`; and the first edit created `OBJECT.default` with `isDefault: true`, a second default list view in the package (seeded with no columns). - The entry's default is now resolved against the merged views **and** this package's drafted list views of the object (`draftedListViewsOf`, over the draft-overlaid view list the switcher already reads; no new request). A flagged merged view still wins; a flagged drafted view comes next; `OBJECT.default` stays the fallback. - That draft read is asynchronous. Until it answers, an entry whose default no merged view flags is held (`holding`): no buffer is loaded and nothing is listed, so no edit can be made against the fallback name before the drafted default is known. A failed or refused draft read lists nothing drafted, and the entry falls back to `OBJECT.default` as before. - This applies to every object, not only draft-only ones. The draft-only set (`draftOnlyObjects`) is itself read asynchronously and can be stale across a publish, and a published object whose package drafts its first default view had the same phantom; resolving against the drafted views avoids both without depending on that set. Changeset: `@object-ui/app-shell` patch. ## Tests New top-level `describe` in the existing hook harness `ObjectListViewInspector.draftOnlyObject-12508.test.tsx` (real hook, real `MetadataClient` over an in-memory server; the server now also serves the drafts ledger, the draft-overlaid view list, `?state=draft` reads and records PUTs): - draft-only object + drafted `acmetrial_deal.all` (`isDefault: true`) + entry with no `viewName` → the panel targets and loads `acmetrial_deal.all`; the switcher lists only it; the first edit (made as the panel makes it) PUTs `view/acmetrial_deal.all` and no other view carries `isDefault`; - while the draft-overlaid view list is held, nothing is loaded or listed and no `acmetrial_deal.default` request is sent; once it answers, the entry is `acmetrial_deal.all`; - CONTROL: a published object with no flagged view still targets `OBJECT.default`; - CONTROL: a draft-only object with no drafted view still targets `OBJECT.default`, and the first edit creates it flagged default. <details><summary>Ablation (fix committed first; scripts/ablation-replace.mjs, restored to the HEAD blob, git diff HEAD empty)</summary> - Draft read removed (`{ ...publishedViews, ...draftedListViewsOf(objectName, draftViewRows) }` → `publishedViews`): 2 failed, 6 passed. The first pin reads `viewId` `acmetrial_deal.default` where `acmetrial_deal.all` is expected; the hold pin's settled target is `acmetrial_deal.default`. - Hold removed (`holding` forced `false`): 1 failed, 7 passed. `loadedFor` reads `listView:acmetrial_deal.default` while the drafts are unread. </details> <details><summary>Test runs</summary> - At `d0d0f1e6` (before merging `main`): every app-shell test file importing `ObjectListViewInspector` or `StudioDesignSurface`, 210 files, `--maxWorkers=2`, under the verify lock: `Test Files 210 passed (210)`, `Tests 1344 passed (1344)`, VERDICT command-exit 0. - At `bfd2fbff` (after merging `main` `959c26be`, rebuilt): the 33 list-view, Interfaces-pillar and draft-only files of that set: `Test Files 33 passed (33)`, `Tests 183 passed (183)`, VERDICT 0. `pnpm --filter @object-ui/app-shell run type-check`: VERDICT 0. - eslint on the two touched source/test files from the package dir: 0 errors (22 warnings, all pre-existing patterns on lines this PR does not touch). </details> <details><summary>Gates at bfd2fbf</summary> `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (3 paths vs merge base `959c26be1`): 35 commands, run serially under one verify lock (VERDICT command-exit 0), every one exit 0, including `node scripts/check-sdui-manifest.mjs` and `pnpm --dir ui exec vitest run scripts/` (`Test Files 170 passed | 2 skipped (172)`, `Tests 4985 passed | 2 skipped`). `--ran` with the recorded exit codes: 35 derived, 35 run, 0 NOT-MEASURED (a derived zero). The record is the project's `ui-orders/30177-gates-ran.list`. </details> Not run locally: the full ui suite (CI's Test UI shards run it). `origin/main` moved after the merge; its only overlap with this PR's area is four added lines in `StudioDesignSurface.tsx`, a file this PR does not edit. ## Verification Dev boot of `examples/app-crm` (`objectstack dev --seed-admin --fresh`) after a full build, console Vite pointed at it, Playwright Chromium, signed in as the dev admin. Seeded over the REST door as the card describes: writable package `com.acme.trial`, then with `?mode=draft` the object `acmetrial_deal`, the list view `acmetrial_deal.all` (`label: All deals`, `isDefault: true`) and the app `acmetrial_app` whose one nav entry is the object with no `viewName`. Opened the entry in Studio › Interfaces. | | Before (main's `ObjectListViewInspector.tsx`, Vite restarted with `--force`, served module checked) | After (this branch) | |:--|:--|:--| | Panel opens | `acmetrial_deal.default`, "There is no list view "Default view" yet", 0 columns | `acmetrial_deal.all`, "Unpublished draft", columns Name, Stage | | Switcher lists | Default view ✓, All deals | All deals ✓ | | First edit (Add field → Amount) PUTs | `view/acmetrial_deal.default`, `isDefault: true`, columns `[amount]` | `view/acmetrial_deal.all`, `isDefault: true`, columns `[name, stage, amount]` | | Package's view drafts afterwards | `acmetrial_deal.all` and `acmetrial_deal.default`, both `isDefault: true` | `acmetrial_deal.all` | Shots (2x, 1440 wide) and the request readings are in the project's `ui-orders/30177-shots/` folder (`before-switcher.png`, `before-edited.png`, `after-switcher.png`, `after-edited.png`); this repository has no place to commit them, per `ui/AGENTS.md` Housekeeping. ## Acceptance notes - **Create View / first-edit column seed for a draft-only object, not fixed here.** `seedColumns` is `defaultListColumnsFromObject(objectDef, 5)` and `objectDef` comes from the shell's merged objects, which do not hold a draft-only object, so the seed is empty: the before leg's first edit wrote `columns: [amount]` only, and New view's fallback columns are empty the same way. Seeding from the object's draft fields is a different read (the object's draft definition, not the view drafts this PR reuses), so it stays out of this PR per the dispatch and is reported to the seat for its own card. - While the package's view drafts are unread, the header title of an object with no flagged merged view still names the fallback view ("Default view") for that moment; the switcher lists nothing and the panel shows its loading state, so nothing can be edited. --- _Generated by [Claude Code](https://claude.ai/code/session_01WGufzigMc8dMgZMjwQtjCq)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent c8e6d38 commit 1013ef1

3 files changed

Lines changed: 190 additions & 14 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
---
4+
5+
fix(app-shell): in Studio Interfaces, a draft-only object's nav entry opens the object's drafted default list view, not a non-existent "Default view"
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
An object that is still only a draft in the package is absent from the shell's published metadata, so a nav entry for it with no `viewName` used to open `<object>.default`, a view that does not exist. The view switcher listed that "Default view" beside the view the author had drafted as the default, and the first edit in the panel created a second default list view in the package. The entry's default list view is now resolved against the published views and the package's drafted list views together, so it opens the drafted default view, the switcher lists only the views that exist, and an edit writes to that view. Until the package's drafts have been read, a default view that no published view flags is not loaded or listed, so no edit can be made against the wrong view. A published object with no default list view still opens `<object>.default`, created by the first edit, as before.

‎ui/packages/app-shell/src/views/studio-design/ObjectListViewInspector.draftOnlyObject-12508.test.tsx‎

Lines changed: 139 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,12 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3131
import { act, cleanup, render, screen } from '@testing-library/react';
3232
import { MetadataCtx, type MetadataContextValue } from '@object-ui/react';
3333
import { MetadataClient } from '@object-ui/data-objectstack';
34-
import { useObjectListViewDraft, type DraftOnlyObjects } from './ObjectListViewInspector';
34+
import {
35+
newListViewItem,
36+
useObjectListViewDraft,
37+
type DraftOnlyObjects,
38+
type ObjectListViewDraft,
39+
} from './ObjectListViewInspector';
3540
import type { Surface } from './navSurface';
3641

3742
const PKG = 'com.default.studio-test';
@@ -40,8 +45,14 @@ const PUBLISHED = 'st_case';
4045

4146
const server = {
4247
active: new Map<string, Record<string, unknown>>(),
48+
/** objectstack#30177 — this package's pending view drafts, by `view/<name>`. */
49+
drafts: new Map<string, Record<string, unknown>>(),
4350
requests: [] as Array<{ method: string; path: string; status: number }>,
51+
/** objectstack#30177 — the PUTs, by `view/<name>`, with the body sent. */
52+
writes: [] as Array<{ path: string; body: Record<string, unknown> }>,
4453
failLayers: new Set<string>(),
54+
/** objectstack#30177 — while set, the draft-overlaid view list waits for it. */
55+
holdViewList: null as Promise<void> | null,
4556
};
4657

4758
const json = (status: number, body: unknown) =>
@@ -55,14 +66,26 @@ async function serve(input: RequestInfo | URL, init?: RequestInit): Promise<Resp
5566
const meta = url.pathname.match(/^\/api\/v1\/meta\/(.+)$/);
5667
if (meta) {
5768
const seg = meta[1].split('/').map(decodeURIComponent);
58-
if (seg.length === 3 && seg[2] === 'layers' && server.failLayers.has(`${seg[0]}/${seg[1]}`)) {
69+
if (method === 'PUT' && seg.length === 2) {
70+
server.writes.push({ path: `${seg[0]}/${seg[1]}`, body: JSON.parse(String(init?.body ?? '{}')) });
71+
body = { success: true, version: 'v2', state: 'draft' };
72+
} else if (seg.length === 1 && seg[0] === '_drafts') {
73+
body = [...server.drafts.keys()].map((key) => ({ type: key.split('/')[0], name: key.slice(key.indexOf('/') + 1), packageId: PKG }));
74+
} else if (seg.length === 1 && url.searchParams.get('preview') === 'draft') {
75+
if (server.holdViewList) await server.holdViewList;
76+
body = [...server.drafts.entries()]
77+
.filter(([key]) => key.startsWith(`${seg[0]}/`))
78+
.map(([, row]) => ({ ...row, _draft: true, _packageId: PKG }));
79+
} else if (seg.length === 2 && url.searchParams.get('state') === 'draft' && server.drafts.has(`${seg[0]}/${seg[1]}`)) {
80+
body = { type: seg[0], name: seg[1], item: { ...server.drafts.get(`${seg[0]}/${seg[1]}`), _draft: true }, version: 'v1' };
81+
} else if (seg.length === 3 && seg[2] === 'layers' && server.failLayers.has(`${seg[0]}/${seg[1]}`)) {
5982
[status, body] = [500, { error: { code: 'INTERNAL_ERROR', message: 'metadata store unavailable' } }];
6083
} else if (seg.length === 3 && seg[2] === 'layers') {
6184
const row = server.active.get(`${seg[0]}/${seg[1]}`);
6285
if (row) body = { code: null, overlay: row, overlayScope: 'env', effective: row };
6386
else [status, body] = [404, { error: { code: 'NOT_FOUND', message: 'absent' } }];
6487
} else if (seg.length === 2) {
65-
// No view draft is pending in this suite.
88+
// No draft of that view is pending.
6689
[status, body] = [404, { error: { code: 'NO_DRAFT', message: 'absent' } }];
6790
}
6891
}
@@ -131,8 +154,11 @@ const knownUnder = (publishNonce: number): DraftOnlyObjects => ({ publishNonce,
131154

132155
beforeEach(() => {
133156
server.active.clear();
157+
server.drafts.clear();
134158
server.requests = [];
159+
server.writes = [];
135160
server.failLayers.clear();
161+
server.holdViewList = null;
136162
});
137163
afterEach(cleanup);
138164

@@ -169,3 +195,113 @@ describe('useObjectListViewDraft — a view of a draft-only object is not asked
169195
expect(seen.failure).toEqual({ during: 'load', error: expect.stringContaining('metadata store unavailable') });
170196
});
171197
});
198+
199+
/**
200+
* objectstack#30177 — a draft-only object's nav entry with no `viewName`
201+
* opens the object's drafted default list view. The shell's merged metadata
202+
* does not hold the object, so its views are known from this package's
203+
* drafts alone: the entry is resolved against them, the view switcher lists
204+
* no "Default view" beside them, and the first edit writes to the drafted
205+
* view rather than creating a second default one.
206+
*/
207+
describe('useObjectListViewDraft — a draft-only object opens its drafted default view (objectstack#30177)', () => {
208+
const DEAL = 'acmetrial_deal';
209+
const ALL_DEALS = {
210+
name: `${DEAL}.all`,
211+
object: DEAL,
212+
viewKind: 'list',
213+
label: 'All deals',
214+
isDefault: true,
215+
config: { type: 'grid', data: { provider: 'object', object: DEAL }, columns: ['name'] },
216+
};
217+
const dealsKnown: DraftOnlyObjects = { publishNonce: 0, names: new Set([DEAL]) };
218+
219+
let latest: ObjectListViewDraft | null = null;
220+
const report = (d: ObjectListViewDraft) => {
221+
latest = d;
222+
};
223+
function HookProbe({ object, known }: { object: string; known: DraftOnlyObjects | null }) {
224+
const leaf: Surface = { type: 'object', name: object, label: object };
225+
const d = useObjectListViewDraft({ client, packageId: PKG, leaf, publishNonce: 0, draftOnlyObjects: known });
226+
React.useLayoutEffect(() => report(d));
227+
return null;
228+
}
229+
230+
const settle = () =>
231+
act(async () => {
232+
for (let i = 0; i < 20; i++) await new Promise((r) => setTimeout(r, 0));
233+
});
234+
235+
async function mount(object: string, known: DraftOnlyObjects | null) {
236+
render(
237+
<MetadataCtx.Provider value={METADATA}>
238+
<HookProbe object={object} known={known} />
239+
</MetadataCtx.Provider>,
240+
);
241+
await settle();
242+
return () => latest!;
243+
}
244+
245+
/** The panel's first edit, as the panel makes it: one column added to the view it shows. */
246+
async function firstEdit(d: () => ObjectListViewDraft) {
247+
const { target, row, seedColumns } = d();
248+
const working = row ?? newListViewItem(target!, seedColumns);
249+
const config = working.config as Record<string, unknown>;
250+
await act(async () => {
251+
d().edit({ ...working, config: { ...config, columns: [...(config.columns as unknown[]), 'stage'] } });
252+
});
253+
await act(async () => {
254+
await d().save({ unmoved: () => true });
255+
});
256+
await settle();
257+
}
258+
259+
it('the entry opens the drafted default view, the switcher lists no "Default view", and the first edit writes to that view', async () => {
260+
server.drafts.set(`view/${ALL_DEALS.name}`, ALL_DEALS);
261+
const d = await mount(DEAL, dealsKnown);
262+
263+
expect(d().target).toEqual({ objectName: DEAL, viewId: `${DEAL}.all`, isDefault: true });
264+
expect(d().loadedFor).toBe(`listView:${DEAL}.all`);
265+
expect(d().row).toMatchObject({ name: `${DEAL}.all`, isDefault: true });
266+
expect(d().views.map((v) => v.target.viewId)).toEqual([`${DEAL}.all`]);
267+
268+
await firstEdit(d);
269+
expect(server.writes.map((w) => w.path)).toEqual([`view/${DEAL}.all`]);
270+
expect(server.writes.filter((w) => w.body.isDefault === true).map((w) => w.body.name)).toEqual([`${DEAL}.all`]);
271+
});
272+
273+
it('until the package\'s view drafts answer, no view is loaded and none is listed', async () => {
274+
server.drafts.set(`view/${ALL_DEALS.name}`, ALL_DEALS);
275+
let release = () => {};
276+
server.holdViewList = new Promise<void>((r) => {
277+
release = r;
278+
});
279+
const d = await mount(DEAL, dealsKnown);
280+
281+
expect(d().loadedFor).toBe('');
282+
expect(d().views).toEqual([]);
283+
expect(server.requests.filter((r) => r.path.startsWith(`/api/v1/meta/view/${DEAL}.default`))).toEqual([]);
284+
285+
release();
286+
await settle();
287+
expect(d().target?.viewId).toBe(`${DEAL}.all`);
288+
expect(d().loadedFor).toBe(`listView:${DEAL}.all`);
289+
});
290+
291+
it('CONTROL: a published object\'s entry with no flagged view still opens `<object>.default`', async () => {
292+
const d = await mount(PUBLISHED, dealsKnown);
293+
294+
expect(d().target).toEqual({ objectName: PUBLISHED, viewId: `${PUBLISHED}.default`, isDefault: true });
295+
expect(d().views.map((v) => v.target.viewId)).toEqual([`${PUBLISHED}.default`]);
296+
});
297+
298+
it('CONTROL: a draft-only object with no drafted view still opens `<object>.default`, created by the first edit', async () => {
299+
const d = await mount(DEAL, dealsKnown);
300+
301+
expect(d().target).toEqual({ objectName: DEAL, viewId: `${DEAL}.default`, isDefault: true });
302+
expect(d().loadedFor).toBe(`listView:${DEAL}.default`);
303+
await firstEdit(d);
304+
expect(server.writes.map((w) => w.path)).toEqual([`view/${DEAL}.default`]);
305+
expect(server.writes[0]!.body).toMatchObject({ name: `${DEAL}.default`, isDefault: true });
306+
});
307+
});

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

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,13 @@
1818
* (`resolveViewId`). A name that matches none is the view the entry opens
1919
* once it exists, `<object>.<viewName>`;
2020
* - any other entry → the object's default list view: the merged view
21-
* flagged `isDefault`, else `<object>.default`, the identity the
22-
* platform's own composer gives a container's default `list`
23-
* (`expandViewContainer`).
21+
* flagged `isDefault`, else this package's drafted list view flagged so
22+
* (objectstack#30177: a draft-only object is absent from the merged
23+
* metadata, so its default is known from the drafts alone), else
24+
* `<object>.default`, the identity the platform's own composer gives a
25+
* container's default `list` (`expandViewContainer`). Until the drafts
26+
* are read, a default no merged view flags is not known yet: nothing is
27+
* loaded or listed for it, so no edit can create a second default view.
2428
*
2529
* ## What is written, and where
2630
*
@@ -161,7 +165,8 @@ function qualifiedViewId(objectName: string, key: string): string {
161165
* `landingViewName` is the view the entry lands on as the runtime reads the
162166
* entry (`navEntryListTarget(...).viewName`), not the raw `viewName`: an entry
163167
* that also slices (`filters`) lands on the data surface, and its view is then
164-
* the default one. `views` is the object's merged `listViews` map.
168+
* the default one. `views` is the object's merged `listViews` map, with this
169+
* package's drafted list views of it (objectstack#30177).
165170
*/
166171
export function listViewTargetOf(
167172
objectName: string,
@@ -296,6 +301,19 @@ export function listViewChoicesOf(
296301
return choices;
297302
}
298303

304+
/**
305+
* objectstack#30177 — this package's drafted list views of `objectName` (the
306+
* draft-overlaid list's `_draft` rows), by item name: the views a draft-only
307+
* object has, which the shell's merged `listViews` never holds.
308+
*/
309+
function draftedListViewsOf(objectName: string, drafts: readonly Row[]): Record<string, Row> {
310+
const views: Record<string, Row> = {};
311+
for (const row of drafts) {
312+
if (row.object === objectName && typeof row.name === 'string' && isListViewRow(row)) views[row.name] = row;
313+
}
314+
return views;
315+
}
316+
299317
/** What `useDraftAutoSave` hands a save: whether the buffer is still what it sent. */
300318
interface DraftSendClaim {
301319
unmoved: () => boolean;
@@ -475,7 +493,6 @@ export function useObjectListViewDraft({
475493
}, [objectName, navId, filtersKey, entryViewName, currentUserId, currentOrgId]);
476494
const objectDef = objectName ? findByName(metadata.objects, objectName) : undefined;
477495
const publishedViews = listViewsOf(objectDef);
478-
const entryTarget = objectName ? listViewTargetOf(objectName, landingViewName, publishedViews) : null;
479496
// objectstack#27836, objectstack#27896 — a view New view created or the
480497
// view switcher opened, stamped with the nav entry it was opened on, so it
481498
// expires by construction when another opens.
@@ -490,8 +507,6 @@ export function useObjectListViewDraft({
490507
React.useEffect(() => {
491508
if (link && leafOpen && !linkHolds) setLink(null);
492509
}, [link, leafOpen, linkHolds]);
493-
const chosenViewId = (picked && picked.for === entryKey ? picked.viewId : null) ?? (linkHolds ? link.viewId : null);
494-
const pickedViewId = chosenViewId && chosenViewId !== entryTarget?.viewId ? chosenViewId : null;
495510
// objectstack#27896 — this package's pending drafts of the object's views,
496511
// read with the drafts overlaid: the published world is the merged map
497512
// above. A failed or refused read (a caller the server serves no pending
@@ -518,7 +533,21 @@ export function useObjectListViewDraft({
518533
cancelled = true;
519534
};
520535
}, [client, packageId, objectName, publishNonce, viewsNonce]);
521-
const draftViewRows = viewDrafts.for === viewDraftsKey ? viewDrafts.rows : [];
536+
const viewDraftsRead = viewDrafts.for === viewDraftsKey;
537+
const draftViewRows = viewDraftsRead ? viewDrafts.rows : [];
538+
// objectstack#30177 — the entry's view among the merged views and this
539+
// package's drafted list views (the module docblock's "Which view").
540+
const entryTarget = objectName
541+
? listViewTargetOf(objectName, landingViewName, { ...publishedViews, ...draftedListViewsOf(objectName, draftViewRows) })
542+
: null;
543+
const entryUnsettled =
544+
!!entryTarget?.isDefault &&
545+
!viewDraftsRead &&
546+
!Object.values(publishedViews).some((v) => isPlainObject(v) && v.isDefault === true);
547+
const chosenViewId = (picked && picked.for === entryKey ? picked.viewId : null) ?? (linkHolds ? link.viewId : null);
548+
const pickedViewId = chosenViewId && chosenViewId !== entryTarget?.viewId ? chosenViewId : null;
549+
// The entry's default is not known yet: nothing is loaded or listed for it.
550+
const holding = !pickedViewId && entryUnsettled;
522551
const pickedIsDefault =
523552
!!objectName &&
524553
!!pickedViewId &&
@@ -527,7 +556,8 @@ export function useObjectListViewDraft({
527556
);
528557
const target: ListViewTarget | null =
529558
objectName && pickedViewId ? { objectName, viewId: pickedViewId, isDefault: pickedIsDefault } : entryTarget;
530-
const views = objectName ? listViewChoicesOf(objectName, publishedViews, draftViewRows, entryTarget, target) : [];
559+
const views =
560+
objectName && !holding ? listViewChoicesOf(objectName, publishedViews, draftViewRows, entryTarget, target) : [];
531561
const viewId = target?.viewId ?? '';
532562
const targetKey = viewId ? listViewBufferKey(viewId) : '';
533563
const seedColumns = React.useMemo(() => defaultListColumnsFromObject(objectDef, 5), [objectDef]);
@@ -571,7 +601,7 @@ export function useObjectListViewDraft({
571601

572602
React.useEffect(() => {
573603
setFailure(null);
574-
if (!viewId) {
604+
if (!viewId || holding) {
575605
setBuffer({ for: '', row: null, dirty: false, hasDraft: false, draftChanges: null, published: undefined });
576606
return;
577607
}
@@ -667,7 +697,7 @@ export function useObjectListViewDraft({
667697
return () => {
668698
cancelled = true;
669699
};
670-
}, [client, viewId, objectName, packageId, publishNonce, reloadNonce, recordRead, customizing]);
700+
}, [client, viewId, holding, objectName, packageId, publishNonce, reloadNonce, recordRead, customizing]);
671701

672702
const edit = React.useCallback((row: Row) => {
673703
setBuffer((b) => ({ ...b, row, dirty: true }));

0 commit comments

Comments
 (0)