@object-ui/fields 17.3.0 → 17.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1655 -0
- package/README.md +1 -12
- package/dist/FieldEditWidget.d.ts +23 -3
- package/dist/currency.d.ts +46 -0
- package/dist/field-type-alias.d.ts +35 -1
- package/dist/index.cjs +4 -4
- package/dist/index.css +651 -0
- package/dist/index.d.ts +474 -85
- package/dist/index.js +5418 -5023
- package/dist/widgets/AddressField.d.ts +4 -12
- package/dist/widgets/AutoNumberField.d.ts +1 -1
- package/dist/widgets/AvatarField.d.ts +1 -1
- package/dist/widgets/BooleanField.d.ts +1 -1
- package/dist/widgets/CapabilityMultiSelectField.d.ts +1 -1
- package/dist/widgets/CheckboxesField.d.ts +1 -1
- package/dist/widgets/CodeField.d.ts +1 -1
- package/dist/widgets/ColorField.d.ts +1 -1
- package/dist/widgets/CurrencyField.d.ts +1 -1
- package/dist/widgets/DateField.d.ts +1 -1
- package/dist/widgets/DateTimeField.d.ts +1 -1
- package/dist/widgets/EmailField.d.ts +1 -1
- package/dist/widgets/FileField.d.ts +18 -2
- package/dist/widgets/FilterConditionField.d.ts +28 -1
- package/dist/widgets/FormulaField.d.ts +1 -1
- package/dist/widgets/FullscreenFieldEditor.d.ts +49 -152
- package/dist/widgets/GeolocationField.d.ts +1 -1
- package/dist/widgets/GridField.d.ts +68 -6
- package/dist/widgets/ImageField.d.ts +1 -1
- package/dist/widgets/LocationField.d.ts +1 -1
- package/dist/widgets/LookupField.d.ts +1 -1
- package/dist/widgets/MasterDetailField.d.ts +1 -1
- package/dist/widgets/MultiSelectField.d.ts +1 -1
- package/dist/widgets/NumberField.d.ts +1 -1
- package/dist/widgets/ObjectField.d.ts +1 -1
- package/dist/widgets/ObjectRefField.d.ts +1 -1
- package/dist/widgets/OptionsEmptyState.d.ts +21 -1
- package/dist/widgets/PasswordField.d.ts +1 -1
- package/dist/widgets/PercentField.d.ts +1 -1
- package/dist/widgets/PhoneField.d.ts +1 -1
- package/dist/widgets/QRCodeField.d.ts +1 -1
- package/dist/widgets/RadioField.d.ts +1 -1
- package/dist/widgets/RatingField.d.ts +1 -1
- package/dist/widgets/RecipientPickerField.d.ts +1 -1
- package/dist/widgets/RecordPickerDialog.d.ts +34 -10
- package/dist/widgets/RichTextField.d.ts +37 -1
- package/dist/widgets/SelectField.d.ts +26 -6
- package/dist/widgets/SignatureField.d.ts +32 -2
- package/dist/widgets/SliderField.d.ts +37 -2
- package/dist/widgets/SummaryField.d.ts +1 -1
- package/dist/widgets/TagsField.d.ts +1 -1
- package/dist/widgets/TextAreaField.d.ts +6 -2
- package/dist/widgets/TextField.d.ts +1 -1
- package/dist/widgets/TimeField.d.ts +1 -1
- package/dist/widgets/UrlField.d.ts +1 -1
- package/dist/widgets/UserField.d.ts +1 -1
- package/dist/widgets/VectorField.d.ts +1 -1
- package/dist/widgets/_cell-renderer-bridge.d.ts +1 -1
- package/dist/widgets/address-format.d.ts +88 -0
- package/dist/widgets/file-size-guard.d.ts +41 -0
- package/dist/widgets/lookupColumnDisplay.d.ts +93 -0
- package/dist/widgets/nativeDateValue.d.ts +37 -0
- package/dist/widgets/richTextDisplay.d.ts +80 -0
- package/dist/widgets/toDomProps.d.ts +30 -10
- package/dist/widgets/toHostGroupProps.d.ts +189 -0
- package/dist/widgets/types.d.ts +8 -3
- package/dist/widgets/useRecordQuery.d.ts +7 -1
- package/package.json +17 -14
- package/dist/FieldEditWidget.test.d.ts +0 -8
- package/dist/__tests__/DateCellRenderer.test.d.ts +0 -0
- package/dist/__tests__/NumberCellRenderer.test.d.ts +0 -0
- package/dist/__tests__/cell-truncation.test.d.ts +0 -0
- package/dist/__tests__/field-carrier-sdui.test.d.ts +0 -0
- package/dist/__tests__/lookupCellDisplayField.test.d.ts +0 -0
- package/dist/__tests__/lookupCellDisplayName.test.d.ts +0 -0
- package/dist/__tests__/lookupCellReferenceKey.test.d.ts +0 -0
- package/dist/__tests__/registry-collision.test.d.ts +0 -0
- package/dist/__tests__/resolveFieldCurrency.test.d.ts +0 -1
- package/dist/__tests__/spec-symbol-batch7.test.d.ts +0 -8
- package/dist/__tests__/validation-feedback.test.d.ts +0 -0
- package/dist/__tests__/widget-aria-invalid-e2e.test.d.ts +0 -0
- package/dist/__tests__/widget-aria-invalid-registry-e2e.test.d.ts +0 -0
- package/dist/__tests__/widget-dom-leak-e2e.test.d.ts +0 -0
- package/dist/__tests__/widget-props-contract.test.d.ts +0 -8
- package/dist/coerce-safe-value.test.d.ts +0 -1
- package/dist/complex-widgets.test.d.ts +0 -1
- package/dist/datetime-widgets.test.d.ts +0 -1
- package/dist/field-type-coverage.test.d.ts +0 -1
- package/dist/standard-widgets.test.d.ts +0 -0
- package/dist/widgets/AddressField.uniqueIds.test.d.ts +0 -0
- package/dist/widgets/CapabilityMultiSelectField.test.d.ts +0 -0
- package/dist/widgets/CharacterCount.d.ts +0 -56
- package/dist/widgets/CheckboxesField.cascade.test.d.ts +0 -0
- package/dist/widgets/GeolocationField.uniqueIds.test.d.ts +0 -0
- package/dist/widgets/GridField.test.d.ts +0 -1
- package/dist/widgets/ImageLightbox.test.d.ts +0 -1
- package/dist/widgets/LookupField.dependsOn.test.d.ts +0 -8
- package/dist/widgets/LookupField.gateHintLabel.test.d.ts +0 -8
- package/dist/widgets/LookupField.hydration.test.d.ts +0 -1
- package/dist/widgets/LookupField.idField.test.d.ts +0 -1
- package/dist/widgets/MultiSelectField.cascade.test.d.ts +0 -0
- package/dist/widgets/OptionsEmptyState.no-provider.test.d.ts +0 -0
- package/dist/widgets/OptionsEmptyState.test.d.ts +0 -0
- package/dist/widgets/PeoplePicker.test.d.ts +0 -8
- package/dist/widgets/PeoplePickerComponents.test.d.ts +0 -8
- package/dist/widgets/RadioField.cascade.test.d.ts +0 -0
- package/dist/widgets/RecipientPickerField.test.d.ts +0 -0
- package/dist/widgets/RecordPickerDialog.filterOptions.test.d.ts +0 -8
- package/dist/widgets/RecordPickerDialog.selectLabel.test.d.ts +0 -8
- package/dist/widgets/SelectField.cascade.test.d.ts +0 -0
- package/dist/widgets/TagsField.placeholder.no-provider.test.d.ts +0 -0
- package/dist/widgets/TagsField.placeholder.test.d.ts +0 -0
- package/dist/widgets/UserFieldPicker.test.d.ts +0 -8
- package/dist/widgets/__tests__/FilterConditionField.matchAll.test.d.ts +0 -8
- package/dist/widgets/__tests__/FilterConditionField.operators.test.d.ts +0 -8
- package/dist/widgets/__tests__/FullscreenFieldEditor.i18n.test.d.ts +0 -0
- package/dist/widgets/__tests__/FullscreenFieldEditor.no-provider.test.d.ts +0 -0
- package/dist/widgets/__tests__/RichTextField.fullscreenDisabled.test.d.ts +0 -0
- package/dist/widgets/__tests__/RichTextField.mobileFullscreen.test.d.ts +0 -0
- package/dist/widgets/__tests__/TextAreaField.characterCount.announcements.test.d.ts +0 -0
- package/dist/widgets/__tests__/TextAreaField.characterCount.i18n.test.d.ts +0 -0
- package/dist/widgets/__tests__/TextAreaField.characterCount.no-provider.test.d.ts +0 -0
- package/dist/widgets/__tests__/TextAreaField.fullscreenCharacterCount.i18n.test.d.ts +0 -0
- package/dist/widgets/__tests__/TextAreaField.fullscreenCharacterCount.test.d.ts +0 -0
- package/dist/widgets/__tests__/TextAreaField.fullscreenDisabled.test.d.ts +0 -0
- package/dist/widgets/__tests__/TextAreaField.mobileFullscreen.test.d.ts +0 -0
- package/dist/widgets/deriveLookupColumns.test.d.ts +0 -1
- package/dist/widgets/file-value.test.d.ts +0 -1
- package/dist/widgets/personDisplay.test.d.ts +0 -8
- package/dist/widgets/recentLookups.test.d.ts +0 -1
- package/dist/widgets/useRecordQuery.test.d.ts +0 -8
- package/dist/widgets/useUploadingSignal.test.d.ts +0 -1
- package/src/FieldEditWidget.test.ts +0 -133
- package/src/FieldEditWidget.tsx +0 -191
- package/src/__tests__/DateCellRenderer.test.tsx +0 -73
- package/src/__tests__/NumberCellRenderer.test.tsx +0 -60
- package/src/__tests__/cell-truncation.test.tsx +0 -96
- package/src/__tests__/field-carrier-sdui.test.tsx +0 -169
- package/src/__tests__/lookupCellDisplayField.test.tsx +0 -107
- package/src/__tests__/lookupCellDisplayName.test.tsx +0 -119
- package/src/__tests__/lookupCellReferenceKey.test.tsx +0 -57
- package/src/__tests__/registry-collision.test.ts +0 -91
- package/src/__tests__/resolveFieldCurrency.test.ts +0 -20
- package/src/__tests__/spec-symbol-batch7.test.ts +0 -126
- package/src/__tests__/validation-feedback.test.tsx +0 -361
- package/src/__tests__/widget-aria-invalid-e2e.test.tsx +0 -265
- package/src/__tests__/widget-aria-invalid-registry-e2e.test.tsx +0 -338
- package/src/__tests__/widget-dom-leak-e2e.test.tsx +0 -598
- package/src/__tests__/widget-props-contract.test.tsx +0 -142
- package/src/coerce-safe-value.test.ts +0 -34
- package/src/complex-widgets.test.tsx +0 -908
- package/src/currency.ts +0 -14
- package/src/datetime-widgets.test.tsx +0 -93
- package/src/field-type-alias.ts +0 -100
- package/src/field-type-coverage.test.ts +0 -67
- package/src/index.tsx +0 -2503
- package/src/standard-widgets.test.tsx +0 -357
- package/src/widgets/AddressField.tsx +0 -132
- package/src/widgets/AddressField.uniqueIds.test.tsx +0 -92
- package/src/widgets/AutoNumberField.tsx +0 -18
- package/src/widgets/AvatarField.tsx +0 -117
- package/src/widgets/BooleanField.tsx +0 -52
- package/src/widgets/CapabilityMultiSelectField.test.tsx +0 -168
- package/src/widgets/CapabilityMultiSelectField.tsx +0 -229
- package/src/widgets/CharacterCount.tsx +0 -263
- package/src/widgets/CheckboxesField.cascade.test.tsx +0 -143
- package/src/widgets/CheckboxesField.tsx +0 -136
- package/src/widgets/CodeField.tsx +0 -41
- package/src/widgets/ColorField.tsx +0 -53
- package/src/widgets/CurrencyField.tsx +0 -95
- package/src/widgets/DateField.tsx +0 -31
- package/src/widgets/DateTimeField.tsx +0 -37
- package/src/widgets/EmailField.tsx +0 -37
- package/src/widgets/FileField.tsx +0 -494
- package/src/widgets/FilterConditionField.tsx +0 -454
- package/src/widgets/FormulaField.tsx +0 -33
- package/src/widgets/FullscreenFieldEditor.tsx +0 -271
- package/src/widgets/GeolocationField.tsx +0 -167
- package/src/widgets/GeolocationField.uniqueIds.test.tsx +0 -88
- package/src/widgets/GridField.test.tsx +0 -406
- package/src/widgets/GridField.tsx +0 -966
- package/src/widgets/ImageCropperDialog.tsx +0 -245
- package/src/widgets/ImageField.tsx +0 -255
- package/src/widgets/ImageLightbox.test.tsx +0 -52
- package/src/widgets/ImageLightbox.tsx +0 -108
- package/src/widgets/LocationField.tsx +0 -57
- package/src/widgets/LookupField.dependsOn.test.tsx +0 -233
- package/src/widgets/LookupField.gateHintLabel.test.tsx +0 -135
- package/src/widgets/LookupField.hydration.test.tsx +0 -136
- package/src/widgets/LookupField.idField.test.tsx +0 -61
- package/src/widgets/LookupField.tsx +0 -1291
- package/src/widgets/MarkdownContent.tsx +0 -19
- package/src/widgets/MasterDetailField.tsx +0 -131
- package/src/widgets/MultiSelectField.cascade.test.tsx +0 -143
- package/src/widgets/MultiSelectField.tsx +0 -142
- package/src/widgets/NumberField.tsx +0 -49
- package/src/widgets/ObjectField.tsx +0 -94
- package/src/widgets/ObjectRefField.tsx +0 -121
- package/src/widgets/OptionsEmptyState.no-provider.test.tsx +0 -88
- package/src/widgets/OptionsEmptyState.test.tsx +0 -169
- package/src/widgets/OptionsEmptyState.tsx +0 -83
- package/src/widgets/PasswordField.tsx +0 -49
- package/src/widgets/PeoplePicker.test.tsx +0 -360
- package/src/widgets/PeoplePicker.tsx +0 -579
- package/src/widgets/PeoplePickerComponents.test.tsx +0 -104
- package/src/widgets/PercentField.tsx +0 -95
- package/src/widgets/PersonRow.tsx +0 -117
- package/src/widgets/PhoneField.tsx +0 -37
- package/src/widgets/QRCodeField.tsx +0 -103
- package/src/widgets/RadioField.cascade.test.tsx +0 -141
- package/src/widgets/RadioField.tsx +0 -111
- package/src/widgets/RatingField.tsx +0 -81
- package/src/widgets/RecipientPickerField.test.tsx +0 -157
- package/src/widgets/RecipientPickerField.tsx +0 -188
- package/src/widgets/RecordPickerDialog.filterOptions.test.tsx +0 -611
- package/src/widgets/RecordPickerDialog.selectLabel.test.tsx +0 -112
- package/src/widgets/RecordPickerDialog.tsx +0 -1316
- package/src/widgets/RichTextField.tsx +0 -176
- package/src/widgets/SelectField.cascade.test.tsx +0 -138
- package/src/widgets/SelectField.tsx +0 -162
- package/src/widgets/SelectionTray.tsx +0 -110
- package/src/widgets/SignatureField.tsx +0 -140
- package/src/widgets/SliderField.tsx +0 -39
- package/src/widgets/SummaryField.tsx +0 -31
- package/src/widgets/TagsField.placeholder.no-provider.test.tsx +0 -68
- package/src/widgets/TagsField.placeholder.test.tsx +0 -95
- package/src/widgets/TagsField.tsx +0 -88
- package/src/widgets/TextAreaField.tsx +0 -213
- package/src/widgets/TextField.tsx +0 -45
- package/src/widgets/TimeField.tsx +0 -31
- package/src/widgets/UrlField.tsx +0 -46
- package/src/widgets/UserField.tsx +0 -58
- package/src/widgets/UserFieldPicker.test.tsx +0 -78
- package/src/widgets/VectorField.tsx +0 -29
- package/src/widgets/__tests__/FilterConditionField.matchAll.test.ts +0 -66
- package/src/widgets/__tests__/FilterConditionField.operators.test.ts +0 -142
- package/src/widgets/__tests__/FullscreenFieldEditor.i18n.test.tsx +0 -248
- package/src/widgets/__tests__/FullscreenFieldEditor.no-provider.test.tsx +0 -116
- package/src/widgets/__tests__/RichTextField.fullscreenDisabled.test.tsx +0 -244
- package/src/widgets/__tests__/RichTextField.mobileFullscreen.test.tsx +0 -195
- package/src/widgets/__tests__/TextAreaField.characterCount.announcements.test.tsx +0 -417
- package/src/widgets/__tests__/TextAreaField.characterCount.i18n.test.tsx +0 -287
- package/src/widgets/__tests__/TextAreaField.characterCount.no-provider.test.tsx +0 -160
- package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.i18n.test.tsx +0 -161
- package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.test.tsx +0 -346
- package/src/widgets/__tests__/TextAreaField.fullscreenDisabled.test.tsx +0 -246
- package/src/widgets/__tests__/TextAreaField.mobileFullscreen.test.tsx +0 -175
- package/src/widgets/_cell-renderer-bridge.ts +0 -19
- package/src/widgets/deriveLookupColumns.test.ts +0 -112
- package/src/widgets/deriveLookupColumns.ts +0 -108
- package/src/widgets/file-value.test.ts +0 -230
- package/src/widgets/file-value.ts +0 -246
- package/src/widgets/openNativePicker.ts +0 -18
- package/src/widgets/personDisplay.test.ts +0 -113
- package/src/widgets/personDisplay.ts +0 -92
- package/src/widgets/recentLookups.test.ts +0 -45
- package/src/widgets/recentLookups.ts +0 -47
- package/src/widgets/toDomProps.ts +0 -175
- package/src/widgets/types.ts +0 -242
- package/src/widgets/useCascadingOptions.ts +0 -46
- package/src/widgets/useFieldTranslation.ts +0 -150
- package/src/widgets/useRecordQuery.test.ts +0 -174
- package/src/widgets/useRecordQuery.ts +0 -264
- package/src/widgets/useUploadingSignal.test.tsx +0 -62
- package/src/widgets/useUploadingSignal.ts +0 -20
- package/src/withFieldCarrier.tsx +0 -75
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,133 +0,0 @@
|
|
|
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
|
-
import { describe, it, expect } from 'vitest';
|
|
10
|
-
import { FieldType } from '@objectstack/spec/data';
|
|
11
|
-
import {
|
|
12
|
-
FORM_FIELD_TYPES,
|
|
13
|
-
INLINE_EXCLUDED_FIELD_TYPES,
|
|
14
|
-
hasFieldEditWidget,
|
|
15
|
-
isInlineExcludedFieldType,
|
|
16
|
-
mapFieldTypeToFormType,
|
|
17
|
-
} from './index';
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* A field type the form can render — either a direct widget-map key, or a spec
|
|
21
|
-
* spelling that resolves onto one through the alias table (`secret` →
|
|
22
|
-
* `field:password`, `autonumber` → `field:auto_number`, …).
|
|
23
|
-
*
|
|
24
|
-
* `FORM_FIELD_TYPES` alone is `Object.keys(fieldWidgetMap)`, which does NOT
|
|
25
|
-
* include the alias-only spellings — see the note at index.tsx's
|
|
26
|
-
* `resolveFormWidgetType`. Using it as the definition of "a real type" made the
|
|
27
|
-
* staleness check below reject `secret`, a genuine spec field type the form
|
|
28
|
-
* renders, purely because it arrives via an alias.
|
|
29
|
-
*/
|
|
30
|
-
function isRenderableFormType(t: string): boolean {
|
|
31
|
-
// `text` is the alias table's fallback, so a non-`field:text` result means an
|
|
32
|
-
// explicit entry exists. `text` itself is a direct widget key, so it is
|
|
33
|
-
// already covered by the first clause and no real type is missed here.
|
|
34
|
-
return FORM_FIELD_TYPES.includes(t) || mapFieldTypeToFormType(t) !== 'field:text';
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Drift-guard: the inline cell editor reuses the form's field widgets, but the
|
|
39
|
-
* two lists were hand-maintained separately and drifted — `lookup` (and other
|
|
40
|
-
* relational types) had a perfectly good form widget yet fell back to a plain
|
|
41
|
-
* text box inline, because nobody wired it up. This pins the contract: every
|
|
42
|
-
* type the FORM can render must have an explicit inline decision — an editor or
|
|
43
|
-
* a documented exclusion — so a new form widget can never again silently become
|
|
44
|
-
* an editable text box (or a missing one) in the grid.
|
|
45
|
-
*/
|
|
46
|
-
describe('inline editor ↔ form widget parity', () => {
|
|
47
|
-
it('every form field type either has an inline editor or is explicitly excluded', () => {
|
|
48
|
-
const undecided = FORM_FIELD_TYPES.filter(
|
|
49
|
-
(t) => !hasFieldEditWidget(t) && !INLINE_EXCLUDED_FIELD_TYPES.has(t),
|
|
50
|
-
);
|
|
51
|
-
// If this fails: a form widget type has no inline decision. Either add it to
|
|
52
|
-
// EDIT_WIDGETS (inline-editable) or to INLINE_EXCLUDED_FIELD_TYPES (with a
|
|
53
|
-
// reason) in FieldEditWidget.tsx.
|
|
54
|
-
expect(undecided).toEqual([]);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('the exclusion set lists only real form types (no stale entries)', () => {
|
|
58
|
-
const stale = [...INLINE_EXCLUDED_FIELD_TYPES].filter((t) => !isRenderableFormType(t));
|
|
59
|
-
expect(stale).toEqual([]);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('credential types are never inline-editable', () => {
|
|
63
|
-
// The grid's fallback for a type with no inline editor is a PLAIN TEXT input.
|
|
64
|
-
// Both of these are masked on read, so that input would show the mask as the
|
|
65
|
-
// value and write it straight back; `secret` also round-trips through an
|
|
66
|
-
// encrypted store (ADR-0100), so the cell holds an opaque ref, not the value.
|
|
67
|
-
for (const t of ['password', 'secret']) {
|
|
68
|
-
expect(hasFieldEditWidget(t), `${t} must not have an inline editor`).toBe(false);
|
|
69
|
-
expect(INLINE_EXCLUDED_FIELD_TYPES.has(t), `${t} must be explicitly excluded`).toBe(true);
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it('relational fields use the standard picker inline (regression: lookup was a text box)', () => {
|
|
74
|
-
for (const t of ['lookup', 'master_detail', 'user', 'owner']) {
|
|
75
|
-
expect(hasFieldEditWidget(t)).toBe(true);
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('computed / binary form types are NOT inline-editable (excluded)', () => {
|
|
80
|
-
for (const t of ['formula', 'summary', 'auto_number', 'file', 'image']) {
|
|
81
|
-
expect(hasFieldEditWidget(t)).toBe(false);
|
|
82
|
-
expect(INLINE_EXCLUDED_FIELD_TYPES.has(t)).toBe(true);
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it('structured-value types edit inline with their form widget (color/address/location/code/…)', () => {
|
|
87
|
-
for (const t of ['color', 'address', 'location', 'geolocation', 'code', 'qrcode']) {
|
|
88
|
-
expect(hasFieldEditWidget(t)).toBe(true);
|
|
89
|
-
expect(INLINE_EXCLUDED_FIELD_TYPES.has(t)).toBe(false);
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* #2942 — the guard above iterates FORM_FIELD_TYPES (the form widget-map
|
|
96
|
-
* keys), so a SPEC field type that reaches the form only through the alias
|
|
97
|
-
* table could sit in neither set and silently fall back to the grid's plain
|
|
98
|
-
* text input: `json`, `composite`, `record`, `repeater`, `tree`, `video`,
|
|
99
|
-
* `audio`, `autonumber` all did. This pins the contract at the spec boundary:
|
|
100
|
-
* every `FieldType` member must resolve (alias-aware) to an inline editor or
|
|
101
|
-
* a documented exclusion.
|
|
102
|
-
*/
|
|
103
|
-
describe('inline editor ↔ SPEC FieldType parity (#2942)', () => {
|
|
104
|
-
const specTypes: string[] = Array.isArray((FieldType as { options?: readonly string[] }).options)
|
|
105
|
-
? [...(FieldType as { options: readonly string[] }).options]
|
|
106
|
-
: [];
|
|
107
|
-
|
|
108
|
-
it('reads a non-empty enum from the spec', () => {
|
|
109
|
-
expect(specTypes, 'could not read FieldType.options from the spec').not.toEqual([]);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it('every spec field type resolves to an inline decision (editor or exclusion)', () => {
|
|
113
|
-
const undecided = specTypes.filter(
|
|
114
|
-
(t) => !hasFieldEditWidget(t) && !isInlineExcludedFieldType(t),
|
|
115
|
-
);
|
|
116
|
-
// If this fails: a spec field type would fall back to a plain text input
|
|
117
|
-
// inline. Give it a widget (EDIT_WIDGETS / the alias table) or a
|
|
118
|
-
// documented exclusion (INLINE_EXCLUDED_FIELD_TYPES).
|
|
119
|
-
expect(undecided).toEqual([]);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it('the structured/computed spec spellings are closed corruption paths', () => {
|
|
123
|
-
// Excluded (alias-aware): editing these through a text box corrupts the value.
|
|
124
|
-
for (const t of ['composite', 'record', 'repeater', 'video', 'audio', 'autonumber']) {
|
|
125
|
-
expect(isInlineExcludedFieldType(t), `${t} must be excluded from inline editing`).toBe(true);
|
|
126
|
-
expect(hasFieldEditWidget(t), `${t} must not resolve to an editor`).toBe(false);
|
|
127
|
-
}
|
|
128
|
-
// Editable through their form widgets: json → code editor, tree → lookup picker.
|
|
129
|
-
for (const t of ['json', 'tree']) {
|
|
130
|
-
expect(hasFieldEditWidget(t), `${t} must resolve to its form widget`).toBe(true);
|
|
131
|
-
}
|
|
132
|
-
});
|
|
133
|
-
});
|
package/src/FieldEditWidget.tsx
DELETED
|
@@ -1,191 +0,0 @@
|
|
|
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
|
-
import React from 'react';
|
|
10
|
-
import type { FieldWidgetComponentProps } from './widgets/types';
|
|
11
|
-
|
|
12
|
-
// The SAME dedicated widgets the form renders — reused for in-place editing
|
|
13
|
-
// (e.g. the data grid's inline cell editor) so a select edits as a dropdown, a
|
|
14
|
-
// boolean as a checkbox, a date as a date picker, etc. — never a bare text box.
|
|
15
|
-
import { TextField } from './widgets/TextField';
|
|
16
|
-
import { TextAreaField } from './widgets/TextAreaField';
|
|
17
|
-
import { NumberField } from './widgets/NumberField';
|
|
18
|
-
import { CurrencyField } from './widgets/CurrencyField';
|
|
19
|
-
import { PercentField } from './widgets/PercentField';
|
|
20
|
-
import { SliderField } from './widgets/SliderField';
|
|
21
|
-
import { RatingField } from './widgets/RatingField';
|
|
22
|
-
import { BooleanField } from './widgets/BooleanField';
|
|
23
|
-
import { SelectField } from './widgets/SelectField';
|
|
24
|
-
import { MultiSelectField } from './widgets/MultiSelectField';
|
|
25
|
-
import { RadioField } from './widgets/RadioField';
|
|
26
|
-
import { CheckboxesField } from './widgets/CheckboxesField';
|
|
27
|
-
import { TagsField } from './widgets/TagsField';
|
|
28
|
-
import { DateField } from './widgets/DateField';
|
|
29
|
-
import { DateTimeField } from './widgets/DateTimeField';
|
|
30
|
-
import { TimeField } from './widgets/TimeField';
|
|
31
|
-
import { EmailField } from './widgets/EmailField';
|
|
32
|
-
import { PhoneField } from './widgets/PhoneField';
|
|
33
|
-
import { UrlField } from './widgets/UrlField';
|
|
34
|
-
// Relational pickers — the SAME standard widgets the form uses. They read the
|
|
35
|
-
// related-object dataSource from SchemaRendererContext (which the grid already
|
|
36
|
-
// provides), so they drop straight into an inline cell.
|
|
37
|
-
import { LookupField } from './widgets/LookupField';
|
|
38
|
-
import { UserField } from './widgets/UserField';
|
|
39
|
-
// Structured-value editors — lightweight (no map/code-editor deps), same
|
|
40
|
-
// widgets the form uses. Drop into a cell like the rest.
|
|
41
|
-
import { ColorField } from './widgets/ColorField';
|
|
42
|
-
import { AddressField } from './widgets/AddressField';
|
|
43
|
-
import { LocationField } from './widgets/LocationField';
|
|
44
|
-
import { GeolocationField } from './widgets/GeolocationField';
|
|
45
|
-
import { CodeField } from './widgets/CodeField';
|
|
46
|
-
import { QRCodeField } from './widgets/QRCodeField';
|
|
47
|
-
// The FORM's spec-alias table (`json` → `field:code`, `tree` → `field:lookup`,
|
|
48
|
-
// …) — inline resolution reuses it so spec spellings get the form's decision.
|
|
49
|
-
import { mapFieldTypeToFormType } from './field-type-alias';
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Field types that edit in place with a dedicated widget. Keyed by the raw
|
|
53
|
-
* field `type` (the widget map mirrors the form's `mapFieldTypeToFormType`).
|
|
54
|
-
* Rich/heavy types (file, image, lookup, richtext, …) are intentionally absent
|
|
55
|
-
* so callers fall back to their own simpler editor.
|
|
56
|
-
*/
|
|
57
|
-
const EDIT_WIDGETS: Record<string, React.ComponentType<FieldWidgetComponentProps<any>>> = {
|
|
58
|
-
text: TextField,
|
|
59
|
-
textarea: TextAreaField,
|
|
60
|
-
number: NumberField,
|
|
61
|
-
currency: CurrencyField,
|
|
62
|
-
percent: PercentField,
|
|
63
|
-
slider: SliderField,
|
|
64
|
-
progress: SliderField,
|
|
65
|
-
rating: RatingField,
|
|
66
|
-
boolean: BooleanField,
|
|
67
|
-
toggle: BooleanField,
|
|
68
|
-
select: SelectField,
|
|
69
|
-
status: SelectField,
|
|
70
|
-
multiselect: MultiSelectField,
|
|
71
|
-
radio: RadioField,
|
|
72
|
-
checkboxes: CheckboxesField,
|
|
73
|
-
tags: TagsField,
|
|
74
|
-
date: DateField,
|
|
75
|
-
datetime: DateTimeField,
|
|
76
|
-
time: TimeField,
|
|
77
|
-
email: EmailField,
|
|
78
|
-
phone: PhoneField,
|
|
79
|
-
url: UrlField,
|
|
80
|
-
// Relational — the record/user pickers, same as the form (the form maps
|
|
81
|
-
// master_detail to the single-value LookupField too, see fieldWidgetMap).
|
|
82
|
-
lookup: LookupField,
|
|
83
|
-
master_detail: LookupField,
|
|
84
|
-
user: UserField,
|
|
85
|
-
owner: UserField,
|
|
86
|
-
// Structured-value editors — same widgets the form uses.
|
|
87
|
-
color: ColorField,
|
|
88
|
-
address: AddressField,
|
|
89
|
-
location: LocationField,
|
|
90
|
-
geolocation: GeolocationField,
|
|
91
|
-
code: CodeField,
|
|
92
|
-
qrcode: QRCodeField,
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Form field types that are deliberately NOT given an inline editor — every
|
|
97
|
-
* other form widget type must be in {@link EDIT_WIDGETS}. This pairing is
|
|
98
|
-
* enforced by a test against the form's widget map (`FORM_FIELD_TYPES`) so the
|
|
99
|
-
* two can't silently drift again (which is how `lookup` was missed). To add a
|
|
100
|
-
* type to inline editing, move it from here into EDIT_WIDGETS.
|
|
101
|
-
*/
|
|
102
|
-
export const INLINE_EXCLUDED_FIELD_TYPES = new Set<string>([
|
|
103
|
-
// Computed / read-only — the grid keeps these non-editable (no value to author).
|
|
104
|
-
'formula', 'summary', 'auto_number',
|
|
105
|
-
// Binary / attachment — edited from the record form, shown read-only in the grid.
|
|
106
|
-
'file', 'image', 'avatar', 'signature',
|
|
107
|
-
// Heavy / full editors — better in the record form than a cell.
|
|
108
|
-
'markdown', 'html', 'richtext',
|
|
109
|
-
// Credentials — never inline-editable. Both are masked on read, so the cell has
|
|
110
|
-
// no real value to seed an editor with, and the grid's fallback is a PLAIN TEXT
|
|
111
|
-
// input: it would render the mask as if it were the value and write it straight
|
|
112
|
-
// back. `secret` additionally round-trips through an encrypted store (ADR-0100,
|
|
113
|
-
// data/field.zod.ts) — the cell holds an opaque ref, not the secret.
|
|
114
|
-
'password', 'secret',
|
|
115
|
-
// Containers / non-authorable — a sub-form / sub-grid / embedding vector
|
|
116
|
-
// doesn't belong in a single cell.
|
|
117
|
-
'object', 'grid', 'vector',
|
|
118
|
-
// Widget-hint-only pickers — authored in the record form (they depend on
|
|
119
|
-
// sibling fields / a loaded object catalog), not inline in a grid cell.
|
|
120
|
-
'object-ref', 'filter-condition', 'recipient-picker',
|
|
121
|
-
]);
|
|
122
|
-
|
|
123
|
-
/** Field types whose value is chosen in one discrete gesture (no free typing). */
|
|
124
|
-
export const DISCRETE_EDIT_TYPES = new Set<string>([
|
|
125
|
-
'boolean', 'toggle', 'select', 'status', 'radio', 'rating',
|
|
126
|
-
]);
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Resolve a field type to its inline-editing key: the raw type when the
|
|
130
|
-
* widget/exclusion tables name it directly, otherwise its form-alias target
|
|
131
|
-
* (`mapFieldTypeToFormType`, `field:` prefix stripped).
|
|
132
|
-
*
|
|
133
|
-
* The tables are keyed by the FORM's spellings, but spec field types reach
|
|
134
|
-
* this layer in the SPEC's spellings — `json`, `tree`, `composite`, `record`,
|
|
135
|
-
* `repeater`, `video`, `audio`, `autonumber` are aliases in the form map and
|
|
136
|
-
* were keys in NEITHER table here, so the grid silently fell back to a plain
|
|
137
|
-
* text input for all of them: typing over structured JSON or a hierarchy ref
|
|
138
|
-
* is a value-corruption path (#2942). Resolving through the same alias table
|
|
139
|
-
* the form uses gives each spec type the form's own decision: `json` → the
|
|
140
|
-
* code editor, `tree` → the lookup picker, and the container/computed/binary
|
|
141
|
-
* families → their documented exclusions.
|
|
142
|
-
*/
|
|
143
|
-
function resolveInlineEditType(type: string): string {
|
|
144
|
-
if (type in EDIT_WIDGETS || INLINE_EXCLUDED_FIELD_TYPES.has(type)) return type;
|
|
145
|
-
return mapFieldTypeToFormType(type).replace(/^field:/, '');
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/** True when a field type has a dedicated in-place edit widget. */
|
|
149
|
-
export function hasFieldEditWidget(type: string | undefined): boolean {
|
|
150
|
-
return !!type && resolveInlineEditType(type) in EDIT_WIDGETS;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* True when a field type is deliberately NOT inline-editable (alias-aware:
|
|
155
|
-
* `composite` resolves to the excluded `object`, `video` to `file`, …).
|
|
156
|
-
* Grid hosts must treat these as read-only cells — falling back to a plain
|
|
157
|
-
* text editor is exactly the corruption path the exclusion exists to close.
|
|
158
|
-
*/
|
|
159
|
-
export function isInlineExcludedFieldType(type: string | undefined): boolean {
|
|
160
|
-
return !!type && INLINE_EXCLUDED_FIELD_TYPES.has(resolveInlineEditType(type));
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Relational picker widgets (lookup / master_detail / user / owner) render best
|
|
165
|
-
* in a grid cell as a single-line, borderless trigger — so the selected
|
|
166
|
-
* record's NAME shows inside the trigger instead of a chip stacked above a
|
|
167
|
-
* separate "Select…" button (which double-stacks and wastes the row height).
|
|
168
|
-
* This mirrors how the line-item grid (`GridField`) renders its lookup cells.
|
|
169
|
-
*/
|
|
170
|
-
const COMPACT_EDIT_TYPES = new Set<string>(['lookup', 'master_detail', 'user', 'owner']);
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Render the dedicated edit widget for a field's type — the SAME control the
|
|
174
|
-
* form uses — as a controlled `{ value, onChange, field }` input. Returns
|
|
175
|
-
* `null` for types without a registered widget so the caller can fall back to
|
|
176
|
-
* a plain editor.
|
|
177
|
-
*/
|
|
178
|
-
export function FieldEditWidget({
|
|
179
|
-
field,
|
|
180
|
-
value,
|
|
181
|
-
onChange,
|
|
182
|
-
readonly,
|
|
183
|
-
}: FieldWidgetComponentProps<any>): React.ReactElement | null {
|
|
184
|
-
const resolved = field?.type ? resolveInlineEditType(field.type) : undefined;
|
|
185
|
-
const Widget = resolved ? EDIT_WIDGETS[resolved] : undefined;
|
|
186
|
-
if (!Widget) return null;
|
|
187
|
-
// `compact` is a declared widget prop (objectui#3221 closed this type), so
|
|
188
|
-
// the spread no longer needs an `any` escape hatch to get past it.
|
|
189
|
-
const compactProps = resolved && COMPACT_EDIT_TYPES.has(resolved) ? { compact: true } : {};
|
|
190
|
-
return <Widget field={field} value={value} onChange={onChange} readonly={readonly} {...compactProps} />;
|
|
191
|
-
}
|
|
@@ -1,73 +0,0 @@
|
|
|
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
|
-
import { describe, it, expect } from 'vitest';
|
|
10
|
-
import React from 'react';
|
|
11
|
-
import { render, screen } from '@testing-library/react';
|
|
12
|
-
import '@testing-library/jest-dom';
|
|
13
|
-
import { DateCellRenderer, formatRelativeDate } from '../index';
|
|
14
|
-
|
|
15
|
-
function daysAgo(n: number): string {
|
|
16
|
-
const d = new Date();
|
|
17
|
-
d.setDate(d.getDate() - n);
|
|
18
|
-
return d.toISOString();
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
describe('formatRelativeDate', () => {
|
|
22
|
-
it('renders "Overdue Nd" only when dueLike is set', () => {
|
|
23
|
-
expect(formatRelativeDate(daysAgo(3), { dueLike: true })).toBe('Overdue 3d');
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it('renders neutral "N days ago" for a past date that is not due-like', () => {
|
|
27
|
-
expect(formatRelativeDate(daysAgo(3))).toBe('3 days ago');
|
|
28
|
-
expect(formatRelativeDate(daysAgo(3), { dueLike: false })).toBe('3 days ago');
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('localizes the near-today window per locale (framework#3040)', () => {
|
|
32
|
-
expect(formatRelativeDate(daysAgo(0), { locale: 'zh-CN' })).toBe('今天');
|
|
33
|
-
expect(formatRelativeDate(daysAgo(-1), { locale: 'zh-CN' })).toBe('明天');
|
|
34
|
-
expect(formatRelativeDate(daysAgo(1), { locale: 'zh-CN' })).toBe('昨天');
|
|
35
|
-
expect(formatRelativeDate(daysAgo(-3), { locale: 'zh-CN' })).toBe('3天后');
|
|
36
|
-
expect(formatRelativeDate(daysAgo(3), { locale: 'zh-CN' })).toBe('3天前');
|
|
37
|
-
// English output is sentence-cased.
|
|
38
|
-
expect(formatRelativeDate(daysAgo(-1))).toBe('Tomorrow');
|
|
39
|
-
expect(formatRelativeDate(daysAgo(-3))).toBe('In 3 days');
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('resolves the overdue phrase through the i18n translate fn when provided', () => {
|
|
43
|
-
const t = (key: string, params?: Record<string, unknown>) =>
|
|
44
|
-
key === 'fields.relativeDate.overdue' ? `逾期 ${params?.count} 天` : key;
|
|
45
|
-
expect(formatRelativeDate(daysAgo(3), { dueLike: true, t })).toBe('逾期 3 天');
|
|
46
|
-
// A t() that misses (returns the key) keeps the English fallback.
|
|
47
|
-
expect(formatRelativeDate(daysAgo(3), { dueLike: true, t: (k) => k })).toBe('Overdue 3d');
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('degrades to English when the locale tag is invalid', () => {
|
|
51
|
-
expect(formatRelativeDate(daysAgo(3), { locale: 'not a locale' })).toBe('3 days ago');
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
describe('DateCellRenderer', () => {
|
|
56
|
-
it('does not label a past start_date as "Overdue" (regression: field-role-agnostic formatter)', () => {
|
|
57
|
-
render(<DateCellRenderer value={daysAgo(6)} field={{ type: 'date', name: 'start_date' } as any} />);
|
|
58
|
-
expect(screen.getByText('6 days ago')).toBeInTheDocument();
|
|
59
|
-
expect(screen.queryByText(/Overdue/)).not.toBeInTheDocument();
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('labels a past due_date as "Overdue Nd" and colors it red', () => {
|
|
63
|
-
render(<DateCellRenderer value={daysAgo(6)} field={{ type: 'date', name: 'due_date' } as any} />);
|
|
64
|
-
const el = screen.getByText('Overdue 6d');
|
|
65
|
-
expect(el).toBeInTheDocument();
|
|
66
|
-
expect(el.className).toMatch(/text-red-600/);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('respects an explicit dueLike:true override regardless of field name', () => {
|
|
70
|
-
render(<DateCellRenderer value={daysAgo(2)} field={{ type: 'date', name: 'end_date', dueLike: true } as any} />);
|
|
71
|
-
expect(screen.getByText('Overdue 2d')).toBeInTheDocument();
|
|
72
|
-
});
|
|
73
|
-
});
|
|
@@ -1,60 +0,0 @@
|
|
|
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
|
-
import { describe, it, expect } from 'vitest';
|
|
10
|
-
import React from 'react';
|
|
11
|
-
import { render, screen } from '@testing-library/react';
|
|
12
|
-
import '@testing-library/jest-dom';
|
|
13
|
-
import { NumberCellRenderer } from '../index';
|
|
14
|
-
|
|
15
|
-
const renderNumber = (value: unknown, field: Record<string, unknown> = {}) =>
|
|
16
|
-
render(<NumberCellRenderer value={value as any} field={{ type: 'number', ...field } as any} />);
|
|
17
|
-
|
|
18
|
-
describe('NumberCellRenderer', () => {
|
|
19
|
-
it('does not pad trailing zeros using `precision` (total digits, not decimal places)', () => {
|
|
20
|
-
// Regression: a decimal(10, 0) column exposes precision: 10, which used to
|
|
21
|
-
// render `1` as "1.0000000000".
|
|
22
|
-
renderNumber(1, { precision: 10 });
|
|
23
|
-
expect(screen.getByText('1')).toBeInTheDocument();
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it('renders an integer without decimals when no scale is declared', () => {
|
|
27
|
-
renderNumber(16);
|
|
28
|
-
expect(screen.getByText('16')).toBeInTheDocument();
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('preserves the value\'s natural decimals when no scale is declared', () => {
|
|
32
|
-
renderNumber(16.5);
|
|
33
|
-
expect(screen.getByText('16.5')).toBeInTheDocument();
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('pads to a fixed number of decimals when `scale` is declared', () => {
|
|
37
|
-
renderNumber(16, { scale: 2 });
|
|
38
|
-
expect(screen.getByText('16.00')).toBeInTheDocument();
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('honours a scale of 3 with zero padding', () => {
|
|
42
|
-
renderNumber(3.14, { scale: 3 });
|
|
43
|
-
expect(screen.getByText('3.140')).toBeInTheDocument();
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it('rounds to the declared scale', () => {
|
|
47
|
-
renderNumber(3.14159, { scale: 2 });
|
|
48
|
-
expect(screen.getByText('3.14')).toBeInTheDocument();
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('treats scale: 0 as an integer display', () => {
|
|
52
|
-
renderNumber(2, { scale: 0 });
|
|
53
|
-
expect(screen.getByText('2')).toBeInTheDocument();
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('renders an empty indicator for nullish values', () => {
|
|
57
|
-
const { container } = renderNumber(null);
|
|
58
|
-
expect(container.textContent).not.toContain('0');
|
|
59
|
-
});
|
|
60
|
-
});
|