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
60 changes: 60 additions & 0 deletions packages/fields/src/__tests__/NumberCellRenderer.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* 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.
*/

import { describe, it, expect } from 'vitest';
import React from 'react';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import { NumberCellRenderer } from '../index';

const renderNumber = (value: unknown, field: Record<string, unknown> = {}) =>
render(<NumberCellRenderer value={value as any} field={{ type: 'number', ...field } as any} />);

describe('NumberCellRenderer', () => {
it('does not pad trailing zeros using `precision` (total digits, not decimal places)', () => {
// Regression: a decimal(10, 0) column exposes precision: 10, which used to
// render `1` as "1.0000000000".
renderNumber(1, { precision: 10 });
expect(screen.getByText('1')).toBeInTheDocument();
});

it('renders an integer without decimals when no scale is declared', () => {
renderNumber(16);
expect(screen.getByText('16')).toBeInTheDocument();
});

it('preserves the value\'s natural decimals when no scale is declared', () => {
renderNumber(16.5);
expect(screen.getByText('16.5')).toBeInTheDocument();
});

it('pads to a fixed number of decimals when `scale` is declared', () => {
renderNumber(16, { scale: 2 });
expect(screen.getByText('16.00')).toBeInTheDocument();
});

it('honours a scale of 3 with zero padding', () => {
renderNumber(3.14, { scale: 3 });
expect(screen.getByText('3.140')).toBeInTheDocument();
});

it('rounds to the declared scale', () => {
renderNumber(3.14159, { scale: 2 });
expect(screen.getByText('3.14')).toBeInTheDocument();
});

it('treats scale: 0 as an integer display', () => {
renderNumber(2, { scale: 0 });
expect(screen.getByText('2')).toBeInTheDocument();
});

it('renders an empty indicator for nullish values', () => {
const { container } = renderNumber(null);
expect(container.textContent).not.toContain('0');
});
});
15 changes: 13 additions & 2 deletions packages/fields/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -453,10 +453,21 @@ export function NumberCellRenderer({ value, field }: CellRendererProps): React.R

const safe = coerceToSafeValue(value);
const numField = field as any;
const precision = numField.precision ?? 0;
// Decimal places come from `scale` (the `s` in a `decimal(p, s)` column),
// NOT `precision` — `precision` is the TOTAL digit count (`p`), and reading
// it here padded every value out to that width (e.g. `1` from a
// decimal(10, 0) column rendered as "1.0000000000"). When `scale` is
// declared we pad to it so a fixed display is honoured (e.g. an amount with
// scale 2 → "16.00", a field with scale 3 → "3.140"); when it is absent we
// keep the minimum at 0 so trailing zeros are trimmed and only cap the
// maximum (20 = Intl max) to preserve the value's natural precision.
const scale = typeof numField.scale === 'number' ? numField.scale : undefined;
const num = Number(safe);
const formatted = !isNaN(num)
? new Intl.NumberFormat('en-US', { minimumFractionDigits: precision, maximumFractionDigits: precision }).format(num)
? new Intl.NumberFormat('en-US', {
minimumFractionDigits: scale ?? 0,
maximumFractionDigits: scale ?? 20,
}).format(num)
: String(safe);

return <span className="tabular-nums">{formatted}</span>;
Expand Down
6 changes: 4 additions & 2 deletions packages/fields/src/widgets/NumberField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ export function NumberField({ value, onChange, field, readonly, ...props }: Fiel
}

const numberField = (field || (props as any).schema) as NumberFieldMetadata;
const precision = numberField?.precision;
// Step follows `scale` (decimal places), not `precision` (total digit count):
// a decimal(10, 0) column has 0 decimal places, so the input should step by 1.
const scale = numberField?.scale;

// Filter out non-DOM props
const { inputType, ...domProps } = props as any;
Expand All @@ -29,7 +31,7 @@ export function NumberField({ value, onChange, field, readonly, ...props }: Fiel
}}
placeholder={numberField?.placeholder}
disabled={readonly || domProps.disabled}
step={precision ? Math.pow(10, -precision) : 'any'}
step={scale ? Math.pow(10, -scale) : 'any'}
/>
);
}
1 change: 1 addition & 0 deletions packages/plugin-detail/src/DetailSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ export const DetailSection: React.FC<DetailSectionProps> = ({
if (objectDefField.options && !enrichedField.options) enrichedField.options = objectDefField.options;
if (objectDefField.currency && !enrichedField.currency) enrichedField.currency = objectDefField.currency;
if (objectDefField.precision !== undefined && enrichedField.precision === undefined) enrichedField.precision = objectDefField.precision;
if ((objectDefField as any).scale !== undefined && (enrichedField as any).scale === undefined) (enrichedField as any).scale = (objectDefField as any).scale;
if (objectDefField.format && !enrichedField.format) enrichedField.format = objectDefField.format;
const refTarget = objectDefField.reference_to || objectDefField.reference;
if (refTarget && !enrichedField.reference_to) enrichedField.reference_to = refTarget;
Expand Down
1 change: 1 addition & 0 deletions packages/plugin-detail/src/HeaderHighlight.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export const HeaderHighlight: React.FC<HeaderHighlightProps> = ({
...(objectDefField?.options && { options: objectDefField.options }),
...(objectDefField?.currency && { currency: objectDefField.currency }),
...(objectDefField?.precision !== undefined && { precision: objectDefField.precision }),
...((objectDefField as any)?.scale !== undefined && { scale: (objectDefField as any).scale }),
...(objectDefField?.format && { format: objectDefField.format }),
};

Expand Down
1 change: 1 addition & 0 deletions packages/plugin-detail/src/RecordDetailDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ export function RecordDetailDrawer({
options: def.options,
currency: def.currency,
precision: def.precision,
scale: (def as any).scale,
format: def.format,
reference_to: def.reference_to ?? def.referenceTo ?? def.target,
reference_field: def.reference_field ?? def.referenceField,
Expand Down
1 change: 1 addition & 0 deletions packages/plugin-detail/src/RelatedList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,7 @@ export const RelatedList: React.FC<RelatedListProps> = ({
...((lookupOptions || def.options) && { options: lookupOptions || def.options }),
...(def.currency && { currency: def.currency }),
...(def.precision !== undefined && { precision: def.precision }),
...((def as any).scale !== undefined && { scale: (def as any).scale }),
...(def.format && { format: def.format }),
...((def.reference_to || def.reference) && { reference_to: def.reference_to || def.reference }),
...(def.reference_field && { reference_field: def.reference_field }),
Expand Down
3 changes: 3 additions & 0 deletions packages/plugin-grid/src/ObjectGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -894,6 +894,7 @@ export const ObjectGrid: React.FC<ObjectGridProps> = ({
if (objectDefField.label) fieldMeta.label = objectDefField.label;
if (objectDefField.currency) fieldMeta.currency = objectDefField.currency;
if (objectDefField.precision !== undefined) fieldMeta.precision = objectDefField.precision;
if ((objectDefField as any).scale !== undefined) (fieldMeta as any).scale = (objectDefField as any).scale;
if (objectDefField.format) fieldMeta.format = objectDefField.format;
if (objectDefField.options) fieldMeta.options = translateOptions(schema.objectName, col.field, objectDefField.options);
}
Expand Down Expand Up @@ -1051,6 +1052,7 @@ export const ObjectGrid: React.FC<ObjectGridProps> = ({
if (fieldDef.label) fieldMeta.label = fieldDef.label;
if (fieldDef.currency) fieldMeta.currency = fieldDef.currency;
if (fieldDef.precision !== undefined) fieldMeta.precision = fieldDef.precision;
if ((fieldDef as any).scale !== undefined) fieldMeta.scale = (fieldDef as any).scale;
if (fieldDef.format) fieldMeta.format = fieldDef.format;
if (fieldDef.options) fieldMeta.options = translateOptions(schema.objectName, fieldName, fieldDef.options);
}
Expand Down Expand Up @@ -1125,6 +1127,7 @@ export const ObjectGrid: React.FC<ObjectGridProps> = ({
if (fieldDef.label) fieldMeta.label = fieldDef.label;
if (fieldDef.currency) fieldMeta.currency = fieldDef.currency;
if (fieldDef.precision !== undefined) fieldMeta.precision = fieldDef.precision;
if ((fieldDef as any).scale !== undefined) fieldMeta.scale = (fieldDef as any).scale;
if (fieldDef.format) fieldMeta.format = fieldDef.format;
if (fieldDef.options) fieldMeta.options = translateOptions(schema.objectName, fieldName, fieldDef.options);
}
Expand Down
3 changes: 3 additions & 0 deletions packages/types/src/field-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,10 @@ export interface NumberFieldMetadata extends BaseFieldMetadata {
type: 'number';
min?: number;
max?: number;
/** Total number of digits (the `p` in a `decimal(p, s)` column). Not a display setting. */
precision?: number;
/** Number of decimal places to display (the `s` in a `decimal(p, s)` column). */
scale?: number;
step?: number;
}

Expand Down
Loading