Skip to content
This repository was archived by the owner on Oct 26, 2026. It is now read-only.

Commit c1763e5

Browse files
fix(plugin-gantt,fields): the gantt numeric tooltip row and NumberCellRenderer read the field width through resolveFieldScale (objectui#11254) (#11412)
Fixes #11254 Clause-②: no The two same-family carriers the director's ruling (card comment 5933283974) left on this card now read the field width through `resolveFieldScale` from `@objectstack/spec/data` (ruling A′, objectstack-ai/objectstack#19628): the gantt tooltip's numeric row and `NumberCellRenderer`. Out, as ruled: `formatPercent`'s `precision = 0` default, `progress`, `GroupRow`'s `toFixed(2)`, and the tile's `sum` / `avg` over an undeclared `number` (ruling A, deferred to its trigger). None of them is touched. ## What changes - **`packages/plugin-gantt/src/ObjectGantt.tsx`**, `formatFieldValue`, the `number` / `integer` / `float` / `decimal` case. It called `formatNumber(Number(value), undefined, displayLocale)`. The `undefined` took `formatNumber`'s two-place parameter default, so every numeric row read two decimals whatever the field declared. The row now resolves `resolveFieldScale({ type: def?.type, scale: def?.scale })` and renders through `formatDisplayNumber` (from `@object-ui/i18n`, already a dependency) in the two arms `NumberCellRenderer` uses. A resolved width fixes both bounds. `undefined` means no fixed width: minimum 0, maximum 20, the value's natural precision. `formatNumber` is no longer imported here. **Width only:** no `scale` reaches the grouping policy on this row, so it groups exactly as before (see Acceptance notes). - **`packages/fields/src/index.tsx`**, `NumberCellRenderer`. `typeof numField.scale === 'number' ? numField.scale : undefined` becomes `resolveFieldScale({ type: numField.type, scale: numField.scale })`. The `minimumFractionDigits: scale ?? 0` / `maximumFractionDigits: scale ?? 20` pair becomes the same explicit two-arm spread, so no `?? N` spelling sits beside the resolver. A well-formed or absent declaration renders byte-identically. The resolved value feeds `formatDisplayNumber`'s `scale` (the grouping-policy input) exactly as the raw one did. - **Two existing gantt pins rode on the retired two-place constant** (fixture triage, one disposition each): - `ObjectGantt.test.tsx`, `resolves and formats tooltipFields per record`: `effort` declares no `scale`, so its expectation moves from `Effort=12.00` to `Effort=12`. A comment beside it names the ruling. - `ObjectGantt.numberLocale.test.tsx`: that file measures the locale, not the width. Its `qty` fixture now **declares** `scale: 2`, so every locale expectation (`1.234,50`, `1,234.50`) keeps its bytes through the declared path. The width itself is pinned in the new file. - Pins: `packages/plugin-gantt/src/ObjectGantt.numberResolvedWidth-11254.test.tsx` (6) and `packages/fields/src/__tests__/NumberCellRenderer.resolvedWidth-11254.test.tsx` (6). - Changesets (both `patch`): `.changeset/11254-gantt-number-resolved-width.md` (`@object-ui/plugin-gantt`) and `.changeset/11254-number-cell-resolved-width.md` (`@object-ui/fields`). No manifest moves. Both packages already declare `@objectstack/spec` `^17.5.0` and already imported `resolveFieldScale` on `main`. ## Premises measured first (the dispatch's Zone 2) 1. **Gantt row: confirmed.** At base `7728c67c86` the case returns `formatNumber(Number(value), undefined, displayLocale)`. `formatNumber(value, decimals = 2, locale)` sets `minimumFractionDigits` and `maximumFractionDigits` both to `decimals`, so `undefined` pads to two places. It **cannot** express "no fixed width": every input fixes both bounds to one number. The existing helper that can is `formatDisplayNumber`, which `formatNumber` and `NumberCellRenderer` both call. No new helper and no new export. 2. **The resolver, read in the installed `@objectstack/spec` 17.5.0** (`dist/data/index.mjs`, `resolveFieldScale` and `ABSENT_SCALE_BY_TYPE`). A declared `scale` is returned when `Number.isInteger(scale)` and it is at least 0. Otherwise the type's row applies, and the table has **one row, `percent: 0`**. So `number`, `integer`, `float` and `decimal` answer the declared width when well-formed and `undefined` otherwise. A malformed `scale: "2"`, `1.5` or `-1` answers `undefined`. There is no clamp, so `101` comes back verbatim; `FieldSchema` refuses it at authoring. **The `integer` answer is `undefined`**, so the gantt `integer` row changes too: a stored `7` read `7.00` and now reads `7`. 3. **`NumberCellRenderer`: confirmed, with a correction to the example.** It differs from the resolver only for a malformed declaration that is still a JS number. The string `"2"` is **not** such a case: `typeof "2"` is not `'number'`, so both reads answer `undefined`, and it is pinned as a control. The cases that differ are: - `scale: 1.5`: `Intl` floors it, so `3.14159` read `3.1`; - `scale: -1`: `Intl` throws `RangeError: minimumFractionDigits value is out of range.` and takes the row down. `NaN` behaves the same way. Grouping: every malformed value is not `0`, so the old read grouped, and the resolver's `undefined` groups too. A declared `0` stays `0` and renders ungrouped. Both halves are pinned as controls. ## Pins: red first, then green - **Red at `cee8642`** (pins only, base source): `pnpm exec vitest run --maxWorkers=2` over the two new files gave `Tests 7 failed | 5 passed (12)`, VERDICT command-exit 1, and every direction matched the one predicted in the file headers: - gantt `scale: 3`: `Expected "1.500" Received "1.50"`; - gantt `scale: 0`: `Expected "2" Received "1.50"`; - gantt undeclared: `Expected "1.5" Received "1.50"`; - gantt `integer`: `Expected "7" Received "7.00"`; - gantt malformed `"2"`: `Expected "1.5" Received "1.50"`; - cell `scale: 1.5`: `Expected "3.14159" Received "3.1"`; - cell `scale: -1`: `RangeError: minimumFractionDigits value is out of range.` The green ones were the gantt `scale: 2` row (the old default equals the declaration) and the cell controls (`"2"`, `2`, absent, and the `scale: 0` / absent grouping pair). - **Green at `b4f818f`**: the two new files plus `ObjectGantt.test.tsx`, `ObjectGantt.numberLocale.test.tsx` and `ObjectGantt.percentResolvedWidth-11254.test.tsx` gave `Test Files 5 passed (5)`, `Tests 55 passed (55)`, VERDICT command-exit 0. - The gantt rows also assert agreement with the **real** `NumberCellRenderer` for the same value and field, beside absolute bytes. The vitest config aliases `@object-ui/*` to `src`, and neither package had a `dist` at the time, so both legs measured source. The red leg is the before-state of the ablation: the same pins, with no line of the fix present. ## Gates, all at head `b4f818f` - Build closure: `pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-gantt^...' --filter '@object-ui/fields^...' build` built `Scope: 13 of 47 workspace projects`, VERDICT command-exit 0. Then `--filter @object-ui/plugin-gantt --filter @object-ui/fields build`, VERDICT command-exit 0. - `type-check`: `--filter @object-ui/plugin-gantt --filter @object-ui/fields type-check` echoed `packages/fields type-check: Done` and `packages/plugin-gantt type-check: Done`, VERDICT command-exit 0. `tsc -p tsconfig.test.json --listFilesOnly` lists both new pins and both moved files. - Suites: `pnpm exec vitest run --maxWorkers=2 packages/plugin-gantt/ packages/fields/` gave `Test Files 325 passed | 2 skipped (327)` and `Tests 4291 passed | 121 skipped (4412)`, VERDICT command-exit 0 (shared box). - Spec floors: `check-spec-range-floors.mjs` `analyze`, scoped to the two packages over fresh `dist`, gave `findings 0`. Control: the same run with both floors at `^17.0.0` gave `floor-too-low` naming `resolveFieldScale` for both packages (3 findings; fields adds `InlineGridColumn`). - eslint over the 6 changed lintable files (`--format json`) gave 0 errors. Warnings on the four edited files equal BASE through `--stdin`: `fields/src/index.tsx` 138/138, `ObjectGantt.tsx` 81/81, `ObjectGantt.test.tsx` 30/30, `ObjectGantt.numberLocale.test.tsx` 5/5. The new pins carry only `no-explicit-any` (2 and 6). Narrowing proof: (i) the population is the 6 changed lintable files, and none is ignored by `eslint.config.js`; (ii) the JSON output holds 6 results; (iii) `eslint.config.js` sets no `parserOptions.project` or `projectService`, so type-aware linting is off and this diff cannot move an untouched file's verdict. - Exit 0: `check-changeset-presence` (`6 source file(s) of 2 released package(s) changed, and this change declares 2 changeset(s)`), `changeset:check`, `check:control-bytes`, `check:new-line-citations` (`VERDICT new-cross-file-line-citations: 0 new citation(s)`), `check:pending-changeset-literals`, `check:phantom-deps`, `check:unused-deps`, `check:spec-symbols`, `check:unreferenced-sources`, `check:test-path-roots`, `check:vi-mock-specifiers`, `check:vi-mock-inherit`, `check:vi-mock-override-shape`, and `check:changeset-claims` (report-only; see Acceptance notes). - NOT MEASURED: the full-workspace `pnpm check:spec-floors` (reason: it needs every package's `dist`, and only the two changed packages and their closure were built; the scoped run and its control are above), and the full lint and test farm (reason: CI owns it). ## Acceptance notes - **The gantt tooltip's numeric row still ignores the grouping policy** (the ordinal no-grouping rule for a declared `scale: 0`, objectui#4033, and the authored `useGrouping`, objectui#11026). A `scale: 0` year reads `2,026` in the tooltip and `2026` in the list cell. That was already so before this PR (it read `2,026.00`). It is a different class from this card's width, so it is left as is and recorded here. No first-party `tooltipFields` producer over a `number` field was found. Carrier: none. - The pending `.changeset/10071-number-scale-ceiling.md` says "In-range, negative and non-integer widths are unchanged". That is true of the change it describes (the retired clamp). It ships in the same release as this PR's changeset, which says those widths now count as no declaration in the number cell. The two read together without contradiction, but the seat may add a dated note there if it judges the release text ambiguous. Not edited here. - The `scale` above 100 path is not changed: the resolver returns it verbatim, and `FieldSchema` refuses it at authoring. - Local install: the container runs node `v22.22.0`, and `jsdom@30.0.1` declares `^22.22.2`. The root `.npmrc` sets `engine-strict=true`, so this worktree was installed with `pnpm install --config.engine-strict=false`. That is a one-off override on the command line; no tracked file changed. Implemented by the `domain:ui` seat 1 dev, branch `claude/issue-11254-number-faces-resolver`, session `https://claude.ai/code/session_01YLg8XqWGJ785fwQ5v4pH37`. --- _Generated by [Claude Code](https://claude.ai/code/session_01YLg8XqWGJ785fwQ5v4pH37)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 549aaa8 commit c1763e5

8 files changed

Lines changed: 369 additions & 12 deletions
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@object-ui/plugin-gantt': patch
3+
---
4+
5+
fix(plugin-gantt): a number row in the gantt tooltip shows the field's declared decimals, and none when it declares none
6+
7+
A tooltip row over a `number` field (and the `integer`, `float` and `decimal`
8+
spellings the tooltip groups with it) always read two decimals, whatever the field
9+
declared. A field declaring `scale: 3` read `1.50`, a field declaring `scale: 0` read
10+
`1.50`, and a `number` with no `scale` read `1.50` while the list cell for the same
11+
value read `1.5`. The row now reads the field's width through `resolveFieldScale`
12+
from `@objectstack/spec/data`, as the list cell does. A declared `scale` is that
13+
width. A field with no `scale` shows the value as stored (`1.5`, `12`, `7`), because
14+
the protocol gives these types no default width.
15+
16+
Only the decimals change. The row still uses the display locale, and it still groups
17+
thousands as before.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@object-ui/fields': patch
3+
---
4+
5+
fix(fields): the number cell ignores a malformed `scale` instead of flooring it or crashing
6+
7+
`NumberCellRenderer` took any JavaScript number in `scale` as the decimal width. A
8+
non-integer `scale: 1.5` rendered `3.14159` as `3.1`, a width nobody declared. A
9+
negative `scale: -1` made the cell throw a `RangeError`, which took the row down. The
10+
cell now reads the width through `resolveFieldScale` from `@objectstack/spec/data`,
11+
the function the percent cell, the detail view and the grid footer already use. A
12+
malformed declaration counts as no declaration, so the value shows as stored.
13+
14+
A well-formed `scale`, or no `scale` at all, renders exactly as before. The grouping
15+
rule is unchanged too: a declared `scale: 0` (a year, for example) still renders
16+
without thousands separators, and a field with no `scale` still groups.
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
/**
10+
* objectui#11254 — `NumberCellRenderer` reads its width through
11+
* `resolveFieldScale`, a carrier of ruling A′ (objectstack-ai/objectstack#19628),
12+
* instead of `typeof scale === 'number'`.
13+
*
14+
* The two reads agree for every WELL-FORMED declaration and for an absent one.
15+
* They part only on a malformed declaration that is still a JS number — the
16+
* resolver's door is `Number.isInteger(scale) && scale >= 0`, the one
17+
* `objectql`'s record validator applies, and anything else is "no declaration":
18+
* - `scale: 1.5` — `Intl` floored it to one place, so `3.14159` read `3.1`, a
19+
* width nobody declared, while the grid footer (already on the resolver)
20+
* read the same column at its natural precision;
21+
* - `scale: -1` — `Intl` refused it, so the cell threw a `RangeError` and
22+
* took its row down.
23+
* A string `scale: "2"` was already no declaration on both reads (a control).
24+
*
25+
* The resolved value also feeds the grouping POLICY (objectui#4033) the way
26+
* the raw one did: a declared `scale: 0` is an ordinal and renders ungrouped,
27+
* an absent one keeps grouping.
28+
*
29+
* ── Directions on the base tree (predicted before the first run) ──────────
30+
* `scale: 1.5` RED (`3.1` before)
31+
* `scale: -1` RED (RangeError before)
32+
* `scale: "2"` GREEN (no declaration on both reads)
33+
* `scale: 2` / absent GREEN
34+
* `scale: 0` year / absent year GREEN (ungrouped / grouped)
35+
*/
36+
37+
import { describe, it, expect } from 'vitest';
38+
import React from 'react';
39+
import { render } from '@testing-library/react';
40+
import { LocalizationProvider } from '@object-ui/i18n';
41+
import { NumberCellRenderer } from '../index';
42+
43+
const cellText = (value: unknown, field: Record<string, unknown> = {}): string => {
44+
const { container, unmount } = render(
45+
<LocalizationProvider value={{ locale: 'en' }}>
46+
<NumberCellRenderer value={value as any} field={{ type: 'number', ...field } as any} />
47+
</LocalizationProvider>,
48+
);
49+
const text = container.textContent ?? '';
50+
unmount();
51+
return text;
52+
};
53+
54+
describe('NumberCellRenderer reads the resolved width (objectui#11254)', () => {
55+
it('a non-integer `scale: 1.5` is no declaration: natural precision, not a floored width', () => {
56+
expect(cellText(3.14159, { scale: 1.5 })).toBe('3.14159');
57+
});
58+
59+
it('a negative `scale: -1` is no declaration: the cell renders instead of throwing', () => {
60+
expect(cellText(3.14159, { scale: -1 })).toBe('3.14159');
61+
});
62+
63+
it('a string `scale: "2"` is no declaration (control)', () => {
64+
expect(cellText(3.14159, { scale: '2' })).toBe('3.14159');
65+
});
66+
67+
it('a declared `scale: 2` is that width (control)', () => {
68+
expect(cellText(3.14159, { scale: 2 })).toBe('3.14');
69+
});
70+
71+
it('an absent `scale` has no fixed width (control)', () => {
72+
expect(cellText(3.14159)).toBe('3.14159');
73+
});
74+
75+
it('a declared `scale: 0` still renders ungrouped, an absent one still groups (objectui#4033)', () => {
76+
expect(cellText(2026, { scale: 0 })).toBe('2026');
77+
expect(cellText(2026)).toBe('2,026');
78+
});
79+
});

‎packages/fields/src/index.tsx‎

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -784,7 +784,20 @@ export function NumberCellRenderer({ value, field }: CellRendererProps): React.R
784784
//
785785
// No clamp: the objectui#10071 one retired at the objectui#9808 SUNSET
786786
// (objectui#11073) — `@objectstack/spec` 17.5.0 refuses a `scale` above 100.
787-
const scale = typeof numField.scale === 'number' ? numField.scale : undefined;
787+
//
788+
// objectui#11254 — the width is read through `resolveFieldScale` (ruling A′
789+
// on objectstack-ai/objectstack#19628), the function the percent cell, the
790+
// detail chip, the grid footer and the gantt tooltip ask. For `number` the
791+
// protocol has no absent-`scale` row, so an undeclared field answers
792+
// `undefined`, no fixed width, exactly as before. The read changed only for a
793+
// malformed declaration that is still a JS number: `typeof === 'number'`
794+
// let `1.5` through for `Intl` to floor to one place (a width nobody
795+
// declared) and `-1` through for `Intl` to throw on, taking the row down.
796+
// The resolver's door is the record validator's own (`Number.isInteger` and
797+
// `>= 0`), and it answers "no declaration" for both. The resolved value feeds
798+
// the grouping policy exactly as the raw one did: a declared `0` stays `0`.
799+
// ⛔ No `?? N` beside this call: `undefined` is the protocol's answer.
800+
const scale = resolveFieldScale({ type: numField.type, scale: numField.scale });
788801
// The author's digit-grouping hint (objectui#11026): `FieldSchema.useGrouping`,
789802
// declared on `NumberFieldMetadata`, so it is read off the typed `field` and
790803
// not through `numField`. An authored boolean overrides the `scale` heuristic
@@ -796,13 +809,17 @@ export function NumberCellRenderer({ value, field }: CellRendererProps): React.R
796809
? field.useGrouping
797810
: undefined;
798811
const num = Number(safe);
812+
// Two arms, spelled out: a resolved width is fixed at both bounds; no width
813+
// is the value's natural precision (the minimum 0 / maximum 20 described
814+
// above), which is not a width of its own.
799815
const formatted = !isNaN(num)
800816
? formatDisplayNumber(num, {
801817
locale,
802818
scale,
803819
useGrouping,
804-
minimumFractionDigits: scale ?? 0,
805-
maximumFractionDigits: scale ?? 20,
820+
...(scale === undefined
821+
? { minimumFractionDigits: 0, maximumFractionDigits: 20 }
822+
: { minimumFractionDigits: scale, maximumFractionDigits: scale }),
806823
})
807824
: String(safe);
808825

‎packages/plugin-gantt/src/ObjectGantt.numberLocale.test.tsx‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,14 @@ const OBJECT_SCHEMA = {
154154
name: { type: 'text' },
155155
start_date: { type: 'date' },
156156
end_date: { type: 'date' },
157-
qty: { type: 'number', label: 'Qty' },
157+
// `scale: 2` is DECLARED since objectui#11254. This file measures the
158+
// locale, not the width: before, the row padded every number to two places
159+
// whatever it declared, and these expectations rode on that constant. Under
160+
// ruling A′ (objectstack-ai/objectstack#19628) an undeclared `number` has
161+
// no fixed width, so the two places are now the field's own, and every
162+
// expectation below keeps its bytes. The width itself is pinned in
163+
// `ObjectGantt.numberResolvedWidth-11254.test.tsx`.
164+
qty: { type: 'number', label: 'Qty', scale: 2 },
158165
amount: { type: 'currency', label: 'Amount', currency: 'EUR' },
159166
ratio: { type: 'percent', label: 'Ratio' },
160167
due_date: { type: 'date', label: 'Due' },
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
/**
10+
* objectui#11254 — the gantt tooltip's numeric row (`number` / `integer` /
11+
* `float` / `decimal`) reads its width through `resolveFieldScale`, a carrier
12+
* of ruling A′ (objectstack-ai/objectstack#19628).
13+
*
14+
* `formatFieldValue` handed `formatNumber` an `undefined` width, so the
15+
* function's own parameter default (two places) decided: every numeric field
16+
* read two decimals in the tooltip, whatever it declared. A declared `scale: 3`
17+
* read `1.50`, a declared `scale: 0` read `1.50`, and an undeclared `number`
18+
* read `1.50` beside a list cell reading `1.5`. Under A′ a `number` has no
19+
* absent-`scale` row, so absent means NO fixed width (the value's natural
20+
* precision), and a declared width is that width.
21+
*
22+
* The agreement rows render the REAL list cell (`NumberCellRenderer`) for the
23+
* same value and field; the absolute-byte expectations guard against a joint
24+
* move of both faces.
25+
*
26+
* ── Directions on the base tree (predicted before the first run) ──────────
27+
* `scale: 3` RED (`1.50` before)
28+
* `scale: 2` GREEN (the old default equals the declaration)
29+
* `scale: 0` RED (`1.50` before)
30+
* no `scale` RED (`1.50` before)
31+
* `integer`, no `scale` RED (`7.00` before; the protocol has no row)
32+
* malformed `scale: "2"` RED (`1.50` before; no declaration, no width)
33+
*
34+
* Harness: the GanttView stub and the module-constant schema / data source of
35+
* `ObjectGantt.numberLocale.test.tsx`, for the reasons stated there.
36+
*/
37+
38+
import React from 'react';
39+
import { describe, it, expect, vi, afterEach } from 'vitest';
40+
import { render, screen, waitFor, cleanup } from '@testing-library/react';
41+
import { I18nProvider, LocalizationProvider } from '@object-ui/i18n';
42+
import { NumberCellRenderer } from '@object-ui/fields';
43+
import { ObjectGantt } from './ObjectGantt';
44+
import type { DataSource } from '@object-ui/types';
45+
46+
vi.mock('./GanttView', () => ({
47+
GanttView: ({ tasks }: any) => (
48+
<div data-testid="gantt-view">
49+
{tasks.map((t: any) => (
50+
<div key={t.id} data-testid="gantt-task">
51+
<span>{t.title}</span>
52+
{t.fields ? (
53+
<div data-testid={`gv-fields-${t.id}`}>
54+
{t.fields.map((f: any, i: number) => (
55+
<span key={i} data-testid={`gv-field-${t.id}-${i}`}>{f.value}</span>
56+
))}
57+
</div>
58+
) : null}
59+
</div>
60+
))}
61+
</div>
62+
),
63+
}));
64+
65+
const STORED = 1.5;
66+
const STORED_INTEGER = 7;
67+
68+
const FIELDS = {
69+
scale3: { type: 'number', label: 'Scale 3', scale: 3 },
70+
scale2: { type: 'number', label: 'Scale 2', scale: 2 },
71+
scale0: { type: 'number', label: 'Scale 0', scale: 0 },
72+
undeclared: { type: 'number', label: 'Undeclared' },
73+
integer: { type: 'integer', label: 'Integer' },
74+
malformed: { type: 'number', label: 'Malformed', scale: '2' },
75+
} as const;
76+
77+
/** Tooltip order, and each row's stored value. */
78+
const ORDER = ['scale3', 'scale2', 'scale0', 'undeclared', 'integer', 'malformed'] as const;
79+
const VALUE: Record<(typeof ORDER)[number], number> = {
80+
scale3: STORED,
81+
scale2: STORED,
82+
scale0: STORED,
83+
undeclared: STORED,
84+
integer: STORED_INTEGER,
85+
malformed: STORED,
86+
};
87+
88+
const ROWS = [
89+
{
90+
id: '1',
91+
name: 'Task 1',
92+
start_date: '2024-01-01',
93+
end_date: '2024-01-10',
94+
...VALUE,
95+
},
96+
];
97+
98+
const OBJECT_SCHEMA = {
99+
fields: {
100+
name: { type: 'text' },
101+
start_date: { type: 'date' },
102+
end_date: { type: 'date' },
103+
...FIELDS,
104+
},
105+
};
106+
107+
const DATA_SOURCE: DataSource = {
108+
find: vi.fn().mockResolvedValue({ data: ROWS }),
109+
findOne: vi.fn(),
110+
create: vi.fn(),
111+
update: vi.fn(),
112+
delete: vi.fn(),
113+
getObjectSchema: vi.fn().mockResolvedValue(OBJECT_SCHEMA),
114+
} as any;
115+
116+
const SCHEMA: any = {
117+
type: 'gantt',
118+
gantt: {
119+
titleField: 'name',
120+
startDateField: 'start_date',
121+
endDateField: 'end_date',
122+
tooltipFields: [...ORDER],
123+
},
124+
data: { provider: 'object', object: 'tasks' },
125+
};
126+
127+
function Providers({ children }: { children: React.ReactNode }) {
128+
return (
129+
<I18nProvider config={{ defaultLanguage: 'en', detectBrowserLanguage: false }} persistLanguage={false}>
130+
<LocalizationProvider value={{ locale: 'en' }}>{children}</LocalizationProvider>
131+
</I18nProvider>
132+
);
133+
}
134+
135+
/** What the list cell renders for `key`'s stored value on its field. */
136+
function cellText(key: (typeof ORDER)[number]): string {
137+
const { container, unmount } = render(
138+
<Providers>
139+
<NumberCellRenderer value={VALUE[key]} field={FIELDS[key] as any} />
140+
</Providers>,
141+
);
142+
const text = container.textContent ?? '';
143+
unmount();
144+
return text;
145+
}
146+
147+
async function tooltipRow(key: (typeof ORDER)[number]): Promise<string> {
148+
render(
149+
<Providers>
150+
<ObjectGantt schema={SCHEMA} dataSource={DATA_SOURCE} />
151+
</Providers>,
152+
);
153+
await waitFor(() => expect(screen.getByTestId('gv-fields-1')).toBeDefined());
154+
return screen.getByTestId(`gv-field-1-${ORDER.indexOf(key)}`).textContent ?? '';
155+
}
156+
157+
afterEach(() => cleanup());
158+
159+
describe('the gantt tooltip numeric row reads the resolved width (objectui#11254)', () => {
160+
it('a declared `scale: 3` reads three places, as the list cell does', async () => {
161+
const expected = cellText('scale3');
162+
expect(expected).toBe('1.500');
163+
expect(await tooltipRow('scale3')).toBe(expected);
164+
});
165+
166+
it('a declared `scale: 2` reads two places, as the list cell does', async () => {
167+
const expected = cellText('scale2');
168+
expect(expected).toBe('1.50');
169+
expect(await tooltipRow('scale2')).toBe(expected);
170+
});
171+
172+
it('a declared `scale: 0` reads no places, as the list cell does', async () => {
173+
const expected = cellText('scale0');
174+
expect(expected).toBe('2');
175+
expect(await tooltipRow('scale0')).toBe(expected);
176+
});
177+
178+
it('an undeclared `number` has no fixed width: its natural precision, as the list cell reads', async () => {
179+
const expected = cellText('undeclared');
180+
expect(expected).toBe('1.5');
181+
expect(await tooltipRow('undeclared')).toBe(expected);
182+
});
183+
184+
it('an undeclared `integer` has no protocol row either: no padded places', async () => {
185+
const expected = cellText('integer');
186+
expect(expected).toBe('7');
187+
expect(await tooltipRow('integer')).toBe(expected);
188+
});
189+
190+
it('a malformed `scale: "2"` is no declaration: no fixed width', async () => {
191+
const expected = cellText('malformed');
192+
expect(expected).toBe('1.5');
193+
expect(await tooltipRow('malformed')).toBe(expected);
194+
});
195+
});

‎packages/plugin-gantt/src/ObjectGantt.test.tsx‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,12 @@ describe('ObjectGantt', () => {
138138
expect(screen.getByTestId('gv-field-1-1').textContent).toBe('Status=In Progress');
139139
// Date field formatted (not the raw ISO string).
140140
expect(screen.getByTestId('gv-field-1-2').textContent).not.toContain('2024-01-05');
141-
// Number field formatted.
142-
expect(screen.getByTestId('gv-field-1-3').textContent).toBe('Effort=12.00');
141+
// Number field formatted. MOVED by objectui#11254: `effort` declares no
142+
// `scale`, and under ruling A′ (objectstack-ai/objectstack#19628) an
143+
// undeclared `number` has no fixed width, so the stored `12` reads `12`
144+
// (as the list cell reads it), not the `12.00` a constant two-place
145+
// default padded it to.
146+
expect(screen.getByTestId('gv-field-1-3').textContent).toBe('Effort=12');
143147
});
144148

145149
it('formats a multi-value lookup (array of records) by joining display names', async () => {

0 commit comments

Comments
 (0)