44 changes: 44 additions & 0 deletions .changeset/7210-non-grid-row-ceiling.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
---
'@object-ui/react': minor
'@object-ui/i18n': patch
'@object-ui/plugin-gantt': patch
'@object-ui/plugin-calendar': patch
'@object-ui/plugin-map': patch
'@object-ui/plugin-tree': patch
---

A non-grid view's fetch now carries a platform row ceiling, and crossing it is
never silent (objectui#7210, maintainer ruling a′, 2026-09-02).

Before this, `ObjectGantt`, `ObjectCalendar`, `ObjectMap` and `ObjectTree` each
issued a `find` with **no `$top` at all**, so the request returned the entire
filtered result set. At the 186 rows the card was filed from that is invisible;
on an object with 100k scheduled rows it is the whole table into the browser,
and nothing an author could write — `pagination.pageSize` included — could
bound a request that never carried a cap to begin with.

**What changed.** Those four fetches now ask for `NON_GRID_ROW_CEILING_TOP`
rows, draw at most `NON_GRID_ROW_CEILING` of them, and when the result set was
larger they render a footnote naming both numbers: *"Showing the first 2,000 of
41,234 records. Narrow the filter."* Below the ceiling nothing changes: the full
set draws and no footnote appears.

**The ceiling is a platform constant, not an authorable key** — `2000`, exported
from `@object-ui/react` as `NON_GRID_ROW_CEILING`. An authored `limit` or
`dataSource: { limit }` still does not reach these queries, by the same ruling;
three alternatives were rejected with it (a documentation note only — still the
whole table; truncating at `pageSize` — silent, and a complete schedule capped
at one page; an authorable `maxRows` — a new permanent key every author sets).

**Why 2,000.** One constant for all four, so the binding view sets it. Measured
in this repo's jsdom lane: gantt, calendar and map hold their DOM flat as rows
grow (virtualised task list; four events per day cell; auto-clustering above
100 markers), while `ObjectTree` flattens every expanded node into the document
at a linear **5.2 DOM elements per record** with no virtualisation. 2,000 rows
is where the worst of the four lands at ~10,400 elements — an order of
magnitude above Lighthouse's "excessive DOM size" warning, and still ~10x the
real application result set this card came from.

New exports on `@object-ui/react`: `NON_GRID_ROW_CEILING`,
`NON_GRID_ROW_CEILING_TOP`, `applyNonGridRowCeiling`, `NonGridRowCeilingNote`.
Two new `common.*` i18n keys carry the footnote copy in all ten packs.
51 changes: 51 additions & 0 deletions .changeset/7225-settled-schema-convergence.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
---
'@object-ui/plugin-kanban': patch
'@object-ui/plugin-view': patch
'@object-ui/plugin-calendar': patch
'@object-ui/plugin-gantt': patch
---

The settled-schema convergence, and the gantt's duplicate query gated
(objectui#7225, maintainer ruling B, 2026-09-02).

`useSettledSchema` was extracted and published in PR #6690 with exactly **one**
non-test adopter (`ObjectTree`, the component that had an actual defect —
objectui#6481's unkeyed latch). `ObjectKanban`, `plugin-view/ObjectView` and
`ObjectCalendar` kept their own hand copies of the same shape, so a published
export was owed compatibility forever **and** the duplication it was named for
stayed. All three now call the hook.

The migration is a pure deduplication with no behaviour delta — the hook was
extracted *from* these three shapes, so each becomes a one-line call.
`ObjectCalendar`, which objectui#6482 named as the obstacle, fits via the
recipe the hook's own doc comment prescribes for it by name: pass the data
source as `undefined` for a render that must not read metadata
(`hasInlineData ? undefined : dataSource`), so "inline value data set" is
expressed as "there is no source to read from" rather than as a second enable
flag. GATE PLACEMENT stays local in all three, which is what #6482 ruled and
what made the calendar's obstacle a non-obstacle: it was about the gate half.

**One observable change:** `ObjectKanban`'s rejected definition read now logs
on `console.error` with a `[useSettledSchema]` prefix instead of
`console.warn`. Its test spy moves with it, and now asserts on the channel
rather than merely silencing it.

**The gantt's duplicate query is gated** (ask 2 of the card; #6482's
undischarged half). `ObjectGantt` listed `objectSchema` in `reload`'s
dependency list, so every load issued two unbounded queries — the first with no
`$expand` at all. Measured on this component across three latency profiles, the
cost is not the mild "round trip bought and thrown away": when the metadata
read is the slower of the two, which is the common case on a cold
`MetadataCache`, the user sees the full three-step paint — raw foreign-key ids,
back to the loading placeholder, then the expanded rows. It now issues one
query, already expanded.

Gating the gantt required its schema resolution to settle on EVERY exit
(objectui#7232): the hand-rolled effect returned without settling on
`!effectiveDataSource`, on `!resource` and in its `catch` — harmless while
nothing waited on it, and a chart that never loads once something does.
`useSettledSchema` settles on all three by construction, which is what makes
the gate safe; both exits are pinned.

⛔ Gating is not capping. The row ceiling on these fetches is objectui#7210's
separate ruling, in its own commit on the same branch.
24 changes: 21 additions & 3 deletions content/docs/guide/data-source.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -252,12 +252,12 @@ ignores would be accepted and dropped, which is the defect this binding removes.
| `object-grid` | ✅ | ✅ | ✅ | ✅ | ✅ |
| `element:record_picker` | ✅ | ✅ | ✅ | ✅ | ✅ |
| `record:related_list` | ✅ | columns / filter / sort / limit | ✅ | ✅ | ✅ |
| `object-calendar` | ✅ | filter / sort | ✅ | ✅ | — no row cap |
| `object-calendar` | ✅ | filter / sort | ✅ | ✅ | — platform ceiling |
| `object-kanban` | ✅ | filter / limit | ✅ | — no ordering | ✅ (`limit`) |
| `object-chart` | ✅ | filter | ✅ | — engine orders | — no page |
| `object-metric` | ✅ | filter | ✅ | — single value | — single value |
| `object-gantt` | ✅ | filter / sort | ✅ | ✅ | — no row cap |
| `object-map` | ✅ | filter / sort | ✅ | ✅ | — no row cap |
| `object-gantt` | ✅ | filter / sort | ✅ | ✅ | — platform ceiling |
| `object-map` | ✅ | filter / sort | ✅ | ✅ | — platform ceiling |
| `object-pivot` | ✅ | filter | ✅ | — grouping orders | — totals need all rows |
| `object-timeline` | ✅ | filter / sort / limit | ✅ | ✅ | ✅ (`limit`) |
| `object-form` | ✅ | error-checked only | — no collection query | — | — |
Expand DownExpand Up@@ -322,6 +322,24 @@ cap above is `limit` on the **board**, while `limit` on a **column** is that
lane's WIP limit (how many cards it may hold before it warns), which is display
behaviour and never touches the query.

Reading `— platform ceiling` on `object-calendar` / `object-gantt` / `object-map`
(and on `object-tree`, which predates this table): those four are the **non-grid**
visualisations, and objectui#7210's maintainer ruling settled what their row
behaviour is. They fetch the whole **filtered** result set — a gantt cannot
compute a truthful `min(start) → max(end)` from one page, a map fits its camera to
every marker, and a tree assembled from a page loses every node whose parent fell
outside it — but the fetch is **bounded** by `NON_GRID_ROW_CEILING`
(`@object-ui/react`, currently 2,000). Past it the view draws the first N rows and
shows a footnote naming both N and the total. Silent truncation is the failure
that ruling exists to prevent: a cut-off schedule still looks like a schedule.

The `limit` cell stays "not ✅" for all four because the ceiling is **not
authorable and must not become one** — a named constant in the renderer, by the
same ruling. An authored `limit`, a binding's `limit` and a named view's
`pagination.pageSize` all still fail to reach these queries, which is what the
cell has always meant. What changed is only that "no cap at all" is no longer
true.

Remaining gap, recorded rather than papered over:

- `object-form` / `embeddable-form` / `object-master-detail-form` resolve `view`
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/ar.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -129,6 +129,13 @@ const ar = {
record: "سجل",
retry: "إعادة المحاولة",
printDialogHint: "يفتح مربع حوار الطباعة في المتصفح (ليس تصديرًا إلى PDF)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "يتم عرض أول {{shown}} من أصل {{total}} سجل. ضيّق عامل التصفية.",
rowCeilingNoteUnknownTotal: "يتم عرض أول {{shown}} سجل. ضيّق عامل التصفية.",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/de.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,6 +125,13 @@ const de = {
record: "Datensatz",
retry: "Erneut versuchen",
printDialogHint: "Öffnet den Druckdialog Ihres Browsers (kein PDF-Export)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "Erste {{shown}} von {{total}} Datensätzen. Filter eingrenzen.",
rowCeilingNoteUnknownTotal: "Erste {{shown}} Datensätze. Filter eingrenzen.",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/en.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -149,6 +149,13 @@ const en = {
record: 'Record',
retry: 'Retry',
printDialogHint: 'Opens your browser’s print dialog (not a PDF export)',
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: 'Showing the first {{shown}} of {{total}} records. Narrow the filter.',
rowCeilingNoteUnknownTotal: 'Showing the first {{shown}} records. Narrow the filter.',
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/es.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -124,6 +124,13 @@ const es = {
record: "Registro",
retry: "Reintentar",
printDialogHint: "Abre el cuadro de diálogo de impresión de tu navegador (no es una exportación a PDF)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "Mostrando los primeros {{shown}} de {{total}} registros. Acota el filtro.",
rowCeilingNoteUnknownTotal: "Mostrando los primeros {{shown}} registros. Acota el filtro.",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/fr.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,6 +125,13 @@ const fr = {
record: "Enregistrement",
retry: "Réessayer",
printDialogHint: "Ouvre la boîte de dialogue d'impression de votre navigateur (ce n'est pas un export PDF)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "Affichage des {{shown}} premiers enregistrements sur {{total}}. Affinez le filtre.",
rowCeilingNoteUnknownTotal: "Affichage des {{shown}} premiers enregistrements. Affinez le filtre.",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/ja.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,6 +125,13 @@ const ja = {
record: "レコード",
retry: "再試行",
printDialogHint: "ブラウザーの印刷ダイアログを開きます(PDF エクスポートではありません)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "{{total}} 件中、最初の {{shown}} 件を表示しています。フィルターを絞り込んでください。",
rowCeilingNoteUnknownTotal: "最初の {{shown}} 件を表示しています。フィルターを絞り込んでください。",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/ko.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,6 +125,13 @@ const ko = {
record: "레코드",
retry: "다시 시도",
printDialogHint: "브라우저의 인쇄 대화 상자를 엽니다(PDF 내보내기가 아닙니다)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "전체 {{total}}개 중 처음 {{shown}}개를 표시합니다. 필터를 좁히세요.",
rowCeilingNoteUnknownTotal: "처음 {{shown}}개를 표시합니다. 필터를 좁히세요.",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/pt.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -124,6 +124,13 @@ const pt = {
record: "Registro",
retry: "Tentar novamente",
printDialogHint: "Abre a caixa de diálogo de impressão do navegador (não é uma exportação para PDF)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "Mostrando os primeiros {{shown}} de {{total}} registros. Restrinja o filtro.",
rowCeilingNoteUnknownTotal: "Mostrando os primeiros {{shown}} registros. Restrinja o filtro.",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/ru.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -131,6 +131,13 @@ const ru = {
record: "Запись",
retry: "Повторить",
printDialogHint: "Открывает диалог печати браузера (это не экспорт в PDF)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "Показаны первые {{shown}} из {{total}} записей. Сузьте фильтр.",
rowCeilingNoteUnknownTotal: "Показаны первые {{shown}} записей. Сузьте фильтр.",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/zh.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -132,6 +132,13 @@ const zh = {
record: '记录',
retry: '重试',
printDialogHint: '打开浏览器打印对话框(不是导出 PDF)',
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: '仅显示 {{total}} 条记录中的前 {{shown}} 条。请缩小筛选范围。',
rowCeilingNoteUnknownTotal: '仅显示前 {{shown}} 条记录。请缩小筛选范围。',
},
actions: {
decisionOutput: {
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
44 changes: 44 additions & 0 deletions .changeset/7210-non-grid-row-ceiling.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
---
'@object-ui/react': minor
'@object-ui/i18n': patch
'@object-ui/plugin-gantt': patch
'@object-ui/plugin-calendar': patch
'@object-ui/plugin-map': patch
'@object-ui/plugin-tree': patch
---

A non-grid view's fetch now carries a platform row ceiling, and crossing it is
never silent (objectui#7210, maintainer ruling a′, 2026-09-02).

Before this, `ObjectGantt`, `ObjectCalendar`, `ObjectMap` and `ObjectTree` each
issued a `find` with **no `$top` at all**, so the request returned the entire
filtered result set. At the 186 rows the card was filed from that is invisible;
on an object with 100k scheduled rows it is the whole table into the browser,
and nothing an author could write — `pagination.pageSize` included — could
bound a request that never carried a cap to begin with.

**What changed.** Those four fetches now ask for `NON_GRID_ROW_CEILING_TOP`
rows, draw at most `NON_GRID_ROW_CEILING` of them, and when the result set was
larger they render a footnote naming both numbers: *"Showing the first 2,000 of
41,234 records. Narrow the filter."* Below the ceiling nothing changes: the full
set draws and no footnote appears.

**The ceiling is a platform constant, not an authorable key** — `2000`, exported
from `@object-ui/react` as `NON_GRID_ROW_CEILING`. An authored `limit` or
`dataSource: { limit }` still does not reach these queries, by the same ruling;
three alternatives were rejected with it (a documentation note only — still the
whole table; truncating at `pageSize` — silent, and a complete schedule capped
at one page; an authorable `maxRows` — a new permanent key every author sets).

**Why 2,000.** One constant for all four, so the binding view sets it. Measured
in this repo's jsdom lane: gantt, calendar and map hold their DOM flat as rows
grow (virtualised task list; four events per day cell; auto-clustering above
100 markers), while `ObjectTree` flattens every expanded node into the document
at a linear **5.2 DOM elements per record** with no virtualisation. 2,000 rows
is where the worst of the four lands at ~10,400 elements — an order of
magnitude above Lighthouse's "excessive DOM size" warning, and still ~10x the
real application result set this card came from.

New exports on `@object-ui/react`: `NON_GRID_ROW_CEILING`,
`NON_GRID_ROW_CEILING_TOP`, `applyNonGridRowCeiling`, `NonGridRowCeilingNote`.
Two new `common.*` i18n keys carry the footnote copy in all ten packs.
51 changes: 51 additions & 0 deletions .changeset/7225-settled-schema-convergence.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
---
'@object-ui/plugin-kanban': patch
'@object-ui/plugin-view': patch
'@object-ui/plugin-calendar': patch
'@object-ui/plugin-gantt': patch
---

The settled-schema convergence, and the gantt's duplicate query gated
(objectui#7225, maintainer ruling B, 2026-09-02).

`useSettledSchema` was extracted and published in PR #6690 with exactly **one**
non-test adopter (`ObjectTree`, the component that had an actual defect —
objectui#6481's unkeyed latch). `ObjectKanban`, `plugin-view/ObjectView` and
`ObjectCalendar` kept their own hand copies of the same shape, so a published
export was owed compatibility forever **and** the duplication it was named for
stayed. All three now call the hook.

The migration is a pure deduplication with no behaviour delta — the hook was
extracted *from* these three shapes, so each becomes a one-line call.
`ObjectCalendar`, which objectui#6482 named as the obstacle, fits via the
recipe the hook's own doc comment prescribes for it by name: pass the data
source as `undefined` for a render that must not read metadata
(`hasInlineData ? undefined : dataSource`), so "inline value data set" is
expressed as "there is no source to read from" rather than as a second enable
flag. GATE PLACEMENT stays local in all three, which is what #6482 ruled and
what made the calendar's obstacle a non-obstacle: it was about the gate half.

**One observable change:** `ObjectKanban`'s rejected definition read now logs
on `console.error` with a `[useSettledSchema]` prefix instead of
`console.warn`. Its test spy moves with it, and now asserts on the channel
rather than merely silencing it.

**The gantt's duplicate query is gated** (ask 2 of the card; #6482's
undischarged half). `ObjectGantt` listed `objectSchema` in `reload`'s
dependency list, so every load issued two unbounded queries — the first with no
`$expand` at all. Measured on this component across three latency profiles, the
cost is not the mild "round trip bought and thrown away": when the metadata
read is the slower of the two, which is the common case on a cold
`MetadataCache`, the user sees the full three-step paint — raw foreign-key ids,
back to the loading placeholder, then the expanded rows. It now issues one
query, already expanded.

Gating the gantt required its schema resolution to settle on EVERY exit
(objectui#7232): the hand-rolled effect returned without settling on
`!effectiveDataSource`, on `!resource` and in its `catch` — harmless while
nothing waited on it, and a chart that never loads once something does.
`useSettledSchema` settles on all three by construction, which is what makes
the gate safe; both exits are pinned.

⛔ Gating is not capping. The row ceiling on these fetches is objectui#7210's
separate ruling, in its own commit on the same branch.
24 changes: 21 additions & 3 deletions content/docs/guide/data-source.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -252,12 +252,12 @@ ignores would be accepted and dropped, which is the defect this binding removes.
| `object-grid` | ✅ | ✅ | ✅ | ✅ | ✅ |
| `element:record_picker` | ✅ | ✅ | ✅ | ✅ | ✅ |
| `record:related_list` | ✅ | columns / filter / sort / limit | ✅ | ✅ | ✅ |
| `object-calendar` | ✅ | filter / sort | ✅ | ✅ | — no row cap |
| `object-calendar` | ✅ | filter / sort | ✅ | ✅ | — platform ceiling |
| `object-kanban` | ✅ | filter / limit | ✅ | — no ordering | ✅ (`limit`) |
| `object-chart` | ✅ | filter | ✅ | — engine orders | — no page |
| `object-metric` | ✅ | filter | ✅ | — single value | — single value |
| `object-gantt` | ✅ | filter / sort | ✅ | ✅ | — no row cap |
| `object-map` | ✅ | filter / sort | ✅ | ✅ | — no row cap |
| `object-gantt` | ✅ | filter / sort | ✅ | ✅ | — platform ceiling |
| `object-map` | ✅ | filter / sort | ✅ | ✅ | — platform ceiling |
| `object-pivot` | ✅ | filter | ✅ | — grouping orders | — totals need all rows |
| `object-timeline` | ✅ | filter / sort / limit | ✅ | ✅ | ✅ (`limit`) |
| `object-form` | ✅ | error-checked only | — no collection query | — | — |
Expand DownExpand Up@@ -322,6 +322,24 @@ cap above is `limit` on the **board**, while `limit` on a **column** is that
lane's WIP limit (how many cards it may hold before it warns), which is display
behaviour and never touches the query.

Reading `— platform ceiling` on `object-calendar` / `object-gantt` / `object-map`
(and on `object-tree`, which predates this table): those four are the **non-grid**
visualisations, and objectui#7210's maintainer ruling settled what their row
behaviour is. They fetch the whole **filtered** result set — a gantt cannot
compute a truthful `min(start) → max(end)` from one page, a map fits its camera to
every marker, and a tree assembled from a page loses every node whose parent fell
outside it — but the fetch is **bounded** by `NON_GRID_ROW_CEILING`
(`@object-ui/react`, currently 2,000). Past it the view draws the first N rows and
shows a footnote naming both N and the total. Silent truncation is the failure
that ruling exists to prevent: a cut-off schedule still looks like a schedule.

The `limit` cell stays "not ✅" for all four because the ceiling is **not
authorable and must not become one** — a named constant in the renderer, by the
same ruling. An authored `limit`, a binding's `limit` and a named view's
`pagination.pageSize` all still fail to reach these queries, which is what the
cell has always meant. What changed is only that "no cap at all" is no longer
true.

Remaining gap, recorded rather than papered over:

- `object-form` / `embeddable-form` / `object-master-detail-form` resolve `view`
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/ar.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -129,6 +129,13 @@ const ar = {
record: "سجل",
retry: "إعادة المحاولة",
printDialogHint: "يفتح مربع حوار الطباعة في المتصفح (ليس تصديرًا إلى PDF)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "يتم عرض أول {{shown}} من أصل {{total}} سجل. ضيّق عامل التصفية.",
rowCeilingNoteUnknownTotal: "يتم عرض أول {{shown}} سجل. ضيّق عامل التصفية.",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/de.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,6 +125,13 @@ const de = {
record: "Datensatz",
retry: "Erneut versuchen",
printDialogHint: "Öffnet den Druckdialog Ihres Browsers (kein PDF-Export)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "Erste {{shown}} von {{total}} Datensätzen. Filter eingrenzen.",
rowCeilingNoteUnknownTotal: "Erste {{shown}} Datensätze. Filter eingrenzen.",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/en.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -149,6 +149,13 @@ const en = {
record: 'Record',
retry: 'Retry',
printDialogHint: 'Opens your browser’s print dialog (not a PDF export)',
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: 'Showing the first {{shown}} of {{total}} records. Narrow the filter.',
rowCeilingNoteUnknownTotal: 'Showing the first {{shown}} records. Narrow the filter.',
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/es.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -124,6 +124,13 @@ const es = {
record: "Registro",
retry: "Reintentar",
printDialogHint: "Abre el cuadro de diálogo de impresión de tu navegador (no es una exportación a PDF)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "Mostrando los primeros {{shown}} de {{total}} registros. Acota el filtro.",
rowCeilingNoteUnknownTotal: "Mostrando los primeros {{shown}} registros. Acota el filtro.",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/fr.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,6 +125,13 @@ const fr = {
record: "Enregistrement",
retry: "Réessayer",
printDialogHint: "Ouvre la boîte de dialogue d'impression de votre navigateur (ce n'est pas un export PDF)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "Affichage des {{shown}} premiers enregistrements sur {{total}}. Affinez le filtre.",
rowCeilingNoteUnknownTotal: "Affichage des {{shown}} premiers enregistrements. Affinez le filtre.",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/ja.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,6 +125,13 @@ const ja = {
record: "レコード",
retry: "再試行",
printDialogHint: "ブラウザーの印刷ダイアログを開きます(PDF エクスポートではありません)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "{{total}} 件中、最初の {{shown}} 件を表示しています。フィルターを絞り込んでください。",
rowCeilingNoteUnknownTotal: "最初の {{shown}} 件を表示しています。フィルターを絞り込んでください。",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/ko.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,6 +125,13 @@ const ko = {
record: "레코드",
retry: "다시 시도",
printDialogHint: "브라우저의 인쇄 대화 상자를 엽니다(PDF 내보내기가 아닙니다)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "전체 {{total}}개 중 처음 {{shown}}개를 표시합니다. 필터를 좁히세요.",
rowCeilingNoteUnknownTotal: "처음 {{shown}}개를 표시합니다. 필터를 좁히세요.",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/pt.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -124,6 +124,13 @@ const pt = {
record: "Registro",
retry: "Tentar novamente",
printDialogHint: "Abre a caixa de diálogo de impressão do navegador (não é uma exportação para PDF)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "Mostrando os primeiros {{shown}} de {{total}} registros. Restrinja o filtro.",
rowCeilingNoteUnknownTotal: "Mostrando os primeiros {{shown}} registros. Restrinja o filtro.",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/ru.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -131,6 +131,13 @@ const ru = {
record: "Запись",
retry: "Повторить",
printDialogHint: "Открывает диалог печати браузера (это не экспорт в PDF)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "Показаны первые {{shown}} из {{total}} записей. Сузьте фильтр.",
rowCeilingNoteUnknownTotal: "Показаны первые {{shown}} записей. Сузьте фильтр.",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/zh.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -132,6 +132,13 @@ const zh = {
record: '记录',
retry: '重试',
printDialogHint: '打开浏览器打印对话框(不是导出 PDF)',
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: '仅显示 {{total}} 条记录中的前 {{shown}} 条。请缩小筛选范围。',
rowCeilingNoteUnknownTotal: '仅显示前 {{shown}} 条记录。请缩小筛选范围。',
},
actions: {
decisionOutput: {
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
44 changes: 44 additions & 0 deletions .changeset/7210-non-grid-row-ceiling.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
---
'@object-ui/react': minor
'@object-ui/i18n': patch
'@object-ui/plugin-gantt': patch
'@object-ui/plugin-calendar': patch
'@object-ui/plugin-map': patch
'@object-ui/plugin-tree': patch
---

A non-grid view's fetch now carries a platform row ceiling, and crossing it is
never silent (objectui#7210, maintainer ruling a′, 2026-09-02).

Before this, `ObjectGantt`, `ObjectCalendar`, `ObjectMap` and `ObjectTree` each
issued a `find` with **no `$top` at all**, so the request returned the entire
filtered result set. At the 186 rows the card was filed from that is invisible;
on an object with 100k scheduled rows it is the whole table into the browser,
and nothing an author could write — `pagination.pageSize` included — could
bound a request that never carried a cap to begin with.

**What changed.** Those four fetches now ask for `NON_GRID_ROW_CEILING_TOP`
rows, draw at most `NON_GRID_ROW_CEILING` of them, and when the result set was
larger they render a footnote naming both numbers: *"Showing the first 2,000 of
41,234 records. Narrow the filter."* Below the ceiling nothing changes: the full
set draws and no footnote appears.

**The ceiling is a platform constant, not an authorable key** — `2000`, exported
from `@object-ui/react` as `NON_GRID_ROW_CEILING`. An authored `limit` or
`dataSource: { limit }` still does not reach these queries, by the same ruling;
three alternatives were rejected with it (a documentation note only — still the
whole table; truncating at `pageSize` — silent, and a complete schedule capped
at one page; an authorable `maxRows` — a new permanent key every author sets).

**Why 2,000.** One constant for all four, so the binding view sets it. Measured
in this repo's jsdom lane: gantt, calendar and map hold their DOM flat as rows
grow (virtualised task list; four events per day cell; auto-clustering above
100 markers), while `ObjectTree` flattens every expanded node into the document
at a linear **5.2 DOM elements per record** with no virtualisation. 2,000 rows
is where the worst of the four lands at ~10,400 elements — an order of
magnitude above Lighthouse's "excessive DOM size" warning, and still ~10x the
real application result set this card came from.

New exports on `@object-ui/react`: `NON_GRID_ROW_CEILING`,
`NON_GRID_ROW_CEILING_TOP`, `applyNonGridRowCeiling`, `NonGridRowCeilingNote`.
Two new `common.*` i18n keys carry the footnote copy in all ten packs.
51 changes: 51 additions & 0 deletions .changeset/7225-settled-schema-convergence.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
---
'@object-ui/plugin-kanban': patch
'@object-ui/plugin-view': patch
'@object-ui/plugin-calendar': patch
'@object-ui/plugin-gantt': patch
---

The settled-schema convergence, and the gantt's duplicate query gated
(objectui#7225, maintainer ruling B, 2026-09-02).

`useSettledSchema` was extracted and published in PR #6690 with exactly **one**
non-test adopter (`ObjectTree`, the component that had an actual defect —
objectui#6481's unkeyed latch). `ObjectKanban`, `plugin-view/ObjectView` and
`ObjectCalendar` kept their own hand copies of the same shape, so a published
export was owed compatibility forever **and** the duplication it was named for
stayed. All three now call the hook.

The migration is a pure deduplication with no behaviour delta — the hook was
extracted *from* these three shapes, so each becomes a one-line call.
`ObjectCalendar`, which objectui#6482 named as the obstacle, fits via the
recipe the hook's own doc comment prescribes for it by name: pass the data
source as `undefined` for a render that must not read metadata
(`hasInlineData ? undefined : dataSource`), so "inline value data set" is
expressed as "there is no source to read from" rather than as a second enable
flag. GATE PLACEMENT stays local in all three, which is what #6482 ruled and
what made the calendar's obstacle a non-obstacle: it was about the gate half.

**One observable change:** `ObjectKanban`'s rejected definition read now logs
on `console.error` with a `[useSettledSchema]` prefix instead of
`console.warn`. Its test spy moves with it, and now asserts on the channel
rather than merely silencing it.

**The gantt's duplicate query is gated** (ask 2 of the card; #6482's
undischarged half). `ObjectGantt` listed `objectSchema` in `reload`'s
dependency list, so every load issued two unbounded queries — the first with no
`$expand` at all. Measured on this component across three latency profiles, the
cost is not the mild "round trip bought and thrown away": when the metadata
read is the slower of the two, which is the common case on a cold
`MetadataCache`, the user sees the full three-step paint — raw foreign-key ids,
back to the loading placeholder, then the expanded rows. It now issues one
query, already expanded.

Gating the gantt required its schema resolution to settle on EVERY exit
(objectui#7232): the hand-rolled effect returned without settling on
`!effectiveDataSource`, on `!resource` and in its `catch` — harmless while
nothing waited on it, and a chart that never loads once something does.
`useSettledSchema` settles on all three by construction, which is what makes
the gate safe; both exits are pinned.

⛔ Gating is not capping. The row ceiling on these fetches is objectui#7210's
separate ruling, in its own commit on the same branch.
24 changes: 21 additions & 3 deletions content/docs/guide/data-source.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -252,12 +252,12 @@ ignores would be accepted and dropped, which is the defect this binding removes.
| `object-grid` | ✅ | ✅ | ✅ | ✅ | ✅ |
| `element:record_picker` | ✅ | ✅ | ✅ | ✅ | ✅ |
| `record:related_list` | ✅ | columns / filter / sort / limit | ✅ | ✅ | ✅ |
| `object-calendar` | ✅ | filter / sort | ✅ | ✅ | — no row cap |
| `object-calendar` | ✅ | filter / sort | ✅ | ✅ | — platform ceiling |
| `object-kanban` | ✅ | filter / limit | ✅ | — no ordering | ✅ (`limit`) |
| `object-chart` | ✅ | filter | ✅ | — engine orders | — no page |
| `object-metric` | ✅ | filter | ✅ | — single value | — single value |
| `object-gantt` | ✅ | filter / sort | ✅ | ✅ | — no row cap |
| `object-map` | ✅ | filter / sort | ✅ | ✅ | — no row cap |
| `object-gantt` | ✅ | filter / sort | ✅ | ✅ | — platform ceiling |
| `object-map` | ✅ | filter / sort | ✅ | ✅ | — platform ceiling |
| `object-pivot` | ✅ | filter | ✅ | — grouping orders | — totals need all rows |
| `object-timeline` | ✅ | filter / sort / limit | ✅ | ✅ | ✅ (`limit`) |
| `object-form` | ✅ | error-checked only | — no collection query | — | — |
Expand DownExpand Up@@ -322,6 +322,24 @@ cap above is `limit` on the **board**, while `limit` on a **column** is that
lane's WIP limit (how many cards it may hold before it warns), which is display
behaviour and never touches the query.

Reading `— platform ceiling` on `object-calendar` / `object-gantt` / `object-map`
(and on `object-tree`, which predates this table): those four are the **non-grid**
visualisations, and objectui#7210's maintainer ruling settled what their row
behaviour is. They fetch the whole **filtered** result set — a gantt cannot
compute a truthful `min(start) → max(end)` from one page, a map fits its camera to
every marker, and a tree assembled from a page loses every node whose parent fell
outside it — but the fetch is **bounded** by `NON_GRID_ROW_CEILING`
(`@object-ui/react`, currently 2,000). Past it the view draws the first N rows and
shows a footnote naming both N and the total. Silent truncation is the failure
that ruling exists to prevent: a cut-off schedule still looks like a schedule.

The `limit` cell stays "not ✅" for all four because the ceiling is **not
authorable and must not become one** — a named constant in the renderer, by the
same ruling. An authored `limit`, a binding's `limit` and a named view's
`pagination.pageSize` all still fail to reach these queries, which is what the
cell has always meant. What changed is only that "no cap at all" is no longer
true.

Remaining gap, recorded rather than papered over:

- `object-form` / `embeddable-form` / `object-master-detail-form` resolve `view`
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/ar.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -129,6 +129,13 @@ const ar = {
record: "سجل",
retry: "إعادة المحاولة",
printDialogHint: "يفتح مربع حوار الطباعة في المتصفح (ليس تصديرًا إلى PDF)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "يتم عرض أول {{shown}} من أصل {{total}} سجل. ضيّق عامل التصفية.",
rowCeilingNoteUnknownTotal: "يتم عرض أول {{shown}} سجل. ضيّق عامل التصفية.",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/de.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,6 +125,13 @@ const de = {
record: "Datensatz",
retry: "Erneut versuchen",
printDialogHint: "Öffnet den Druckdialog Ihres Browsers (kein PDF-Export)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "Erste {{shown}} von {{total}} Datensätzen. Filter eingrenzen.",
rowCeilingNoteUnknownTotal: "Erste {{shown}} Datensätze. Filter eingrenzen.",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/en.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -149,6 +149,13 @@ const en = {
record: 'Record',
retry: 'Retry',
printDialogHint: 'Opens your browser’s print dialog (not a PDF export)',
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: 'Showing the first {{shown}} of {{total}} records. Narrow the filter.',
rowCeilingNoteUnknownTotal: 'Showing the first {{shown}} records. Narrow the filter.',
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/es.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -124,6 +124,13 @@ const es = {
record: "Registro",
retry: "Reintentar",
printDialogHint: "Abre el cuadro de diálogo de impresión de tu navegador (no es una exportación a PDF)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "Mostrando los primeros {{shown}} de {{total}} registros. Acota el filtro.",
rowCeilingNoteUnknownTotal: "Mostrando los primeros {{shown}} registros. Acota el filtro.",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/fr.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,6 +125,13 @@ const fr = {
record: "Enregistrement",
retry: "Réessayer",
printDialogHint: "Ouvre la boîte de dialogue d'impression de votre navigateur (ce n'est pas un export PDF)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "Affichage des {{shown}} premiers enregistrements sur {{total}}. Affinez le filtre.",
rowCeilingNoteUnknownTotal: "Affichage des {{shown}} premiers enregistrements. Affinez le filtre.",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/ja.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,6 +125,13 @@ const ja = {
record: "レコード",
retry: "再試行",
printDialogHint: "ブラウザーの印刷ダイアログを開きます(PDF エクスポートではありません)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "{{total}} 件中、最初の {{shown}} 件を表示しています。フィルターを絞り込んでください。",
rowCeilingNoteUnknownTotal: "最初の {{shown}} 件を表示しています。フィルターを絞り込んでください。",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/ko.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,6 +125,13 @@ const ko = {
record: "레코드",
retry: "다시 시도",
printDialogHint: "브라우저의 인쇄 대화 상자를 엽니다(PDF 내보내기가 아닙니다)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "전체 {{total}}개 중 처음 {{shown}}개를 표시합니다. 필터를 좁히세요.",
rowCeilingNoteUnknownTotal: "처음 {{shown}}개를 표시합니다. 필터를 좁히세요.",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/pt.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -124,6 +124,13 @@ const pt = {
record: "Registro",
retry: "Tentar novamente",
printDialogHint: "Abre a caixa de diálogo de impressão do navegador (não é uma exportação para PDF)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "Mostrando os primeiros {{shown}} de {{total}} registros. Restrinja o filtro.",
rowCeilingNoteUnknownTotal: "Mostrando os primeiros {{shown}} registros. Restrinja o filtro.",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/ru.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -131,6 +131,13 @@ const ru = {
record: "Запись",
retry: "Повторить",
printDialogHint: "Открывает диалог печати браузера (это не экспорт в PDF)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "Показаны первые {{shown}} из {{total}} записей. Сузьте фильтр.",
rowCeilingNoteUnknownTotal: "Показаны первые {{shown}} записей. Сузьте фильтр.",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/zh.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -132,6 +132,13 @@ const zh = {
record: '记录',
retry: '重试',
printDialogHint: '打开浏览器打印对话框(不是导出 PDF)',
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: '仅显示 {{total}} 条记录中的前 {{shown}} 条。请缩小筛选范围。',
rowCeilingNoteUnknownTotal: '仅显示前 {{shown}} 条记录。请缩小筛选范围。',
},
actions: {
decisionOutput: {
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
44 changes: 44 additions & 0 deletions .changeset/7210-non-grid-row-ceiling.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
---
'@object-ui/react': minor
'@object-ui/i18n': patch
'@object-ui/plugin-gantt': patch
'@object-ui/plugin-calendar': patch
'@object-ui/plugin-map': patch
'@object-ui/plugin-tree': patch
---

A non-grid view's fetch now carries a platform row ceiling, and crossing it is
never silent (objectui#7210, maintainer ruling a′, 2026-09-02).

Before this, `ObjectGantt`, `ObjectCalendar`, `ObjectMap` and `ObjectTree` each
issued a `find` with **no `$top` at all**, so the request returned the entire
filtered result set. At the 186 rows the card was filed from that is invisible;
on an object with 100k scheduled rows it is the whole table into the browser,
and nothing an author could write — `pagination.pageSize` included — could
bound a request that never carried a cap to begin with.

**What changed.** Those four fetches now ask for `NON_GRID_ROW_CEILING_TOP`
rows, draw at most `NON_GRID_ROW_CEILING` of them, and when the result set was
larger they render a footnote naming both numbers: *"Showing the first 2,000 of
41,234 records. Narrow the filter."* Below the ceiling nothing changes: the full
set draws and no footnote appears.

**The ceiling is a platform constant, not an authorable key** — `2000`, exported
from `@object-ui/react` as `NON_GRID_ROW_CEILING`. An authored `limit` or
`dataSource: { limit }` still does not reach these queries, by the same ruling;
three alternatives were rejected with it (a documentation note only — still the
whole table; truncating at `pageSize` — silent, and a complete schedule capped
at one page; an authorable `maxRows` — a new permanent key every author sets).

**Why 2,000.** One constant for all four, so the binding view sets it. Measured
in this repo's jsdom lane: gantt, calendar and map hold their DOM flat as rows
grow (virtualised task list; four events per day cell; auto-clustering above
100 markers), while `ObjectTree` flattens every expanded node into the document
at a linear **5.2 DOM elements per record** with no virtualisation. 2,000 rows
is where the worst of the four lands at ~10,400 elements — an order of
magnitude above Lighthouse's "excessive DOM size" warning, and still ~10x the
real application result set this card came from.

New exports on `@object-ui/react`: `NON_GRID_ROW_CEILING`,
`NON_GRID_ROW_CEILING_TOP`, `applyNonGridRowCeiling`, `NonGridRowCeilingNote`.
Two new `common.*` i18n keys carry the footnote copy in all ten packs.
51 changes: 51 additions & 0 deletions .changeset/7225-settled-schema-convergence.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
---
'@object-ui/plugin-kanban': patch
'@object-ui/plugin-view': patch
'@object-ui/plugin-calendar': patch
'@object-ui/plugin-gantt': patch
---

The settled-schema convergence, and the gantt's duplicate query gated
(objectui#7225, maintainer ruling B, 2026-09-02).

`useSettledSchema` was extracted and published in PR #6690 with exactly **one**
non-test adopter (`ObjectTree`, the component that had an actual defect —
objectui#6481's unkeyed latch). `ObjectKanban`, `plugin-view/ObjectView` and
`ObjectCalendar` kept their own hand copies of the same shape, so a published
export was owed compatibility forever **and** the duplication it was named for
stayed. All three now call the hook.

The migration is a pure deduplication with no behaviour delta — the hook was
extracted *from* these three shapes, so each becomes a one-line call.
`ObjectCalendar`, which objectui#6482 named as the obstacle, fits via the
recipe the hook's own doc comment prescribes for it by name: pass the data
source as `undefined` for a render that must not read metadata
(`hasInlineData ? undefined : dataSource`), so "inline value data set" is
expressed as "there is no source to read from" rather than as a second enable
flag. GATE PLACEMENT stays local in all three, which is what #6482 ruled and
what made the calendar's obstacle a non-obstacle: it was about the gate half.

**One observable change:** `ObjectKanban`'s rejected definition read now logs
on `console.error` with a `[useSettledSchema]` prefix instead of
`console.warn`. Its test spy moves with it, and now asserts on the channel
rather than merely silencing it.

**The gantt's duplicate query is gated** (ask 2 of the card; #6482's
undischarged half). `ObjectGantt` listed `objectSchema` in `reload`'s
dependency list, so every load issued two unbounded queries — the first with no
`$expand` at all. Measured on this component across three latency profiles, the
cost is not the mild "round trip bought and thrown away": when the metadata
read is the slower of the two, which is the common case on a cold
`MetadataCache`, the user sees the full three-step paint — raw foreign-key ids,
back to the loading placeholder, then the expanded rows. It now issues one
query, already expanded.

Gating the gantt required its schema resolution to settle on EVERY exit
(objectui#7232): the hand-rolled effect returned without settling on
`!effectiveDataSource`, on `!resource` and in its `catch` — harmless while
nothing waited on it, and a chart that never loads once something does.
`useSettledSchema` settles on all three by construction, which is what makes
the gate safe; both exits are pinned.

⛔ Gating is not capping. The row ceiling on these fetches is objectui#7210's
separate ruling, in its own commit on the same branch.
24 changes: 21 additions & 3 deletions content/docs/guide/data-source.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -252,12 +252,12 @@ ignores would be accepted and dropped, which is the defect this binding removes.
| `object-grid` | ✅ | ✅ | ✅ | ✅ | ✅ |
| `element:record_picker` | ✅ | ✅ | ✅ | ✅ | ✅ |
| `record:related_list` | ✅ | columns / filter / sort / limit | ✅ | ✅ | ✅ |
| `object-calendar` | ✅ | filter / sort | ✅ | ✅ | — no row cap |
| `object-calendar` | ✅ | filter / sort | ✅ | ✅ | — platform ceiling |
| `object-kanban` | ✅ | filter / limit | ✅ | — no ordering | ✅ (`limit`) |
| `object-chart` | ✅ | filter | ✅ | — engine orders | — no page |
| `object-metric` | ✅ | filter | ✅ | — single value | — single value |
| `object-gantt` | ✅ | filter / sort | ✅ | ✅ | — no row cap |
| `object-map` | ✅ | filter / sort | ✅ | ✅ | — no row cap |
| `object-gantt` | ✅ | filter / sort | ✅ | ✅ | — platform ceiling |
| `object-map` | ✅ | filter / sort | ✅ | ✅ | — platform ceiling |
| `object-pivot` | ✅ | filter | ✅ | — grouping orders | — totals need all rows |
| `object-timeline` | ✅ | filter / sort / limit | ✅ | ✅ | ✅ (`limit`) |
| `object-form` | ✅ | error-checked only | — no collection query | — | — |
Expand DownExpand Up@@ -322,6 +322,24 @@ cap above is `limit` on the **board**, while `limit` on a **column** is that
lane's WIP limit (how many cards it may hold before it warns), which is display
behaviour and never touches the query.

Reading `— platform ceiling` on `object-calendar` / `object-gantt` / `object-map`
(and on `object-tree`, which predates this table): those four are the **non-grid**
visualisations, and objectui#7210's maintainer ruling settled what their row
behaviour is. They fetch the whole **filtered** result set — a gantt cannot
compute a truthful `min(start) → max(end)` from one page, a map fits its camera to
every marker, and a tree assembled from a page loses every node whose parent fell
outside it — but the fetch is **bounded** by `NON_GRID_ROW_CEILING`
(`@object-ui/react`, currently 2,000). Past it the view draws the first N rows and
shows a footnote naming both N and the total. Silent truncation is the failure
that ruling exists to prevent: a cut-off schedule still looks like a schedule.

The `limit` cell stays "not ✅" for all four because the ceiling is **not
authorable and must not become one** — a named constant in the renderer, by the
same ruling. An authored `limit`, a binding's `limit` and a named view's
`pagination.pageSize` all still fail to reach these queries, which is what the
cell has always meant. What changed is only that "no cap at all" is no longer
true.

Remaining gap, recorded rather than papered over:

- `object-form` / `embeddable-form` / `object-master-detail-form` resolve `view`
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/ar.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -129,6 +129,13 @@ const ar = {
record: "سجل",
retry: "إعادة المحاولة",
printDialogHint: "يفتح مربع حوار الطباعة في المتصفح (ليس تصديرًا إلى PDF)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "يتم عرض أول {{shown}} من أصل {{total}} سجل. ضيّق عامل التصفية.",
rowCeilingNoteUnknownTotal: "يتم عرض أول {{shown}} سجل. ضيّق عامل التصفية.",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/de.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,6 +125,13 @@ const de = {
record: "Datensatz",
retry: "Erneut versuchen",
printDialogHint: "Öffnet den Druckdialog Ihres Browsers (kein PDF-Export)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "Erste {{shown}} von {{total}} Datensätzen. Filter eingrenzen.",
rowCeilingNoteUnknownTotal: "Erste {{shown}} Datensätze. Filter eingrenzen.",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/en.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -149,6 +149,13 @@ const en = {
record: 'Record',
retry: 'Retry',
printDialogHint: 'Opens your browser’s print dialog (not a PDF export)',
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: 'Showing the first {{shown}} of {{total}} records. Narrow the filter.',
rowCeilingNoteUnknownTotal: 'Showing the first {{shown}} records. Narrow the filter.',
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/es.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -124,6 +124,13 @@ const es = {
record: "Registro",
retry: "Reintentar",
printDialogHint: "Abre el cuadro de diálogo de impresión de tu navegador (no es una exportación a PDF)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "Mostrando los primeros {{shown}} de {{total}} registros. Acota el filtro.",
rowCeilingNoteUnknownTotal: "Mostrando los primeros {{shown}} registros. Acota el filtro.",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/fr.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,6 +125,13 @@ const fr = {
record: "Enregistrement",
retry: "Réessayer",
printDialogHint: "Ouvre la boîte de dialogue d'impression de votre navigateur (ce n'est pas un export PDF)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "Affichage des {{shown}} premiers enregistrements sur {{total}}. Affinez le filtre.",
rowCeilingNoteUnknownTotal: "Affichage des {{shown}} premiers enregistrements. Affinez le filtre.",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/ja.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,6 +125,13 @@ const ja = {
record: "レコード",
retry: "再試行",
printDialogHint: "ブラウザーの印刷ダイアログを開きます(PDF エクスポートではありません)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "{{total}} 件中、最初の {{shown}} 件を表示しています。フィルターを絞り込んでください。",
rowCeilingNoteUnknownTotal: "最初の {{shown}} 件を表示しています。フィルターを絞り込んでください。",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/ko.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,6 +125,13 @@ const ko = {
record: "레코드",
retry: "다시 시도",
printDialogHint: "브라우저의 인쇄 대화 상자를 엽니다(PDF 내보내기가 아닙니다)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "전체 {{total}}개 중 처음 {{shown}}개를 표시합니다. 필터를 좁히세요.",
rowCeilingNoteUnknownTotal: "처음 {{shown}}개를 표시합니다. 필터를 좁히세요.",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/pt.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -124,6 +124,13 @@ const pt = {
record: "Registro",
retry: "Tentar novamente",
printDialogHint: "Abre a caixa de diálogo de impressão do navegador (não é uma exportação para PDF)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "Mostrando os primeiros {{shown}} de {{total}} registros. Restrinja o filtro.",
rowCeilingNoteUnknownTotal: "Mostrando os primeiros {{shown}} registros. Restrinja o filtro.",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/ru.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -131,6 +131,13 @@ const ru = {
record: "Запись",
retry: "Повторить",
printDialogHint: "Открывает диалог печати браузера (это не экспорт в PDF)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "Показаны первые {{shown}} из {{total}} записей. Сузьте фильтр.",
rowCeilingNoteUnknownTotal: "Показаны первые {{shown}} записей. Сузьте фильтр.",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/zh.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -132,6 +132,13 @@ const zh = {
record: '记录',
retry: '重试',
printDialogHint: '打开浏览器打印对话框(不是导出 PDF)',
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: '仅显示 {{total}} 条记录中的前 {{shown}} 条。请缩小筛选范围。',
rowCeilingNoteUnknownTotal: '仅显示前 {{shown}} 条记录。请缩小筛选范围。',
},
actions: {
decisionOutput: {
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
44 changes: 44 additions & 0 deletions .changeset/7210-non-grid-row-ceiling.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
---
'@object-ui/react': minor
'@object-ui/i18n': patch
'@object-ui/plugin-gantt': patch
'@object-ui/plugin-calendar': patch
'@object-ui/plugin-map': patch
'@object-ui/plugin-tree': patch
---

A non-grid view's fetch now carries a platform row ceiling, and crossing it is
never silent (objectui#7210, maintainer ruling a′, 2026-09-02).

Before this, `ObjectGantt`, `ObjectCalendar`, `ObjectMap` and `ObjectTree` each
issued a `find` with **no `$top` at all**, so the request returned the entire
filtered result set. At the 186 rows the card was filed from that is invisible;
on an object with 100k scheduled rows it is the whole table into the browser,
and nothing an author could write — `pagination.pageSize` included — could
bound a request that never carried a cap to begin with.

**What changed.** Those four fetches now ask for `NON_GRID_ROW_CEILING_TOP`
rows, draw at most `NON_GRID_ROW_CEILING` of them, and when the result set was
larger they render a footnote naming both numbers: *"Showing the first 2,000 of
41,234 records. Narrow the filter."* Below the ceiling nothing changes: the full
set draws and no footnote appears.

**The ceiling is a platform constant, not an authorable key** — `2000`, exported
from `@object-ui/react` as `NON_GRID_ROW_CEILING`. An authored `limit` or
`dataSource: { limit }` still does not reach these queries, by the same ruling;
three alternatives were rejected with it (a documentation note only — still the
whole table; truncating at `pageSize` — silent, and a complete schedule capped
at one page; an authorable `maxRows` — a new permanent key every author sets).

**Why 2,000.** One constant for all four, so the binding view sets it. Measured
in this repo's jsdom lane: gantt, calendar and map hold their DOM flat as rows
grow (virtualised task list; four events per day cell; auto-clustering above
100 markers), while `ObjectTree` flattens every expanded node into the document
at a linear **5.2 DOM elements per record** with no virtualisation. 2,000 rows
is where the worst of the four lands at ~10,400 elements — an order of
magnitude above Lighthouse's "excessive DOM size" warning, and still ~10x the
real application result set this card came from.

New exports on `@object-ui/react`: `NON_GRID_ROW_CEILING`,
`NON_GRID_ROW_CEILING_TOP`, `applyNonGridRowCeiling`, `NonGridRowCeilingNote`.
Two new `common.*` i18n keys carry the footnote copy in all ten packs.
51 changes: 51 additions & 0 deletions .changeset/7225-settled-schema-convergence.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
---
'@object-ui/plugin-kanban': patch
'@object-ui/plugin-view': patch
'@object-ui/plugin-calendar': patch
'@object-ui/plugin-gantt': patch
---

The settled-schema convergence, and the gantt's duplicate query gated
(objectui#7225, maintainer ruling B, 2026-09-02).

`useSettledSchema` was extracted and published in PR #6690 with exactly **one**
non-test adopter (`ObjectTree`, the component that had an actual defect —
objectui#6481's unkeyed latch). `ObjectKanban`, `plugin-view/ObjectView` and
`ObjectCalendar` kept their own hand copies of the same shape, so a published
export was owed compatibility forever **and** the duplication it was named for
stayed. All three now call the hook.

The migration is a pure deduplication with no behaviour delta — the hook was
extracted *from* these three shapes, so each becomes a one-line call.
`ObjectCalendar`, which objectui#6482 named as the obstacle, fits via the
recipe the hook's own doc comment prescribes for it by name: pass the data
source as `undefined` for a render that must not read metadata
(`hasInlineData ? undefined : dataSource`), so "inline value data set" is
expressed as "there is no source to read from" rather than as a second enable
flag. GATE PLACEMENT stays local in all three, which is what #6482 ruled and
what made the calendar's obstacle a non-obstacle: it was about the gate half.

**One observable change:** `ObjectKanban`'s rejected definition read now logs
on `console.error` with a `[useSettledSchema]` prefix instead of
`console.warn`. Its test spy moves with it, and now asserts on the channel
rather than merely silencing it.

**The gantt's duplicate query is gated** (ask 2 of the card; #6482's
undischarged half). `ObjectGantt` listed `objectSchema` in `reload`'s
dependency list, so every load issued two unbounded queries — the first with no
`$expand` at all. Measured on this component across three latency profiles, the
cost is not the mild "round trip bought and thrown away": when the metadata
read is the slower of the two, which is the common case on a cold
`MetadataCache`, the user sees the full three-step paint — raw foreign-key ids,
back to the loading placeholder, then the expanded rows. It now issues one
query, already expanded.

Gating the gantt required its schema resolution to settle on EVERY exit
(objectui#7232): the hand-rolled effect returned without settling on
`!effectiveDataSource`, on `!resource` and in its `catch` — harmless while
nothing waited on it, and a chart that never loads once something does.
`useSettledSchema` settles on all three by construction, which is what makes
the gate safe; both exits are pinned.

⛔ Gating is not capping. The row ceiling on these fetches is objectui#7210's
separate ruling, in its own commit on the same branch.
24 changes: 21 additions & 3 deletions content/docs/guide/data-source.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -252,12 +252,12 @@ ignores would be accepted and dropped, which is the defect this binding removes.
| `object-grid` | ✅ | ✅ | ✅ | ✅ | ✅ |
| `element:record_picker` | ✅ | ✅ | ✅ | ✅ | ✅ |
| `record:related_list` | ✅ | columns / filter / sort / limit | ✅ | ✅ | ✅ |
| `object-calendar` | ✅ | filter / sort | ✅ | ✅ | — no row cap |
| `object-calendar` | ✅ | filter / sort | ✅ | ✅ | — platform ceiling |
| `object-kanban` | ✅ | filter / limit | ✅ | — no ordering | ✅ (`limit`) |
| `object-chart` | ✅ | filter | ✅ | — engine orders | — no page |
| `object-metric` | ✅ | filter | ✅ | — single value | — single value |
| `object-gantt` | ✅ | filter / sort | ✅ | ✅ | — no row cap |
| `object-map` | ✅ | filter / sort | ✅ | ✅ | — no row cap |
| `object-gantt` | ✅ | filter / sort | ✅ | ✅ | — platform ceiling |
| `object-map` | ✅ | filter / sort | ✅ | ✅ | — platform ceiling |
| `object-pivot` | ✅ | filter | ✅ | — grouping orders | — totals need all rows |
| `object-timeline` | ✅ | filter / sort / limit | ✅ | ✅ | ✅ (`limit`) |
| `object-form` | ✅ | error-checked only | — no collection query | — | — |
Expand DownExpand Up@@ -322,6 +322,24 @@ cap above is `limit` on the **board**, while `limit` on a **column** is that
lane's WIP limit (how many cards it may hold before it warns), which is display
behaviour and never touches the query.

Reading `— platform ceiling` on `object-calendar` / `object-gantt` / `object-map`
(and on `object-tree`, which predates this table): those four are the **non-grid**
visualisations, and objectui#7210's maintainer ruling settled what their row
behaviour is. They fetch the whole **filtered** result set — a gantt cannot
compute a truthful `min(start) → max(end)` from one page, a map fits its camera to
every marker, and a tree assembled from a page loses every node whose parent fell
outside it — but the fetch is **bounded** by `NON_GRID_ROW_CEILING`
(`@object-ui/react`, currently 2,000). Past it the view draws the first N rows and
shows a footnote naming both N and the total. Silent truncation is the failure
that ruling exists to prevent: a cut-off schedule still looks like a schedule.

The `limit` cell stays "not ✅" for all four because the ceiling is **not
authorable and must not become one** — a named constant in the renderer, by the
same ruling. An authored `limit`, a binding's `limit` and a named view's
`pagination.pageSize` all still fail to reach these queries, which is what the
cell has always meant. What changed is only that "no cap at all" is no longer
true.

Remaining gap, recorded rather than papered over:

- `object-form` / `embeddable-form` / `object-master-detail-form` resolve `view`
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/ar.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -129,6 +129,13 @@ const ar = {
record: "سجل",
retry: "إعادة المحاولة",
printDialogHint: "يفتح مربع حوار الطباعة في المتصفح (ليس تصديرًا إلى PDF)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "يتم عرض أول {{shown}} من أصل {{total}} سجل. ضيّق عامل التصفية.",
rowCeilingNoteUnknownTotal: "يتم عرض أول {{shown}} سجل. ضيّق عامل التصفية.",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/de.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,6 +125,13 @@ const de = {
record: "Datensatz",
retry: "Erneut versuchen",
printDialogHint: "Öffnet den Druckdialog Ihres Browsers (kein PDF-Export)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "Erste {{shown}} von {{total}} Datensätzen. Filter eingrenzen.",
rowCeilingNoteUnknownTotal: "Erste {{shown}} Datensätze. Filter eingrenzen.",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/en.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -149,6 +149,13 @@ const en = {
record: 'Record',
retry: 'Retry',
printDialogHint: 'Opens your browser’s print dialog (not a PDF export)',
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: 'Showing the first {{shown}} of {{total}} records. Narrow the filter.',
rowCeilingNoteUnknownTotal: 'Showing the first {{shown}} records. Narrow the filter.',
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/es.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -124,6 +124,13 @@ const es = {
record: "Registro",
retry: "Reintentar",
printDialogHint: "Abre el cuadro de diálogo de impresión de tu navegador (no es una exportación a PDF)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "Mostrando los primeros {{shown}} de {{total}} registros. Acota el filtro.",
rowCeilingNoteUnknownTotal: "Mostrando los primeros {{shown}} registros. Acota el filtro.",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/fr.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,6 +125,13 @@ const fr = {
record: "Enregistrement",
retry: "Réessayer",
printDialogHint: "Ouvre la boîte de dialogue d'impression de votre navigateur (ce n'est pas un export PDF)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "Affichage des {{shown}} premiers enregistrements sur {{total}}. Affinez le filtre.",
rowCeilingNoteUnknownTotal: "Affichage des {{shown}} premiers enregistrements. Affinez le filtre.",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/ja.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,6 +125,13 @@ const ja = {
record: "レコード",
retry: "再試行",
printDialogHint: "ブラウザーの印刷ダイアログを開きます(PDF エクスポートではありません)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "{{total}} 件中、最初の {{shown}} 件を表示しています。フィルターを絞り込んでください。",
rowCeilingNoteUnknownTotal: "最初の {{shown}} 件を表示しています。フィルターを絞り込んでください。",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/ko.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,6 +125,13 @@ const ko = {
record: "레코드",
retry: "다시 시도",
printDialogHint: "브라우저의 인쇄 대화 상자를 엽니다(PDF 내보내기가 아닙니다)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "전체 {{total}}개 중 처음 {{shown}}개를 표시합니다. 필터를 좁히세요.",
rowCeilingNoteUnknownTotal: "처음 {{shown}}개를 표시합니다. 필터를 좁히세요.",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/pt.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -124,6 +124,13 @@ const pt = {
record: "Registro",
retry: "Tentar novamente",
printDialogHint: "Abre a caixa de diálogo de impressão do navegador (não é uma exportação para PDF)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "Mostrando os primeiros {{shown}} de {{total}} registros. Restrinja o filtro.",
rowCeilingNoteUnknownTotal: "Mostrando os primeiros {{shown}} registros. Restrinja o filtro.",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/ru.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -131,6 +131,13 @@ const ru = {
record: "Запись",
retry: "Повторить",
printDialogHint: "Открывает диалог печати браузера (это не экспорт в PDF)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "Показаны первые {{shown}} из {{total}} записей. Сузьте фильтр.",
rowCeilingNoteUnknownTotal: "Показаны первые {{shown}} записей. Сузьте фильтр.",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/zh.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -132,6 +132,13 @@ const zh = {
record: '记录',
retry: '重试',
printDialogHint: '打开浏览器打印对话框(不是导出 PDF)',
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: '仅显示 {{total}} 条记录中的前 {{shown}} 条。请缩小筛选范围。',
rowCeilingNoteUnknownTotal: '仅显示前 {{shown}} 条记录。请缩小筛选范围。',
},
actions: {
decisionOutput: {
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
44 changes: 44 additions & 0 deletions .changeset/7210-non-grid-row-ceiling.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
---
'@object-ui/react': minor
'@object-ui/i18n': patch
'@object-ui/plugin-gantt': patch
'@object-ui/plugin-calendar': patch
'@object-ui/plugin-map': patch
'@object-ui/plugin-tree': patch
---

A non-grid view's fetch now carries a platform row ceiling, and crossing it is
never silent (objectui#7210, maintainer ruling a′, 2026-09-02).

Before this, `ObjectGantt`, `ObjectCalendar`, `ObjectMap` and `ObjectTree` each
issued a `find` with **no `$top` at all**, so the request returned the entire
filtered result set. At the 186 rows the card was filed from that is invisible;
on an object with 100k scheduled rows it is the whole table into the browser,
and nothing an author could write — `pagination.pageSize` included — could
bound a request that never carried a cap to begin with.

**What changed.** Those four fetches now ask for `NON_GRID_ROW_CEILING_TOP`
rows, draw at most `NON_GRID_ROW_CEILING` of them, and when the result set was
larger they render a footnote naming both numbers: *"Showing the first 2,000 of
41,234 records. Narrow the filter."* Below the ceiling nothing changes: the full
set draws and no footnote appears.

**The ceiling is a platform constant, not an authorable key** — `2000`, exported
from `@object-ui/react` as `NON_GRID_ROW_CEILING`. An authored `limit` or
`dataSource: { limit }` still does not reach these queries, by the same ruling;
three alternatives were rejected with it (a documentation note only — still the
whole table; truncating at `pageSize` — silent, and a complete schedule capped
at one page; an authorable `maxRows` — a new permanent key every author sets).

**Why 2,000.** One constant for all four, so the binding view sets it. Measured
in this repo's jsdom lane: gantt, calendar and map hold their DOM flat as rows
grow (virtualised task list; four events per day cell; auto-clustering above
100 markers), while `ObjectTree` flattens every expanded node into the document
at a linear **5.2 DOM elements per record** with no virtualisation. 2,000 rows
is where the worst of the four lands at ~10,400 elements — an order of
magnitude above Lighthouse's "excessive DOM size" warning, and still ~10x the
real application result set this card came from.

New exports on `@object-ui/react`: `NON_GRID_ROW_CEILING`,
`NON_GRID_ROW_CEILING_TOP`, `applyNonGridRowCeiling`, `NonGridRowCeilingNote`.
Two new `common.*` i18n keys carry the footnote copy in all ten packs.
51 changes: 51 additions & 0 deletions .changeset/7225-settled-schema-convergence.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
---
'@object-ui/plugin-kanban': patch
'@object-ui/plugin-view': patch
'@object-ui/plugin-calendar': patch
'@object-ui/plugin-gantt': patch
---

The settled-schema convergence, and the gantt's duplicate query gated
(objectui#7225, maintainer ruling B, 2026-09-02).

`useSettledSchema` was extracted and published in PR #6690 with exactly **one**
non-test adopter (`ObjectTree`, the component that had an actual defect —
objectui#6481's unkeyed latch). `ObjectKanban`, `plugin-view/ObjectView` and
`ObjectCalendar` kept their own hand copies of the same shape, so a published
export was owed compatibility forever **and** the duplication it was named for
stayed. All three now call the hook.

The migration is a pure deduplication with no behaviour delta — the hook was
extracted *from* these three shapes, so each becomes a one-line call.
`ObjectCalendar`, which objectui#6482 named as the obstacle, fits via the
recipe the hook's own doc comment prescribes for it by name: pass the data
source as `undefined` for a render that must not read metadata
(`hasInlineData ? undefined : dataSource`), so "inline value data set" is
expressed as "there is no source to read from" rather than as a second enable
flag. GATE PLACEMENT stays local in all three, which is what #6482 ruled and
what made the calendar's obstacle a non-obstacle: it was about the gate half.

**One observable change:** `ObjectKanban`'s rejected definition read now logs
on `console.error` with a `[useSettledSchema]` prefix instead of
`console.warn`. Its test spy moves with it, and now asserts on the channel
rather than merely silencing it.

**The gantt's duplicate query is gated** (ask 2 of the card; #6482's
undischarged half). `ObjectGantt` listed `objectSchema` in `reload`'s
dependency list, so every load issued two unbounded queries — the first with no
`$expand` at all. Measured on this component across three latency profiles, the
cost is not the mild "round trip bought and thrown away": when the metadata
read is the slower of the two, which is the common case on a cold
`MetadataCache`, the user sees the full three-step paint — raw foreign-key ids,
back to the loading placeholder, then the expanded rows. It now issues one
query, already expanded.

Gating the gantt required its schema resolution to settle on EVERY exit
(objectui#7232): the hand-rolled effect returned without settling on
`!effectiveDataSource`, on `!resource` and in its `catch` — harmless while
nothing waited on it, and a chart that never loads once something does.
`useSettledSchema` settles on all three by construction, which is what makes
the gate safe; both exits are pinned.

⛔ Gating is not capping. The row ceiling on these fetches is objectui#7210's
separate ruling, in its own commit on the same branch.
24 changes: 21 additions & 3 deletions content/docs/guide/data-source.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -252,12 +252,12 @@ ignores would be accepted and dropped, which is the defect this binding removes.
| `object-grid` | ✅ | ✅ | ✅ | ✅ | ✅ |
| `element:record_picker` | ✅ | ✅ | ✅ | ✅ | ✅ |
| `record:related_list` | ✅ | columns / filter / sort / limit | ✅ | ✅ | ✅ |
| `object-calendar` | ✅ | filter / sort | ✅ | ✅ | — no row cap |
| `object-calendar` | ✅ | filter / sort | ✅ | ✅ | — platform ceiling |
| `object-kanban` | ✅ | filter / limit | ✅ | — no ordering | ✅ (`limit`) |
| `object-chart` | ✅ | filter | ✅ | — engine orders | — no page |
| `object-metric` | ✅ | filter | ✅ | — single value | — single value |
| `object-gantt` | ✅ | filter / sort | ✅ | ✅ | — no row cap |
| `object-map` | ✅ | filter / sort | ✅ | ✅ | — no row cap |
| `object-gantt` | ✅ | filter / sort | ✅ | ✅ | — platform ceiling |
| `object-map` | ✅ | filter / sort | ✅ | ✅ | — platform ceiling |
| `object-pivot` | ✅ | filter | ✅ | — grouping orders | — totals need all rows |
| `object-timeline` | ✅ | filter / sort / limit | ✅ | ✅ | ✅ (`limit`) |
| `object-form` | ✅ | error-checked only | — no collection query | — | — |
Expand DownExpand Up@@ -322,6 +322,24 @@ cap above is `limit` on the **board**, while `limit` on a **column** is that
lane's WIP limit (how many cards it may hold before it warns), which is display
behaviour and never touches the query.

Reading `— platform ceiling` on `object-calendar` / `object-gantt` / `object-map`
(and on `object-tree`, which predates this table): those four are the **non-grid**
visualisations, and objectui#7210's maintainer ruling settled what their row
behaviour is. They fetch the whole **filtered** result set — a gantt cannot
compute a truthful `min(start) → max(end)` from one page, a map fits its camera to
every marker, and a tree assembled from a page loses every node whose parent fell
outside it — but the fetch is **bounded** by `NON_GRID_ROW_CEILING`
(`@object-ui/react`, currently 2,000). Past it the view draws the first N rows and
shows a footnote naming both N and the total. Silent truncation is the failure
that ruling exists to prevent: a cut-off schedule still looks like a schedule.

The `limit` cell stays "not ✅" for all four because the ceiling is **not
authorable and must not become one** — a named constant in the renderer, by the
same ruling. An authored `limit`, a binding's `limit` and a named view's
`pagination.pageSize` all still fail to reach these queries, which is what the
cell has always meant. What changed is only that "no cap at all" is no longer
true.

Remaining gap, recorded rather than papered over:

- `object-form` / `embeddable-form` / `object-master-detail-form` resolve `view`
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/ar.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -129,6 +129,13 @@ const ar = {
record: "سجل",
retry: "إعادة المحاولة",
printDialogHint: "يفتح مربع حوار الطباعة في المتصفح (ليس تصديرًا إلى PDF)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "يتم عرض أول {{shown}} من أصل {{total}} سجل. ضيّق عامل التصفية.",
rowCeilingNoteUnknownTotal: "يتم عرض أول {{shown}} سجل. ضيّق عامل التصفية.",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/de.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,6 +125,13 @@ const de = {
record: "Datensatz",
retry: "Erneut versuchen",
printDialogHint: "Öffnet den Druckdialog Ihres Browsers (kein PDF-Export)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "Erste {{shown}} von {{total}} Datensätzen. Filter eingrenzen.",
rowCeilingNoteUnknownTotal: "Erste {{shown}} Datensätze. Filter eingrenzen.",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/en.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -149,6 +149,13 @@ const en = {
record: 'Record',
retry: 'Retry',
printDialogHint: 'Opens your browser’s print dialog (not a PDF export)',
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: 'Showing the first {{shown}} of {{total}} records. Narrow the filter.',
rowCeilingNoteUnknownTotal: 'Showing the first {{shown}} records. Narrow the filter.',
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/es.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -124,6 +124,13 @@ const es = {
record: "Registro",
retry: "Reintentar",
printDialogHint: "Abre el cuadro de diálogo de impresión de tu navegador (no es una exportación a PDF)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "Mostrando los primeros {{shown}} de {{total}} registros. Acota el filtro.",
rowCeilingNoteUnknownTotal: "Mostrando los primeros {{shown}} registros. Acota el filtro.",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/fr.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,6 +125,13 @@ const fr = {
record: "Enregistrement",
retry: "Réessayer",
printDialogHint: "Ouvre la boîte de dialogue d'impression de votre navigateur (ce n'est pas un export PDF)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "Affichage des {{shown}} premiers enregistrements sur {{total}}. Affinez le filtre.",
rowCeilingNoteUnknownTotal: "Affichage des {{shown}} premiers enregistrements. Affinez le filtre.",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/ja.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,6 +125,13 @@ const ja = {
record: "レコード",
retry: "再試行",
printDialogHint: "ブラウザーの印刷ダイアログを開きます(PDF エクスポートではありません)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "{{total}} 件中、最初の {{shown}} 件を表示しています。フィルターを絞り込んでください。",
rowCeilingNoteUnknownTotal: "最初の {{shown}} 件を表示しています。フィルターを絞り込んでください。",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/ko.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,6 +125,13 @@ const ko = {
record: "레코드",
retry: "다시 시도",
printDialogHint: "브라우저의 인쇄 대화 상자를 엽니다(PDF 내보내기가 아닙니다)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "전체 {{total}}개 중 처음 {{shown}}개를 표시합니다. 필터를 좁히세요.",
rowCeilingNoteUnknownTotal: "처음 {{shown}}개를 표시합니다. 필터를 좁히세요.",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/pt.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -124,6 +124,13 @@ const pt = {
record: "Registro",
retry: "Tentar novamente",
printDialogHint: "Abre a caixa de diálogo de impressão do navegador (não é uma exportação para PDF)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "Mostrando os primeiros {{shown}} de {{total}} registros. Restrinja o filtro.",
rowCeilingNoteUnknownTotal: "Mostrando os primeiros {{shown}} registros. Restrinja o filtro.",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/ru.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -131,6 +131,13 @@ const ru = {
record: "Запись",
retry: "Повторить",
printDialogHint: "Открывает диалог печати браузера (это не экспорт в PDF)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "Показаны первые {{shown}} из {{total}} записей. Сузьте фильтр.",
rowCeilingNoteUnknownTotal: "Показаны первые {{shown}} записей. Сузьте фильтр.",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/zh.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -132,6 +132,13 @@ const zh = {
record: '记录',
retry: '重试',
printDialogHint: '打开浏览器打印对话框(不是导出 PDF)',
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: '仅显示 {{total}} 条记录中的前 {{shown}} 条。请缩小筛选范围。',
rowCeilingNoteUnknownTotal: '仅显示前 {{shown}} 条记录。请缩小筛选范围。',
},
actions: {
decisionOutput: {
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
44 changes: 44 additions & 0 deletions .changeset/7210-non-grid-row-ceiling.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
---
'@object-ui/react': minor
'@object-ui/i18n': patch
'@object-ui/plugin-gantt': patch
'@object-ui/plugin-calendar': patch
'@object-ui/plugin-map': patch
'@object-ui/plugin-tree': patch
---

A non-grid view's fetch now carries a platform row ceiling, and crossing it is
never silent (objectui#7210, maintainer ruling a′, 2026-09-02).

Before this, `ObjectGantt`, `ObjectCalendar`, `ObjectMap` and `ObjectTree` each
issued a `find` with **no `$top` at all**, so the request returned the entire
filtered result set. At the 186 rows the card was filed from that is invisible;
on an object with 100k scheduled rows it is the whole table into the browser,
and nothing an author could write — `pagination.pageSize` included — could
bound a request that never carried a cap to begin with.

**What changed.** Those four fetches now ask for `NON_GRID_ROW_CEILING_TOP`
rows, draw at most `NON_GRID_ROW_CEILING` of them, and when the result set was
larger they render a footnote naming both numbers: *"Showing the first 2,000 of
41,234 records. Narrow the filter."* Below the ceiling nothing changes: the full
set draws and no footnote appears.

**The ceiling is a platform constant, not an authorable key** — `2000`, exported
from `@object-ui/react` as `NON_GRID_ROW_CEILING`. An authored `limit` or
`dataSource: { limit }` still does not reach these queries, by the same ruling;
three alternatives were rejected with it (a documentation note only — still the
whole table; truncating at `pageSize` — silent, and a complete schedule capped
at one page; an authorable `maxRows` — a new permanent key every author sets).

**Why 2,000.** One constant for all four, so the binding view sets it. Measured
in this repo's jsdom lane: gantt, calendar and map hold their DOM flat as rows
grow (virtualised task list; four events per day cell; auto-clustering above
100 markers), while `ObjectTree` flattens every expanded node into the document
at a linear **5.2 DOM elements per record** with no virtualisation. 2,000 rows
is where the worst of the four lands at ~10,400 elements — an order of
magnitude above Lighthouse's "excessive DOM size" warning, and still ~10x the
real application result set this card came from.

New exports on `@object-ui/react`: `NON_GRID_ROW_CEILING`,
`NON_GRID_ROW_CEILING_TOP`, `applyNonGridRowCeiling`, `NonGridRowCeilingNote`.
Two new `common.*` i18n keys carry the footnote copy in all ten packs.
51 changes: 51 additions & 0 deletions .changeset/7225-settled-schema-convergence.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
---
'@object-ui/plugin-kanban': patch
'@object-ui/plugin-view': patch
'@object-ui/plugin-calendar': patch
'@object-ui/plugin-gantt': patch
---

The settled-schema convergence, and the gantt's duplicate query gated
(objectui#7225, maintainer ruling B, 2026-09-02).

`useSettledSchema` was extracted and published in PR #6690 with exactly **one**
non-test adopter (`ObjectTree`, the component that had an actual defect —
objectui#6481's unkeyed latch). `ObjectKanban`, `plugin-view/ObjectView` and
`ObjectCalendar` kept their own hand copies of the same shape, so a published
export was owed compatibility forever **and** the duplication it was named for
stayed. All three now call the hook.

The migration is a pure deduplication with no behaviour delta — the hook was
extracted *from* these three shapes, so each becomes a one-line call.
`ObjectCalendar`, which objectui#6482 named as the obstacle, fits via the
recipe the hook's own doc comment prescribes for it by name: pass the data
source as `undefined` for a render that must not read metadata
(`hasInlineData ? undefined : dataSource`), so "inline value data set" is
expressed as "there is no source to read from" rather than as a second enable
flag. GATE PLACEMENT stays local in all three, which is what #6482 ruled and
what made the calendar's obstacle a non-obstacle: it was about the gate half.

**One observable change:** `ObjectKanban`'s rejected definition read now logs
on `console.error` with a `[useSettledSchema]` prefix instead of
`console.warn`. Its test spy moves with it, and now asserts on the channel
rather than merely silencing it.

**The gantt's duplicate query is gated** (ask 2 of the card; #6482's
undischarged half). `ObjectGantt` listed `objectSchema` in `reload`'s
dependency list, so every load issued two unbounded queries — the first with no
`$expand` at all. Measured on this component across three latency profiles, the
cost is not the mild "round trip bought and thrown away": when the metadata
read is the slower of the two, which is the common case on a cold
`MetadataCache`, the user sees the full three-step paint — raw foreign-key ids,
back to the loading placeholder, then the expanded rows. It now issues one
query, already expanded.

Gating the gantt required its schema resolution to settle on EVERY exit
(objectui#7232): the hand-rolled effect returned without settling on
`!effectiveDataSource`, on `!resource` and in its `catch` — harmless while
nothing waited on it, and a chart that never loads once something does.
`useSettledSchema` settles on all three by construction, which is what makes
the gate safe; both exits are pinned.

⛔ Gating is not capping. The row ceiling on these fetches is objectui#7210's
separate ruling, in its own commit on the same branch.
24 changes: 21 additions & 3 deletions content/docs/guide/data-source.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -252,12 +252,12 @@ ignores would be accepted and dropped, which is the defect this binding removes.
| `object-grid` | ✅ | ✅ | ✅ | ✅ | ✅ |
| `element:record_picker` | ✅ | ✅ | ✅ | ✅ | ✅ |
| `record:related_list` | ✅ | columns / filter / sort / limit | ✅ | ✅ | ✅ |
| `object-calendar` | ✅ | filter / sort | ✅ | ✅ | — no row cap |
| `object-calendar` | ✅ | filter / sort | ✅ | ✅ | — platform ceiling |
| `object-kanban` | ✅ | filter / limit | ✅ | — no ordering | ✅ (`limit`) |
| `object-chart` | ✅ | filter | ✅ | — engine orders | — no page |
| `object-metric` | ✅ | filter | ✅ | — single value | — single value |
| `object-gantt` | ✅ | filter / sort | ✅ | ✅ | — no row cap |
| `object-map` | ✅ | filter / sort | ✅ | ✅ | — no row cap |
| `object-gantt` | ✅ | filter / sort | ✅ | ✅ | — platform ceiling |
| `object-map` | ✅ | filter / sort | ✅ | ✅ | — platform ceiling |
| `object-pivot` | ✅ | filter | ✅ | — grouping orders | — totals need all rows |
| `object-timeline` | ✅ | filter / sort / limit | ✅ | ✅ | ✅ (`limit`) |
| `object-form` | ✅ | error-checked only | — no collection query | — | — |
Expand DownExpand Up@@ -322,6 +322,24 @@ cap above is `limit` on the **board**, while `limit` on a **column** is that
lane's WIP limit (how many cards it may hold before it warns), which is display
behaviour and never touches the query.

Reading `— platform ceiling` on `object-calendar` / `object-gantt` / `object-map`
(and on `object-tree`, which predates this table): those four are the **non-grid**
visualisations, and objectui#7210's maintainer ruling settled what their row
behaviour is. They fetch the whole **filtered** result set — a gantt cannot
compute a truthful `min(start) → max(end)` from one page, a map fits its camera to
every marker, and a tree assembled from a page loses every node whose parent fell
outside it — but the fetch is **bounded** by `NON_GRID_ROW_CEILING`
(`@object-ui/react`, currently 2,000). Past it the view draws the first N rows and
shows a footnote naming both N and the total. Silent truncation is the failure
that ruling exists to prevent: a cut-off schedule still looks like a schedule.

The `limit` cell stays "not ✅" for all four because the ceiling is **not
authorable and must not become one** — a named constant in the renderer, by the
same ruling. An authored `limit`, a binding's `limit` and a named view's
`pagination.pageSize` all still fail to reach these queries, which is what the
cell has always meant. What changed is only that "no cap at all" is no longer
true.

Remaining gap, recorded rather than papered over:

- `object-form` / `embeddable-form` / `object-master-detail-form` resolve `view`
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/ar.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -129,6 +129,13 @@ const ar = {
record: "سجل",
retry: "إعادة المحاولة",
printDialogHint: "يفتح مربع حوار الطباعة في المتصفح (ليس تصديرًا إلى PDF)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "يتم عرض أول {{shown}} من أصل {{total}} سجل. ضيّق عامل التصفية.",
rowCeilingNoteUnknownTotal: "يتم عرض أول {{shown}} سجل. ضيّق عامل التصفية.",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/de.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,6 +125,13 @@ const de = {
record: "Datensatz",
retry: "Erneut versuchen",
printDialogHint: "Öffnet den Druckdialog Ihres Browsers (kein PDF-Export)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "Erste {{shown}} von {{total}} Datensätzen. Filter eingrenzen.",
rowCeilingNoteUnknownTotal: "Erste {{shown}} Datensätze. Filter eingrenzen.",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/en.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -149,6 +149,13 @@ const en = {
record: 'Record',
retry: 'Retry',
printDialogHint: 'Opens your browser’s print dialog (not a PDF export)',
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: 'Showing the first {{shown}} of {{total}} records. Narrow the filter.',
rowCeilingNoteUnknownTotal: 'Showing the first {{shown}} records. Narrow the filter.',
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/es.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -124,6 +124,13 @@ const es = {
record: "Registro",
retry: "Reintentar",
printDialogHint: "Abre el cuadro de diálogo de impresión de tu navegador (no es una exportación a PDF)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "Mostrando los primeros {{shown}} de {{total}} registros. Acota el filtro.",
rowCeilingNoteUnknownTotal: "Mostrando los primeros {{shown}} registros. Acota el filtro.",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/fr.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,6 +125,13 @@ const fr = {
record: "Enregistrement",
retry: "Réessayer",
printDialogHint: "Ouvre la boîte de dialogue d'impression de votre navigateur (ce n'est pas un export PDF)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "Affichage des {{shown}} premiers enregistrements sur {{total}}. Affinez le filtre.",
rowCeilingNoteUnknownTotal: "Affichage des {{shown}} premiers enregistrements. Affinez le filtre.",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/ja.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,6 +125,13 @@ const ja = {
record: "レコード",
retry: "再試行",
printDialogHint: "ブラウザーの印刷ダイアログを開きます(PDF エクスポートではありません)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "{{total}} 件中、最初の {{shown}} 件を表示しています。フィルターを絞り込んでください。",
rowCeilingNoteUnknownTotal: "最初の {{shown}} 件を表示しています。フィルターを絞り込んでください。",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/ko.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,6 +125,13 @@ const ko = {
record: "레코드",
retry: "다시 시도",
printDialogHint: "브라우저의 인쇄 대화 상자를 엽니다(PDF 내보내기가 아닙니다)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "전체 {{total}}개 중 처음 {{shown}}개를 표시합니다. 필터를 좁히세요.",
rowCeilingNoteUnknownTotal: "처음 {{shown}}개를 표시합니다. 필터를 좁히세요.",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/pt.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -124,6 +124,13 @@ const pt = {
record: "Registro",
retry: "Tentar novamente",
printDialogHint: "Abre a caixa de diálogo de impressão do navegador (não é uma exportação para PDF)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "Mostrando os primeiros {{shown}} de {{total}} registros. Restrinja o filtro.",
rowCeilingNoteUnknownTotal: "Mostrando os primeiros {{shown}} registros. Restrinja o filtro.",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/ru.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -131,6 +131,13 @@ const ru = {
record: "Запись",
retry: "Повторить",
printDialogHint: "Открывает диалог печати браузера (это не экспорт в PDF)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "Показаны первые {{shown}} из {{total}} записей. Сузьте фильтр.",
rowCeilingNoteUnknownTotal: "Показаны первые {{shown}} записей. Сузьте фильтр.",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/zh.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -132,6 +132,13 @@ const zh = {
record: '记录',
retry: '重试',
printDialogHint: '打开浏览器打印对话框(不是导出 PDF)',
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: '仅显示 {{total}} 条记录中的前 {{shown}} 条。请缩小筛选范围。',
rowCeilingNoteUnknownTotal: '仅显示前 {{shown}} 条记录。请缩小筛选范围。',
},
actions: {
decisionOutput: {
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
44 changes: 44 additions & 0 deletions .changeset/7210-non-grid-row-ceiling.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
---
'@object-ui/react': minor
'@object-ui/i18n': patch
'@object-ui/plugin-gantt': patch
'@object-ui/plugin-calendar': patch
'@object-ui/plugin-map': patch
'@object-ui/plugin-tree': patch
---

A non-grid view's fetch now carries a platform row ceiling, and crossing it is
never silent (objectui#7210, maintainer ruling a′, 2026-09-02).

Before this, `ObjectGantt`, `ObjectCalendar`, `ObjectMap` and `ObjectTree` each
issued a `find` with **no `$top` at all**, so the request returned the entire
filtered result set. At the 186 rows the card was filed from that is invisible;
on an object with 100k scheduled rows it is the whole table into the browser,
and nothing an author could write — `pagination.pageSize` included — could
bound a request that never carried a cap to begin with.

**What changed.** Those four fetches now ask for `NON_GRID_ROW_CEILING_TOP`
rows, draw at most `NON_GRID_ROW_CEILING` of them, and when the result set was
larger they render a footnote naming both numbers: *"Showing the first 2,000 of
41,234 records. Narrow the filter."* Below the ceiling nothing changes: the full
set draws and no footnote appears.

**The ceiling is a platform constant, not an authorable key** — `2000`, exported
from `@object-ui/react` as `NON_GRID_ROW_CEILING`. An authored `limit` or
`dataSource: { limit }` still does not reach these queries, by the same ruling;
three alternatives were rejected with it (a documentation note only — still the
whole table; truncating at `pageSize` — silent, and a complete schedule capped
at one page; an authorable `maxRows` — a new permanent key every author sets).

**Why 2,000.** One constant for all four, so the binding view sets it. Measured
in this repo's jsdom lane: gantt, calendar and map hold their DOM flat as rows
grow (virtualised task list; four events per day cell; auto-clustering above
100 markers), while `ObjectTree` flattens every expanded node into the document
at a linear **5.2 DOM elements per record** with no virtualisation. 2,000 rows
is where the worst of the four lands at ~10,400 elements — an order of
magnitude above Lighthouse's "excessive DOM size" warning, and still ~10x the
real application result set this card came from.

New exports on `@object-ui/react`: `NON_GRID_ROW_CEILING`,
`NON_GRID_ROW_CEILING_TOP`, `applyNonGridRowCeiling`, `NonGridRowCeilingNote`.
Two new `common.*` i18n keys carry the footnote copy in all ten packs.
51 changes: 51 additions & 0 deletions .changeset/7225-settled-schema-convergence.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
---
'@object-ui/plugin-kanban': patch
'@object-ui/plugin-view': patch
'@object-ui/plugin-calendar': patch
'@object-ui/plugin-gantt': patch
---

The settled-schema convergence, and the gantt's duplicate query gated
(objectui#7225, maintainer ruling B, 2026-09-02).

`useSettledSchema` was extracted and published in PR #6690 with exactly **one**
non-test adopter (`ObjectTree`, the component that had an actual defect —
objectui#6481's unkeyed latch). `ObjectKanban`, `plugin-view/ObjectView` and
`ObjectCalendar` kept their own hand copies of the same shape, so a published
export was owed compatibility forever **and** the duplication it was named for
stayed. All three now call the hook.

The migration is a pure deduplication with no behaviour delta — the hook was
extracted *from* these three shapes, so each becomes a one-line call.
`ObjectCalendar`, which objectui#6482 named as the obstacle, fits via the
recipe the hook's own doc comment prescribes for it by name: pass the data
source as `undefined` for a render that must not read metadata
(`hasInlineData ? undefined : dataSource`), so "inline value data set" is
expressed as "there is no source to read from" rather than as a second enable
flag. GATE PLACEMENT stays local in all three, which is what #6482 ruled and
what made the calendar's obstacle a non-obstacle: it was about the gate half.

**One observable change:** `ObjectKanban`'s rejected definition read now logs
on `console.error` with a `[useSettledSchema]` prefix instead of
`console.warn`. Its test spy moves with it, and now asserts on the channel
rather than merely silencing it.

**The gantt's duplicate query is gated** (ask 2 of the card; #6482's
undischarged half). `ObjectGantt` listed `objectSchema` in `reload`'s
dependency list, so every load issued two unbounded queries — the first with no
`$expand` at all. Measured on this component across three latency profiles, the
cost is not the mild "round trip bought and thrown away": when the metadata
read is the slower of the two, which is the common case on a cold
`MetadataCache`, the user sees the full three-step paint — raw foreign-key ids,
back to the loading placeholder, then the expanded rows. It now issues one
query, already expanded.

Gating the gantt required its schema resolution to settle on EVERY exit
(objectui#7232): the hand-rolled effect returned without settling on
`!effectiveDataSource`, on `!resource` and in its `catch` — harmless while
nothing waited on it, and a chart that never loads once something does.
`useSettledSchema` settles on all three by construction, which is what makes
the gate safe; both exits are pinned.

⛔ Gating is not capping. The row ceiling on these fetches is objectui#7210's
separate ruling, in its own commit on the same branch.
24 changes: 21 additions & 3 deletions content/docs/guide/data-source.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -252,12 +252,12 @@ ignores would be accepted and dropped, which is the defect this binding removes.
| `object-grid` | ✅ | ✅ | ✅ | ✅ | ✅ |
| `element:record_picker` | ✅ | ✅ | ✅ | ✅ | ✅ |
| `record:related_list` | ✅ | columns / filter / sort / limit | ✅ | ✅ | ✅ |
| `object-calendar` | ✅ | filter / sort | ✅ | ✅ | — no row cap |
| `object-calendar` | ✅ | filter / sort | ✅ | ✅ | — platform ceiling |
| `object-kanban` | ✅ | filter / limit | ✅ | — no ordering | ✅ (`limit`) |
| `object-chart` | ✅ | filter | ✅ | — engine orders | — no page |
| `object-metric` | ✅ | filter | ✅ | — single value | — single value |
| `object-gantt` | ✅ | filter / sort | ✅ | ✅ | — no row cap |
| `object-map` | ✅ | filter / sort | ✅ | ✅ | — no row cap |
| `object-gantt` | ✅ | filter / sort | ✅ | ✅ | — platform ceiling |
| `object-map` | ✅ | filter / sort | ✅ | ✅ | — platform ceiling |
| `object-pivot` | ✅ | filter | ✅ | — grouping orders | — totals need all rows |
| `object-timeline` | ✅ | filter / sort / limit | ✅ | ✅ | ✅ (`limit`) |
| `object-form` | ✅ | error-checked only | — no collection query | — | — |
Expand DownExpand Up@@ -322,6 +322,24 @@ cap above is `limit` on the **board**, while `limit` on a **column** is that
lane's WIP limit (how many cards it may hold before it warns), which is display
behaviour and never touches the query.

Reading `— platform ceiling` on `object-calendar` / `object-gantt` / `object-map`
(and on `object-tree`, which predates this table): those four are the **non-grid**
visualisations, and objectui#7210's maintainer ruling settled what their row
behaviour is. They fetch the whole **filtered** result set — a gantt cannot
compute a truthful `min(start) → max(end)` from one page, a map fits its camera to
every marker, and a tree assembled from a page loses every node whose parent fell
outside it — but the fetch is **bounded** by `NON_GRID_ROW_CEILING`
(`@object-ui/react`, currently 2,000). Past it the view draws the first N rows and
shows a footnote naming both N and the total. Silent truncation is the failure
that ruling exists to prevent: a cut-off schedule still looks like a schedule.

The `limit` cell stays "not ✅" for all four because the ceiling is **not
authorable and must not become one** — a named constant in the renderer, by the
same ruling. An authored `limit`, a binding's `limit` and a named view's
`pagination.pageSize` all still fail to reach these queries, which is what the
cell has always meant. What changed is only that "no cap at all" is no longer
true.

Remaining gap, recorded rather than papered over:

- `object-form` / `embeddable-form` / `object-master-detail-form` resolve `view`
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/ar.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -129,6 +129,13 @@ const ar = {
record: "سجل",
retry: "إعادة المحاولة",
printDialogHint: "يفتح مربع حوار الطباعة في المتصفح (ليس تصديرًا إلى PDF)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "يتم عرض أول {{shown}} من أصل {{total}} سجل. ضيّق عامل التصفية.",
rowCeilingNoteUnknownTotal: "يتم عرض أول {{shown}} سجل. ضيّق عامل التصفية.",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/de.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,6 +125,13 @@ const de = {
record: "Datensatz",
retry: "Erneut versuchen",
printDialogHint: "Öffnet den Druckdialog Ihres Browsers (kein PDF-Export)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "Erste {{shown}} von {{total}} Datensätzen. Filter eingrenzen.",
rowCeilingNoteUnknownTotal: "Erste {{shown}} Datensätze. Filter eingrenzen.",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/en.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -149,6 +149,13 @@ const en = {
record: 'Record',
retry: 'Retry',
printDialogHint: 'Opens your browser’s print dialog (not a PDF export)',
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: 'Showing the first {{shown}} of {{total}} records. Narrow the filter.',
rowCeilingNoteUnknownTotal: 'Showing the first {{shown}} records. Narrow the filter.',
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/es.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -124,6 +124,13 @@ const es = {
record: "Registro",
retry: "Reintentar",
printDialogHint: "Abre el cuadro de diálogo de impresión de tu navegador (no es una exportación a PDF)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "Mostrando los primeros {{shown}} de {{total}} registros. Acota el filtro.",
rowCeilingNoteUnknownTotal: "Mostrando los primeros {{shown}} registros. Acota el filtro.",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/fr.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,6 +125,13 @@ const fr = {
record: "Enregistrement",
retry: "Réessayer",
printDialogHint: "Ouvre la boîte de dialogue d'impression de votre navigateur (ce n'est pas un export PDF)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "Affichage des {{shown}} premiers enregistrements sur {{total}}. Affinez le filtre.",
rowCeilingNoteUnknownTotal: "Affichage des {{shown}} premiers enregistrements. Affinez le filtre.",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/ja.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,6 +125,13 @@ const ja = {
record: "レコード",
retry: "再試行",
printDialogHint: "ブラウザーの印刷ダイアログを開きます(PDF エクスポートではありません)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "{{total}} 件中、最初の {{shown}} 件を表示しています。フィルターを絞り込んでください。",
rowCeilingNoteUnknownTotal: "最初の {{shown}} 件を表示しています。フィルターを絞り込んでください。",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/ko.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,6 +125,13 @@ const ko = {
record: "레코드",
retry: "다시 시도",
printDialogHint: "브라우저의 인쇄 대화 상자를 엽니다(PDF 내보내기가 아닙니다)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "전체 {{total}}개 중 처음 {{shown}}개를 표시합니다. 필터를 좁히세요.",
rowCeilingNoteUnknownTotal: "처음 {{shown}}개를 표시합니다. 필터를 좁히세요.",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/pt.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -124,6 +124,13 @@ const pt = {
record: "Registro",
retry: "Tentar novamente",
printDialogHint: "Abre a caixa de diálogo de impressão do navegador (não é uma exportação para PDF)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "Mostrando os primeiros {{shown}} de {{total}} registros. Restrinja o filtro.",
rowCeilingNoteUnknownTotal: "Mostrando os primeiros {{shown}} registros. Restrinja o filtro.",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/ru.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -131,6 +131,13 @@ const ru = {
record: "Запись",
retry: "Повторить",
printDialogHint: "Открывает диалог печати браузера (это не экспорт в PDF)",
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: "Показаны первые {{shown}} из {{total}} записей. Сузьте фильтр.",
rowCeilingNoteUnknownTotal: "Показаны первые {{shown}} записей. Сузьте фильтр.",
},
actions: {
decisionOutput: {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/zh.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -132,6 +132,13 @@ const zh = {
record: '记录',
retry: '重试',
printDialogHint: '打开浏览器打印对话框(不是导出 PDF)',
// The non-grid row ceiling's footnote (objectui#7210). Two keys because
// there are two conditions: a reported `total` states the fact with BOTH
// numbers, a missing one cannot name how many. Same split as
// `grid.grouping.partialNotice`. Kept terse deliberately — this copy is
// eagerly loaded, and the per-chunk gzip budget has ~1 KB of headroom.
rowCeilingNote: '仅显示 {{total}} 条记录中的前 {{shown}} 条。请缩小筛选范围。',
rowCeilingNoteUnknownTotal: '仅显示前 {{shown}} 条记录。请缩小筛选范围。',
},
actions: {
decisionOutput: {
Expand Down
Loading
Loading