Skip to content

Commit 6164366

Browse files
xuyushun441-sysos-zhuangclaude
authored
fix: interface page renders empty grid when source view is hollow (#1684)
The Task Workbench (and any ADR-0047 interface page) rendered a grid with only the row-number column against backends that serve the referenced default view WITHOUT columns (columns live only in named views; the default expands hollow as { provider, object }). Two latent bugs, both rooted in an empty `columns: []` reading truthy: - resolveSourceView picked a column-less view via `v?.columns` (an empty array is truthy), and the hydration hollow-check `!resolvedView.columns` computed false — so the per-view overlay fetch that backfills columns never fired. - InterfaceListPage had no default-column fallback, unlike ObjectView (data mode), which derives columns from compactLayout / business fields when a view carries none. Fix: a shared hasColumns() (non-empty check) drives resolution + hollow detection, and the schema build falls back to defaultColumnsFromObject() (compactLayout, else first business fields) — mirroring ObjectView. The grid now renders full columns + data; filters/persistence unaffected. Locked by e2e/live/user-filters.spec.ts ('grid renders data columns even when the source view is hollow'). Found via browser review of the backend-served /_console (a build that serves hollow default views) — the earlier verification ran against a backend that happened to inline columns. Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 830dccd commit 6164366

2 files changed

Lines changed: 60 additions & 5 deletions

File tree

e2e/live/user-filters.spec.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,22 @@ test.describe('ADR-0047 interface mode — Task Workbench', () => {
7070
await expect(page.getByTestId('view-switcher-dropdown')).toHaveCount(0);
7171
});
7272

73+
test('grid renders data columns even when the source view is hollow', async ({ page }) => {
74+
// Regression: some backends serve the referenced default view without
75+
// columns (they live only in named views). The interface page must fall
76+
// back to object-derived columns instead of rendering a bare # column.
77+
await page.goto('/apps/showcase_app/page/showcase_task_workbench');
78+
79+
await expect(page.getByTestId('interface-list-page')).toBeVisible();
80+
const headers = page.locator('table thead th');
81+
// More than just the row-number column.
82+
await expect(async () => {
83+
expect(await headers.count()).toBeGreaterThan(2);
84+
}).toPass({ timeout: 15000 });
85+
// The title/assignee business fields are present (not only "#").
86+
await expect(page.getByRole('cell', { name: /example\.com/ }).first()).toBeVisible();
87+
});
88+
7389
test('dropdown selection filters, keeps counts, persists, and restores', async ({ page }) => {
7490
await page.goto('/apps/showcase_app/page/showcase_task_workbench');
7591

packages/app-shell/src/views/InterfaceListPage.tsx

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,17 @@ interface InterfaceListPageProps {
3535
* Views merged from ADR-0017 ViewItems are keyed `<object>.<key>`;
3636
* the page author writes the bare key (`sourceView: 'default'`).
3737
*/
38+
/** A view "carries columns" only when its column list is actually non-empty. */
39+
function hasColumns(v: any): boolean {
40+
return Array.isArray(v?.columns) && v.columns.length > 0;
41+
}
42+
3843
function resolveSourceView(objectDef: any, sourceView?: string): any | undefined {
3944
const views: Record<string, any> = objectDef?.listViews || objectDef?.list_views || {};
4045
// ADR-0017 expansion can serve a default-view item with an empty config
4146
// while the full body lives on `objectDef.list` — prefer candidates that
42-
// actually carry columns over hollow name matches.
47+
// actually carry columns over hollow name matches. An empty `columns: []`
48+
// is truthy in JS but renders a column-less grid, so check for non-empty.
4349
const candidates = sourceView
4450
? [
4551
views[`${objectDef?.name}.${sourceView}`],
@@ -48,7 +54,32 @@ function resolveSourceView(objectDef: any, sourceView?: string): any | undefined
4854
]
4955
: [objectDef?.list, ...Object.values(views)];
5056
const present = candidates.filter(Boolean);
51-
return present.find((v: any) => v?.columns) ?? present[0];
57+
return present.find(hasColumns) ?? present[0];
58+
}
59+
60+
/**
61+
* Default column set when the resolved view carries none — mirrors
62+
* ObjectView's data-mode fallback so an interface page never renders a
63+
* column-less grid. Priority: curated `compactLayout`, else the first
64+
* business fields (system/audit columns excluded).
65+
*/
66+
const SYSTEM_FIELDS = new Set([
67+
'id', 'created_at', 'createdAt', 'updated_at', 'updatedAt',
68+
'deleted_at', 'deletedAt', 'created_by', 'createdBy',
69+
'updated_by', 'updatedBy', '_version', '_rev',
70+
]);
71+
function defaultColumnsFromObject(objectDef: any): string[] {
72+
if (Array.isArray(objectDef?.compactLayout) && objectDef.compactLayout.length > 0) {
73+
return objectDef.compactLayout.filter((n: string) => objectDef.fields?.[n]);
74+
}
75+
const fields = objectDef?.fields;
76+
if (fields && typeof fields === 'object') {
77+
return Object.entries(fields)
78+
.filter(([name, f]: [string, any]) => f && !f.hidden && !SYSTEM_FIELDS.has(name))
79+
.map(([name]) => name)
80+
.slice(0, 6);
81+
}
82+
return [];
5283
}
5384

5485
export function InterfaceListPage({ page, className }: InterfaceListPageProps) {
@@ -91,7 +122,9 @@ export function InterfaceListPage({ page, className }: InterfaceListPageProps) {
91122
// (full)` into an infinite render/refetch loop the moment anything (e.g.
92123
// a `uf_*` URL write) re-renders this component after hydration settled.
93124
const objectDefName: string | undefined = objectDef?.name;
94-
const resolvedViewHollow = !!resolvedView && !resolvedView.columns;
125+
// Hollow = no *non-empty* column list. An empty `columns: []` reads as
126+
// truthy but renders nothing, so it must still trigger hydration.
127+
const resolvedViewHollow = !!resolvedView && !hasColumns(resolvedView);
95128
const resolvedViewKey = resolvedView?.name
96129
?? (cfg.sourceView ? `${cfg.source}.${cfg.sourceView}` : undefined);
97130
const [hydratedView, setHydratedView] = React.useState<any>(null);
@@ -107,7 +140,7 @@ export function InterfaceListPage({ page, className }: InterfaceListPageProps) {
107140
const all = await ds.listViewOverrides(cfg.source);
108141
full = all?.[resolvedViewKey] ?? null;
109142
}
110-
if (!full?.columns && typeof ds?.getView === 'function') {
143+
if (!hasColumns(full) && typeof ds?.getView === 'function') {
111144
full = await ds.getView(cfg.source, resolvedViewKey);
112145
}
113146
if (!cancelled && full && typeof full === 'object') setHydratedView(full);
@@ -139,11 +172,17 @@ export function InterfaceListPage({ page, className }: InterfaceListPageProps) {
139172
...(Array.isArray(cfg.filterBy) ? cfg.filterBy : []),
140173
];
141174

175+
// Columns: the referenced view's, else a sensible default derived from
176+
// the object (compactLayout / business fields). Some backends serve the
177+
// default view hollow (columns live only in named views), so without
178+
// this fallback the grid would render just the row-number column.
179+
const columns = hasColumns(view) ? view.columns : defaultColumnsFromObject(objectDef);
180+
142181
return {
143182
type: 'list-view' as const,
144183
objectName: objectDef.name,
145184
viewType: (allowed[0] ?? view.type ?? 'grid'),
146-
fields: view.columns,
185+
fields: columns,
147186
...(filters.length ? { filters } : {}),
148187
...(view.sort?.length ? { sort: view.sort } : {}),
149188
grouping: view.grouping,

0 commit comments

Comments
 (0)