@object-ui/fields 17.5.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 +714 -0
- package/dist/FieldEditWidget.d.ts +16 -2
- package/dist/field-type-alias.d.ts +16 -0
- package/dist/index.cjs +4 -4
- package/dist/index.css +27 -9
- package/dist/index.d.ts +425 -72
- package/dist/index.js +2530 -2228
- package/dist/widgets/AddressField.d.ts +3 -3
- 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 +9 -2
- package/dist/widgets/GeolocationField.d.ts +1 -1
- package/dist/widgets/GridField.d.ts +56 -4
- 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 +13 -7
- 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 +2 -4
- package/dist/widgets/RichTextField.d.ts +37 -1
- package/dist/widgets/SelectField.d.ts +1 -1
- 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 +27 -7
- package/dist/widgets/lookupColumnDisplay.d.ts +93 -0
- package/dist/widgets/richTextDisplay.d.ts +80 -0
- package/dist/widgets/toHostGroupProps.d.ts +119 -21
- package/dist/widgets/types.d.ts +8 -3
- package/package.json +9 -10
- package/dist/widgets/CharacterCount.d.ts +0 -56
- package/src/FieldEditWidget.test.ts +0 -133
- package/src/FieldEditWidget.tsx +0 -207
- package/src/__tests__/AddressCellRenderer.test.tsx +0 -166
- package/src/__tests__/CurrencyField.minorUnits.test.tsx +0 -191
- package/src/__tests__/CurrencyField.symbol.test.tsx +0 -198
- package/src/__tests__/DateCellRenderer.test.tsx +0 -82
- package/src/__tests__/NumberCellRenderer.grouping.test.tsx +0 -146
- package/src/__tests__/NumberCellRenderer.test.tsx +0 -60
- package/src/__tests__/PercentCellRenderer.locale.test.tsx +0 -152
- package/src/__tests__/PercentCellRenderer.test.tsx +0 -115
- package/src/__tests__/boolean-label-association-e2e.test.tsx +0 -298
- package/src/__tests__/capability-multiselect-retired.test.ts +0 -109
- package/src/__tests__/cell-truncation.test.tsx +0 -96
- package/src/__tests__/composite-group-label-e2e.test.tsx +0 -436
- package/src/__tests__/composite-group-label-readonly-e2e.test.tsx +0 -403
- package/src/__tests__/date-formatter-residue-4272.test.ts +0 -120
- package/src/__tests__/date-locale-channel.test.tsx +0 -305
- package/src/__tests__/field-carrier-sdui.test.tsx +0 -169
- package/src/__tests__/formatCurrency.centsDigit.test.tsx +0 -158
- package/src/__tests__/formatCurrency.minorUnits.test.tsx +0 -221
- package/src/__tests__/group-labelling-declaration.test.ts +0 -148
- package/src/__tests__/lookupCellDisplayField.test.tsx +0 -107
- package/src/__tests__/lookupCellDisplayName.test.tsx +0 -119
- package/src/__tests__/lookupCellRecordLink.test.tsx +0 -171
- package/src/__tests__/lookupCellReferenceKey.test.tsx +0 -57
- package/src/__tests__/percent-cell-vs-measure-4576.test.ts +0 -115
- package/src/__tests__/percent-formatter-locale-4553.test.ts +0 -147
- package/src/__tests__/percent-tie-halfup-4590.test.ts +0 -224
- package/src/__tests__/registry-collision.test.ts +0 -91
- package/src/__tests__/resolveFieldCurrency.test.ts +0 -20
- package/src/__tests__/select-label-association-e2e.test.tsx +0 -199
- 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 -357
- 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 -924
- package/src/currency.ts +0 -144
- package/src/datetime-widgets.test.tsx +0 -185
- package/src/field-type-alias.multiple.test.ts +0 -81
- package/src/field-type-alias.ts +0 -156
- package/src/field-type-coverage.test.ts +0 -67
- package/src/index.css +0 -44
- package/src/index.tsx +0 -2745
- package/src/standard-widgets.test.tsx +0 -364
- package/src/widgets/AddressField.postalCode.test.tsx +0 -257
- package/src/widgets/AddressField.tsx +0 -182
- package/src/widgets/AddressField.uniqueIds.test.tsx +0 -93
- package/src/widgets/AutoNumberField.tsx +0 -18
- package/src/widgets/AvatarField.tsx +0 -117
- package/src/widgets/BooleanField.tsx +0 -86
- package/src/widgets/CapabilityMultiSelectField.test.tsx +0 -168
- package/src/widgets/CapabilityMultiSelectField.tsx +0 -241
- package/src/widgets/CharacterCount.tsx +0 -263
- package/src/widgets/CheckboxesField.cascade.test.tsx +0 -143
- package/src/widgets/CheckboxesField.tsx +0 -169
- package/src/widgets/CodeField.tsx +0 -41
- package/src/widgets/ColorField.tsx +0 -53
- package/src/widgets/CurrencyField.tsx +0 -135
- package/src/widgets/DateField.tsx +0 -40
- package/src/widgets/DateTimeField.tsx +0 -46
- package/src/widgets/EmailField.tsx +0 -37
- package/src/widgets/FileField.tsx +0 -512
- package/src/widgets/FilterConditionField.tsx +0 -454
- package/src/widgets/FormulaField.tsx +0 -38
- package/src/widgets/FullscreenFieldEditor.tsx +0 -60
- package/src/widgets/GeolocationField.tsx +0 -193
- package/src/widgets/GeolocationField.uniqueIds.test.tsx +0 -88
- package/src/widgets/GridField.test.tsx +0 -632
- package/src/widgets/GridField.tsx +0 -1070
- package/src/widgets/ImageCropperDialog.tsx +0 -245
- package/src/widgets/ImageField.maxSize.test.tsx +0 -154
- package/src/widgets/ImageField.tsx +0 -305
- 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 -1295
- 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 -196
- 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 -99
- 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 -143
- package/src/widgets/RatingField.tsx +0 -94
- package/src/widgets/RecipientPickerField.test.tsx +0 -157
- package/src/widgets/RecipientPickerField.tsx +0 -188
- package/src/widgets/RecordPickerDialog.baseFilterShapes.test.tsx +0 -191
- package/src/widgets/RecordPickerDialog.dateLocale.test.tsx +0 -124
- package/src/widgets/RecordPickerDialog.filterOptions.test.tsx +0 -611
- package/src/widgets/RecordPickerDialog.selectLabel.test.tsx +0 -112
- package/src/widgets/RecordPickerDialog.tsx +0 -1365
- package/src/widgets/RichTextField.tsx +0 -176
- package/src/widgets/SelectField.cascade.test.tsx +0 -138
- package/src/widgets/SelectField.tsx +0 -193
- 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 -59
- 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 -166
- 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/address-format.ts +0 -120
- package/src/widgets/deriveLookupColumns.test.ts +0 -112
- package/src/widgets/deriveLookupColumns.ts +0 -108
- package/src/widgets/file-size-guard.ts +0 -55
- package/src/widgets/file-value.test.ts +0 -230
- package/src/widgets/file-value.ts +0 -246
- package/src/widgets/nativeDateValue.test.ts +0 -115
- package/src/widgets/nativeDateValue.ts +0 -112
- package/src/widgets/openNativePicker.ts +0 -18
- package/src/widgets/optionWidgets.gatedOptions.test.tsx +0 -267
- package/src/widgets/optionWidgets.unconfiguredOptions.test.tsx +0 -150
- 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 -204
- package/src/widgets/toHostGroupProps.ts +0 -102
- package/src/widgets/types.ts +0 -242
- package/src/widgets/useCascadingOptions.ts +0 -46
- package/src/widgets/useFieldTranslation.ts +0 -132
- package/src/widgets/useRecordQuery.test.ts +0 -174
- package/src/widgets/useRecordQuery.ts +0 -291
- package/src/widgets/useUploadingSignal.test.tsx +0 -62
- package/src/widgets/useUploadingSignal.ts +0 -20
- package/src/withFieldCarrier.tsx +0 -75
|
@@ -1,171 +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
|
-
/**
|
|
10
|
-
* objectui#4336 — a lookup value rendered read-only was plain text: the record
|
|
11
|
-
* detail page showed `TYG1WX20260812001001` with a copy button and no way to
|
|
12
|
-
* reach the referenced work order, and the related list's lookup columns had
|
|
13
|
-
* the same dead cells (the reporter's own check: no `<a>` anywhere over the
|
|
14
|
-
* value, every `href` null).
|
|
15
|
-
*
|
|
16
|
-
* `LookupCellRenderer` is the ONE cell renderer both surfaces resolve through
|
|
17
|
-
* (`DetailSection` and `RelatedList` each call `getCellRenderer('lookup')`), so
|
|
18
|
-
* the link belongs here — same path, both halves.
|
|
19
|
-
*
|
|
20
|
-
* The href is NOT assembled here. This package has no router and no idea what
|
|
21
|
-
* an app's record route looks like; the host that owns record URLs publishes
|
|
22
|
-
* them through `RelatedRecordActionsContext` (`recordHref` / `openRecord`),
|
|
23
|
-
* which is the same builder the related list's row navigation already uses. No
|
|
24
|
-
* host → no link, and the value renders exactly as it did before.
|
|
25
|
-
*/
|
|
26
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
27
|
-
import { render, screen, fireEvent } from '@testing-library/react';
|
|
28
|
-
import '@testing-library/jest-dom';
|
|
29
|
-
import React from 'react';
|
|
30
|
-
|
|
31
|
-
import { LookupCellRenderer } from '../index';
|
|
32
|
-
import { RelatedRecordActionsProvider } from '@object-ui/react';
|
|
33
|
-
|
|
34
|
-
/** The reporter's shape: `$expand`-ed lookup value (display name + record id). */
|
|
35
|
-
const WORK_ORDER = { id: 'wo-1', name: 'TYG1WX20260812001001' };
|
|
36
|
-
const FIELD = { type: 'lookup', reference_to: 'mtc_work_order' } as any;
|
|
37
|
-
|
|
38
|
-
/** A host that can route to records, mirroring the console bridge's shape. */
|
|
39
|
-
function makeHost(overrides: Record<string, unknown> = {}) {
|
|
40
|
-
return {
|
|
41
|
-
resolve: () => ({}),
|
|
42
|
-
recordHref: (objectName: string, recordId: string | number) =>
|
|
43
|
-
`/apps/demo/${objectName}/record/${encodeURIComponent(String(recordId))}`,
|
|
44
|
-
openRecord: vi.fn(),
|
|
45
|
-
...overrides,
|
|
46
|
-
} as any;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function renderWithHost(node: React.ReactElement, host: any = makeHost()) {
|
|
50
|
-
return render(
|
|
51
|
-
<RelatedRecordActionsProvider value={host}>{node}</RelatedRecordActionsProvider>,
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
describe('LookupCellRenderer — the value links to the referenced record (#4336)', () => {
|
|
56
|
-
it('expanded value: renders an anchor at the host-built record href', () => {
|
|
57
|
-
const { container } = renderWithHost(
|
|
58
|
-
<LookupCellRenderer value={WORK_ORDER} field={FIELD} />,
|
|
59
|
-
);
|
|
60
|
-
|
|
61
|
-
const anchor = container.querySelector('a');
|
|
62
|
-
expect(anchor).not.toBeNull();
|
|
63
|
-
expect(anchor).toHaveAttribute('href', '/apps/demo/mtc_work_order/record/wo-1');
|
|
64
|
-
// Display-name resolution is untouched — the anchor carries the same text.
|
|
65
|
-
expect(anchor).toHaveTextContent('TYG1WX20260812001001');
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it('primitive id value: links through the same host builder', () => {
|
|
69
|
-
const { container } = renderWithHost(
|
|
70
|
-
<LookupCellRenderer
|
|
71
|
-
value="wo-2"
|
|
72
|
-
field={{ ...FIELD, options: [{ value: 'wo-2', label: 'TYG1WX20260812001002' }] }}
|
|
73
|
-
/>,
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
const anchor = container.querySelector('a');
|
|
77
|
-
expect(anchor).toHaveAttribute('href', '/apps/demo/mtc_work_order/record/wo-2');
|
|
78
|
-
expect(anchor).toHaveTextContent('TYG1WX20260812001002');
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('multi-value: each referenced record gets its own anchor', () => {
|
|
82
|
-
const { container } = renderWithHost(
|
|
83
|
-
<LookupCellRenderer
|
|
84
|
-
value={[WORK_ORDER, { id: 'wo-3', name: 'TYG1WX20260812001003' }]}
|
|
85
|
-
field={FIELD}
|
|
86
|
-
/>,
|
|
87
|
-
);
|
|
88
|
-
|
|
89
|
-
const hrefs = Array.from(container.querySelectorAll('a')).map((a) => a.getAttribute('href'));
|
|
90
|
-
expect(hrefs).toEqual([
|
|
91
|
-
'/apps/demo/mtc_work_order/record/wo-1',
|
|
92
|
-
'/apps/demo/mtc_work_order/record/wo-3',
|
|
93
|
-
]);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('plain click navigates through the host (SPA) instead of reloading', () => {
|
|
97
|
-
const host = makeHost();
|
|
98
|
-
const rowClick = vi.fn();
|
|
99
|
-
const { container } = render(
|
|
100
|
-
<RelatedRecordActionsProvider value={host}>
|
|
101
|
-
{/* The surfaces around this cell carry their own click handlers — the
|
|
102
|
-
detail row copies the value, a related-list row opens the row's own
|
|
103
|
-
record. Activating the link must do neither. */}
|
|
104
|
-
<div onClick={rowClick}>
|
|
105
|
-
<LookupCellRenderer value={WORK_ORDER} field={FIELD} />
|
|
106
|
-
</div>
|
|
107
|
-
</RelatedRecordActionsProvider>,
|
|
108
|
-
);
|
|
109
|
-
|
|
110
|
-
const anchor = container.querySelector('a')!;
|
|
111
|
-
const event = new MouseEvent('click', { bubbles: true, cancelable: true, button: 0 });
|
|
112
|
-
fireEvent(anchor, event);
|
|
113
|
-
|
|
114
|
-
expect(host.openRecord).toHaveBeenCalledWith('mtc_work_order', 'wo-1');
|
|
115
|
-
expect(event.defaultPrevented).toBe(true);
|
|
116
|
-
expect(rowClick).not.toHaveBeenCalled();
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it('modifier click keeps the browser default (open in a new tab)', () => {
|
|
120
|
-
const host = makeHost();
|
|
121
|
-
const { container } = renderWithHost(
|
|
122
|
-
<LookupCellRenderer value={WORK_ORDER} field={FIELD} />,
|
|
123
|
-
host,
|
|
124
|
-
);
|
|
125
|
-
|
|
126
|
-
const anchor = container.querySelector('a')!;
|
|
127
|
-
const event = new MouseEvent('click', {
|
|
128
|
-
bubbles: true,
|
|
129
|
-
cancelable: true,
|
|
130
|
-
button: 0,
|
|
131
|
-
metaKey: true,
|
|
132
|
-
});
|
|
133
|
-
fireEvent(anchor, event);
|
|
134
|
-
|
|
135
|
-
expect(host.openRecord).not.toHaveBeenCalled();
|
|
136
|
-
expect(event.defaultPrevented).toBe(false);
|
|
137
|
-
});
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
describe('LookupCellRenderer — must-not-change (#4336)', () => {
|
|
141
|
-
it('no host: renders exactly as before, with no anchor', () => {
|
|
142
|
-
const { container } = render(<LookupCellRenderer value={WORK_ORDER} field={FIELD} />);
|
|
143
|
-
|
|
144
|
-
expect(container.querySelector('a')).toBeNull();
|
|
145
|
-
expect(screen.getByText('TYG1WX20260812001001')).toBeInTheDocument();
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it('host cannot route to the target object: no anchor, same text', () => {
|
|
149
|
-
const { container } = renderWithHost(
|
|
150
|
-
<LookupCellRenderer value={WORK_ORDER} field={FIELD} />,
|
|
151
|
-
makeHost({ recordHref: () => null }),
|
|
152
|
-
);
|
|
153
|
-
|
|
154
|
-
expect(container.querySelector('a')).toBeNull();
|
|
155
|
-
expect(screen.getByText('TYG1WX20260812001001')).toBeInTheDocument();
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
it('no reference target declared: no anchor', () => {
|
|
159
|
-
const { container } = renderWithHost(
|
|
160
|
-
<LookupCellRenderer value={WORK_ORDER} field={{ type: 'lookup' } as any} />,
|
|
161
|
-
);
|
|
162
|
-
|
|
163
|
-
expect(container.querySelector('a')).toBeNull();
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
it('empty value: the placeholder stays a placeholder — no empty link', () => {
|
|
167
|
-
const { container } = renderWithHost(<LookupCellRenderer value={null} field={FIELD} />);
|
|
168
|
-
|
|
169
|
-
expect(container.querySelector('a')).toBeNull();
|
|
170
|
-
});
|
|
171
|
-
});
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Regression: LookupCellRenderer must resolve a bare foreign-key id to a
|
|
3
|
-
* display name when the field metadata carries the ObjectStack `reference`
|
|
4
|
-
* key (as produced by `Field.lookup('...')` in @objectstack/spec) — not only
|
|
5
|
-
* the objectui `reference_to` alias.
|
|
6
|
-
*
|
|
7
|
-
* Real-world symptom (framework app-showcase): inline-edit a lookup cell, pick
|
|
8
|
-
* a record, click another row → the cell showed a muted "—" forever because
|
|
9
|
-
* the just-picked opaque id could not be resolved. Every other reader in the
|
|
10
|
-
* codebase (LookupField, UserField, DetailSection, RelatedList, …) already
|
|
11
|
-
* accepts `reference_to || reference`; this read cell used to read only
|
|
12
|
-
* `reference_to`.
|
|
13
|
-
*/
|
|
14
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
15
|
-
import { render, screen, waitFor } from '@testing-library/react';
|
|
16
|
-
import '@testing-library/jest-dom';
|
|
17
|
-
import React from 'react';
|
|
18
|
-
|
|
19
|
-
import { LookupCellRenderer } from '../index';
|
|
20
|
-
import { SchemaRendererProvider } from '@object-ui/react';
|
|
21
|
-
|
|
22
|
-
// A ULID-style id: `isLikelyOpaqueId` returns true for it, so the OLD code
|
|
23
|
-
// (which failed to resolve) would fall back to the muted "—" placeholder.
|
|
24
|
-
const OPAQUE_ID = '01ARZ3NDEKTSV4RRFFQ69G5FAV';
|
|
25
|
-
|
|
26
|
-
function makeDataSource() {
|
|
27
|
-
const findOne = vi.fn(async (object: string, id: string) => {
|
|
28
|
-
if (object === 'showcase_account' && id === OPAQUE_ID) {
|
|
29
|
-
return { id, name: 'Globex' };
|
|
30
|
-
}
|
|
31
|
-
return null;
|
|
32
|
-
});
|
|
33
|
-
return { findOne, find: vi.fn() } as any;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
describe('LookupCellRenderer — reference key resolution', () => {
|
|
37
|
-
it('resolves an opaque id to a name via the `reference` key (not just reference_to)', async () => {
|
|
38
|
-
const ds = makeDataSource();
|
|
39
|
-
render(
|
|
40
|
-
<SchemaRendererProvider dataSource={ds}>
|
|
41
|
-
<LookupCellRenderer
|
|
42
|
-
value={OPAQUE_ID}
|
|
43
|
-
// ObjectStack convention: the lookup target lives under `reference`.
|
|
44
|
-
field={{ type: 'lookup', reference: 'showcase_account' } as any}
|
|
45
|
-
/>
|
|
46
|
-
</SchemaRendererProvider>,
|
|
47
|
-
);
|
|
48
|
-
|
|
49
|
-
// The related record's display name must appear…
|
|
50
|
-
await waitFor(() => {
|
|
51
|
-
expect(screen.getByText('Globex')).toBeInTheDocument();
|
|
52
|
-
});
|
|
53
|
-
// …and the muted placeholder must NOT be what the user sees.
|
|
54
|
-
expect(screen.queryByText('—')).not.toBeInTheDocument();
|
|
55
|
-
expect(ds.findOne).toHaveBeenCalledWith('showcase_account', OPAQUE_ID);
|
|
56
|
-
});
|
|
57
|
-
});
|
|
@@ -1,115 +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
|
-
/**
|
|
10
|
-
* objectui#4576, asserted where the two surfaces can actually be compared.
|
|
11
|
-
*
|
|
12
|
-
* The card's complaint is a CROSS-PACKAGE one: the same stored number renders
|
|
13
|
-
* `1.234,5 %` through `formatPercent` (this package, the list cell) and
|
|
14
|
-
* `1.234,5%` through `formatMeasure` (`@object-ui/core`, the dashboard measure).
|
|
15
|
-
* Neither package's own suite can see that — `core` sits below this one and
|
|
16
|
-
* cannot import `formatPercent` — so the agreement is pinned here, the one
|
|
17
|
-
* place both are in scope.
|
|
18
|
-
*
|
|
19
|
-
* This file adds no implementation. `formatPercent` was untouched by #4576; it
|
|
20
|
-
* has rendered through `Intl` since #4553 / PR #4565 and was the SIDE the measure
|
|
21
|
-
* formatter moved onto.
|
|
22
|
-
*
|
|
23
|
-
* ── objectui#4590 ──
|
|
24
|
-
* The last case in this file was a NOT-a-defect pin recording that the two
|
|
25
|
-
* surfaces still disagreed at rounding TIES. #4590 closed that at the
|
|
26
|
-
* `formatPercent` end (it now renders percentage points directly, through the
|
|
27
|
-
* same `style: 'percentPoints'` the measure uses), so that pin is now an
|
|
28
|
-
* AGREEMENT pin. It is declared in place, at the case itself.
|
|
29
|
-
*
|
|
30
|
-
* ── PREDICTIONS, written before the run ──
|
|
31
|
-
* RED before the fix: every locale whose percent convention has a space or a
|
|
32
|
-
* different sign — de, fr, es, ru, tr, ar. `formatMeasure` returned a literal
|
|
33
|
-
* '%' with no space, `formatPercent` returned the locale's convention.
|
|
34
|
-
* GREEN on both sides: en-US, ja-JP, zh-CN, whose convention is a bare
|
|
35
|
-
* trailing '%' — the must-not-change half, and the reason this was invisible in
|
|
36
|
-
* an English session for three cards running.
|
|
37
|
-
*
|
|
38
|
-
* Runner measured: node v22.22.2, ICU 78.2, machine locale en-US.
|
|
39
|
-
*/
|
|
40
|
-
|
|
41
|
-
import { describe, it, expect } from 'vitest';
|
|
42
|
-
import { formatMeasure } from '@object-ui/core';
|
|
43
|
-
import { formatPercent } from '../index';
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Strip the digits out of a rendered percentage, leaving only the CONVENTION —
|
|
47
|
-
* the sign, its position and any separator before it. Comparing conventions
|
|
48
|
-
* rather than whole strings is deliberate: the two formatters are reached with
|
|
49
|
-
* different scaling inputs and different decimal widths, and it is the
|
|
50
|
-
* convention, not the digits, that #4576 is about.
|
|
51
|
-
*/
|
|
52
|
-
function convention(rendered: string): string {
|
|
53
|
-
return rendered.replace(/[\d]/gu, '#').replace(/#+/gu, '#');
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
describe('a percent renders under ONE convention in a cell and in a measure (#4576)', () => {
|
|
57
|
-
it.each([
|
|
58
|
-
['de-DE'],
|
|
59
|
-
['fr-FR'],
|
|
60
|
-
['es-ES'],
|
|
61
|
-
['ru-RU'],
|
|
62
|
-
['tr-TR'],
|
|
63
|
-
['ar-EG'],
|
|
64
|
-
// must-not-change half — no space in the convention, identical before and after
|
|
65
|
-
['en-US'],
|
|
66
|
-
['ja-JP'],
|
|
67
|
-
['zh-CN'],
|
|
68
|
-
])('%s renders the same percent convention on both surfaces', (locale) => {
|
|
69
|
-
// The same underlying value on both paths: `formatPercent` takes the STORED
|
|
70
|
-
// value and scales it through `percentDisplayValue`; `formatMeasure` is
|
|
71
|
-
// told the column is already in whole percentage points.
|
|
72
|
-
const cell = formatPercent(0.805, 1, locale);
|
|
73
|
-
const measure = formatMeasure(80.5, '0.0%', undefined, 'whole', locale);
|
|
74
|
-
expect(convention(measure)).toBe(convention(cell));
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('the German case the card was filed for, spelled out', () => {
|
|
78
|
-
// Both `1.234,5` followed by U+00A0 and the sign. Before #4576 the measure
|
|
79
|
-
// had no space.
|
|
80
|
-
expect(formatPercent(1234.5, 1, 'de-DE')).toBe('1.234,5\u00a0%');
|
|
81
|
-
expect(formatMeasure(1234.5, '0.0%', undefined, 'whole', 'de-DE')).toBe('1.234,5\u00a0%');
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('MUST NOT CHANGE: English was always identical on both paths, and stays so', () => {
|
|
85
|
-
expect(formatPercent(1234.5, 1, 'en-US')).toBe('1,234.5%');
|
|
86
|
-
expect(formatMeasure(1234.5, '0.0%', undefined, 'whole', 'en-US')).toBe('1,234.5%');
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('AGREEMENT pin — the two now round ties the same way (was NOT-a-defect, closed by #4590)', () => {
|
|
90
|
-
// ── PIN MOVED (objectui#4590) ──
|
|
91
|
-
// This case was a NOT-a-defect pin: it asserted the two surfaces DISAGREED
|
|
92
|
-
// at rounding ties, recorded honestly rather than omitted, and said the gap
|
|
93
|
-
// was #4565's residue rather than #4576's business. #4590 closed it at the
|
|
94
|
-
// `formatPercent` end, so the pin flips from recording a divergence to
|
|
95
|
-
// asserting the agreement.
|
|
96
|
-
//
|
|
97
|
-
// Before: formatPercent(1.005, 2, 'en-US') -> '1.00%' (the artefact)
|
|
98
|
-
// formatMeasure(1.005, …) -> '1.01%' (the faithful one)
|
|
99
|
-
// After: both -> '1.01%'.
|
|
100
|
-
//
|
|
101
|
-
// The cause was `formatPercent` dividing by 100 so `Intl`'s
|
|
102
|
-
// `style: 'percent'` could multiply back — lossy at ties, because the
|
|
103
|
-
// quotient's shortest decimal representation is not the authored one.
|
|
104
|
-
// `formatMeasure` had already moved onto `style: 'percentPoints'` in #4576;
|
|
105
|
-
// the cell now renders through the same option, so there is one route and
|
|
106
|
-
// no second rounding behaviour to keep in step.
|
|
107
|
-
expect(formatPercent(1.005, 2, 'en-US')).toBe('1.01%');
|
|
108
|
-
expect(formatMeasure(1.005, '0.00%', undefined, 'whole', 'en-US')).toBe('1.01%');
|
|
109
|
-
// …and asserted as an AGREEMENT rather than two coincidences, so a future
|
|
110
|
-
// divergence at either end fails here whatever the two happen to render.
|
|
111
|
-
expect(formatPercent(1.005, 2, 'en-US')).toBe(
|
|
112
|
-
formatMeasure(1.005, '0.00%', undefined, 'whole', 'en-US'),
|
|
113
|
-
);
|
|
114
|
-
});
|
|
115
|
-
});
|
|
@@ -1,147 +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
|
-
/**
|
|
10
|
-
* objectui#4553 phase 2 — `formatPercent` was the last formatter outside the
|
|
11
|
-
* display-locale channel, and it was outside it in a different way from its
|
|
12
|
-
* siblings.
|
|
13
|
-
*
|
|
14
|
-
* PR #4557 threaded the gantt tooltip's number and currency rows and measured
|
|
15
|
-
* that percent could not follow: `formatPercent(value, precision)` took no
|
|
16
|
-
* locale parameter, and its whole body was
|
|
17
|
-
* `${percentDisplayValue(value).toFixed(precision)}%`. It built no
|
|
18
|
-
* `Intl.NumberFormat` and never reached `formatDisplayNumber`, so its output
|
|
19
|
-
* was not the MACHINE's locale (the defect its siblings had) but NO locale at
|
|
20
|
-
* all: an ASCII decimal mark, never a grouping separator, byte-identical on
|
|
21
|
-
* every machine on earth.
|
|
22
|
-
*
|
|
23
|
-
* That is why grouping and locale land together here. `1235%` is not merely
|
|
24
|
-
* un-German — it is wrong in en-US too, where the number is `1,235%`. So the
|
|
25
|
-
* English output MOVES, and that move is the fix, not a regression.
|
|
26
|
-
*
|
|
27
|
-
* ── Directions, predicted in writing BEFORE the run, then measured ───────
|
|
28
|
-
* Runner: node v22.22.2 / ICU 78.2 / machine locale en-US.
|
|
29
|
-
*
|
|
30
|
-
* en 1234.5 p0 `1235%` → `1,235%` RED — the grouping move
|
|
31
|
-
* de 1234.5 p0 `1235%` → `1.235 %` RED — separators inverted + NBSP
|
|
32
|
-
* de 80 p0 `80%` → `80 %` RED — de writes a no-break space
|
|
33
|
-
* before the sign; en does not
|
|
34
|
-
* de 12.5 p1 `12.5%` → `12,5 %` RED — the ruling's named form
|
|
35
|
-
* en 80 p0 `80%` → `80%` PIN, green both sides
|
|
36
|
-
* en 12.5 p1 `12.5%` → `12.5%` PIN, green both sides
|
|
37
|
-
* fraction scaling (0.8 → 80%) PIN, green both sides
|
|
38
|
-
*
|
|
39
|
-
* The `en` pins are byte-identical because `en` and the runner's `en-US` agree
|
|
40
|
-
* and because grouping only shows from four digits up — they are NOT evidence
|
|
41
|
-
* the fix works; the `de` cases and the en GROUPING case carry that.
|
|
42
|
-
*
|
|
43
|
-
* Provider-less by construction (objectui#4514): these are pure-function cases
|
|
44
|
-
* that touch no hook. The provider-mounted half of this card lives in
|
|
45
|
-
* `PercentCellRenderer.locale.test.tsx`.
|
|
46
|
-
*/
|
|
47
|
-
|
|
48
|
-
import { describe, it, expect } from 'vitest';
|
|
49
|
-
import { formatPercent } from '../index';
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* German puts a NO-BREAK SPACE (U+00A0) between the number and the percent
|
|
53
|
-
* sign. Written as an escape rather than pasted, so the expectation is
|
|
54
|
-
* readable and greppable in both spellings.
|
|
55
|
-
*/
|
|
56
|
-
const NBSP = '\u00a0';
|
|
57
|
-
|
|
58
|
-
describe('formatPercent groups its output (objectui#4553)', () => {
|
|
59
|
-
/**
|
|
60
|
-
* THE en RED CASE, and the one that justifies calling this a defect rather
|
|
61
|
-
* than a localization nicety: four digits went out ungrouped in English too.
|
|
62
|
-
*/
|
|
63
|
-
it('en groups from four digits up — the output MOVES, and that is the fix', () => {
|
|
64
|
-
expect(formatPercent(1234.5, 0, 'en')).toBe('1,235%');
|
|
65
|
-
expect(formatPercent(1000000, 0, 'en')).toBe('1,000,000%');
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
/** PIN, green both sides: below the grouping threshold nothing changes. */
|
|
69
|
-
it('en output below the grouping threshold is byte-identical (must-not-change)', () => {
|
|
70
|
-
expect(formatPercent(80, 0, 'en')).toBe('80%');
|
|
71
|
-
expect(formatPercent(12.5, 1, 'en')).toBe('12.5%');
|
|
72
|
-
expect(formatPercent(33.33, 2, 'en')).toBe('33.33%');
|
|
73
|
-
expect(formatPercent(0, 0, 'en')).toBe('0%');
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
describe('formatPercent follows the display locale (objectui#4553)', () => {
|
|
78
|
-
/**
|
|
79
|
-
* The separators invert AND a no-break space appears before the sign, so the
|
|
80
|
-
* German form cannot coincide with the machine's on this runner.
|
|
81
|
-
*/
|
|
82
|
-
it('de inverts the separators and spaces the percent sign', () => {
|
|
83
|
-
expect(formatPercent(1234.5, 0, 'de')).toBe(`1.235${NBSP}%`);
|
|
84
|
-
expect(formatPercent(12.5, 1, 'de')).toBe(`12,5${NBSP}%`);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Even with no separator in play, German still differs from English: the
|
|
89
|
-
* space before the sign is part of the locale's percent CONVENTION, which is
|
|
90
|
-
* what routing through `Intl` buys over appending a literal '%'.
|
|
91
|
-
*/
|
|
92
|
-
it('de spaces the sign even for a value with no separators', () => {
|
|
93
|
-
expect(formatPercent(80, 0, 'de')).toBe(`80${NBSP}%`);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
/** `Intl` accepts `'zh'` verbatim — no mapping table anywhere. */
|
|
97
|
-
it('zh renders its own convention', () => {
|
|
98
|
-
expect(formatPercent(1234.5, 0, 'zh')).toBe('1,235%');
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* A malformed tag from a tenant config must never take a cell down —
|
|
103
|
-
* `formatDisplayNumber` catches it and retries without the locale.
|
|
104
|
-
*/
|
|
105
|
-
it('a malformed locale tag falls back instead of throwing', () => {
|
|
106
|
-
expect(() => formatPercent(80, 0, 'not a locale')).not.toThrow();
|
|
107
|
-
expect(formatPercent(80, 0, 'not a locale')).toContain('80');
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
describe('formatPercent keeps its existing contract (objectui#4553 must-not-change)', () => {
|
|
112
|
-
/**
|
|
113
|
-
* PIN: the fraction/whole disambiguation is `percentDisplayValue`'s, shared
|
|
114
|
-
* with the dashboard measure formatter. The locale parameter must not have
|
|
115
|
-
* moved it.
|
|
116
|
-
*/
|
|
117
|
-
it('still scales a fraction-stored percent and passes a whole one through', () => {
|
|
118
|
-
expect(formatPercent(0.8, 0, 'en')).toBe('80%');
|
|
119
|
-
expect(formatPercent(0.5, 0, 'en')).toBe('50%');
|
|
120
|
-
expect(formatPercent(0.075, 1, 'en')).toBe('7.5%');
|
|
121
|
-
// >= 1 is already in display magnitude and is NOT scaled again.
|
|
122
|
-
expect(formatPercent(80, 0, 'en')).toBe('80%');
|
|
123
|
-
expect(formatPercent(100, 0, 'en')).toBe('100%');
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it('still honors the precision it is given', () => {
|
|
127
|
-
expect(formatPercent(33.333, 0, 'en')).toBe('33%');
|
|
128
|
-
expect(formatPercent(33.333, 1, 'en')).toBe('33.3%');
|
|
129
|
-
expect(formatPercent(33.333, 2, 'en')).toBe('33.33%');
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it('still handles negatives', () => {
|
|
133
|
-
expect(formatPercent(-45.5, 1, 'en')).toBe('-45.5%');
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* The parameter is OPTIONAL and third, matching `formatNumber(value,
|
|
138
|
-
* decimals, locale)` and `formatCurrency(value, currency, locale)`. An
|
|
139
|
-
* existing caller passing nothing still gets the runtime default locale —
|
|
140
|
-
* though it now also gets grouping, which is the deliberate output move.
|
|
141
|
-
*/
|
|
142
|
-
it('is callable with no locale, and with no precision either', () => {
|
|
143
|
-
expect(() => formatPercent(80)).not.toThrow();
|
|
144
|
-
expect(formatPercent(80)).toBe('80%');
|
|
145
|
-
expect(formatPercent(80, 0)).toBe('80%');
|
|
146
|
-
});
|
|
147
|
-
});
|