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
17 changes: 17 additions & 0 deletions .changeset/27355-console-walkthrough-rows.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
'@object-ui/components': patch
'@object-ui/plugin-list': patch
'@object-ui/plugin-detail': patch
'@object-ui/i18n': patch
---

fix(ui): four KPI tiles sit two across on a phone, a calendar's footer says its count is the loaded dates', and the record not-found page says the record may also be one you cannot read

Clause-②: no
Metadata-protocol: none

Three console rows from a full HotCRM browser pass:

- **A layout `grid` with a bare `columns` of 4 or more draws two tiles across on a phone** (below `sm`), then the authored count from `md`. A home page's KPI strip (`columns: 4` over `object-metric` tiles) took a row per tile and filled the first screen; it now reads as the dashboard's own phone layout for metric tiles. A bare 2 or 3 still stacks on a phone and pairs from `sm`, and an authored breakpoint object (`columns: { xs: 1, md: 4 }`) is drawn as written.
- **A calendar view's footer says `N records loaded for these dates`.** The calendar fetches the window of dates it draws, so the footer's count is that window's records, not the view's total; it read "N records", the words a grid uses for the whole view. A grid, a board and a calendar over rows handed in whole keep "N records". New count family `list.calendarRecordCount` in all ten packs.
- **The record not-found page says the record "does not exist, may have been deleted, or you may not have access to it".** The server answers a record the reader may not see the same as a missing one (`404 RECORD_NOT_FOUND`, so existence is never revealed), so the page cannot tell the two apart and now names both. `empty.recordNotFoundDescription` and `detail.recordNotFoundDescription` change in all ten packs.
9 changes: 5 additions & 4 deletions ui/examples/schema-catalog/test/grid-columns-key.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,20 +129,21 @@ describe('repaired grid examples render the responsive ramp (#4001)', () => {
'components-data-display-statistic/metrics-grid',
'grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4',
],
// columns: 4 — was `grid grid-cols-2 gap-4`
// columns: 4 — was `grid grid-cols-2 gap-4`; two across on a phone
// since objectstack#27355
[
'report/report-header-with-kpis',
'grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4',
'grid grid-cols-2 md:grid-cols-4 gap-4',
],
// columns: 4 + className passthrough — was `grid grid-cols-2 gap-4 mb-6`
[
'components-layout-page/full-dashboard',
'grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4 mb-6',
'grid grid-cols-2 md:grid-cols-4 gap-4 mb-6',
],
// columns: 4 with a non-default gap — was `grid grid-cols-2 gap-3 p-4`
[
'theme/semantic-color-palette',
'grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-3 p-4',
'grid grid-cols-2 md:grid-cols-4 gap-3 p-4',
],
// columns: 2 — the `div` → `grid` migration exemplar that
// `content/docs/components/basic/div.mdx` points authors at. Its column count
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ const ZH: I18nConfig = {
};

const NOT_FOUND_EN = 'Record not found';
const NOT_FOUND_DESCRIPTION_EN = 'The record you are looking for does not exist or may have been deleted.';
const NOT_FOUND_DESCRIPTION_EN = 'The record you are looking for does not exist, may have been deleted, or you may not have access to it.';
const NOT_FOUND_ZH = '未找到记录';

/** Open the record page; returns the adapter's `findOne`, spied, to count the page's reads. */
Expand Down
2 changes: 1 addition & 1 deletion ui/packages/app-shell/src/views/RecordDetailView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3643,7 +3643,7 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
<EmptyDescription>
{t('empty.recordNotFoundDescription', {
defaultValue:
'The record you are looking for does not exist or may have been deleted.',
'The record you are looking for does not exist, may have been deleted, or you may not have access to it.',
})}
</EmptyDescription>
</Empty>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,9 +168,13 @@ describe('ui:grid emits a column class for every breakpoint in the vocabulary (#
expect(classOf({ type: 'grid', columns: { xs: 1, xl: 5 }, gap: 4 })).toBe(
'grid grid-cols-1 xl:grid-cols-5 gap-4',
);
// A bare number keeps its mobile-first ramp (baseCols collapses to 1).
// A bare number keeps its mobile-first ramp (a phone draws two of four
// tiles across, objectstack#27355; two and three across collapse to 1).
expect(classOf({ type: 'grid', columns: 4, gap: 4 })).toBe(
'grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4',
'grid grid-cols-2 md:grid-cols-4 gap-4',
);
expect(classOf({ type: 'grid', columns: 3, gap: 4 })).toBe(
'grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4',
);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,11 +157,11 @@ describe('ui:grid — schema keys never reach the DOM (objectui#4787)', () => {

// Computed layout classes — the renderer's real output, merged with the
// authored className. A filter that dropped `className` would fail here.
// The bare `columns: 4` draws its mobile-first ramp: the canary's flat
// `mdColumns: 2` is read by nothing (objectui#11505), so `md` is the bare
// count, not the flat one.
// The bare `columns: 4` draws its mobile-first ramp (two across on a
// phone, objectstack#27355): the canary's flat `mdColumns: 2` is read by
// nothing (objectui#11505), so `md` is the bare count, not the flat one.
expect(el.className).toContain('grid');
expect(el.className).toContain('sm:grid-cols-2');
expect(el.className).toContain('grid-cols-2');
expect(el.className).toContain('md:grid-cols-4');
expect(el.className).not.toContain('md:grid-cols-2');
expect(el.className).toContain('gap-4');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ describe('`grid` reads and publishes no flat column key (objectui#11505)', () =>

it('a flat key no longer switches the bare count\'s ramp off', () => {
// Lit control for the rows above: the ramp is what a bare `4` draws.
expect(classesOf({ columns: 4 })).toBe('grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4');
expect(classesOf({ columns: 4, mdColumns: 2 })).toBe('grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4');
expect(classesOf({ columns: 4 })).toBe('grid grid-cols-2 md:grid-cols-4 gap-4');
expect(classesOf({ columns: 4, mdColumns: 2 })).toBe('grid grid-cols-2 md:grid-cols-4 gap-4');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* 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.
*/

/**
* A bare `columns` of four or more keeps two tiles across on a phone
* (objectstack#27355, row 11).
*
* A home page's KPI strip is a layout `grid` with a bare count (`columns: 4`)
* over `object-metric` tiles. The bare count's mobile-first ramp collapsed
* every N-across row to one column below `sm`, so four KPI tiles took a row
* each and filled a phone's first screen. A row of four or more holds tiles
* sized for a quarter of the row or less, so the ramp now keeps two across on
* a phone, as the dashboard does for its metric tiles.
*
* The emitted class string is the renderer's whole output on this path (it
* reads no viewport), so each case compares it whole — see
* `grid-breakpoint-columns-7097.test.tsx` for why that observation is sound.
*
* Module-scope import of the renderers, not `beforeAll` (AGENTS.md §测试纪律).
*/
import { describe, it, expect } from 'vitest';
import { render } from '@testing-library/react';
import '../renderers';
import { SchemaRenderer } from '@object-ui/react';

const classOf = (schema: unknown): string => {
const { container } = render(<SchemaRenderer schema={schema as never} />);
const el = container.firstElementChild as HTMLElement | null;
expect(el, 'the grid rendered no element').not.toBeNull();
return el!.className;
};

describe('a bare column count of four or more keeps two tiles across on a phone (objectstack#27355)', () => {
it('four across: two on a phone, four from md', () => {
expect(classOf({ type: 'grid', columns: 4, gap: 4 })).toBe('grid grid-cols-2 md:grid-cols-4 gap-4');
});

it.each([5, 6, 8, 12])('%i across: two on a phone, the authored count from md', (n) => {
expect(classOf({ type: 'grid', columns: n, gap: 4 })).toBe(`grid grid-cols-2 md:grid-cols-${n} gap-4`);
});

it('control: two and three across still stack on a phone and pair from sm', () => {
expect(classOf({ type: 'grid', columns: 2, gap: 4 })).toBe('grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 gap-4');
expect(classOf({ type: 'grid', columns: 3, gap: 4 })).toBe('grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4');
});

it('control: one across is one across everywhere', () => {
expect(classOf({ type: 'grid', columns: 1, gap: 4 })).toBe('grid grid-cols-1 gap-4');
});

it('control: an authored breakpoint object is drawn as written, at four across too', () => {
expect(classOf({ type: 'grid', columns: { xs: 1, md: 2, lg: 4 }, gap: 4 })).toBe(
'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4',
);
expect(classOf({ type: 'grid', columns: { xs: 1, md: 4 }, gap: 4 })).toBe('grid grid-cols-1 md:grid-cols-4 gap-4');
expect(classOf({ type: 'grid', columns: { md: 4 }, gap: 4 })).toBe('grid grid-cols-1 md:grid-cols-4 gap-4');
});
});
13 changes: 11 additions & 2 deletions ui/packages/components/src/renderers/layout/grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,19 @@ ComponentRegistry.register('grid',
// an N-across row doesn't render as unreadable slivers on a phone. Authors
// who pass the breakpoint object keep full control. The bare-number branch
// sets no breakpoint count, so the ramp's only condition is that branch.
//
// Four or more across is a row of tiles sized for a quarter of the row or
// less (a KPI strip): a phone keeps two side by side, the dashboard's own
// phone layout for its metric tiles, instead of one tile per row filling
// the first screen. Two and three across stack on a phone and pair from `sm`.
if (typeof schema.columns === 'number' && baseCols > 1) {
mdCols = baseCols;
smCols = Math.min(2, baseCols);
baseCols = 1;
if (baseCols >= 4) {
baseCols = 2;
} else {
smCols = 2;
baseCols = 1;
}
}

const gap = schema.gap ?? 4;
Expand Down
13 changes: 11 additions & 2 deletions ui/packages/i18n/src/locales/ar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -890,6 +890,15 @@ const ar = {
recordCount_few: "{{count}} سجلات",
recordCount_many: "{{count}} سجلًا",
recordCount_other: "{{count}} سجل",
// objectstack#27355 — a windowed calendar's footer: the count is the records
// loaded for the dates it draws, not the records the view matches.
calendarRecordCount: "عدد السجلات المحمّلة لهذه التواريخ: {{count}}",
calendarRecordCount_zero: "{{count}} سجل محمّل لهذه التواريخ",
calendarRecordCount_one: "{{count}} سجل محمّل لهذه التواريخ",
calendarRecordCount_two: "سجلان محمّلان لهذه التواريخ ({{count}})",
calendarRecordCount_few: "{{count}} سجلات محمّلة لهذه التواريخ",
calendarRecordCount_many: "{{count}} سجلًا محمّلًا لهذه التواريخ",
calendarRecordCount_other: "{{count}} سجل محمّل لهذه التواريخ",
addRecord: "إضافة سجل",
// objectui#12703 — the compact list's footer link to the object's list or the block's view.
viewAll: "عرض الكل",
Expand Down Expand Up @@ -1260,7 +1269,7 @@ const ar = {
nextRecord: "السجل التالي",
recordOf: "{{current}} من {{total}}",
recordNotFound: "لم يتم العثور على السجل",
recordNotFoundDescription: "السجل الذي تبحث عنه غير موجود أو ربما تم حذفه.",
recordNotFoundDescription: "السجل الذي تبحث عنه غير موجود، أو ربما تم حذفه، أو ربما لا تملك صلاحية الوصول إليه.",
goBack: "رجوع",
details: "التفاصيل",
related: "ذات صلة",
Expand Down Expand Up @@ -3434,7 +3443,7 @@ const ar = {
systemSettings: "إعدادات النظام",
back: "رجوع",
recordNotFound: "السجل غير موجود",
recordNotFoundDescription: "السجل الذي تبحث عنه غير موجود أو ربما تم حذفه.",
recordNotFoundDescription: "السجل الذي تبحث عنه غير موجود، أو ربما تم حذفه، أو ربما لا تملك صلاحية الوصول إليه.",
recordAccessDenied: "ليس لديك صلاحية الوصول إلى سجلات {{object}}",
recordAccessDeniedDescription: "ليس لديك إذن لعرض السجلات من هذا النوع. تواصل مع المسؤول إذا كنت تعتقد أنه يجب أن يكون لديك وصول.",
recordLoadFailed: "تعذّر تحميل هذا السجل",
Expand Down
9 changes: 7 additions & 2 deletions ui/packages/i18n/src/locales/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -793,6 +793,11 @@ const de = {
recordCount: "{{count}} Datensätze",
recordCount_one: "{{count}} Datensatz",
recordCount_other: "{{count}} Datensätze",
// objectstack#27355 — a windowed calendar's footer: the count is the records
// loaded for the dates it draws, not the records the view matches.
calendarRecordCount: "{{count}} Datensätze für diesen Zeitraum geladen",
calendarRecordCount_one: "{{count}} Datensatz für diesen Zeitraum geladen",
calendarRecordCount_other: "{{count}} Datensätze für diesen Zeitraum geladen",
addRecord: "Datensatz hinzufügen",
// objectui#12703 — the compact list's footer link to the object's list or the block's view.
viewAll: "Alle anzeigen",
Expand Down Expand Up @@ -1134,7 +1139,7 @@ const de = {
nextRecord: "Nächster Datensatz",
recordOf: "{{current}} von {{total}}",
recordNotFound: "Datensatz nicht gefunden",
recordNotFoundDescription: "Der gesuchte Datensatz existiert nicht oder wurde möglicherweise gelöscht.",
recordNotFoundDescription: "Der gesuchte Datensatz existiert nicht, wurde möglicherweise gelöscht, oder Sie haben keinen Zugriff darauf.",
goBack: "Zurück",
details: "Details",
related: "Verknüpft",
Expand Down Expand Up @@ -3185,7 +3190,7 @@ const de = {
systemSettings: "Systemeinstellungen",
back: "Zurück",
recordNotFound: "Datensatz nicht gefunden",
recordNotFoundDescription: "Der gesuchte Datensatz existiert nicht oder wurde möglicherweise gelöscht.",
recordNotFoundDescription: "Der gesuchte Datensatz existiert nicht, wurde möglicherweise gelöscht, oder Sie haben keinen Zugriff darauf.",
recordAccessDenied: "Kein Zugriff auf {{object}}-Datensätze",
recordAccessDeniedDescription: "Sie haben keine Berechtigung, Datensätze dieses Typs anzuzeigen. Wenden Sie sich an Ihren Administrator, wenn Sie Zugriff benötigen.",
recordLoadFailed: "Dieser Datensatz konnte nicht geladen werden",
Expand Down
9 changes: 7 additions & 2 deletions ui/packages/i18n/src/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1004,6 +1004,11 @@ const en = {
recordCount: '{{count}} records',
recordCount_one: '{{count}} record',
recordCount_other: '{{count}} records',
// objectstack#27355 — a windowed calendar's footer: the count is the records
// loaded for the dates it draws, not the records the view matches.
calendarRecordCount: '{{count}} records loaded for these dates',
calendarRecordCount_one: '{{count}} record loaded for these dates',
calendarRecordCount_other: '{{count}} records loaded for these dates',
addRecord: 'Add record',
// objectui#12703 — the compact list's footer link to the object's list or the block's view.
viewAll: 'View all',
Expand Down Expand Up @@ -1479,7 +1484,7 @@ const en = {
nextRecord: 'Next record',
recordOf: '{{current}} of {{total}}',
recordNotFound: 'Record not found',
recordNotFoundDescription: 'The record you are looking for does not exist or may have been deleted.',
recordNotFoundDescription: 'The record you are looking for does not exist, may have been deleted, or you may not have access to it.',
goBack: 'Go back',
details: 'Details',
related: 'Related',
Expand Down Expand Up @@ -3749,7 +3754,7 @@ const en = {
objectNotFoundDescription: 'Object "{{name}}" definition missing. Check your configuration or navigate back to select a valid object.',
interfacePageSourceMissing: 'This interface page references "{{name}}", which is not available.',
recordNotFound: 'Record not found',
recordNotFoundDescription: 'The record you are looking for does not exist or may have been deleted.',
recordNotFoundDescription: 'The record you are looking for does not exist, may have been deleted, or you may not have access to it.',
recordAccessDenied: 'You don’t have access to {{object}} records',
recordAccessDeniedDescription: 'You don’t have permission to view records of this type. Contact your administrator if you think you should have access.',
recordLoadFailed: 'Couldn’t load this record',
Expand Down
10 changes: 8 additions & 2 deletions ui/packages/i18n/src/locales/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -820,6 +820,12 @@ const es = {
recordCount_one: "{{count}} registro",
recordCount_many: "{{count}} de registros",
recordCount_other: "{{count}} registros",
// objectstack#27355 — a windowed calendar's footer: the count is the records
// loaded for the dates it draws, not the records the view matches.
calendarRecordCount: "{{count}} registros cargados para estas fechas",
calendarRecordCount_one: "{{count}} registro cargado para estas fechas",
calendarRecordCount_many: "{{count}} de registros cargados para estas fechas",
calendarRecordCount_other: "{{count}} registros cargados para estas fechas",
addRecord: "Agregar registro",
// objectui#12703 — the compact list's footer link to the object's list or the block's view.
viewAll: "Ver todo",
Expand Down Expand Up @@ -1167,7 +1173,7 @@ const es = {
nextRecord: "Siguiente registro",
recordOf: "{{current}} de {{total}}",
recordNotFound: "Registro no encontrado",
recordNotFoundDescription: "El registro que busca no existe o puede haber sido eliminado.",
recordNotFoundDescription: "El registro que busca no existe, puede haber sido eliminado o es posible que no tenga acceso a él.",
goBack: "Volver",
details: "Detalles",
related: "Relacionados",
Expand Down Expand Up @@ -3248,7 +3254,7 @@ const es = {
systemSettings: "Configuración del sistema",
back: "Atrás",
recordNotFound: "Registro no encontrado",
recordNotFoundDescription: "El registro que busca no existe o puede haber sido eliminado.",
recordNotFoundDescription: "El registro que busca no existe, puede haber sido eliminado o es posible que no tenga acceso a él.",
recordAccessDenied: "No tiene acceso a los registros de {{object}}",
recordAccessDeniedDescription: "No tiene permiso para ver registros de este tipo. Contacte a su administrador si cree que debería tener acceso.",
recordLoadFailed: "No se pudo cargar este registro",
Expand Down
Loading
Loading