Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 14 additions & 9 deletions .changeset/7210-non-grid-row-ceiling.md
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
---
'@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
'@object-ui/plugin-gantt': minor
'@object-ui/plugin-calendar': minor
'@object-ui/plugin-map': minor
'@object-ui/plugin-tree': minor
---

A non-grid view's fetch now carries a platform row ceiling, and crossing it is
Expand All@@ -19,9 +19,13 @@ 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.
larger they render a footnote naming both numbers, verbatim as it renders:
*"Showing the first 2000 of 41234 records. Narrow the filter."* Below the
ceiling nothing changes: the full set draws and no footnote appears.

The four view packages take a **minor**, not a patch: a result set above the
ceiling is no longer drawn in full, which is a behaviour break whatever the
fixed group does to the released version number.

**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
Expand All@@ -40,5 +44,6 @@ 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.
`NON_GRID_ROW_CEILING_TOP`, `applyNonGridRowCeiling`, `NonGridRowCeilingNote`
and the type `NonGridCeilingResult`. Two new `common.*` i18n keys carry the
footnote copy in all ten packs.
24 changes: 24 additions & 0 deletions .changeset/7507-non-grid-ceiling-correctives.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
---
---

Post-merge correctives to objectui#7210's non-grid row ceiling (objectui#7507),
all of them inside a change that has not been released yet — so nothing here is
a user-visible fix, and the ceiling's own changeset carries the release note.

- `.changeset/7210-non-grid-row-ceiling.md`: the four view packages move from
`patch` to `minor` (they carry the behaviour break), the example footnote is
replaced with the copy that actually renders — no thousands separators, since
the i18next config declares no `format` and the provider-less path
interpolates through `String(v)` — and the export list names
`NonGridCeilingResult`.
- `ObjectCalendar`'s external-`data` sync now also clears `rowCeiling`, so a
footnote raised by this component's own truncated fetch cannot outlive the
rows it described. Latent today: the only host that passes `data` passes it
from mount. Pinned in `ObjectCalendar.externalDataCeiling-7507.test.tsx`.
- The map and calendar ceiling pins now assert the row count handed to the
view, not only `$top` and the footnote; the four pins' reverse-verification
docblocks are rewritten to the mechanism that was measured rather than the
one that was predicted.
- Comment-only: the false "the only package all four already depend on"
rationale on `@object-ui/react`'s entry, and the stale "~1 KB of headroom"
note in the ten locale packs.
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/ar.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -133,7 +133,10 @@ const ar = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "يتم عرض أول {{shown}} من أصل {{total}} سجل. ضيّق عامل التصفية.",
rowCeilingNoteUnknownTotal: "يتم عرض أول {{shown}} سجل. ضيّق عامل التصفية.",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/de.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -129,7 +129,10 @@ const de = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "Erste {{shown}} von {{total}} Datensätzen. Filter eingrenzen.",
rowCeilingNoteUnknownTotal: "Erste {{shown}} Datensätze. Filter eingrenzen.",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/en.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -153,7 +153,10 @@ const en = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: 'Showing the first {{shown}} of {{total}} records. Narrow the filter.',
rowCeilingNoteUnknownTotal: 'Showing the first {{shown}} records. Narrow the filter.',
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/es.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -128,7 +128,10 @@ const es = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "Mostrando los primeros {{shown}} de {{total}} registros. Acota el filtro.",
rowCeilingNoteUnknownTotal: "Mostrando los primeros {{shown}} registros. Acota el filtro.",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/fr.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -129,7 +129,10 @@ const fr = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "Affichage des {{shown}} premiers enregistrements sur {{total}}. Affinez le filtre.",
rowCeilingNoteUnknownTotal: "Affichage des {{shown}} premiers enregistrements. Affinez le filtre.",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/ja.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -129,7 +129,10 @@ const ja = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "{{total}} 件中、最初の {{shown}} 件を表示しています。フィルターを絞り込んでください。",
rowCeilingNoteUnknownTotal: "最初の {{shown}} 件を表示しています。フィルターを絞り込んでください。",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/ko.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -129,7 +129,10 @@ const ko = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "전체 {{total}}개 중 처음 {{shown}}개를 표시합니다. 필터를 좁히세요.",
rowCeilingNoteUnknownTotal: "처음 {{shown}}개를 표시합니다. 필터를 좁히세요.",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/pt.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -128,7 +128,10 @@ const pt = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "Mostrando os primeiros {{shown}} de {{total}} registros. Restrinja o filtro.",
rowCeilingNoteUnknownTotal: "Mostrando os primeiros {{shown}} registros. Restrinja o filtro.",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/ru.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -135,7 +135,10 @@ const ru = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "Показаны первые {{shown}} из {{total}} записей. Сузьте фильтр.",
rowCeilingNoteUnknownTotal: "Показаны первые {{shown}} записей. Сузьте фильтр.",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/zh.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -136,7 +136,10 @@ const zh = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: '仅显示 {{total}} 条记录中的前 {{shown}} 条。请缩小筛选范围。',
rowCeilingNoteUnknownTotal: '仅显示前 {{shown}} 条记录。请缩小筛选范围。',
},
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
/**
* ObjectUI
* Copyright (c) 2024-present ObjectStack Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

/**
* objectui#7507 — the row-ceiling footnote must not outlive the fetch that
* raised it (objectui#7210's ceiling, ruling a′).
*
* `ObjectCalendar` has five `setData` paths. Four of them also reset
* `rowCeiling`; the external-`data` sync was the one that did not, so a
* component that first drew its OWN truncated fetch and then had a short
* `data` array handed to it kept showing "Showing the first 2000 of 9876
* records" over twelve rows. The note would be describing a result set that is
* no longer on screen — which is the same defect the ceiling exists to
* prevent, pointing the other way: not a silent cut, but a loud claim of one
* that did not happen.
*
* ⚠️ Latent, not reported. Today's only host that passes `data` is
* `ObjectView`, and it passes it from mount, so `hasExternalData` is true
* before the internal fetch can ever run and the stale state is unreachable.
* That is exactly why it is pinned rather than only fixed: nothing about the
* prop's contract says a host may not start without `data` and supply it
* later, and a latent defect with no test is one refactor away from being a
* reported one.
*
* REVERSE VERIFICATION — direction predicted before running: remove the
* `setRowCeiling({ truncated: false })` from the external-`data` effect in
* `ObjectCalendar` and this file goes red at the "the note is gone" assertion
* (the note keeps rendering, still naming 2000 and 9876), while the control
* case — a component that never truncated — stays green.
*/

import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { NON_GRID_ROW_CEILING } from '@object-ui/react';
import { ObjectCalendar } from './ObjectCalendar';

vi.mock('@object-ui/plugin-detail', () => ({
RecordDetailDrawer: () => null,
deriveRecordPageHref: () => null,
}));

// Same reason as the sibling rowCeiling pin: the month grid draws at most four
// events per day cell, so the count has to come off an attribute.
vi.mock('./CalendarView', async (importOriginal) => {
const actual = await importOriginal<any>();
return {
...actual,
CalendarView: ({ events }: any) => (
<div data-testid="calendar-view" data-event-count={String(events.length)} />
),
};
});

const TOTAL_ROWS = 9876;
const NOW = new Date();

function makeRows(n: number, offset = 0) {
return Array.from({ length: n }, (_, i) => {
const d = new Date(NOW.getFullYear(), NOW.getMonth(), ((i + offset) % 28) + 1);
const iso = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(
d.getDate(),
).padStart(2, '0')}`;
return {
id: String(i + offset + 1),
subject: `Event ${i + offset + 1}`,
start_at: iso,
end_at: iso,
};
});
}

function makeDataSource(storeSize: number) {
const store = makeRows(storeSize);
return {
find: vi.fn(async (_resource: string, params: any) => {
const top = typeof params?.$top === 'number' ? params.$top : store.length;
return { data: store.slice(0, top), total: store.length };
}),
findOne: vi.fn(),
create: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
getObjectSchema: vi.fn(async () => ({
name: 'event',
fields: {
id: { name: 'id', type: 'text' },
subject: { name: 'subject', type: 'text' },
start_at: { name: 'start_at', type: 'date' },
end_at: { name: 'end_at', type: 'date' },
},
})),
} as any;
}

const schema: any = {
type: 'calendar',
objectName: 'event',
calendar: { titleField: 'subject', startDateField: 'start_at', endDateField: 'end_at' },
data: { provider: 'object', object: 'event' },
};

describe('objectui#7507 — an external `data` hand-off clears the row-ceiling note', () => {
it('a truncated own fetch, then a short external `data`: the note is GONE', async () => {
const dataSource = makeDataSource(TOTAL_ROWS);
const { rerender } = render(<ObjectCalendar schema={schema} dataSource={dataSource} />);

// Live control: the note really was raised by this component's own fetch,
// so its absence below is a change of state and not a query that never
// matched anything.
const note = await screen.findByRole('note');
expect(note.textContent).toContain(String(NON_GRID_ROW_CEILING));
expect(note.textContent).toContain(String(TOTAL_ROWS));
await waitFor(() =>
expect(screen.getByTestId('calendar-view').getAttribute('data-event-count')).toBe(
String(NON_GRID_ROW_CEILING),
),
);

const short = makeRows(12);
rerender(<ObjectCalendar schema={schema} dataSource={dataSource} data={short} />);

await waitFor(() =>
expect(screen.getByTestId('calendar-view').getAttribute('data-event-count')).toBe('12'),
);
expect(screen.queryByRole('note')).toBeNull();
});

it('external `data` from mount: still no note, and the rows are the parent’s', async () => {
const dataSource = makeDataSource(TOTAL_ROWS);
const short = makeRows(9);

render(<ObjectCalendar schema={schema} dataSource={dataSource} data={short} />);

await waitFor(() =>
expect(screen.getByTestId('calendar-view').getAttribute('data-event-count')).toBe('9'),
);
// The parent owns the query here, so this component never fetched and has
// nothing to report a ceiling about.
expect(dataSource.find).not.toHaveBeenCalled();
expect(screen.queryByRole('note')).toBeNull();
});
});
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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 14 additions & 9 deletions .changeset/7210-non-grid-row-ceiling.md
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
---
'@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
'@object-ui/plugin-gantt': minor
'@object-ui/plugin-calendar': minor
'@object-ui/plugin-map': minor
'@object-ui/plugin-tree': minor
---

A non-grid view's fetch now carries a platform row ceiling, and crossing it is
Expand All@@ -19,9 +19,13 @@ 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.
larger they render a footnote naming both numbers, verbatim as it renders:
*"Showing the first 2000 of 41234 records. Narrow the filter."* Below the
ceiling nothing changes: the full set draws and no footnote appears.

The four view packages take a **minor**, not a patch: a result set above the
ceiling is no longer drawn in full, which is a behaviour break whatever the
fixed group does to the released version number.

**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
Expand All@@ -40,5 +44,6 @@ 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.
`NON_GRID_ROW_CEILING_TOP`, `applyNonGridRowCeiling`, `NonGridRowCeilingNote`
and the type `NonGridCeilingResult`. Two new `common.*` i18n keys carry the
footnote copy in all ten packs.
24 changes: 24 additions & 0 deletions .changeset/7507-non-grid-ceiling-correctives.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
---
---

Post-merge correctives to objectui#7210's non-grid row ceiling (objectui#7507),
all of them inside a change that has not been released yet — so nothing here is
a user-visible fix, and the ceiling's own changeset carries the release note.

- `.changeset/7210-non-grid-row-ceiling.md`: the four view packages move from
`patch` to `minor` (they carry the behaviour break), the example footnote is
replaced with the copy that actually renders — no thousands separators, since
the i18next config declares no `format` and the provider-less path
interpolates through `String(v)` — and the export list names
`NonGridCeilingResult`.
- `ObjectCalendar`'s external-`data` sync now also clears `rowCeiling`, so a
footnote raised by this component's own truncated fetch cannot outlive the
rows it described. Latent today: the only host that passes `data` passes it
from mount. Pinned in `ObjectCalendar.externalDataCeiling-7507.test.tsx`.
- The map and calendar ceiling pins now assert the row count handed to the
view, not only `$top` and the footnote; the four pins' reverse-verification
docblocks are rewritten to the mechanism that was measured rather than the
one that was predicted.
- Comment-only: the false "the only package all four already depend on"
rationale on `@object-ui/react`'s entry, and the stale "~1 KB of headroom"
note in the ten locale packs.
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/ar.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -133,7 +133,10 @@ const ar = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "يتم عرض أول {{shown}} من أصل {{total}} سجل. ضيّق عامل التصفية.",
rowCeilingNoteUnknownTotal: "يتم عرض أول {{shown}} سجل. ضيّق عامل التصفية.",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/de.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -129,7 +129,10 @@ const de = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "Erste {{shown}} von {{total}} Datensätzen. Filter eingrenzen.",
rowCeilingNoteUnknownTotal: "Erste {{shown}} Datensätze. Filter eingrenzen.",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/en.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -153,7 +153,10 @@ const en = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: 'Showing the first {{shown}} of {{total}} records. Narrow the filter.',
rowCeilingNoteUnknownTotal: 'Showing the first {{shown}} records. Narrow the filter.',
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/es.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -128,7 +128,10 @@ const es = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "Mostrando los primeros {{shown}} de {{total}} registros. Acota el filtro.",
rowCeilingNoteUnknownTotal: "Mostrando los primeros {{shown}} registros. Acota el filtro.",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/fr.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -129,7 +129,10 @@ const fr = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "Affichage des {{shown}} premiers enregistrements sur {{total}}. Affinez le filtre.",
rowCeilingNoteUnknownTotal: "Affichage des {{shown}} premiers enregistrements. Affinez le filtre.",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/ja.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -129,7 +129,10 @@ const ja = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "{{total}} 件中、最初の {{shown}} 件を表示しています。フィルターを絞り込んでください。",
rowCeilingNoteUnknownTotal: "最初の {{shown}} 件を表示しています。フィルターを絞り込んでください。",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/ko.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -129,7 +129,10 @@ const ko = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "전체 {{total}}개 중 처음 {{shown}}개를 표시합니다. 필터를 좁히세요.",
rowCeilingNoteUnknownTotal: "처음 {{shown}}개를 표시합니다. 필터를 좁히세요.",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/pt.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -128,7 +128,10 @@ const pt = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "Mostrando os primeiros {{shown}} de {{total}} registros. Restrinja o filtro.",
rowCeilingNoteUnknownTotal: "Mostrando os primeiros {{shown}} registros. Restrinja o filtro.",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/ru.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -135,7 +135,10 @@ const ru = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "Показаны первые {{shown}} из {{total}} записей. Сузьте фильтр.",
rowCeilingNoteUnknownTotal: "Показаны первые {{shown}} записей. Сузьте фильтр.",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/zh.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -136,7 +136,10 @@ const zh = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: '仅显示 {{total}} 条记录中的前 {{shown}} 条。请缩小筛选范围。',
rowCeilingNoteUnknownTotal: '仅显示前 {{shown}} 条记录。请缩小筛选范围。',
},
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
/**
* ObjectUI
* Copyright (c) 2024-present ObjectStack Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

/**
* objectui#7507 — the row-ceiling footnote must not outlive the fetch that
* raised it (objectui#7210's ceiling, ruling a′).
*
* `ObjectCalendar` has five `setData` paths. Four of them also reset
* `rowCeiling`; the external-`data` sync was the one that did not, so a
* component that first drew its OWN truncated fetch and then had a short
* `data` array handed to it kept showing "Showing the first 2000 of 9876
* records" over twelve rows. The note would be describing a result set that is
* no longer on screen — which is the same defect the ceiling exists to
* prevent, pointing the other way: not a silent cut, but a loud claim of one
* that did not happen.
*
* ⚠️ Latent, not reported. Today's only host that passes `data` is
* `ObjectView`, and it passes it from mount, so `hasExternalData` is true
* before the internal fetch can ever run and the stale state is unreachable.
* That is exactly why it is pinned rather than only fixed: nothing about the
* prop's contract says a host may not start without `data` and supply it
* later, and a latent defect with no test is one refactor away from being a
* reported one.
*
* REVERSE VERIFICATION — direction predicted before running: remove the
* `setRowCeiling({ truncated: false })` from the external-`data` effect in
* `ObjectCalendar` and this file goes red at the "the note is gone" assertion
* (the note keeps rendering, still naming 2000 and 9876), while the control
* case — a component that never truncated — stays green.
*/

import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { NON_GRID_ROW_CEILING } from '@object-ui/react';
import { ObjectCalendar } from './ObjectCalendar';

vi.mock('@object-ui/plugin-detail', () => ({
RecordDetailDrawer: () => null,
deriveRecordPageHref: () => null,
}));

// Same reason as the sibling rowCeiling pin: the month grid draws at most four
// events per day cell, so the count has to come off an attribute.
vi.mock('./CalendarView', async (importOriginal) => {
const actual = await importOriginal<any>();
return {
...actual,
CalendarView: ({ events }: any) => (
<div data-testid="calendar-view" data-event-count={String(events.length)} />
),
};
});

const TOTAL_ROWS = 9876;
const NOW = new Date();

function makeRows(n: number, offset = 0) {
return Array.from({ length: n }, (_, i) => {
const d = new Date(NOW.getFullYear(), NOW.getMonth(), ((i + offset) % 28) + 1);
const iso = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(
d.getDate(),
).padStart(2, '0')}`;
return {
id: String(i + offset + 1),
subject: `Event ${i + offset + 1}`,
start_at: iso,
end_at: iso,
};
});
}

function makeDataSource(storeSize: number) {
const store = makeRows(storeSize);
return {
find: vi.fn(async (_resource: string, params: any) => {
const top = typeof params?.$top === 'number' ? params.$top : store.length;
return { data: store.slice(0, top), total: store.length };
}),
findOne: vi.fn(),
create: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
getObjectSchema: vi.fn(async () => ({
name: 'event',
fields: {
id: { name: 'id', type: 'text' },
subject: { name: 'subject', type: 'text' },
start_at: { name: 'start_at', type: 'date' },
end_at: { name: 'end_at', type: 'date' },
},
})),
} as any;
}

const schema: any = {
type: 'calendar',
objectName: 'event',
calendar: { titleField: 'subject', startDateField: 'start_at', endDateField: 'end_at' },
data: { provider: 'object', object: 'event' },
};

describe('objectui#7507 — an external `data` hand-off clears the row-ceiling note', () => {
it('a truncated own fetch, then a short external `data`: the note is GONE', async () => {
const dataSource = makeDataSource(TOTAL_ROWS);
const { rerender } = render(<ObjectCalendar schema={schema} dataSource={dataSource} />);

// Live control: the note really was raised by this component's own fetch,
// so its absence below is a change of state and not a query that never
// matched anything.
const note = await screen.findByRole('note');
expect(note.textContent).toContain(String(NON_GRID_ROW_CEILING));
expect(note.textContent).toContain(String(TOTAL_ROWS));
await waitFor(() =>
expect(screen.getByTestId('calendar-view').getAttribute('data-event-count')).toBe(
String(NON_GRID_ROW_CEILING),
),
);

const short = makeRows(12);
rerender(<ObjectCalendar schema={schema} dataSource={dataSource} data={short} />);

await waitFor(() =>
expect(screen.getByTestId('calendar-view').getAttribute('data-event-count')).toBe('12'),
);
expect(screen.queryByRole('note')).toBeNull();
});

it('external `data` from mount: still no note, and the rows are the parent’s', async () => {
const dataSource = makeDataSource(TOTAL_ROWS);
const short = makeRows(9);

render(<ObjectCalendar schema={schema} dataSource={dataSource} data={short} />);

await waitFor(() =>
expect(screen.getByTestId('calendar-view').getAttribute('data-event-count')).toBe('9'),
);
// The parent owns the query here, so this component never fetched and has
// nothing to report a ceiling about.
expect(dataSource.find).not.toHaveBeenCalled();
expect(screen.queryByRole('note')).toBeNull();
});
});
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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 14 additions & 9 deletions .changeset/7210-non-grid-row-ceiling.md
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
---
'@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
'@object-ui/plugin-gantt': minor
'@object-ui/plugin-calendar': minor
'@object-ui/plugin-map': minor
'@object-ui/plugin-tree': minor
---

A non-grid view's fetch now carries a platform row ceiling, and crossing it is
Expand All@@ -19,9 +19,13 @@ 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.
larger they render a footnote naming both numbers, verbatim as it renders:
*"Showing the first 2000 of 41234 records. Narrow the filter."* Below the
ceiling nothing changes: the full set draws and no footnote appears.

The four view packages take a **minor**, not a patch: a result set above the
ceiling is no longer drawn in full, which is a behaviour break whatever the
fixed group does to the released version number.

**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
Expand All@@ -40,5 +44,6 @@ 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.
`NON_GRID_ROW_CEILING_TOP`, `applyNonGridRowCeiling`, `NonGridRowCeilingNote`
and the type `NonGridCeilingResult`. Two new `common.*` i18n keys carry the
footnote copy in all ten packs.
24 changes: 24 additions & 0 deletions .changeset/7507-non-grid-ceiling-correctives.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
---
---

Post-merge correctives to objectui#7210's non-grid row ceiling (objectui#7507),
all of them inside a change that has not been released yet — so nothing here is
a user-visible fix, and the ceiling's own changeset carries the release note.

- `.changeset/7210-non-grid-row-ceiling.md`: the four view packages move from
`patch` to `minor` (they carry the behaviour break), the example footnote is
replaced with the copy that actually renders — no thousands separators, since
the i18next config declares no `format` and the provider-less path
interpolates through `String(v)` — and the export list names
`NonGridCeilingResult`.
- `ObjectCalendar`'s external-`data` sync now also clears `rowCeiling`, so a
footnote raised by this component's own truncated fetch cannot outlive the
rows it described. Latent today: the only host that passes `data` passes it
from mount. Pinned in `ObjectCalendar.externalDataCeiling-7507.test.tsx`.
- The map and calendar ceiling pins now assert the row count handed to the
view, not only `$top` and the footnote; the four pins' reverse-verification
docblocks are rewritten to the mechanism that was measured rather than the
one that was predicted.
- Comment-only: the false "the only package all four already depend on"
rationale on `@object-ui/react`'s entry, and the stale "~1 KB of headroom"
note in the ten locale packs.
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/ar.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -133,7 +133,10 @@ const ar = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "يتم عرض أول {{shown}} من أصل {{total}} سجل. ضيّق عامل التصفية.",
rowCeilingNoteUnknownTotal: "يتم عرض أول {{shown}} سجل. ضيّق عامل التصفية.",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/de.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -129,7 +129,10 @@ const de = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "Erste {{shown}} von {{total}} Datensätzen. Filter eingrenzen.",
rowCeilingNoteUnknownTotal: "Erste {{shown}} Datensätze. Filter eingrenzen.",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/en.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -153,7 +153,10 @@ const en = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: 'Showing the first {{shown}} of {{total}} records. Narrow the filter.',
rowCeilingNoteUnknownTotal: 'Showing the first {{shown}} records. Narrow the filter.',
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/es.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -128,7 +128,10 @@ const es = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "Mostrando los primeros {{shown}} de {{total}} registros. Acota el filtro.",
rowCeilingNoteUnknownTotal: "Mostrando los primeros {{shown}} registros. Acota el filtro.",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/fr.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -129,7 +129,10 @@ const fr = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "Affichage des {{shown}} premiers enregistrements sur {{total}}. Affinez le filtre.",
rowCeilingNoteUnknownTotal: "Affichage des {{shown}} premiers enregistrements. Affinez le filtre.",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/ja.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -129,7 +129,10 @@ const ja = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "{{total}} 件中、最初の {{shown}} 件を表示しています。フィルターを絞り込んでください。",
rowCeilingNoteUnknownTotal: "最初の {{shown}} 件を表示しています。フィルターを絞り込んでください。",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/ko.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -129,7 +129,10 @@ const ko = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "전체 {{total}}개 중 처음 {{shown}}개를 표시합니다. 필터를 좁히세요.",
rowCeilingNoteUnknownTotal: "처음 {{shown}}개를 표시합니다. 필터를 좁히세요.",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/pt.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -128,7 +128,10 @@ const pt = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "Mostrando os primeiros {{shown}} de {{total}} registros. Restrinja o filtro.",
rowCeilingNoteUnknownTotal: "Mostrando os primeiros {{shown}} registros. Restrinja o filtro.",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/ru.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -135,7 +135,10 @@ const ru = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "Показаны первые {{shown}} из {{total}} записей. Сузьте фильтр.",
rowCeilingNoteUnknownTotal: "Показаны первые {{shown}} записей. Сузьте фильтр.",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/zh.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -136,7 +136,10 @@ const zh = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: '仅显示 {{total}} 条记录中的前 {{shown}} 条。请缩小筛选范围。',
rowCeilingNoteUnknownTotal: '仅显示前 {{shown}} 条记录。请缩小筛选范围。',
},
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
/**
* ObjectUI
* Copyright (c) 2024-present ObjectStack Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

/**
* objectui#7507 — the row-ceiling footnote must not outlive the fetch that
* raised it (objectui#7210's ceiling, ruling a′).
*
* `ObjectCalendar` has five `setData` paths. Four of them also reset
* `rowCeiling`; the external-`data` sync was the one that did not, so a
* component that first drew its OWN truncated fetch and then had a short
* `data` array handed to it kept showing "Showing the first 2000 of 9876
* records" over twelve rows. The note would be describing a result set that is
* no longer on screen — which is the same defect the ceiling exists to
* prevent, pointing the other way: not a silent cut, but a loud claim of one
* that did not happen.
*
* ⚠️ Latent, not reported. Today's only host that passes `data` is
* `ObjectView`, and it passes it from mount, so `hasExternalData` is true
* before the internal fetch can ever run and the stale state is unreachable.
* That is exactly why it is pinned rather than only fixed: nothing about the
* prop's contract says a host may not start without `data` and supply it
* later, and a latent defect with no test is one refactor away from being a
* reported one.
*
* REVERSE VERIFICATION — direction predicted before running: remove the
* `setRowCeiling({ truncated: false })` from the external-`data` effect in
* `ObjectCalendar` and this file goes red at the "the note is gone" assertion
* (the note keeps rendering, still naming 2000 and 9876), while the control
* case — a component that never truncated — stays green.
*/

import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { NON_GRID_ROW_CEILING } from '@object-ui/react';
import { ObjectCalendar } from './ObjectCalendar';

vi.mock('@object-ui/plugin-detail', () => ({
RecordDetailDrawer: () => null,
deriveRecordPageHref: () => null,
}));

// Same reason as the sibling rowCeiling pin: the month grid draws at most four
// events per day cell, so the count has to come off an attribute.
vi.mock('./CalendarView', async (importOriginal) => {
const actual = await importOriginal<any>();
return {
...actual,
CalendarView: ({ events }: any) => (
<div data-testid="calendar-view" data-event-count={String(events.length)} />
),
};
});

const TOTAL_ROWS = 9876;
const NOW = new Date();

function makeRows(n: number, offset = 0) {
return Array.from({ length: n }, (_, i) => {
const d = new Date(NOW.getFullYear(), NOW.getMonth(), ((i + offset) % 28) + 1);
const iso = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(
d.getDate(),
).padStart(2, '0')}`;
return {
id: String(i + offset + 1),
subject: `Event ${i + offset + 1}`,
start_at: iso,
end_at: iso,
};
});
}

function makeDataSource(storeSize: number) {
const store = makeRows(storeSize);
return {
find: vi.fn(async (_resource: string, params: any) => {
const top = typeof params?.$top === 'number' ? params.$top : store.length;
return { data: store.slice(0, top), total: store.length };
}),
findOne: vi.fn(),
create: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
getObjectSchema: vi.fn(async () => ({
name: 'event',
fields: {
id: { name: 'id', type: 'text' },
subject: { name: 'subject', type: 'text' },
start_at: { name: 'start_at', type: 'date' },
end_at: { name: 'end_at', type: 'date' },
},
})),
} as any;
}

const schema: any = {
type: 'calendar',
objectName: 'event',
calendar: { titleField: 'subject', startDateField: 'start_at', endDateField: 'end_at' },
data: { provider: 'object', object: 'event' },
};

describe('objectui#7507 — an external `data` hand-off clears the row-ceiling note', () => {
it('a truncated own fetch, then a short external `data`: the note is GONE', async () => {
const dataSource = makeDataSource(TOTAL_ROWS);
const { rerender } = render(<ObjectCalendar schema={schema} dataSource={dataSource} />);

// Live control: the note really was raised by this component's own fetch,
// so its absence below is a change of state and not a query that never
// matched anything.
const note = await screen.findByRole('note');
expect(note.textContent).toContain(String(NON_GRID_ROW_CEILING));
expect(note.textContent).toContain(String(TOTAL_ROWS));
await waitFor(() =>
expect(screen.getByTestId('calendar-view').getAttribute('data-event-count')).toBe(
String(NON_GRID_ROW_CEILING),
),
);

const short = makeRows(12);
rerender(<ObjectCalendar schema={schema} dataSource={dataSource} data={short} />);

await waitFor(() =>
expect(screen.getByTestId('calendar-view').getAttribute('data-event-count')).toBe('12'),
);
expect(screen.queryByRole('note')).toBeNull();
});

it('external `data` from mount: still no note, and the rows are the parent’s', async () => {
const dataSource = makeDataSource(TOTAL_ROWS);
const short = makeRows(9);

render(<ObjectCalendar schema={schema} dataSource={dataSource} data={short} />);

await waitFor(() =>
expect(screen.getByTestId('calendar-view').getAttribute('data-event-count')).toBe('9'),
);
// The parent owns the query here, so this component never fetched and has
// nothing to report a ceiling about.
expect(dataSource.find).not.toHaveBeenCalled();
expect(screen.queryByRole('note')).toBeNull();
});
});
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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 14 additions & 9 deletions .changeset/7210-non-grid-row-ceiling.md
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
---
'@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
'@object-ui/plugin-gantt': minor
'@object-ui/plugin-calendar': minor
'@object-ui/plugin-map': minor
'@object-ui/plugin-tree': minor
---

A non-grid view's fetch now carries a platform row ceiling, and crossing it is
Expand All@@ -19,9 +19,13 @@ 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.
larger they render a footnote naming both numbers, verbatim as it renders:
*"Showing the first 2000 of 41234 records. Narrow the filter."* Below the
ceiling nothing changes: the full set draws and no footnote appears.

The four view packages take a **minor**, not a patch: a result set above the
ceiling is no longer drawn in full, which is a behaviour break whatever the
fixed group does to the released version number.

**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
Expand All@@ -40,5 +44,6 @@ 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.
`NON_GRID_ROW_CEILING_TOP`, `applyNonGridRowCeiling`, `NonGridRowCeilingNote`
and the type `NonGridCeilingResult`. Two new `common.*` i18n keys carry the
footnote copy in all ten packs.
24 changes: 24 additions & 0 deletions .changeset/7507-non-grid-ceiling-correctives.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
---
---

Post-merge correctives to objectui#7210's non-grid row ceiling (objectui#7507),
all of them inside a change that has not been released yet — so nothing here is
a user-visible fix, and the ceiling's own changeset carries the release note.

- `.changeset/7210-non-grid-row-ceiling.md`: the four view packages move from
`patch` to `minor` (they carry the behaviour break), the example footnote is
replaced with the copy that actually renders — no thousands separators, since
the i18next config declares no `format` and the provider-less path
interpolates through `String(v)` — and the export list names
`NonGridCeilingResult`.
- `ObjectCalendar`'s external-`data` sync now also clears `rowCeiling`, so a
footnote raised by this component's own truncated fetch cannot outlive the
rows it described. Latent today: the only host that passes `data` passes it
from mount. Pinned in `ObjectCalendar.externalDataCeiling-7507.test.tsx`.
- The map and calendar ceiling pins now assert the row count handed to the
view, not only `$top` and the footnote; the four pins' reverse-verification
docblocks are rewritten to the mechanism that was measured rather than the
one that was predicted.
- Comment-only: the false "the only package all four already depend on"
rationale on `@object-ui/react`'s entry, and the stale "~1 KB of headroom"
note in the ten locale packs.
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/ar.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -133,7 +133,10 @@ const ar = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "يتم عرض أول {{shown}} من أصل {{total}} سجل. ضيّق عامل التصفية.",
rowCeilingNoteUnknownTotal: "يتم عرض أول {{shown}} سجل. ضيّق عامل التصفية.",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/de.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -129,7 +129,10 @@ const de = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "Erste {{shown}} von {{total}} Datensätzen. Filter eingrenzen.",
rowCeilingNoteUnknownTotal: "Erste {{shown}} Datensätze. Filter eingrenzen.",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/en.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -153,7 +153,10 @@ const en = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: 'Showing the first {{shown}} of {{total}} records. Narrow the filter.',
rowCeilingNoteUnknownTotal: 'Showing the first {{shown}} records. Narrow the filter.',
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/es.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -128,7 +128,10 @@ const es = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "Mostrando los primeros {{shown}} de {{total}} registros. Acota el filtro.",
rowCeilingNoteUnknownTotal: "Mostrando los primeros {{shown}} registros. Acota el filtro.",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/fr.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -129,7 +129,10 @@ const fr = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "Affichage des {{shown}} premiers enregistrements sur {{total}}. Affinez le filtre.",
rowCeilingNoteUnknownTotal: "Affichage des {{shown}} premiers enregistrements. Affinez le filtre.",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/ja.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -129,7 +129,10 @@ const ja = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "{{total}} 件中、最初の {{shown}} 件を表示しています。フィルターを絞り込んでください。",
rowCeilingNoteUnknownTotal: "最初の {{shown}} 件を表示しています。フィルターを絞り込んでください。",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/ko.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -129,7 +129,10 @@ const ko = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "전체 {{total}}개 중 처음 {{shown}}개를 표시합니다. 필터를 좁히세요.",
rowCeilingNoteUnknownTotal: "처음 {{shown}}개를 표시합니다. 필터를 좁히세요.",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/pt.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -128,7 +128,10 @@ const pt = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "Mostrando os primeiros {{shown}} de {{total}} registros. Restrinja o filtro.",
rowCeilingNoteUnknownTotal: "Mostrando os primeiros {{shown}} registros. Restrinja o filtro.",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/ru.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -135,7 +135,10 @@ const ru = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "Показаны первые {{shown}} из {{total}} записей. Сузьте фильтр.",
rowCeilingNoteUnknownTotal: "Показаны первые {{shown}} записей. Сузьте фильтр.",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/zh.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -136,7 +136,10 @@ const zh = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: '仅显示 {{total}} 条记录中的前 {{shown}} 条。请缩小筛选范围。',
rowCeilingNoteUnknownTotal: '仅显示前 {{shown}} 条记录。请缩小筛选范围。',
},
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
/**
* ObjectUI
* Copyright (c) 2024-present ObjectStack Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

/**
* objectui#7507 — the row-ceiling footnote must not outlive the fetch that
* raised it (objectui#7210's ceiling, ruling a′).
*
* `ObjectCalendar` has five `setData` paths. Four of them also reset
* `rowCeiling`; the external-`data` sync was the one that did not, so a
* component that first drew its OWN truncated fetch and then had a short
* `data` array handed to it kept showing "Showing the first 2000 of 9876
* records" over twelve rows. The note would be describing a result set that is
* no longer on screen — which is the same defect the ceiling exists to
* prevent, pointing the other way: not a silent cut, but a loud claim of one
* that did not happen.
*
* ⚠️ Latent, not reported. Today's only host that passes `data` is
* `ObjectView`, and it passes it from mount, so `hasExternalData` is true
* before the internal fetch can ever run and the stale state is unreachable.
* That is exactly why it is pinned rather than only fixed: nothing about the
* prop's contract says a host may not start without `data` and supply it
* later, and a latent defect with no test is one refactor away from being a
* reported one.
*
* REVERSE VERIFICATION — direction predicted before running: remove the
* `setRowCeiling({ truncated: false })` from the external-`data` effect in
* `ObjectCalendar` and this file goes red at the "the note is gone" assertion
* (the note keeps rendering, still naming 2000 and 9876), while the control
* case — a component that never truncated — stays green.
*/

import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { NON_GRID_ROW_CEILING } from '@object-ui/react';
import { ObjectCalendar } from './ObjectCalendar';

vi.mock('@object-ui/plugin-detail', () => ({
RecordDetailDrawer: () => null,
deriveRecordPageHref: () => null,
}));

// Same reason as the sibling rowCeiling pin: the month grid draws at most four
// events per day cell, so the count has to come off an attribute.
vi.mock('./CalendarView', async (importOriginal) => {
const actual = await importOriginal<any>();
return {
...actual,
CalendarView: ({ events }: any) => (
<div data-testid="calendar-view" data-event-count={String(events.length)} />
),
};
});

const TOTAL_ROWS = 9876;
const NOW = new Date();

function makeRows(n: number, offset = 0) {
return Array.from({ length: n }, (_, i) => {
const d = new Date(NOW.getFullYear(), NOW.getMonth(), ((i + offset) % 28) + 1);
const iso = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(
d.getDate(),
).padStart(2, '0')}`;
return {
id: String(i + offset + 1),
subject: `Event ${i + offset + 1}`,
start_at: iso,
end_at: iso,
};
});
}

function makeDataSource(storeSize: number) {
const store = makeRows(storeSize);
return {
find: vi.fn(async (_resource: string, params: any) => {
const top = typeof params?.$top === 'number' ? params.$top : store.length;
return { data: store.slice(0, top), total: store.length };
}),
findOne: vi.fn(),
create: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
getObjectSchema: vi.fn(async () => ({
name: 'event',
fields: {
id: { name: 'id', type: 'text' },
subject: { name: 'subject', type: 'text' },
start_at: { name: 'start_at', type: 'date' },
end_at: { name: 'end_at', type: 'date' },
},
})),
} as any;
}

const schema: any = {
type: 'calendar',
objectName: 'event',
calendar: { titleField: 'subject', startDateField: 'start_at', endDateField: 'end_at' },
data: { provider: 'object', object: 'event' },
};

describe('objectui#7507 — an external `data` hand-off clears the row-ceiling note', () => {
it('a truncated own fetch, then a short external `data`: the note is GONE', async () => {
const dataSource = makeDataSource(TOTAL_ROWS);
const { rerender } = render(<ObjectCalendar schema={schema} dataSource={dataSource} />);

// Live control: the note really was raised by this component's own fetch,
// so its absence below is a change of state and not a query that never
// matched anything.
const note = await screen.findByRole('note');
expect(note.textContent).toContain(String(NON_GRID_ROW_CEILING));
expect(note.textContent).toContain(String(TOTAL_ROWS));
await waitFor(() =>
expect(screen.getByTestId('calendar-view').getAttribute('data-event-count')).toBe(
String(NON_GRID_ROW_CEILING),
),
);

const short = makeRows(12);
rerender(<ObjectCalendar schema={schema} dataSource={dataSource} data={short} />);

await waitFor(() =>
expect(screen.getByTestId('calendar-view').getAttribute('data-event-count')).toBe('12'),
);
expect(screen.queryByRole('note')).toBeNull();
});

it('external `data` from mount: still no note, and the rows are the parent’s', async () => {
const dataSource = makeDataSource(TOTAL_ROWS);
const short = makeRows(9);

render(<ObjectCalendar schema={schema} dataSource={dataSource} data={short} />);

await waitFor(() =>
expect(screen.getByTestId('calendar-view').getAttribute('data-event-count')).toBe('9'),
);
// The parent owns the query here, so this component never fetched and has
// nothing to report a ceiling about.
expect(dataSource.find).not.toHaveBeenCalled();
expect(screen.queryByRole('note')).toBeNull();
});
});
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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 14 additions & 9 deletions .changeset/7210-non-grid-row-ceiling.md
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
---
'@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
'@object-ui/plugin-gantt': minor
'@object-ui/plugin-calendar': minor
'@object-ui/plugin-map': minor
'@object-ui/plugin-tree': minor
---

A non-grid view's fetch now carries a platform row ceiling, and crossing it is
Expand All@@ -19,9 +19,13 @@ 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.
larger they render a footnote naming both numbers, verbatim as it renders:
*"Showing the first 2000 of 41234 records. Narrow the filter."* Below the
ceiling nothing changes: the full set draws and no footnote appears.

The four view packages take a **minor**, not a patch: a result set above the
ceiling is no longer drawn in full, which is a behaviour break whatever the
fixed group does to the released version number.

**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
Expand All@@ -40,5 +44,6 @@ 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.
`NON_GRID_ROW_CEILING_TOP`, `applyNonGridRowCeiling`, `NonGridRowCeilingNote`
and the type `NonGridCeilingResult`. Two new `common.*` i18n keys carry the
footnote copy in all ten packs.
24 changes: 24 additions & 0 deletions .changeset/7507-non-grid-ceiling-correctives.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
---
---

Post-merge correctives to objectui#7210's non-grid row ceiling (objectui#7507),
all of them inside a change that has not been released yet — so nothing here is
a user-visible fix, and the ceiling's own changeset carries the release note.

- `.changeset/7210-non-grid-row-ceiling.md`: the four view packages move from
`patch` to `minor` (they carry the behaviour break), the example footnote is
replaced with the copy that actually renders — no thousands separators, since
the i18next config declares no `format` and the provider-less path
interpolates through `String(v)` — and the export list names
`NonGridCeilingResult`.
- `ObjectCalendar`'s external-`data` sync now also clears `rowCeiling`, so a
footnote raised by this component's own truncated fetch cannot outlive the
rows it described. Latent today: the only host that passes `data` passes it
from mount. Pinned in `ObjectCalendar.externalDataCeiling-7507.test.tsx`.
- The map and calendar ceiling pins now assert the row count handed to the
view, not only `$top` and the footnote; the four pins' reverse-verification
docblocks are rewritten to the mechanism that was measured rather than the
one that was predicted.
- Comment-only: the false "the only package all four already depend on"
rationale on `@object-ui/react`'s entry, and the stale "~1 KB of headroom"
note in the ten locale packs.
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/ar.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -133,7 +133,10 @@ const ar = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "يتم عرض أول {{shown}} من أصل {{total}} سجل. ضيّق عامل التصفية.",
rowCeilingNoteUnknownTotal: "يتم عرض أول {{shown}} سجل. ضيّق عامل التصفية.",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/de.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -129,7 +129,10 @@ const de = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "Erste {{shown}} von {{total}} Datensätzen. Filter eingrenzen.",
rowCeilingNoteUnknownTotal: "Erste {{shown}} Datensätze. Filter eingrenzen.",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/en.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -153,7 +153,10 @@ const en = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: 'Showing the first {{shown}} of {{total}} records. Narrow the filter.',
rowCeilingNoteUnknownTotal: 'Showing the first {{shown}} records. Narrow the filter.',
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/es.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -128,7 +128,10 @@ const es = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "Mostrando los primeros {{shown}} de {{total}} registros. Acota el filtro.",
rowCeilingNoteUnknownTotal: "Mostrando los primeros {{shown}} registros. Acota el filtro.",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/fr.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -129,7 +129,10 @@ const fr = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "Affichage des {{shown}} premiers enregistrements sur {{total}}. Affinez le filtre.",
rowCeilingNoteUnknownTotal: "Affichage des {{shown}} premiers enregistrements. Affinez le filtre.",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/ja.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -129,7 +129,10 @@ const ja = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "{{total}} 件中、最初の {{shown}} 件を表示しています。フィルターを絞り込んでください。",
rowCeilingNoteUnknownTotal: "最初の {{shown}} 件を表示しています。フィルターを絞り込んでください。",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/ko.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -129,7 +129,10 @@ const ko = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "전체 {{total}}개 중 처음 {{shown}}개를 표시합니다. 필터를 좁히세요.",
rowCeilingNoteUnknownTotal: "처음 {{shown}}개를 표시합니다. 필터를 좁히세요.",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/pt.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -128,7 +128,10 @@ const pt = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "Mostrando os primeiros {{shown}} de {{total}} registros. Restrinja o filtro.",
rowCeilingNoteUnknownTotal: "Mostrando os primeiros {{shown}} registros. Restrinja o filtro.",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/ru.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -135,7 +135,10 @@ const ru = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "Показаны первые {{shown}} из {{total}} записей. Сузьте фильтр.",
rowCeilingNoteUnknownTotal: "Показаны первые {{shown}} записей. Сузьте фильтр.",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/zh.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -136,7 +136,10 @@ const zh = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: '仅显示 {{total}} 条记录中的前 {{shown}} 条。请缩小筛选范围。',
rowCeilingNoteUnknownTotal: '仅显示前 {{shown}} 条记录。请缩小筛选范围。',
},
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
/**
* ObjectUI
* Copyright (c) 2024-present ObjectStack Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

/**
* objectui#7507 — the row-ceiling footnote must not outlive the fetch that
* raised it (objectui#7210's ceiling, ruling a′).
*
* `ObjectCalendar` has five `setData` paths. Four of them also reset
* `rowCeiling`; the external-`data` sync was the one that did not, so a
* component that first drew its OWN truncated fetch and then had a short
* `data` array handed to it kept showing "Showing the first 2000 of 9876
* records" over twelve rows. The note would be describing a result set that is
* no longer on screen — which is the same defect the ceiling exists to
* prevent, pointing the other way: not a silent cut, but a loud claim of one
* that did not happen.
*
* ⚠️ Latent, not reported. Today's only host that passes `data` is
* `ObjectView`, and it passes it from mount, so `hasExternalData` is true
* before the internal fetch can ever run and the stale state is unreachable.
* That is exactly why it is pinned rather than only fixed: nothing about the
* prop's contract says a host may not start without `data` and supply it
* later, and a latent defect with no test is one refactor away from being a
* reported one.
*
* REVERSE VERIFICATION — direction predicted before running: remove the
* `setRowCeiling({ truncated: false })` from the external-`data` effect in
* `ObjectCalendar` and this file goes red at the "the note is gone" assertion
* (the note keeps rendering, still naming 2000 and 9876), while the control
* case — a component that never truncated — stays green.
*/

import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { NON_GRID_ROW_CEILING } from '@object-ui/react';
import { ObjectCalendar } from './ObjectCalendar';

vi.mock('@object-ui/plugin-detail', () => ({
RecordDetailDrawer: () => null,
deriveRecordPageHref: () => null,
}));

// Same reason as the sibling rowCeiling pin: the month grid draws at most four
// events per day cell, so the count has to come off an attribute.
vi.mock('./CalendarView', async (importOriginal) => {
const actual = await importOriginal<any>();
return {
...actual,
CalendarView: ({ events }: any) => (
<div data-testid="calendar-view" data-event-count={String(events.length)} />
),
};
});

const TOTAL_ROWS = 9876;
const NOW = new Date();

function makeRows(n: number, offset = 0) {
return Array.from({ length: n }, (_, i) => {
const d = new Date(NOW.getFullYear(), NOW.getMonth(), ((i + offset) % 28) + 1);
const iso = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(
d.getDate(),
).padStart(2, '0')}`;
return {
id: String(i + offset + 1),
subject: `Event ${i + offset + 1}`,
start_at: iso,
end_at: iso,
};
});
}

function makeDataSource(storeSize: number) {
const store = makeRows(storeSize);
return {
find: vi.fn(async (_resource: string, params: any) => {
const top = typeof params?.$top === 'number' ? params.$top : store.length;
return { data: store.slice(0, top), total: store.length };
}),
findOne: vi.fn(),
create: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
getObjectSchema: vi.fn(async () => ({
name: 'event',
fields: {
id: { name: 'id', type: 'text' },
subject: { name: 'subject', type: 'text' },
start_at: { name: 'start_at', type: 'date' },
end_at: { name: 'end_at', type: 'date' },
},
})),
} as any;
}

const schema: any = {
type: 'calendar',
objectName: 'event',
calendar: { titleField: 'subject', startDateField: 'start_at', endDateField: 'end_at' },
data: { provider: 'object', object: 'event' },
};

describe('objectui#7507 — an external `data` hand-off clears the row-ceiling note', () => {
it('a truncated own fetch, then a short external `data`: the note is GONE', async () => {
const dataSource = makeDataSource(TOTAL_ROWS);
const { rerender } = render(<ObjectCalendar schema={schema} dataSource={dataSource} />);

// Live control: the note really was raised by this component's own fetch,
// so its absence below is a change of state and not a query that never
// matched anything.
const note = await screen.findByRole('note');
expect(note.textContent).toContain(String(NON_GRID_ROW_CEILING));
expect(note.textContent).toContain(String(TOTAL_ROWS));
await waitFor(() =>
expect(screen.getByTestId('calendar-view').getAttribute('data-event-count')).toBe(
String(NON_GRID_ROW_CEILING),
),
);

const short = makeRows(12);
rerender(<ObjectCalendar schema={schema} dataSource={dataSource} data={short} />);

await waitFor(() =>
expect(screen.getByTestId('calendar-view').getAttribute('data-event-count')).toBe('12'),
);
expect(screen.queryByRole('note')).toBeNull();
});

it('external `data` from mount: still no note, and the rows are the parent’s', async () => {
const dataSource = makeDataSource(TOTAL_ROWS);
const short = makeRows(9);

render(<ObjectCalendar schema={schema} dataSource={dataSource} data={short} />);

await waitFor(() =>
expect(screen.getByTestId('calendar-view').getAttribute('data-event-count')).toBe('9'),
);
// The parent owns the query here, so this component never fetched and has
// nothing to report a ceiling about.
expect(dataSource.find).not.toHaveBeenCalled();
expect(screen.queryByRole('note')).toBeNull();
});
});
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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 14 additions & 9 deletions .changeset/7210-non-grid-row-ceiling.md
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
---
'@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
'@object-ui/plugin-gantt': minor
'@object-ui/plugin-calendar': minor
'@object-ui/plugin-map': minor
'@object-ui/plugin-tree': minor
---

A non-grid view's fetch now carries a platform row ceiling, and crossing it is
Expand All@@ -19,9 +19,13 @@ 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.
larger they render a footnote naming both numbers, verbatim as it renders:
*"Showing the first 2000 of 41234 records. Narrow the filter."* Below the
ceiling nothing changes: the full set draws and no footnote appears.

The four view packages take a **minor**, not a patch: a result set above the
ceiling is no longer drawn in full, which is a behaviour break whatever the
fixed group does to the released version number.

**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
Expand All@@ -40,5 +44,6 @@ 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.
`NON_GRID_ROW_CEILING_TOP`, `applyNonGridRowCeiling`, `NonGridRowCeilingNote`
and the type `NonGridCeilingResult`. Two new `common.*` i18n keys carry the
footnote copy in all ten packs.
24 changes: 24 additions & 0 deletions .changeset/7507-non-grid-ceiling-correctives.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
---
---

Post-merge correctives to objectui#7210's non-grid row ceiling (objectui#7507),
all of them inside a change that has not been released yet — so nothing here is
a user-visible fix, and the ceiling's own changeset carries the release note.

- `.changeset/7210-non-grid-row-ceiling.md`: the four view packages move from
`patch` to `minor` (they carry the behaviour break), the example footnote is
replaced with the copy that actually renders — no thousands separators, since
the i18next config declares no `format` and the provider-less path
interpolates through `String(v)` — and the export list names
`NonGridCeilingResult`.
- `ObjectCalendar`'s external-`data` sync now also clears `rowCeiling`, so a
footnote raised by this component's own truncated fetch cannot outlive the
rows it described. Latent today: the only host that passes `data` passes it
from mount. Pinned in `ObjectCalendar.externalDataCeiling-7507.test.tsx`.
- The map and calendar ceiling pins now assert the row count handed to the
view, not only `$top` and the footnote; the four pins' reverse-verification
docblocks are rewritten to the mechanism that was measured rather than the
one that was predicted.
- Comment-only: the false "the only package all four already depend on"
rationale on `@object-ui/react`'s entry, and the stale "~1 KB of headroom"
note in the ten locale packs.
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/ar.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -133,7 +133,10 @@ const ar = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "يتم عرض أول {{shown}} من أصل {{total}} سجل. ضيّق عامل التصفية.",
rowCeilingNoteUnknownTotal: "يتم عرض أول {{shown}} سجل. ضيّق عامل التصفية.",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/de.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -129,7 +129,10 @@ const de = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "Erste {{shown}} von {{total}} Datensätzen. Filter eingrenzen.",
rowCeilingNoteUnknownTotal: "Erste {{shown}} Datensätze. Filter eingrenzen.",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/en.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -153,7 +153,10 @@ const en = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: 'Showing the first {{shown}} of {{total}} records. Narrow the filter.',
rowCeilingNoteUnknownTotal: 'Showing the first {{shown}} records. Narrow the filter.',
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/es.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -128,7 +128,10 @@ const es = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "Mostrando los primeros {{shown}} de {{total}} registros. Acota el filtro.",
rowCeilingNoteUnknownTotal: "Mostrando los primeros {{shown}} registros. Acota el filtro.",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/fr.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -129,7 +129,10 @@ const fr = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "Affichage des {{shown}} premiers enregistrements sur {{total}}. Affinez le filtre.",
rowCeilingNoteUnknownTotal: "Affichage des {{shown}} premiers enregistrements. Affinez le filtre.",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/ja.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -129,7 +129,10 @@ const ja = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "{{total}} 件中、最初の {{shown}} 件を表示しています。フィルターを絞り込んでください。",
rowCeilingNoteUnknownTotal: "最初の {{shown}} 件を表示しています。フィルターを絞り込んでください。",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/ko.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -129,7 +129,10 @@ const ko = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "전체 {{total}}개 중 처음 {{shown}}개를 표시합니다. 필터를 좁히세요.",
rowCeilingNoteUnknownTotal: "처음 {{shown}}개를 표시합니다. 필터를 좁히세요.",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/pt.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -128,7 +128,10 @@ const pt = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "Mostrando os primeiros {{shown}} de {{total}} registros. Restrinja o filtro.",
rowCeilingNoteUnknownTotal: "Mostrando os primeiros {{shown}} registros. Restrinja o filtro.",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/ru.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -135,7 +135,10 @@ const ru = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "Показаны первые {{shown}} из {{total}} записей. Сузьте фильтр.",
rowCeilingNoteUnknownTotal: "Показаны первые {{shown}} записей. Сузьте фильтр.",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/zh.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -136,7 +136,10 @@ const zh = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: '仅显示 {{total}} 条记录中的前 {{shown}} 条。请缩小筛选范围。',
rowCeilingNoteUnknownTotal: '仅显示前 {{shown}} 条记录。请缩小筛选范围。',
},
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
/**
* ObjectUI
* Copyright (c) 2024-present ObjectStack Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

/**
* objectui#7507 — the row-ceiling footnote must not outlive the fetch that
* raised it (objectui#7210's ceiling, ruling a′).
*
* `ObjectCalendar` has five `setData` paths. Four of them also reset
* `rowCeiling`; the external-`data` sync was the one that did not, so a
* component that first drew its OWN truncated fetch and then had a short
* `data` array handed to it kept showing "Showing the first 2000 of 9876
* records" over twelve rows. The note would be describing a result set that is
* no longer on screen — which is the same defect the ceiling exists to
* prevent, pointing the other way: not a silent cut, but a loud claim of one
* that did not happen.
*
* ⚠️ Latent, not reported. Today's only host that passes `data` is
* `ObjectView`, and it passes it from mount, so `hasExternalData` is true
* before the internal fetch can ever run and the stale state is unreachable.
* That is exactly why it is pinned rather than only fixed: nothing about the
* prop's contract says a host may not start without `data` and supply it
* later, and a latent defect with no test is one refactor away from being a
* reported one.
*
* REVERSE VERIFICATION — direction predicted before running: remove the
* `setRowCeiling({ truncated: false })` from the external-`data` effect in
* `ObjectCalendar` and this file goes red at the "the note is gone" assertion
* (the note keeps rendering, still naming 2000 and 9876), while the control
* case — a component that never truncated — stays green.
*/

import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { NON_GRID_ROW_CEILING } from '@object-ui/react';
import { ObjectCalendar } from './ObjectCalendar';

vi.mock('@object-ui/plugin-detail', () => ({
RecordDetailDrawer: () => null,
deriveRecordPageHref: () => null,
}));

// Same reason as the sibling rowCeiling pin: the month grid draws at most four
// events per day cell, so the count has to come off an attribute.
vi.mock('./CalendarView', async (importOriginal) => {
const actual = await importOriginal<any>();
return {
...actual,
CalendarView: ({ events }: any) => (
<div data-testid="calendar-view" data-event-count={String(events.length)} />
),
};
});

const TOTAL_ROWS = 9876;
const NOW = new Date();

function makeRows(n: number, offset = 0) {
return Array.from({ length: n }, (_, i) => {
const d = new Date(NOW.getFullYear(), NOW.getMonth(), ((i + offset) % 28) + 1);
const iso = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(
d.getDate(),
).padStart(2, '0')}`;
return {
id: String(i + offset + 1),
subject: `Event ${i + offset + 1}`,
start_at: iso,
end_at: iso,
};
});
}

function makeDataSource(storeSize: number) {
const store = makeRows(storeSize);
return {
find: vi.fn(async (_resource: string, params: any) => {
const top = typeof params?.$top === 'number' ? params.$top : store.length;
return { data: store.slice(0, top), total: store.length };
}),
findOne: vi.fn(),
create: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
getObjectSchema: vi.fn(async () => ({
name: 'event',
fields: {
id: { name: 'id', type: 'text' },
subject: { name: 'subject', type: 'text' },
start_at: { name: 'start_at', type: 'date' },
end_at: { name: 'end_at', type: 'date' },
},
})),
} as any;
}

const schema: any = {
type: 'calendar',
objectName: 'event',
calendar: { titleField: 'subject', startDateField: 'start_at', endDateField: 'end_at' },
data: { provider: 'object', object: 'event' },
};

describe('objectui#7507 — an external `data` hand-off clears the row-ceiling note', () => {
it('a truncated own fetch, then a short external `data`: the note is GONE', async () => {
const dataSource = makeDataSource(TOTAL_ROWS);
const { rerender } = render(<ObjectCalendar schema={schema} dataSource={dataSource} />);

// Live control: the note really was raised by this component's own fetch,
// so its absence below is a change of state and not a query that never
// matched anything.
const note = await screen.findByRole('note');
expect(note.textContent).toContain(String(NON_GRID_ROW_CEILING));
expect(note.textContent).toContain(String(TOTAL_ROWS));
await waitFor(() =>
expect(screen.getByTestId('calendar-view').getAttribute('data-event-count')).toBe(
String(NON_GRID_ROW_CEILING),
),
);

const short = makeRows(12);
rerender(<ObjectCalendar schema={schema} dataSource={dataSource} data={short} />);

await waitFor(() =>
expect(screen.getByTestId('calendar-view').getAttribute('data-event-count')).toBe('12'),
);
expect(screen.queryByRole('note')).toBeNull();
});

it('external `data` from mount: still no note, and the rows are the parent’s', async () => {
const dataSource = makeDataSource(TOTAL_ROWS);
const short = makeRows(9);

render(<ObjectCalendar schema={schema} dataSource={dataSource} data={short} />);

await waitFor(() =>
expect(screen.getByTestId('calendar-view').getAttribute('data-event-count')).toBe('9'),
);
// The parent owns the query here, so this component never fetched and has
// nothing to report a ceiling about.
expect(dataSource.find).not.toHaveBeenCalled();
expect(screen.queryByRole('note')).toBeNull();
});
});
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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 14 additions & 9 deletions .changeset/7210-non-grid-row-ceiling.md
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
---
'@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
'@object-ui/plugin-gantt': minor
'@object-ui/plugin-calendar': minor
'@object-ui/plugin-map': minor
'@object-ui/plugin-tree': minor
---

A non-grid view's fetch now carries a platform row ceiling, and crossing it is
Expand All@@ -19,9 +19,13 @@ 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.
larger they render a footnote naming both numbers, verbatim as it renders:
*"Showing the first 2000 of 41234 records. Narrow the filter."* Below the
ceiling nothing changes: the full set draws and no footnote appears.

The four view packages take a **minor**, not a patch: a result set above the
ceiling is no longer drawn in full, which is a behaviour break whatever the
fixed group does to the released version number.

**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
Expand All@@ -40,5 +44,6 @@ 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.
`NON_GRID_ROW_CEILING_TOP`, `applyNonGridRowCeiling`, `NonGridRowCeilingNote`
and the type `NonGridCeilingResult`. Two new `common.*` i18n keys carry the
footnote copy in all ten packs.
24 changes: 24 additions & 0 deletions .changeset/7507-non-grid-ceiling-correctives.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
---
---

Post-merge correctives to objectui#7210's non-grid row ceiling (objectui#7507),
all of them inside a change that has not been released yet — so nothing here is
a user-visible fix, and the ceiling's own changeset carries the release note.

- `.changeset/7210-non-grid-row-ceiling.md`: the four view packages move from
`patch` to `minor` (they carry the behaviour break), the example footnote is
replaced with the copy that actually renders — no thousands separators, since
the i18next config declares no `format` and the provider-less path
interpolates through `String(v)` — and the export list names
`NonGridCeilingResult`.
- `ObjectCalendar`'s external-`data` sync now also clears `rowCeiling`, so a
footnote raised by this component's own truncated fetch cannot outlive the
rows it described. Latent today: the only host that passes `data` passes it
from mount. Pinned in `ObjectCalendar.externalDataCeiling-7507.test.tsx`.
- The map and calendar ceiling pins now assert the row count handed to the
view, not only `$top` and the footnote; the four pins' reverse-verification
docblocks are rewritten to the mechanism that was measured rather than the
one that was predicted.
- Comment-only: the false "the only package all four already depend on"
rationale on `@object-ui/react`'s entry, and the stale "~1 KB of headroom"
note in the ten locale packs.
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/ar.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -133,7 +133,10 @@ const ar = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "يتم عرض أول {{shown}} من أصل {{total}} سجل. ضيّق عامل التصفية.",
rowCeilingNoteUnknownTotal: "يتم عرض أول {{shown}} سجل. ضيّق عامل التصفية.",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/de.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -129,7 +129,10 @@ const de = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "Erste {{shown}} von {{total}} Datensätzen. Filter eingrenzen.",
rowCeilingNoteUnknownTotal: "Erste {{shown}} Datensätze. Filter eingrenzen.",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/en.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -153,7 +153,10 @@ const en = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: 'Showing the first {{shown}} of {{total}} records. Narrow the filter.',
rowCeilingNoteUnknownTotal: 'Showing the first {{shown}} records. Narrow the filter.',
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/es.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -128,7 +128,10 @@ const es = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "Mostrando los primeros {{shown}} de {{total}} registros. Acota el filtro.",
rowCeilingNoteUnknownTotal: "Mostrando los primeros {{shown}} registros. Acota el filtro.",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/fr.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -129,7 +129,10 @@ const fr = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "Affichage des {{shown}} premiers enregistrements sur {{total}}. Affinez le filtre.",
rowCeilingNoteUnknownTotal: "Affichage des {{shown}} premiers enregistrements. Affinez le filtre.",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/ja.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -129,7 +129,10 @@ const ja = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "{{total}} 件中、最初の {{shown}} 件を表示しています。フィルターを絞り込んでください。",
rowCeilingNoteUnknownTotal: "最初の {{shown}} 件を表示しています。フィルターを絞り込んでください。",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/ko.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -129,7 +129,10 @@ const ko = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "전체 {{total}}개 중 처음 {{shown}}개를 표시합니다. 필터를 좁히세요.",
rowCeilingNoteUnknownTotal: "처음 {{shown}}개를 표시합니다. 필터를 좁히세요.",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/pt.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -128,7 +128,10 @@ const pt = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "Mostrando os primeiros {{shown}} de {{total}} registros. Restrinja o filtro.",
rowCeilingNoteUnknownTotal: "Mostrando os primeiros {{shown}} registros. Restrinja o filtro.",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/ru.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -135,7 +135,10 @@ const ru = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "Показаны первые {{shown}} из {{total}} записей. Сузьте фильтр.",
rowCeilingNoteUnknownTotal: "Показаны первые {{shown}} записей. Сузьте фильтр.",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/zh.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -136,7 +136,10 @@ const zh = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: '仅显示 {{total}} 条记录中的前 {{shown}} 条。请缩小筛选范围。',
rowCeilingNoteUnknownTotal: '仅显示前 {{shown}} 条记录。请缩小筛选范围。',
},
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
/**
* ObjectUI
* Copyright (c) 2024-present ObjectStack Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

/**
* objectui#7507 — the row-ceiling footnote must not outlive the fetch that
* raised it (objectui#7210's ceiling, ruling a′).
*
* `ObjectCalendar` has five `setData` paths. Four of them also reset
* `rowCeiling`; the external-`data` sync was the one that did not, so a
* component that first drew its OWN truncated fetch and then had a short
* `data` array handed to it kept showing "Showing the first 2000 of 9876
* records" over twelve rows. The note would be describing a result set that is
* no longer on screen — which is the same defect the ceiling exists to
* prevent, pointing the other way: not a silent cut, but a loud claim of one
* that did not happen.
*
* ⚠️ Latent, not reported. Today's only host that passes `data` is
* `ObjectView`, and it passes it from mount, so `hasExternalData` is true
* before the internal fetch can ever run and the stale state is unreachable.
* That is exactly why it is pinned rather than only fixed: nothing about the
* prop's contract says a host may not start without `data` and supply it
* later, and a latent defect with no test is one refactor away from being a
* reported one.
*
* REVERSE VERIFICATION — direction predicted before running: remove the
* `setRowCeiling({ truncated: false })` from the external-`data` effect in
* `ObjectCalendar` and this file goes red at the "the note is gone" assertion
* (the note keeps rendering, still naming 2000 and 9876), while the control
* case — a component that never truncated — stays green.
*/

import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { NON_GRID_ROW_CEILING } from '@object-ui/react';
import { ObjectCalendar } from './ObjectCalendar';

vi.mock('@object-ui/plugin-detail', () => ({
RecordDetailDrawer: () => null,
deriveRecordPageHref: () => null,
}));

// Same reason as the sibling rowCeiling pin: the month grid draws at most four
// events per day cell, so the count has to come off an attribute.
vi.mock('./CalendarView', async (importOriginal) => {
const actual = await importOriginal<any>();
return {
...actual,
CalendarView: ({ events }: any) => (
<div data-testid="calendar-view" data-event-count={String(events.length)} />
),
};
});

const TOTAL_ROWS = 9876;
const NOW = new Date();

function makeRows(n: number, offset = 0) {
return Array.from({ length: n }, (_, i) => {
const d = new Date(NOW.getFullYear(), NOW.getMonth(), ((i + offset) % 28) + 1);
const iso = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(
d.getDate(),
).padStart(2, '0')}`;
return {
id: String(i + offset + 1),
subject: `Event ${i + offset + 1}`,
start_at: iso,
end_at: iso,
};
});
}

function makeDataSource(storeSize: number) {
const store = makeRows(storeSize);
return {
find: vi.fn(async (_resource: string, params: any) => {
const top = typeof params?.$top === 'number' ? params.$top : store.length;
return { data: store.slice(0, top), total: store.length };
}),
findOne: vi.fn(),
create: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
getObjectSchema: vi.fn(async () => ({
name: 'event',
fields: {
id: { name: 'id', type: 'text' },
subject: { name: 'subject', type: 'text' },
start_at: { name: 'start_at', type: 'date' },
end_at: { name: 'end_at', type: 'date' },
},
})),
} as any;
}

const schema: any = {
type: 'calendar',
objectName: 'event',
calendar: { titleField: 'subject', startDateField: 'start_at', endDateField: 'end_at' },
data: { provider: 'object', object: 'event' },
};

describe('objectui#7507 — an external `data` hand-off clears the row-ceiling note', () => {
it('a truncated own fetch, then a short external `data`: the note is GONE', async () => {
const dataSource = makeDataSource(TOTAL_ROWS);
const { rerender } = render(<ObjectCalendar schema={schema} dataSource={dataSource} />);

// Live control: the note really was raised by this component's own fetch,
// so its absence below is a change of state and not a query that never
// matched anything.
const note = await screen.findByRole('note');
expect(note.textContent).toContain(String(NON_GRID_ROW_CEILING));
expect(note.textContent).toContain(String(TOTAL_ROWS));
await waitFor(() =>
expect(screen.getByTestId('calendar-view').getAttribute('data-event-count')).toBe(
String(NON_GRID_ROW_CEILING),
),
);

const short = makeRows(12);
rerender(<ObjectCalendar schema={schema} dataSource={dataSource} data={short} />);

await waitFor(() =>
expect(screen.getByTestId('calendar-view').getAttribute('data-event-count')).toBe('12'),
);
expect(screen.queryByRole('note')).toBeNull();
});

it('external `data` from mount: still no note, and the rows are the parent’s', async () => {
const dataSource = makeDataSource(TOTAL_ROWS);
const short = makeRows(9);

render(<ObjectCalendar schema={schema} dataSource={dataSource} data={short} />);

await waitFor(() =>
expect(screen.getByTestId('calendar-view').getAttribute('data-event-count')).toBe('9'),
);
// The parent owns the query here, so this component never fetched and has
// nothing to report a ceiling about.
expect(dataSource.find).not.toHaveBeenCalled();
expect(screen.queryByRole('note')).toBeNull();
});
});
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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 14 additions & 9 deletions .changeset/7210-non-grid-row-ceiling.md
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
---
'@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
'@object-ui/plugin-gantt': minor
'@object-ui/plugin-calendar': minor
'@object-ui/plugin-map': minor
'@object-ui/plugin-tree': minor
---

A non-grid view's fetch now carries a platform row ceiling, and crossing it is
Expand All@@ -19,9 +19,13 @@ 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.
larger they render a footnote naming both numbers, verbatim as it renders:
*"Showing the first 2000 of 41234 records. Narrow the filter."* Below the
ceiling nothing changes: the full set draws and no footnote appears.

The four view packages take a **minor**, not a patch: a result set above the
ceiling is no longer drawn in full, which is a behaviour break whatever the
fixed group does to the released version number.

**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
Expand All@@ -40,5 +44,6 @@ 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.
`NON_GRID_ROW_CEILING_TOP`, `applyNonGridRowCeiling`, `NonGridRowCeilingNote`
and the type `NonGridCeilingResult`. Two new `common.*` i18n keys carry the
footnote copy in all ten packs.
24 changes: 24 additions & 0 deletions .changeset/7507-non-grid-ceiling-correctives.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
---
---

Post-merge correctives to objectui#7210's non-grid row ceiling (objectui#7507),
all of them inside a change that has not been released yet — so nothing here is
a user-visible fix, and the ceiling's own changeset carries the release note.

- `.changeset/7210-non-grid-row-ceiling.md`: the four view packages move from
`patch` to `minor` (they carry the behaviour break), the example footnote is
replaced with the copy that actually renders — no thousands separators, since
the i18next config declares no `format` and the provider-less path
interpolates through `String(v)` — and the export list names
`NonGridCeilingResult`.
- `ObjectCalendar`'s external-`data` sync now also clears `rowCeiling`, so a
footnote raised by this component's own truncated fetch cannot outlive the
rows it described. Latent today: the only host that passes `data` passes it
from mount. Pinned in `ObjectCalendar.externalDataCeiling-7507.test.tsx`.
- The map and calendar ceiling pins now assert the row count handed to the
view, not only `$top` and the footnote; the four pins' reverse-verification
docblocks are rewritten to the mechanism that was measured rather than the
one that was predicted.
- Comment-only: the false "the only package all four already depend on"
rationale on `@object-ui/react`'s entry, and the stale "~1 KB of headroom"
note in the ten locale packs.
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/ar.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -133,7 +133,10 @@ const ar = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "يتم عرض أول {{shown}} من أصل {{total}} سجل. ضيّق عامل التصفية.",
rowCeilingNoteUnknownTotal: "يتم عرض أول {{shown}} سجل. ضيّق عامل التصفية.",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/de.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -129,7 +129,10 @@ const de = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "Erste {{shown}} von {{total}} Datensätzen. Filter eingrenzen.",
rowCeilingNoteUnknownTotal: "Erste {{shown}} Datensätze. Filter eingrenzen.",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/en.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -153,7 +153,10 @@ const en = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: 'Showing the first {{shown}} of {{total}} records. Narrow the filter.',
rowCeilingNoteUnknownTotal: 'Showing the first {{shown}} records. Narrow the filter.',
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/es.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -128,7 +128,10 @@ const es = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "Mostrando los primeros {{shown}} de {{total}} registros. Acota el filtro.",
rowCeilingNoteUnknownTotal: "Mostrando los primeros {{shown}} registros. Acota el filtro.",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/fr.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -129,7 +129,10 @@ const fr = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "Affichage des {{shown}} premiers enregistrements sur {{total}}. Affinez le filtre.",
rowCeilingNoteUnknownTotal: "Affichage des {{shown}} premiers enregistrements. Affinez le filtre.",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/ja.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -129,7 +129,10 @@ const ja = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "{{total}} 件中、最初の {{shown}} 件を表示しています。フィルターを絞り込んでください。",
rowCeilingNoteUnknownTotal: "最初の {{shown}} 件を表示しています。フィルターを絞り込んでください。",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/ko.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -129,7 +129,10 @@ const ko = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "전체 {{total}}개 중 처음 {{shown}}개를 표시합니다. 필터를 좁히세요.",
rowCeilingNoteUnknownTotal: "처음 {{shown}}개를 표시합니다. 필터를 좁히세요.",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/pt.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -128,7 +128,10 @@ const pt = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "Mostrando os primeiros {{shown}} de {{total}} registros. Restrinja o filtro.",
rowCeilingNoteUnknownTotal: "Mostrando os primeiros {{shown}} registros. Restrinja o filtro.",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/ru.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -135,7 +135,10 @@ const ru = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: "Показаны первые {{shown}} из {{total}} записей. Сузьте фильтр.",
rowCeilingNoteUnknownTotal: "Показаны первые {{shown}} записей. Сузьте фильтр.",
},
Expand Down
5 changes: 4 additions & 1 deletion packages/i18n/src/locales/zh.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -136,7 +136,10 @@ const zh = {
// 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.
// eagerly loaded, and since objectui#7399 these bytes are budgeted by the
// `i18n-locales` chunk, not `framework` — a ceiling set 8,924 B above the
// baseline it was measured from, about sixty short keys' worth across ten
// locales. `pnpm check:eager-closure` prints the figure in force.
rowCeilingNote: '仅显示 {{total}} 条记录中的前 {{shown}} 条。请缩小筛选范围。',
rowCeilingNoteUnknownTotal: '仅显示前 {{shown}} 条记录。请缩小筛选范围。',
},
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
/**
* ObjectUI
* Copyright (c) 2024-present ObjectStack Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

/**
* objectui#7507 — the row-ceiling footnote must not outlive the fetch that
* raised it (objectui#7210's ceiling, ruling a′).
*
* `ObjectCalendar` has five `setData` paths. Four of them also reset
* `rowCeiling`; the external-`data` sync was the one that did not, so a
* component that first drew its OWN truncated fetch and then had a short
* `data` array handed to it kept showing "Showing the first 2000 of 9876
* records" over twelve rows. The note would be describing a result set that is
* no longer on screen — which is the same defect the ceiling exists to
* prevent, pointing the other way: not a silent cut, but a loud claim of one
* that did not happen.
*
* ⚠️ Latent, not reported. Today's only host that passes `data` is
* `ObjectView`, and it passes it from mount, so `hasExternalData` is true
* before the internal fetch can ever run and the stale state is unreachable.
* That is exactly why it is pinned rather than only fixed: nothing about the
* prop's contract says a host may not start without `data` and supply it
* later, and a latent defect with no test is one refactor away from being a
* reported one.
*
* REVERSE VERIFICATION — direction predicted before running: remove the
* `setRowCeiling({ truncated: false })` from the external-`data` effect in
* `ObjectCalendar` and this file goes red at the "the note is gone" assertion
* (the note keeps rendering, still naming 2000 and 9876), while the control
* case — a component that never truncated — stays green.
*/

import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { NON_GRID_ROW_CEILING } from '@object-ui/react';
import { ObjectCalendar } from './ObjectCalendar';

vi.mock('@object-ui/plugin-detail', () => ({
RecordDetailDrawer: () => null,
deriveRecordPageHref: () => null,
}));

// Same reason as the sibling rowCeiling pin: the month grid draws at most four
// events per day cell, so the count has to come off an attribute.
vi.mock('./CalendarView', async (importOriginal) => {
const actual = await importOriginal<any>();
return {
...actual,
CalendarView: ({ events }: any) => (
<div data-testid="calendar-view" data-event-count={String(events.length)} />
),
};
});

const TOTAL_ROWS = 9876;
const NOW = new Date();

function makeRows(n: number, offset = 0) {
return Array.from({ length: n }, (_, i) => {
const d = new Date(NOW.getFullYear(), NOW.getMonth(), ((i + offset) % 28) + 1);
const iso = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(
d.getDate(),
).padStart(2, '0')}`;
return {
id: String(i + offset + 1),
subject: `Event ${i + offset + 1}`,
start_at: iso,
end_at: iso,
};
});
}

function makeDataSource(storeSize: number) {
const store = makeRows(storeSize);
return {
find: vi.fn(async (_resource: string, params: any) => {
const top = typeof params?.$top === 'number' ? params.$top : store.length;
return { data: store.slice(0, top), total: store.length };
}),
findOne: vi.fn(),
create: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
getObjectSchema: vi.fn(async () => ({
name: 'event',
fields: {
id: { name: 'id', type: 'text' },
subject: { name: 'subject', type: 'text' },
start_at: { name: 'start_at', type: 'date' },
end_at: { name: 'end_at', type: 'date' },
},
})),
} as any;
}

const schema: any = {
type: 'calendar',
objectName: 'event',
calendar: { titleField: 'subject', startDateField: 'start_at', endDateField: 'end_at' },
data: { provider: 'object', object: 'event' },
};

describe('objectui#7507 — an external `data` hand-off clears the row-ceiling note', () => {
it('a truncated own fetch, then a short external `data`: the note is GONE', async () => {
const dataSource = makeDataSource(TOTAL_ROWS);
const { rerender } = render(<ObjectCalendar schema={schema} dataSource={dataSource} />);

// Live control: the note really was raised by this component's own fetch,
// so its absence below is a change of state and not a query that never
// matched anything.
const note = await screen.findByRole('note');
expect(note.textContent).toContain(String(NON_GRID_ROW_CEILING));
expect(note.textContent).toContain(String(TOTAL_ROWS));
await waitFor(() =>
expect(screen.getByTestId('calendar-view').getAttribute('data-event-count')).toBe(
String(NON_GRID_ROW_CEILING),
),
);

const short = makeRows(12);
rerender(<ObjectCalendar schema={schema} dataSource={dataSource} data={short} />);

await waitFor(() =>
expect(screen.getByTestId('calendar-view').getAttribute('data-event-count')).toBe('12'),
);
expect(screen.queryByRole('note')).toBeNull();
});

it('external `data` from mount: still no note, and the rows are the parent’s', async () => {
const dataSource = makeDataSource(TOTAL_ROWS);
const short = makeRows(9);

render(<ObjectCalendar schema={schema} dataSource={dataSource} data={short} />);

await waitFor(() =>
expect(screen.getByTestId('calendar-view').getAttribute('data-event-count')).toBe('9'),
);
// The parent owns the query here, so this component never fetched and has
// nothing to report a ceiling about.
expect(dataSource.find).not.toHaveBeenCalled();
expect(screen.queryByRole('note')).toBeNull();
});
});
Loading
Loading