Skip to content

Commit a364093

Browse files
fix(plugin-detail): on a desktop the enlarged related tab draws the child object's list grid (#28902)
<!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} --> _Requested by **Steedos** · [project thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9UbbC5yRMtRXouSfKD16A2gwFn)_ Fixes #28865 Clause-②: no Metadata-protocol: none ## 维护者速读 - **改了什么**:记录页放大的相关列表页签,在桌面端改为渲染子对象列表页同款的 `object-grid`(手机端自 #28484 起已是如此),不再是通用 `data-table`。行高、密度、名称链接、行操作都与列表页一致;全部芯片已显示数量时,工具栏下方不再重复显示「N 条」。 - **为什么改**:#27524 第 9 项规定放大页签就是「子对象的列表面」;实测桌面端行高约 65 px(列表页 41 px)、名称是纯文本点不开、数量显示两次。 - **风险与回滚**:中低。分页、排序仍由相关列表自己掌控(服务端 `$top/$skip/$orderby` 不变),通过 grid 的宿主属性传入;行操作仍经宿主的 `onRowAction` 派发到子对象。唯一行为差异:筛选框有文字时,查找类列的表头不再提供排序(与所有列表页一致)。回滚即 revert 本 PR。 - **先看**:① `ui/packages/plugin-detail/src/RelatedList.tsx` 的 `viewSchema` 放大分支与 `enlargedGridHost`;② 同文件 `enlargedGridActions`(把 grid 的行操作派发路由回宿主);③ 新测试 `RelatedList.enlargedListGrid-28865.test.tsx`。 - **两分钟验证**:`pnpm dev:crm` 后打开 客户 Acme Corp → 商机列表 页签(1440 px):行高与商机列表页相同,名称是蓝色链接、点开即打开记录抽屉,数量只在「全部 4」芯片上出现一次。 - **需要你做的**:无,席位落地。 ## What changes `RelatedList`'s enlarged face (the record page's active related tab) now renders `object-grid` on every width, the branch the phone has taken since #28484. On a desktop it fell through to the generic `data-table`. - **One branch, phone and desktop.** `viewSchema` returns `object-grid` over the page of rows this list already read (`data: { provider: 'value', items }`), with the list's columns, the grid's own pager and search off (`showPagination: false`, `showSearch: false`). Under 768 px the grid draws the phone list's card rows, as before. - **The list stays the source of truth.** Its window, page and sort reach the grid on the grid's host channel: React props spread on `SchemaRenderer` (`manualPagination`, `rowCount`, `page`, `pageSize`, `onPageChange`, `sort`, `onSortChange`), the same ones `plugin-list`'s `ListView` hands the grid. Paging and sort stay the server's `$top` / `$skip` / `$orderby`; the list's own pager stays the one pager, because it also answers a window whose total the server did not send (`hasMore`). `objectFields` rides along so no column paints withheld while the grid reads the definition (objectui#10657). - **Row actions come from the grid.** The host's localized row actions (`list_item`, and `record_related` inside a parent record) ride to the grid's row menu as `rowActionDefs`, the non-author host key the console's object page uses. Row Edit is the host's `onRowEdit`; row Delete is this list's confirm-then-delete. - **Row-action dispatch stays the host's.** The grid runs a row action def through the surrounding action runner with the row under `params._rowRecord` and no object of its own; on the record page that runner's object is the parent's, and nothing would tell the list to read its rows again. `RelatedList` provides the grid an action context (`enlargedGridActions`) that sends a dispatch carrying one of its own row action defs and a row to the host's `onRowAction` (child object, row context, and the related-changed signal that refetches the list), and every other dispatch to the surrounding runner unchanged. - **One count.** While the 全部 chip is on and nothing narrows the rows, the line under the toolbar no longer repeats the chip's count; it keeps the page line when there is one. A narrowing chip (我的 / 逾期) or filter text keeps the line's count, which is then a different number. - Compact panels in the right column and lists that are not enlarged are unchanged. No change to `plugin-grid`. ## Why #27524 item 9: the enlarged tab is "the full-width table (all columns, paging, sort, inline edit where the FK allows — **the child object's list face**)". Measured on `main` before this PR (account Acme Corp → 商机, 1440 px, `examples/app-crm`): rows 65 px against the opportunity list page's 41 px, the name as plain text, and the count shown twice (「全部 4」 and 「4 条」). ## Tests - New `RelatedList.enlargedListGrid-28865.test.tsx` (11 tests): the desktop branch is `object-grid` over the read rows with the grid's pager and search off; the host channel carries the window, page and sort and none of it rides as node keys; a header sort from the grid becomes one server `$orderby` from page 1; a page from the grid reads the next window (`$skip` 25); the pinned deviation below; the count shows once on 全部 and a narrowing chip keeps its own count; a grid row-action dispatch reaches `onRowAction` with the def and the row, and (control) any other dispatch does not; row Edit is the host's and row Delete asks first; control: a list that is not enlarged keeps the `data-table` with no host props. - `RelatedList.enlargedPhoneCards-28484.test.tsx`: its desktop control now asserts the same `object-grid` branch. - `RelatedList.enlargedToolbar-27524.test.tsx`: registers a stand-in `object-grid` (this package cannot import `plugin-grid`, which depends on it) and asserts the status line is not drawn when the 全部 chip carries the count. - `app-shell` `RecordDetailView.childWriteRefreshesParent-12244.test.tsx`: the enlarged tab's row menu is now the grid's, so the test opens it by `row-action-trigger`. The delete still re-reads the parent. Ablations (each through `scripts/ablation-replace.mjs`, restored to `HEAD` byte-identical): - desktop branch gated back to the phone (`enlargedFace && isMobile && …`): 7 of 11 red in the new file; - the routed dispatch sent to the surrounding runner instead of `onRowAction`: 1 red (the routing test); - the one-count rule switched off: 2 red (the new count test and the 27524 toolbar test). ## Acceptance notes - **Deviation (pinned):** while the filter box has text the list sorts its in-memory rows itself; the grid is still handed that sort on the host channel, and under a host sort the grid offers no header sort on a relational column (its stored value is an id), as on every list page. The `data-table` sorted such a column by its label in that one state. Pinned in the new test file. - Screenshots (1440 px, zh-CN, `examples/app-crm`, fresh boot) are in the project folder `issue-28865/`; the seat links them here: `before-account-opportunities-1440.png`, `after-account-opportunities-1440.png`, and for comparison `opportunity-list-page-1440.png`; also `after-name-opens-record-1440.png`, `after-sorted-amount-desc-1440.png`, `after-leads-row-actions-1440.png` and the phone face `after-account-opportunities-390.png`. - Measured in the running app after the change: rows 41 px on the tab and on the list page; a header click sends `sort=amount` then `amount desc` and the rows follow; the name opens the record drawer stepping 1 / 4; the 全部 chip says 4 and no line repeats it; on the leads tab the row menu holds 编辑, 删除 and Convert Lead, and Convert Lead posts `actions/crm_lead/crm_convert_lead` and the list then re-reads `crm_lead`. - The 操作 header over a column with no visible buttons is the grid's own behaviour, the same as on the list page (#28871); it is not changed here. ## Verification At head `47c53a5a` (this branch with `origin/main` `7fa9873f` merged in; 4 later commits on main touch none of the changed files): - `pnpm exec vitest run packages/plugin-detail/` (from `ui/`, `--maxWorkers=2`, under `os-verify-lock`): 361 files passed, 1 skipped; 3200 tests passed, 18 skipped (run before the main merge; the merge touched no file under `plugin-detail`, `plugin-grid` or `react`). - 30 `app-shell` / `console` / `plugin-view` / `plugin-grid` test files that name `RelatedList`, the enlarged face or `?related=`: 30 / 30 files, 496 / 496 tests after the row-menu update above. - The touched test files again at the final head: the three `plugin-detail` files plus the 30 above, 33 / 33 files, 516 / 516 tests. - `pnpm --filter @object-ui/plugin-detail run type-check`: exit 0. `eslint` on `RelatedList.tsx`: 0 errors, 99 warnings, the same count as `main`. - Derived gates (`node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`, 34 commands, run at `47c53a5a`): 34 / 34 exit 0, reconciled by `--ran` (34 derived, 34 run, 0 NOT-MEASURED, every line with its exit code); `pnpm --dir ui run check:spec-symbols` exit 0. `node scripts/check-sdui-manifest.mjs`: exit 0 (manifest present and intact, 106 components). - `check-changeset-no-major.mjs --base origin/main` with this body as `GITHUB_EVENT_PATH`: exit 0 (`Metadata-protocol: none`, patch); `check-adr-0087-registration`, `check-closing-keyword-parity` and `check-partof-closing-keyword` on this body: exit 0. ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (not touched) - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes. - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: not touched. - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01QsR2jV95ABcxitqSGp6kmv --- _Generated by [Claude Code](https://claude.ai/code/session_01QsR2jV95ABcxitqSGp6kmv)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 3ac22c9 commit a364093

6 files changed

Lines changed: 437 additions & 30 deletions
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@object-ui/plugin-detail': patch
3+
---
4+
5+
fix(plugin-detail): on a desktop the record page's enlarged related tab draws the child object's list grid, as the phone already does
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
The enlarged related tab (the record page's active tab) drew a generic `data-table` on a desktop: rows about 65 px tall where the child object's list page draws 41 px, a name in plain text that did not open the record, and the count twice (on the 全部 chip and on the line under the toolbar). It now renders `object-grid` over the rows the related list already read, the branch the phone has taken since the phone card rows landed, so the tab is the child object's list face: the list page's row height and density, the name as a link that opens the record, and the grid's row actions.
11+
12+
The related list stays the source of truth. Its window, page and sort reach the grid as host props (the ones the list view hands the same grid), so paging and sort are still the server's `$top` / `$skip` / `$orderby`, and its own pager stays the one pager. The filter box, the 全部 / 我的 / 逾期 chips, 新建 and 导出 are unchanged. The host's row actions (`list_item`, and `record_related` inside a parent record) ride to the grid's row menu and still dispatch through the host against the child object and its row, which refreshes the list. While the 全部 chip is on and nothing narrows the rows, the line under the toolbar no longer repeats the chip's count.
13+
14+
One behaviour changes: while the filter box has text, the grid offers no header sort on a relational column (its stored value is an id), as on every list page; the data-table sorted that column by its label in that state. Compact panels in the right column and lists that are not enlarged are unchanged.

‎ui/packages/app-shell/src/views/RecordDetailView.childWriteRefreshesParent-12244.test.tsx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,9 @@ describe('RecordDetailView — a child write in a related list refreshes the par
279279
expect(headerBefore).toBeTruthy();
280280

281281
const row = screen.getByText('Seat licence').closest('tr')!;
282-
await userEvent.click(within(row).getByRole('button', { name: 'Row actions' }));
282+
// The enlarged tab draws the child object's list grid (objectstack#28865),
283+
// so the row menu is the grid's.
284+
await userEvent.click(within(row).getByTestId('row-action-trigger'));
283285
await userEvent.click(await screen.findByTestId('row-action-builtin-delete'));
284286
const dialog = await screen.findByRole('alertdialog');
285287
await userEvent.click(within(dialog).getByRole('button', { name: /delete/i }));

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

Lines changed: 152 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import {
3131
useIsMobile,
3232
RelatedCountStore,
3333
} from '@object-ui/components';
34-
import { SchemaRenderer, useCapabilityGate, useCondition, toPredicateInput, useSettledSchema, useFilterScope, useResolvedFilter, usePredicateScope, classifyLoadError, declaredUserMessage, extractWriteErrorMessage, type LoadErrorKind, type RelatedRowActionDef } from '@object-ui/react';
34+
import { SchemaRenderer, ActionCtxReact, useCapabilityGate, useCondition, toPredicateInput, useSettledSchema, useFilterScope, useResolvedFilter, usePredicateScope, classifyLoadError, declaredUserMessage, extractWriteErrorMessage, type LoadErrorKind, type RelatedRowActionDef } from '@object-ui/react';
3535
import {
3636
Plus,
3737
ExternalLink,
@@ -80,6 +80,9 @@ import {
8080
resolveAffordance,
8181
isDeadlineSettled,
8282
isActionSwitchedOff,
83+
ActionRunner,
84+
type ActionDef,
85+
type ActionResult,
8386
type FilterNode,
8487
} from '@object-ui/core';
8588
import { useSafeFieldLabel } from '@object-ui/react';
@@ -408,6 +411,26 @@ type FieldReadPolicy = ReturnType<typeof usePermissions>;
408411
* so the field the card reads and the field the request carries cannot drift
409412
* apart. The value is `ObjectGallery`'s own default, so the cover is unchanged.
410413
*/
414+
/**
415+
* The action context a component reading `useAction()` gets when no
416+
* `ActionProvider` is above it: a local runner, in the shape `useAction()`'s
417+
* own fallback returns. The enlarged grid's routed context
418+
* (`enlargedGridActions`) hands every dispatch that is not this list's row
419+
* action to it, so the grid behaves under the routed context exactly as it
420+
* would have without one.
421+
*/
422+
function localActionContext(runner: ActionRunner): React.ContextType<typeof ActionCtxReact> & object {
423+
return {
424+
execute: (action) => runner.execute(action),
425+
executeChain: (actions, mode) => runner.executeChain(actions, mode),
426+
loading: false,
427+
error: null,
428+
result: null,
429+
runner,
430+
updateContext: (ctx) => runner.updateContext(ctx),
431+
};
432+
}
433+
411434
const MOBILE_GALLERY_COVER_FIELD = 'image';
412435

413436
/**
@@ -2728,28 +2751,45 @@ export const RelatedList: React.FC<RelatedListProps> = ({
27282751
const viewSchema = React.useMemo(() => {
27292752
if (schema) return schema;
27302753

2754+
// objectstack#28865 (#27524 item 9) — the ENLARGED tab is the child
2755+
// object's list face, on a desktop as on a phone (objectstack#28484): the
2756+
// list page's grid, `object-grid`, over the rows this list already read.
2757+
// The grid draws the row height and density, the name link, the row
2758+
// actions and (under 768 px) the phone list's card rows. Resolved through
2759+
// the registry: `plugin-grid` depends on this package, so it cannot be
2760+
// imported here.
2761+
//
2762+
// This list stays the source of truth for everything the rows came from.
2763+
// Its window, page and sort reach the grid on the grid's HOST channel
2764+
// (`enlargedGridHost`, the React props `plugin-list`'s `ListView` hands
2765+
// it), never as node keys; the filter box, the chips, 新建 and 导出 stay
2766+
// in this list's toolbar; its own pager stays the one pager, so the
2767+
// grid's is off.
2768+
if (enlargedFace && (type === 'grid' || type === 'table')) {
2769+
return {
2770+
type: 'object-grid',
2771+
objectName: api,
2772+
data: { provider: 'value', items: paginatedData },
2773+
columns: effectiveColumns
2774+
.map((c: any) => c.accessorKey || columnIdentity(c))
2775+
.filter(Boolean),
2776+
showSearch: false,
2777+
showPagination: false,
2778+
// The host's row actions (`list_item`, and `record_related` inside a
2779+
// parent record), already localized — the non-author host key the
2780+
// console's object page hands the same grid. A dispatch from them is
2781+
// routed back to `onRowAction` (`enlargedGridActions`).
2782+
...(hasCustomRowActions && onRowAction ? { rowActionDefs: rowActions } : {}),
2783+
onRowClick: handleRowClick,
2784+
};
2785+
}
2786+
27312787
// Mobile: render grid/table data as a card gallery — single-column,
27322788
// tap-friendly, visually consistent with the standalone gallery view.
27332789
// Reuses the registered `object-gallery` schema so we don't ship a
27342790
// duplicate renderer. Falls back to the data-table on desktop and
27352791
// when explicit `type='list'` is requested (legacy path).
27362792
if (isMobile && (type === 'grid' || type === 'table')) {
2737-
// objectstack#28484 — the ENLARGED tab's phone cards are the phone
2738-
// list's card rows (`object-grid`'s card face: the name, one subtitle
2739-
// line, one status chip) at the body's full width, over the rows this
2740-
// list already read. Resolved through the registry: `plugin-grid`
2741-
// depends on this package, so it cannot be imported here.
2742-
if (enlargedFace) {
2743-
return {
2744-
type: 'object-grid',
2745-
objectName: api,
2746-
data: { provider: 'value', items: paginatedData },
2747-
columns: effectiveColumns
2748-
.map((c: any) => c.accessorKey || columnIdentity(c))
2749-
.filter(Boolean),
2750-
onRowClick: handleRowClick,
2751-
};
2752-
}
27532793
const titleField = effectiveColumns[0]?.accessorKey || columnIdentity(effectiveColumns[0]);
27542794
const visibleFields = effectiveColumns
27552795
.slice(1, 4)
@@ -2827,6 +2867,80 @@ export const RelatedList: React.FC<RelatedListProps> = ({
28272867
}
28282868
}, [type, paginatedData, tableColumns, effectiveColumns, schema, effectivePageSize, hasRowActions, hasCustomRowActions, rowActions, onRowAction, onRowEdit, onRowDelete, handleDeleteRow, handleRowClick, isMobile, compact, enlargedFace, api, objectSchema, activeSort, handleTableSort]);
28292869

2870+
const enlargedGrid = enlargedFace && !schema && (type === 'grid' || type === 'table');
2871+
2872+
/**
2873+
* objectstack#28865 — the enlarged face's grid, driven by this list on the
2874+
* grid's host channel: the React props `ObjectGrid` reads when a host owns
2875+
* the rows (`ObjectGridExternalPaginationProps`), the channel `plugin-list`'s
2876+
* `ListView` uses. Spread on `SchemaRenderer` as props, never node keys.
2877+
*
2878+
* - `manualPagination` with this list's page and count: `data` IS the page
2879+
* this list holds, so the grid neither slices nor re-pages it. The grid's
2880+
* pager is off (`showPagination: false` on the node); this list's pager
2881+
* answers a window whose total the server did not send (`hasMore`), which
2882+
* a pager over a row count cannot.
2883+
* - `sort` / `onSortChange` are this list's sort (objectui#3106), in BOTH
2884+
* of its modes: windowed it is the server's `$orderby`; while the filter
2885+
* box has text the list sorts its in-memory rows itself. Under a host
2886+
* sort the grid offers no header sort on a relational column (the stored
2887+
* value is an id, not the name the cell shows), in either mode.
2888+
* - `objectFields`: the definition this list already read, so no column
2889+
* paints withheld while the grid reads its own (objectui#10657).
2890+
* - `onEdit` / `onDelete`: this list's row Edit and its confirm-then-delete.
2891+
*/
2892+
const enlargedGridHost = React.useMemo(() => {
2893+
if (!enlargedGrid) return undefined;
2894+
const heldBefore = currentPage * (effectivePageSize || 0);
2895+
const rowCount = windowed
2896+
? total ?? heldBefore + relatedData.length + (hasMore ? 1 : 0)
2897+
: sortedData.length;
2898+
return {
2899+
manualPagination: true,
2900+
rowCount,
2901+
page: currentPage + 1,
2902+
pageSize: Math.max(1, effectivePageSize || paginatedData.length),
2903+
onPageChange: (page: number) => setCurrentPage(Math.max(0, page - 1)),
2904+
sort: activeSort,
2905+
onSortChange: handleTableSort,
2906+
...(objectSchema?.fields ? { objectFields: objectSchema.fields } : {}),
2907+
...(onRowEdit ? { onEdit: onRowEdit } : {}),
2908+
...(onRowDelete ? { onDelete: handleDeleteRow } : {}),
2909+
};
2910+
}, [enlargedGrid, windowed, total, currentPage, effectivePageSize, relatedData.length, hasMore, sortedData.length, paginatedData.length, activeSort, handleTableSort, objectSchema, onRowEdit, onRowDelete, handleDeleteRow]);
2911+
2912+
/**
2913+
* objectstack#28865 — where a row action the enlarged grid dispatches goes.
2914+
*
2915+
* The grid runs a row action def through the surrounding action runner with
2916+
* the row under `params._rowRecord` and no object of its own: on the record
2917+
* page that runner's object is the PARENT's, and nothing tells this list to
2918+
* read its rows again afterwards. The host that handed this list its row
2919+
* actions also handed it `onRowAction`, which dispatches against the child
2920+
* object and its row and signals the child object changed — the path the
2921+
* row menu of the table this face drew before took. So a dispatch carrying
2922+
* one of THIS list's action defs and a row goes there; every other dispatch
2923+
* (a bulk action, anything the grid runs without a row) reaches the
2924+
* surrounding runner unchanged — or, with no `ActionProvider` above, the
2925+
* local runner the grid's own `useAction()` would have made.
2926+
*/
2927+
const outerActions = React.useContext(ActionCtxReact);
2928+
const enlargedGridActions = React.useMemo(() => {
2929+
if (!enlargedGrid || !hasCustomRowActions || !onRowAction) return null;
2930+
const surrounding = outerActions ?? localActionContext(new ActionRunner());
2931+
const execute = async (action: ActionDef): Promise<ActionResult> => {
2932+
const params = action?.params as Record<string, unknown> | undefined;
2933+
const row = params?._rowRecord;
2934+
const def = row && typeof action?.name === 'string'
2935+
? rowActions!.find((candidate) => candidate.name === action.name)
2936+
: undefined;
2937+
if (!def) return surrounding.execute(action);
2938+
await onRowAction(def, row);
2939+
return { success: true };
2940+
};
2941+
return { ...surrounding, execute };
2942+
}, [enlargedGrid, hasCustomRowActions, rowActions, onRowAction, outerActions]);
2943+
28302944
const headerClassName = isCollapsible ? 'cursor-pointer select-none' : undefined;
28312945
const handleHeaderClick = isCollapsible ? () => setCollapsed((c) => !c) : undefined;
28322946

@@ -3165,6 +3279,12 @@ export const RelatedList: React.FC<RelatedListProps> = ({
31653279
// tab names the list, and its count is the 全部 chip's.
31663280
if (enlargedFace) {
31673281
const shownCount = filterActive ? filteredData.length : recordCount;
3282+
// objectstack#28865 — one count: while the 全部 chip is on and nothing
3283+
// narrows the rows, the chip already says how many there are, so the
3284+
// line under the toolbar does not say it again. A chip that narrows (我的,
3285+
// 逾期) or a filter text keeps the line's count, which is then a
3286+
// different number from the chip's.
3287+
const countOnChip = chips.length > 1 && enlargedChip === 'all' && !filterActive && allCount !== null && allCount === shownCount;
31683288
const chipLabel: Record<EnlargedChip, string> = {
31693289
all: t('detail.relatedChipAll', { defaultValue: 'All' }),
31703290
mine: t('detail.relatedChipMine', { defaultValue: 'Mine' }),
@@ -3308,12 +3428,22 @@ export const RelatedList: React.FC<RelatedListProps> = ({
33083428
</div>
33093429
) : (
33103430
<>
3311-
<div className="text-xs text-muted-foreground" data-related-enlarged-status="">
3312-
{t('detail.relatedRows', { count: shownCount, defaultValue: '{{count}} rows' })}
3313-
{showPagination && ` · ${t('detail.pageOf', { current: currentPage + 1, total: totalPages })}`}
3314-
</div>
3431+
{(!countOnChip || showPagination) && (
3432+
<div className="text-xs text-muted-foreground" data-related-enlarged-status="">
3433+
{[
3434+
countOnChip ? null : t('detail.relatedRows', { count: shownCount, defaultValue: '{{count}} rows' }),
3435+
showPagination ? t('detail.pageOf', { current: currentPage + 1, total: totalPages }) : null,
3436+
].filter(Boolean).join(' · ')}
3437+
</div>
3438+
)}
33153439
<div className={cn('min-w-0', loading && 'pointer-events-none opacity-60 transition-opacity')}>
3316-
<SchemaRenderer schema={viewSchema} />
3440+
{enlargedGridActions ? (
3441+
<ActionCtxReact.Provider value={enlargedGridActions}>
3442+
<SchemaRenderer schema={viewSchema} {...enlargedGridHost} />
3443+
</ActionCtxReact.Provider>
3444+
) : (
3445+
<SchemaRenderer schema={viewSchema} {...enlargedGridHost} />
3446+
)}
33173447
</div>
33183448
{showPagination && (
33193449
<div className="flex items-center justify-end gap-2">

0 commit comments

Comments
 (0)