@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,112 +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
|
-
* Value adapters for the native date/time controls (objectui#3127).
|
|
11
|
-
*
|
|
12
|
-
* ## The defect these close
|
|
13
|
-
*
|
|
14
|
-
* `<input type="date">` accepts exactly `YYYY-MM-DD` and
|
|
15
|
-
* `<input type="datetime-local">` exactly `YYYY-MM-DDTHH:mm[:ss]` — a **local
|
|
16
|
-
* wall clock with no zone suffix**. Anything else is not "coerced" or
|
|
17
|
-
* "warned about": the browser SILENTLY rejects it. The attribute still lands
|
|
18
|
-
* in the DOM, so the value looks present to anyone inspecting markup, while
|
|
19
|
-
* `input.value` reads back `""` and the control paints its empty placeholder:
|
|
20
|
-
*
|
|
21
|
-
* ```js
|
|
22
|
-
* el.getAttribute('value') // "2026-08-16T00:33:00.000Z" ← the record's value
|
|
23
|
-
* el.value // "" ← what the user sees
|
|
24
|
-
* ```
|
|
25
|
-
*
|
|
26
|
-
* Records come back from the API as full ISO-8601 (`…T00:33:00.000Z`) — which
|
|
27
|
-
* is precisely one of those rejected shapes. So EVERY datetime field in an
|
|
28
|
-
* edit dialog rendered blank while holding a real value, and users read that
|
|
29
|
-
* as "the date was lost" and retyped it (which is the only way the display bug
|
|
30
|
-
* could turn into a data change).
|
|
31
|
-
*
|
|
32
|
-
* ## Why a round trip, and not just a formatter
|
|
33
|
-
*
|
|
34
|
-
* Formatting on the way IN without converting on the way OUT would be the
|
|
35
|
-
* worse bug: the control would then be read as UTC but written back as a naive
|
|
36
|
-
* local string, so a user in UTC+8 who picked "08:33" would store 08:33Z and
|
|
37
|
-
* see 16:33 on the detail page. Read basis and write basis must be the same
|
|
38
|
-
* one, so `toDateTimeInputValue` / `fromDateTimeInputValue` are a pair and are
|
|
39
|
-
* used as a pair.
|
|
40
|
-
*
|
|
41
|
-
* ## Shape kept in form state
|
|
42
|
-
*
|
|
43
|
-
* `datetime` round-trips through ISO — the shape the API hands us and the one
|
|
44
|
-
* display/format code already assumes. `date` stays a plain `YYYY-MM-DD`
|
|
45
|
-
* string, matching what the inline cell editor in `@object-ui/components`
|
|
46
|
-
* (`data-table.tsx`) and `@object-ui/plugin-detail`'s `InlineFieldInput`
|
|
47
|
-
* already do. Untouched fields are never re-emitted, so a form that opens and
|
|
48
|
-
* closes without an edit still submits nothing for them.
|
|
49
|
-
*/
|
|
50
|
-
|
|
51
|
-
function pad2(n: number): string {
|
|
52
|
-
return String(n).padStart(2, '0');
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Stored value → `<input type="date">` value (`YYYY-MM-DD`).
|
|
57
|
-
*
|
|
58
|
-
* A leading `YYYY-MM-DD` is passed through VERBATIM rather than re-parsed: a
|
|
59
|
-
* date-only string is parsed as UTC midnight by `new Date()`, so reading local
|
|
60
|
-
* calendar components back out of it shifts the day by one everywhere west of
|
|
61
|
-
* Greenwich. `2026-06-17` must stay June 17th in every zone.
|
|
62
|
-
*/
|
|
63
|
-
export function toDateInputValue(value: unknown): string {
|
|
64
|
-
if (value == null || value === '') return '';
|
|
65
|
-
if (typeof value === 'string') {
|
|
66
|
-
const m = value.match(/^(\d{4}-\d{2}-\d{2})/);
|
|
67
|
-
if (m) return m[1];
|
|
68
|
-
}
|
|
69
|
-
const d = value instanceof Date ? value : new Date(String(value));
|
|
70
|
-
if (Number.isNaN(d.getTime())) return '';
|
|
71
|
-
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Stored value → `<input type="datetime-local">` value (`YYYY-MM-DDTHH:mm`),
|
|
76
|
-
* expressed in the viewer's local wall clock — the same instant the detail
|
|
77
|
-
* page shows, so opening the editor never appears to move the time.
|
|
78
|
-
*
|
|
79
|
-
* A value that is ALREADY a zone-less local datetime is truncated to minutes
|
|
80
|
-
* in place instead of being round-tripped through `Date`, which would
|
|
81
|
-
* reinterpret it and then re-render it, twice risking an off-by-a-zone shift.
|
|
82
|
-
*/
|
|
83
|
-
export function toDateTimeInputValue(value: unknown): string {
|
|
84
|
-
if (value == null || value === '') return '';
|
|
85
|
-
if (typeof value === 'string') {
|
|
86
|
-
const local = value.match(/^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2})(?::\d{2}(?:\.\d+)?)?$/);
|
|
87
|
-
if (local) return `${local[1]}T${local[2]}`;
|
|
88
|
-
}
|
|
89
|
-
const d = value instanceof Date ? value : new Date(String(value));
|
|
90
|
-
if (Number.isNaN(d.getTime())) return '';
|
|
91
|
-
return (
|
|
92
|
-
`${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}` +
|
|
93
|
-
`T${pad2(d.getHours())}:${pad2(d.getMinutes())}`
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* `<input type="datetime-local">` value → stored value (ISO-8601, UTC).
|
|
99
|
-
*
|
|
100
|
-
* The control hands us a zone-less local wall clock; `new Date()` reads that
|
|
101
|
-
* form as local time, which is the inverse of what `toDateTimeInputValue`
|
|
102
|
-
* wrote — the pair is what keeps read and write on one basis.
|
|
103
|
-
*
|
|
104
|
-
* An unparseable string is returned untouched rather than blanked: dropping a
|
|
105
|
-
* value the user can see in the box would be a data loss dressed up as a fix.
|
|
106
|
-
*/
|
|
107
|
-
export function fromDateTimeInputValue(value: string): string {
|
|
108
|
-
if (!value) return '';
|
|
109
|
-
const d = new Date(value);
|
|
110
|
-
if (Number.isNaN(d.getTime())) return value;
|
|
111
|
-
return d.toISOString();
|
|
112
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Open the browser's native picker (date / datetime-local / time) for an input.
|
|
3
|
-
*
|
|
4
|
-
* Native date/time inputs only open their picker when the user clicks the small
|
|
5
|
-
* calendar/clock icon. Calling `showPicker()` on click makes the whole field
|
|
6
|
-
* behave like other widgets — clicking anywhere in the box opens the picker.
|
|
7
|
-
*
|
|
8
|
-
* `showPicker()` can throw (unsupported browser, not user-activated, or the input
|
|
9
|
-
* is disabled/hidden), so failures are swallowed and the native fallback stands.
|
|
10
|
-
*/
|
|
11
|
-
export function openNativePicker(input: HTMLInputElement): void {
|
|
12
|
-
if (input.disabled || input.readOnly) return;
|
|
13
|
-
try {
|
|
14
|
-
(input as HTMLInputElement & { showPicker?: () => void }).showPicker?.();
|
|
15
|
-
} catch {
|
|
16
|
-
// Ignore — browsers throw when the picker can't be shown; the icon still works.
|
|
17
|
-
}
|
|
18
|
-
}
|
|
@@ -1,267 +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
|
-
* A GATED option list never deletes the stored value (objectui#4247).
|
|
11
|
-
*
|
|
12
|
-
* The four fixed-option widgets end their cascade resolution with a "drop what
|
|
13
|
-
* is no longer offered" effect (ADR-0058 / #2715). `resolveCascadingOptions`
|
|
14
|
-
* returns an EMPTY offered set whenever the list is gated — a declared
|
|
15
|
-
* `dependsOn` parent is still empty — so on mount the effect found nothing
|
|
16
|
-
* "still offered" and wrote the field empty, while the widget was rendering
|
|
17
|
-
* `OptionsEmptyState` ("select Country first") right next to it: the control
|
|
18
|
-
* tells the user it cannot offer anything, and deletes what they had.
|
|
19
|
-
*
|
|
20
|
-
* Ruling (#4247): **gated means UNKNOWN, not invalid.** The cascade clear exists
|
|
21
|
-
* so a USER-DRIVEN parent change prunes a now-invalid child; a gated set on
|
|
22
|
-
* MOUNT is missing information — the record simply arrived with the parent
|
|
23
|
-
* empty (a later-cleared parent, an import, a partially-migrated row) — and
|
|
24
|
-
* destroying stored data on missing information is silent data loss. Convergence
|
|
25
|
-
* stays where it belongs: once the parent IS chosen and the resolved set
|
|
26
|
-
* genuinely excludes the stored value, the existing prune applies.
|
|
27
|
-
*
|
|
28
|
-
* Three states, kept apart here so a future edit cannot collapse them:
|
|
29
|
-
*
|
|
30
|
-
* 1. never-configured — no authored `options` at all. Guarded since #4220;
|
|
31
|
-
* control only, pinned in `optionWidgets.unconfiguredOptions.test.tsx`.
|
|
32
|
-
* 2. gated — an authored list withheld behind an unmet `dependsOn`. NEW: the
|
|
33
|
-
* value survives.
|
|
34
|
-
* 3. resolved-and-excludes — the parent IS chosen and the resolved set does not
|
|
35
|
-
* offer the stored value. Clears; that is the ADR-0058 contract.
|
|
36
|
-
*
|
|
37
|
-
* The gate is read from the resolver's own `gated` flag, never re-derived from
|
|
38
|
-
* `options.length === 0` — that would collide with state 1, which is a
|
|
39
|
-
* different case with a different guard.
|
|
40
|
-
*/
|
|
41
|
-
|
|
42
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
43
|
-
import React from 'react';
|
|
44
|
-
import { render, screen } from '@testing-library/react';
|
|
45
|
-
import '@testing-library/jest-dom';
|
|
46
|
-
import { SelectField } from './SelectField';
|
|
47
|
-
import { MultiSelectField } from './MultiSelectField';
|
|
48
|
-
import { CheckboxesField } from './CheckboxesField';
|
|
49
|
-
import { RadioField } from './RadioField';
|
|
50
|
-
|
|
51
|
-
/** Options that exist for exactly one country — the cascade the gate withholds. */
|
|
52
|
-
const PROVINCE_OPTIONS = [
|
|
53
|
-
{ label: 'Zhejiang', value: 'zj', visibleWhen: "record.country == 'cn'" },
|
|
54
|
-
{ label: 'California', value: 'ca', visibleWhen: "record.country == 'us'" },
|
|
55
|
-
];
|
|
56
|
-
|
|
57
|
-
const provinceField = (type: string) =>
|
|
58
|
-
({ name: 'province', type, dependsOn: 'country', options: PROVINCE_OPTIONS }) as any;
|
|
59
|
-
|
|
60
|
-
describe('option widgets — a GATED list never deletes the stored value (#4247)', () => {
|
|
61
|
-
it('MultiSelectField keeps its array and shows the parent-first hint', () => {
|
|
62
|
-
const onChange = vi.fn();
|
|
63
|
-
render(
|
|
64
|
-
<MultiSelectField
|
|
65
|
-
value={['zj']}
|
|
66
|
-
onChange={onChange}
|
|
67
|
-
field={provinceField('multiselect')}
|
|
68
|
-
{...({ name: 'province', dependentValues: {} } as any)}
|
|
69
|
-
/>,
|
|
70
|
-
);
|
|
71
|
-
expect(screen.getByTestId('multiselect-empty-province')).toHaveTextContent(
|
|
72
|
-
/select country first/i,
|
|
73
|
-
);
|
|
74
|
-
expect(onChange).not.toHaveBeenCalled();
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('CheckboxesField keeps its array and shows the parent-first hint', () => {
|
|
78
|
-
const onChange = vi.fn();
|
|
79
|
-
render(
|
|
80
|
-
<CheckboxesField
|
|
81
|
-
value={['zj']}
|
|
82
|
-
onChange={onChange}
|
|
83
|
-
field={provinceField('checkboxes')}
|
|
84
|
-
{...({ name: 'province', dependentValues: {} } as any)}
|
|
85
|
-
/>,
|
|
86
|
-
);
|
|
87
|
-
expect(screen.getByTestId('checkboxes-empty-province')).toHaveTextContent(
|
|
88
|
-
/select country first/i,
|
|
89
|
-
);
|
|
90
|
-
expect(onChange).not.toHaveBeenCalled();
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it('RadioField keeps its value and shows the parent-first hint', () => {
|
|
94
|
-
const onChange = vi.fn();
|
|
95
|
-
render(
|
|
96
|
-
<RadioField
|
|
97
|
-
value="zj"
|
|
98
|
-
onChange={onChange}
|
|
99
|
-
field={provinceField('radio')}
|
|
100
|
-
{...({ name: 'province', dependentValues: {} } as any)}
|
|
101
|
-
/>,
|
|
102
|
-
);
|
|
103
|
-
expect(screen.getByTestId('radio-empty-province')).toHaveTextContent(/select country first/i);
|
|
104
|
-
expect(onChange).not.toHaveBeenCalled();
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it('SelectField keeps its value and shows the parent-first hint', () => {
|
|
108
|
-
const onChange = vi.fn();
|
|
109
|
-
render(
|
|
110
|
-
<SelectField
|
|
111
|
-
value="zj"
|
|
112
|
-
onChange={onChange}
|
|
113
|
-
field={provinceField('select')}
|
|
114
|
-
{...({ name: 'province', dependentValues: {} } as any)}
|
|
115
|
-
/>,
|
|
116
|
-
);
|
|
117
|
-
expect(screen.getByTestId('select-empty-province')).toHaveTextContent(/select country first/i);
|
|
118
|
-
expect(onChange).not.toHaveBeenCalled();
|
|
119
|
-
});
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
describe('controls — a RESOLVED list still prunes what it does not offer (ADR-0058)', () => {
|
|
123
|
-
it('MultiSelectField drops the element the chosen parent excludes', () => {
|
|
124
|
-
const onChange = vi.fn();
|
|
125
|
-
render(
|
|
126
|
-
<MultiSelectField
|
|
127
|
-
value={['zj', 'ca']}
|
|
128
|
-
onChange={onChange}
|
|
129
|
-
field={provinceField('multiselect')}
|
|
130
|
-
{...({ name: 'province', dependentValues: { country: 'cn' } } as any)}
|
|
131
|
-
/>,
|
|
132
|
-
);
|
|
133
|
-
expect(onChange).toHaveBeenCalledWith(['zj']);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it('CheckboxesField drops the element the chosen parent excludes', () => {
|
|
137
|
-
const onChange = vi.fn();
|
|
138
|
-
render(
|
|
139
|
-
<CheckboxesField
|
|
140
|
-
value={['zj', 'ca']}
|
|
141
|
-
onChange={onChange}
|
|
142
|
-
field={provinceField('checkboxes')}
|
|
143
|
-
{...({ name: 'province', dependentValues: { country: 'cn' } } as any)}
|
|
144
|
-
/>,
|
|
145
|
-
);
|
|
146
|
-
expect(onChange).toHaveBeenCalledWith(['zj']);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it('RadioField clears a value the chosen parent excludes', () => {
|
|
150
|
-
const onChange = vi.fn();
|
|
151
|
-
render(
|
|
152
|
-
<RadioField
|
|
153
|
-
value="ca"
|
|
154
|
-
onChange={onChange}
|
|
155
|
-
field={provinceField('radio')}
|
|
156
|
-
{...({ name: 'province', dependentValues: { country: 'cn' } } as any)}
|
|
157
|
-
/>,
|
|
158
|
-
);
|
|
159
|
-
expect(onChange).toHaveBeenCalledWith(undefined);
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
it('SelectField clears a value the chosen parent excludes', () => {
|
|
163
|
-
const onChange = vi.fn();
|
|
164
|
-
render(
|
|
165
|
-
<SelectField
|
|
166
|
-
value="ca"
|
|
167
|
-
onChange={onChange}
|
|
168
|
-
field={provinceField('select')}
|
|
169
|
-
{...({ name: 'province', dependentValues: { country: 'cn' } } as any)}
|
|
170
|
-
/>,
|
|
171
|
-
);
|
|
172
|
-
expect(onChange).toHaveBeenCalledWith(undefined);
|
|
173
|
-
});
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
describe('transition — a gated field converges the moment its parent is chosen (#4247)', () => {
|
|
177
|
-
it('MultiSelectField: gated keeps, then prunes once the resolved set excludes', () => {
|
|
178
|
-
const onChange = vi.fn();
|
|
179
|
-
const { rerender } = render(
|
|
180
|
-
<MultiSelectField
|
|
181
|
-
value={['zj']}
|
|
182
|
-
onChange={onChange}
|
|
183
|
-
field={provinceField('multiselect')}
|
|
184
|
-
{...({ name: 'province', dependentValues: {} } as any)}
|
|
185
|
-
/>,
|
|
186
|
-
);
|
|
187
|
-
// Gated: the value survives, nothing is staged.
|
|
188
|
-
expect(onChange).not.toHaveBeenCalled();
|
|
189
|
-
|
|
190
|
-
// The user picks the parent — the set resolves and genuinely excludes 'zj'.
|
|
191
|
-
rerender(
|
|
192
|
-
<MultiSelectField
|
|
193
|
-
value={['zj']}
|
|
194
|
-
onChange={onChange}
|
|
195
|
-
field={provinceField('multiselect')}
|
|
196
|
-
{...({ name: 'province', dependentValues: { country: 'us' } } as any)}
|
|
197
|
-
/>,
|
|
198
|
-
);
|
|
199
|
-
expect(onChange).toHaveBeenCalledWith([]);
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
it('MultiSelectField: a resolved set that INCLUDES the stored value keeps it', () => {
|
|
203
|
-
const onChange = vi.fn();
|
|
204
|
-
const { rerender } = render(
|
|
205
|
-
<MultiSelectField
|
|
206
|
-
value={['zj']}
|
|
207
|
-
onChange={onChange}
|
|
208
|
-
field={provinceField('multiselect')}
|
|
209
|
-
{...({ name: 'province', dependentValues: {} } as any)}
|
|
210
|
-
/>,
|
|
211
|
-
);
|
|
212
|
-
rerender(
|
|
213
|
-
<MultiSelectField
|
|
214
|
-
value={['zj']}
|
|
215
|
-
onChange={onChange}
|
|
216
|
-
field={provinceField('multiselect')}
|
|
217
|
-
{...({ name: 'province', dependentValues: { country: 'cn' } } as any)}
|
|
218
|
-
/>,
|
|
219
|
-
);
|
|
220
|
-
expect(onChange).not.toHaveBeenCalled();
|
|
221
|
-
expect(screen.getByTestId('multiselect-option-zj')).toBeInTheDocument();
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
it('SelectField: gated keeps, then clears once the resolved set excludes', () => {
|
|
225
|
-
const onChange = vi.fn();
|
|
226
|
-
const { rerender } = render(
|
|
227
|
-
<SelectField
|
|
228
|
-
value="zj"
|
|
229
|
-
onChange={onChange}
|
|
230
|
-
field={provinceField('select')}
|
|
231
|
-
{...({ name: 'province', dependentValues: {} } as any)}
|
|
232
|
-
/>,
|
|
233
|
-
);
|
|
234
|
-
expect(onChange).not.toHaveBeenCalled();
|
|
235
|
-
|
|
236
|
-
rerender(
|
|
237
|
-
<SelectField
|
|
238
|
-
value="zj"
|
|
239
|
-
onChange={onChange}
|
|
240
|
-
field={provinceField('select')}
|
|
241
|
-
{...({ name: 'province', dependentValues: { country: 'us' } } as any)}
|
|
242
|
-
/>,
|
|
243
|
-
);
|
|
244
|
-
expect(onChange).toHaveBeenCalledWith(undefined);
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
it('SelectField: a resolved set that INCLUDES the stored value keeps it', () => {
|
|
248
|
-
const onChange = vi.fn();
|
|
249
|
-
const { rerender } = render(
|
|
250
|
-
<SelectField
|
|
251
|
-
value="zj"
|
|
252
|
-
onChange={onChange}
|
|
253
|
-
field={provinceField('select')}
|
|
254
|
-
{...({ name: 'province', dependentValues: {} } as any)}
|
|
255
|
-
/>,
|
|
256
|
-
);
|
|
257
|
-
rerender(
|
|
258
|
-
<SelectField
|
|
259
|
-
value="zj"
|
|
260
|
-
onChange={onChange}
|
|
261
|
-
field={provinceField('select')}
|
|
262
|
-
{...({ name: 'province', dependentValues: { country: 'cn' } } as any)}
|
|
263
|
-
/>,
|
|
264
|
-
);
|
|
265
|
-
expect(onChange).not.toHaveBeenCalled();
|
|
266
|
-
});
|
|
267
|
-
});
|
|
@@ -1,150 +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
|
-
* An option widget with NO authored `options` keeps the value it was given
|
|
11
|
-
* (objectui#4220).
|
|
12
|
-
*
|
|
13
|
-
* The four fixed-option widgets each end their cascade resolution with a
|
|
14
|
-
* "clear what is no longer offered" effect (ADR-0058 / #2715). It ran against
|
|
15
|
-
* the RESOLVED set without asking why that set was empty, and the two causes
|
|
16
|
-
* are opposites:
|
|
17
|
-
*
|
|
18
|
-
* - the list cascaded down to zero — a real decision, and clearing is the
|
|
19
|
-
* documented behaviour;
|
|
20
|
-
* - the field declares no `options` at all — no decision was made anywhere,
|
|
21
|
-
* and the widget's own next branch renders `OptionsEmptyState`, i.e. it
|
|
22
|
-
* tells the user there is nothing to pick. Clearing here deletes the stored
|
|
23
|
-
* value of a field the user was shown nothing but a hint for, on MOUNT,
|
|
24
|
-
* without a single interaction.
|
|
25
|
-
*
|
|
26
|
-
* Measured on two live hosts, both of which mount these widgets outside a form:
|
|
27
|
-
* the grid's inline cell editor (`FieldEditWidget`, since #2942) and — since
|
|
28
|
-
* #4220 — the detail page's inline editor, which stages every `onChange` into
|
|
29
|
-
* the record draft the moment a section enters edit mode. An options-less
|
|
30
|
-
* `checkboxes` there wiped its array as soon as the user started editing any
|
|
31
|
-
* OTHER field in the same section, and the save bar wrote the empty array back.
|
|
32
|
-
*
|
|
33
|
-
* The prune now requires an authored list to prune against. The cascade itself
|
|
34
|
-
* is untouched — the controls below still clear a value the configured list no
|
|
35
|
-
* longer offers.
|
|
36
|
-
*/
|
|
37
|
-
|
|
38
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
39
|
-
import React from 'react';
|
|
40
|
-
import { render, screen } from '@testing-library/react';
|
|
41
|
-
import { SelectField } from './SelectField';
|
|
42
|
-
import { MultiSelectField } from './MultiSelectField';
|
|
43
|
-
import { CheckboxesField } from './CheckboxesField';
|
|
44
|
-
import { RadioField } from './RadioField';
|
|
45
|
-
|
|
46
|
-
const OPTIONS = [{ label: 'Alpha', value: 'alpha' }];
|
|
47
|
-
|
|
48
|
-
describe('option widgets — an unconfigured list never deletes the stored value (#4220)', () => {
|
|
49
|
-
it('MultiSelectField keeps its array and shows the unfillable state', () => {
|
|
50
|
-
const onChange = vi.fn();
|
|
51
|
-
render(
|
|
52
|
-
<MultiSelectField
|
|
53
|
-
value={['alpha', 'beta']}
|
|
54
|
-
onChange={onChange}
|
|
55
|
-
field={{ name: 'topics', type: 'multiselect' } as any}
|
|
56
|
-
/>,
|
|
57
|
-
);
|
|
58
|
-
expect(screen.getByTestId('multiselect-empty-topics')).toBeInTheDocument();
|
|
59
|
-
expect(onChange).not.toHaveBeenCalled();
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('CheckboxesField keeps its array and shows the unfillable state', () => {
|
|
63
|
-
const onChange = vi.fn();
|
|
64
|
-
render(
|
|
65
|
-
<CheckboxesField
|
|
66
|
-
value={['alpha', 'beta']}
|
|
67
|
-
onChange={onChange}
|
|
68
|
-
field={{ name: 'perks', type: 'checkboxes' } as any}
|
|
69
|
-
/>,
|
|
70
|
-
);
|
|
71
|
-
expect(screen.getByTestId('checkboxes-empty-perks')).toBeInTheDocument();
|
|
72
|
-
expect(onChange).not.toHaveBeenCalled();
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('RadioField keeps its value and shows the unfillable state', () => {
|
|
76
|
-
const onChange = vi.fn();
|
|
77
|
-
render(
|
|
78
|
-
<RadioField
|
|
79
|
-
value="alpha"
|
|
80
|
-
onChange={onChange}
|
|
81
|
-
field={{ name: 'tier', type: 'radio' } as any}
|
|
82
|
-
/>,
|
|
83
|
-
);
|
|
84
|
-
expect(screen.getByTestId('radio-empty-tier')).toBeInTheDocument();
|
|
85
|
-
expect(onChange).not.toHaveBeenCalled();
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it('SelectField keeps its value and shows the unfillable state', () => {
|
|
89
|
-
const onChange = vi.fn();
|
|
90
|
-
render(
|
|
91
|
-
<SelectField
|
|
92
|
-
value="alpha"
|
|
93
|
-
onChange={onChange}
|
|
94
|
-
field={{ name: 'stage', type: 'select' } as any}
|
|
95
|
-
/>,
|
|
96
|
-
);
|
|
97
|
-
expect(screen.getByTestId('select-empty-stage')).toBeInTheDocument();
|
|
98
|
-
expect(onChange).not.toHaveBeenCalled();
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
describe('controls — a CONFIGURED list still cascade-clears what it no longer offers', () => {
|
|
103
|
-
it('MultiSelectField drops the unoffered element, keeps the offered one', () => {
|
|
104
|
-
const onChange = vi.fn();
|
|
105
|
-
render(
|
|
106
|
-
<MultiSelectField
|
|
107
|
-
value={['alpha', 'zeta']}
|
|
108
|
-
onChange={onChange}
|
|
109
|
-
field={{ name: 'topics', type: 'multiselect', options: OPTIONS } as any}
|
|
110
|
-
/>,
|
|
111
|
-
);
|
|
112
|
-
expect(onChange).toHaveBeenCalledWith(['alpha']);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it('CheckboxesField drops the unoffered element, keeps the offered one', () => {
|
|
116
|
-
const onChange = vi.fn();
|
|
117
|
-
render(
|
|
118
|
-
<CheckboxesField
|
|
119
|
-
value={['alpha', 'zeta']}
|
|
120
|
-
onChange={onChange}
|
|
121
|
-
field={{ name: 'perks', type: 'checkboxes', options: OPTIONS } as any}
|
|
122
|
-
/>,
|
|
123
|
-
);
|
|
124
|
-
expect(onChange).toHaveBeenCalledWith(['alpha']);
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it('RadioField clears a value the configured list does not offer', () => {
|
|
128
|
-
const onChange = vi.fn();
|
|
129
|
-
render(
|
|
130
|
-
<RadioField
|
|
131
|
-
value="zeta"
|
|
132
|
-
onChange={onChange}
|
|
133
|
-
field={{ name: 'tier', type: 'radio', options: OPTIONS } as any}
|
|
134
|
-
/>,
|
|
135
|
-
);
|
|
136
|
-
expect(onChange).toHaveBeenCalledWith(undefined);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('SelectField clears a value the configured list does not offer', () => {
|
|
140
|
-
const onChange = vi.fn();
|
|
141
|
-
render(
|
|
142
|
-
<SelectField
|
|
143
|
-
value="zeta"
|
|
144
|
-
onChange={onChange}
|
|
145
|
-
field={{ name: 'stage', type: 'select', options: OPTIONS } as any}
|
|
146
|
-
/>,
|
|
147
|
-
);
|
|
148
|
-
expect(onChange).toHaveBeenCalledWith(undefined);
|
|
149
|
-
});
|
|
150
|
-
});
|
|
@@ -1,113 +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 {
|
|
11
|
-
resolvePath,
|
|
12
|
-
getPersonName,
|
|
13
|
-
getPersonInitials,
|
|
14
|
-
getPersonSubtitle,
|
|
15
|
-
getPersonAvatarUrl,
|
|
16
|
-
getPersonId,
|
|
17
|
-
matchRanges,
|
|
18
|
-
} from './personDisplay';
|
|
19
|
-
|
|
20
|
-
describe('personDisplay helpers', () => {
|
|
21
|
-
describe('resolvePath', () => {
|
|
22
|
-
it('reads flat and dotted paths', () => {
|
|
23
|
-
const rec = { name: 'Amy', primary_business_unit_id: { name: 'Sales' } };
|
|
24
|
-
expect(resolvePath(rec, 'name')).toBe('Amy');
|
|
25
|
-
expect(resolvePath(rec, 'primary_business_unit_id.name')).toBe('Sales');
|
|
26
|
-
});
|
|
27
|
-
it('returns undefined for missing paths or nullish records', () => {
|
|
28
|
-
expect(resolvePath({ a: {} }, 'a.b.c')).toBeUndefined();
|
|
29
|
-
expect(resolvePath(null, 'x')).toBeUndefined();
|
|
30
|
-
expect(resolvePath({ a: 1 }, '')).toBeUndefined();
|
|
31
|
-
});
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
describe('getPersonName', () => {
|
|
35
|
-
it('prefers the display field, then common fallbacks', () => {
|
|
36
|
-
expect(getPersonName({ name: 'Amy' })).toBe('Amy');
|
|
37
|
-
expect(getPersonName({ full_name: 'Amy Lin' }, 'full_name')).toBe('Amy Lin');
|
|
38
|
-
expect(getPersonName({ username: 'amy' })).toBe('amy');
|
|
39
|
-
expect(getPersonName('raw-id')).toBe('raw-id');
|
|
40
|
-
expect(getPersonName(null)).toBe('');
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
describe('getPersonInitials', () => {
|
|
45
|
-
it('takes first+last initial for multi-word latin names', () => {
|
|
46
|
-
expect(getPersonInitials('John Doe')).toBe('JD');
|
|
47
|
-
expect(getPersonInitials('mary jane watson')).toBe('MW');
|
|
48
|
-
});
|
|
49
|
-
it('takes first two letters for a single latin token', () => {
|
|
50
|
-
expect(getPersonInitials('John')).toBe('JO');
|
|
51
|
-
});
|
|
52
|
-
it('takes the given-name chars for CJK names', () => {
|
|
53
|
-
expect(getPersonInitials('张三')).toBe('张三');
|
|
54
|
-
expect(getPersonInitials('王小明')).toBe('小明');
|
|
55
|
-
});
|
|
56
|
-
it('falls back to "?" for empty', () => {
|
|
57
|
-
expect(getPersonInitials('')).toBe('?');
|
|
58
|
-
expect(getPersonInitials(' ')).toBe('?');
|
|
59
|
-
});
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
describe('getPersonSubtitle', () => {
|
|
63
|
-
it('joins non-empty resolved fields with a middot', () => {
|
|
64
|
-
const rec = { email: 'amy@x.io', primary_business_unit_id: { name: 'Sales' } };
|
|
65
|
-
expect(getPersonSubtitle(rec, ['primary_business_unit_id.name', 'email'])).toBe(
|
|
66
|
-
'Sales · amy@x.io',
|
|
67
|
-
);
|
|
68
|
-
});
|
|
69
|
-
it('drops empty segments and returns "" when nothing resolves', () => {
|
|
70
|
-
expect(getPersonSubtitle({ email: 'amy@x.io' }, ['primary_business_unit_id.name', 'email'])).toBe(
|
|
71
|
-
'amy@x.io',
|
|
72
|
-
);
|
|
73
|
-
expect(getPersonSubtitle({}, ['a', 'b'])).toBe('');
|
|
74
|
-
expect(getPersonSubtitle({ a: 1 }, undefined)).toBe('');
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
describe('getPersonAvatarUrl', () => {
|
|
79
|
-
it('reads the avatar field, undefined when absent', () => {
|
|
80
|
-
expect(getPersonAvatarUrl({ image: 'http://x/y.png' })).toBe('http://x/y.png');
|
|
81
|
-
expect(getPersonAvatarUrl({})).toBeUndefined();
|
|
82
|
-
expect(getPersonAvatarUrl({ photo: 'p' }, 'photo')).toBe('p');
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
describe('getPersonId', () => {
|
|
87
|
-
it('tolerates id / _id / custom / primitive', () => {
|
|
88
|
-
expect(getPersonId({ id: 1 })).toBe(1);
|
|
89
|
-
expect(getPersonId({ _id: 'a' })).toBe('a');
|
|
90
|
-
expect(getPersonId({ user_id: 9 }, 'user_id')).toBe(9);
|
|
91
|
-
expect(getPersonId('plain')).toBe('plain');
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
describe('matchRanges', () => {
|
|
97
|
-
it('returns no ranges for an empty query or empty text', () => {
|
|
98
|
-
expect(matchRanges('Amy Lin', '')).toEqual([]);
|
|
99
|
-
expect(matchRanges('', 'amy')).toEqual([]);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it('finds case-insensitive substring ranges', () => {
|
|
103
|
-
expect(matchRanges('Amy Lin', 'am')).toEqual([[0, 2]]);
|
|
104
|
-
expect(matchRanges('Sales · amy@x.io', 'amy')).toEqual([[8, 11]]);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it('finds every non-overlapping occurrence', () => {
|
|
108
|
-
expect(matchRanges('banana', 'an')).toEqual([
|
|
109
|
-
[1, 3],
|
|
110
|
-
[3, 5],
|
|
111
|
-
]);
|
|
112
|
-
});
|
|
113
|
-
});
|