@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
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Regression for objectstack-ai/objectui#3466: single-line cell renderers
|
|
3
|
-
* emitted a BARE INLINE `span.truncate`. An inline box has no width box, so
|
|
4
|
-
* `overflow:hidden` / `text-overflow:ellipsis` never engage — the value
|
|
5
|
-
* rendered at full content width and its tail was silently clipped by
|
|
6
|
-
* whatever ancestor happened to clip (the record-detail card edge), with no
|
|
7
|
-
* ellipsis and no way to read the full text (the detail row's `title` is the
|
|
8
|
-
* inline-edit hint, not the value). Same mechanism as the JSON cell fix in
|
|
9
|
-
* objectui#2578.
|
|
10
|
-
*
|
|
11
|
-
* Pin: every single-line value renderer emits a BLOCK-level, `max-w-full`
|
|
12
|
-
* truncating span that carries the full text in `title` (so hovering the
|
|
13
|
-
* value shows the full text, taking precedence over any ancestor `title`).
|
|
14
|
-
*/
|
|
15
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
16
|
-
import { render, screen } from '@testing-library/react';
|
|
17
|
-
import '@testing-library/jest-dom';
|
|
18
|
-
import React from 'react';
|
|
19
|
-
|
|
20
|
-
import {
|
|
21
|
-
TextCellRenderer,
|
|
22
|
-
LookupCellRenderer,
|
|
23
|
-
UserCellRenderer,
|
|
24
|
-
FileCellRenderer,
|
|
25
|
-
SelectCellRenderer,
|
|
26
|
-
} from '../index';
|
|
27
|
-
import { SchemaRendererProvider } from '@object-ui/react';
|
|
28
|
-
|
|
29
|
-
const LONG =
|
|
30
|
-
'A remarkably long referenced-record title that a detail card column can never fit on a single line without an ellipsis';
|
|
31
|
-
|
|
32
|
-
/** The block-level truncation contract every single-line value span must meet. */
|
|
33
|
-
function expectTruncating(el: HTMLElement, fullText: string) {
|
|
34
|
-
expect(el).toHaveClass('block', 'max-w-full', 'truncate');
|
|
35
|
-
expect(el).toHaveAttribute('title', fullText);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
describe('cell renderers truncate for real and expose the full text (issue #3466)', () => {
|
|
39
|
-
it('TextCellRenderer: block-level truncating span with title fallback', () => {
|
|
40
|
-
render(<TextCellRenderer value={LONG} field={{ type: 'text' } as any} />);
|
|
41
|
-
expectTruncating(screen.getByText(LONG), LONG);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('LookupCellRenderer: expanded record object', () => {
|
|
45
|
-
const ds = { find: vi.fn(), findOne: vi.fn() } as any;
|
|
46
|
-
render(
|
|
47
|
-
<SchemaRendererProvider dataSource={ds}>
|
|
48
|
-
<LookupCellRenderer
|
|
49
|
-
value={{ id: 'rec1', name: LONG }}
|
|
50
|
-
field={{ type: 'lookup', reference_to: 'obj_a' } as any}
|
|
51
|
-
/>
|
|
52
|
-
</SchemaRendererProvider>,
|
|
53
|
-
);
|
|
54
|
-
expectTruncating(screen.getByText(LONG), LONG);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('LookupCellRenderer: primitive non-opaque value', () => {
|
|
58
|
-
const ds = { find: vi.fn(), findOne: vi.fn() } as any;
|
|
59
|
-
render(
|
|
60
|
-
<SchemaRendererProvider dataSource={ds}>
|
|
61
|
-
<LookupCellRenderer value={LONG} field={{ type: 'lookup' } as any} />
|
|
62
|
-
</SchemaRendererProvider>,
|
|
63
|
-
);
|
|
64
|
-
expectTruncating(screen.getByText(LONG), LONG);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('UserCellRenderer: display name beside the avatar', () => {
|
|
68
|
-
render(<UserCellRenderer value={{ name: LONG }} field={{ type: 'user' } as any} />);
|
|
69
|
-
expectTruncating(screen.getByText(LONG), LONG);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('FileCellRenderer: single file name', () => {
|
|
73
|
-
render(<FileCellRenderer value={{ name: LONG }} field={{ type: 'file' } as any} />);
|
|
74
|
-
expectTruncating(screen.getByText(LONG), LONG);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('SelectCellRenderer (dot): bounded container with title, shrinkable label', () => {
|
|
78
|
-
render(
|
|
79
|
-
<SelectCellRenderer
|
|
80
|
-
value="opt1"
|
|
81
|
-
field={{
|
|
82
|
-
type: 'select',
|
|
83
|
-
appearance: 'dot',
|
|
84
|
-
options: [{ value: 'opt1', label: LONG }],
|
|
85
|
-
} as any}
|
|
86
|
-
/>,
|
|
87
|
-
);
|
|
88
|
-
const label = screen.getByText(LONG);
|
|
89
|
-
// The label shrinks inside the dot row; the row itself is width-bounded
|
|
90
|
-
// and carries the full text on hover.
|
|
91
|
-
expect(label).toHaveClass('min-w-0', 'truncate');
|
|
92
|
-
const row = label.parentElement!;
|
|
93
|
-
expect(row).toHaveClass('max-w-full');
|
|
94
|
-
expect(row).toHaveAttribute('title', LONG);
|
|
95
|
-
});
|
|
96
|
-
});
|
|
@@ -1,169 +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
|
-
* The SDUI path delivers field metadata on ONE key — objectui#3233.
|
|
11
|
-
*
|
|
12
|
-
* `SchemaRenderer` hands every registered component the authored node as
|
|
13
|
-
* `schema`. That key is the universal SDUI contract (`element:*`, `page:*`,
|
|
14
|
-
* `object-grid`, `ReportRenderer` …) and is NOT retired. What was retired is
|
|
15
|
-
* `schema` on the *field-widget* contract: a widget used to receive the node
|
|
16
|
-
* under `schema` here and the metadata under `field` from the form renderer,
|
|
17
|
-
* which is why ~30 widgets resolved their config as `field || schema`.
|
|
18
|
-
*
|
|
19
|
-
* The translation now happens once, at the registration seam
|
|
20
|
-
* (`withFieldCarrier`), so downstream `field` is the only carrier. That is only
|
|
21
|
-
* correct if the node reaches the widget UNCHANGED — "swap the key, don't drop
|
|
22
|
-
* the cargo". The first test proves it by OBJECT IDENTITY: a spy sits where the
|
|
23
|
-
* registry entry sits, records the object `SchemaRenderer` passed as `schema`,
|
|
24
|
-
* and the widget's `field` must be that exact object. (Identity against the
|
|
25
|
-
* authored literal would be wrong to assert — `SchemaRenderer` shallow-copies
|
|
26
|
-
* the node while evaluating expressions, so the object it delivers is its own.)
|
|
27
|
-
*
|
|
28
|
-
* The last test pins that the REAL registration path goes through the adapter
|
|
29
|
-
* at all: an adapter nobody applied would leave every SDUI-hosted widget
|
|
30
|
-
* reading `undefined`, which is exactly the silent failure this convergence is
|
|
31
|
-
* accused of risking.
|
|
32
|
-
*
|
|
33
|
-
* The sibling half of the invariant — the form path through
|
|
34
|
-
* `renderFieldComponent` — is pinned in `packages/components/src/renderers/
|
|
35
|
-
* form/__tests__/form-field-carrier.test.tsx`.
|
|
36
|
-
*/
|
|
37
|
-
|
|
38
|
-
import React, { Suspense } from 'react';
|
|
39
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
40
|
-
import { render, screen, cleanup } from '@testing-library/react';
|
|
41
|
-
import { ComponentRegistry } from '@object-ui/core';
|
|
42
|
-
import { SchemaRenderer } from '@object-ui/react';
|
|
43
|
-
|
|
44
|
-
import { withFieldCarrier } from '../withFieldCarrier';
|
|
45
|
-
import { registerField } from '../index';
|
|
46
|
-
// Module scope, not a hook: `registerField('boolean')` registers
|
|
47
|
-
// `React.lazy(() => import('./widgets/BooleanField'))`, and under a saturated
|
|
48
|
-
// transform pipeline a first `import()` can outlive RTL's 1000ms budget. The
|
|
49
|
-
// specifier matches `fieldWidgetMap`'s exactly, so ESM's cache makes that
|
|
50
|
-
// factory resolve immediately (AGENTS.md 测试纪律 / objectui#3010).
|
|
51
|
-
import '../widgets/BooleanField';
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Capture holder, not bare module variables: `react-hooks/globals` forbids
|
|
55
|
-
* REASSIGNING an outer binding from a component body, which is what a probe
|
|
56
|
-
* has to do. Mutating a property of a stable object is the repo's usual shape
|
|
57
|
-
* for this (see plugin-detail's `RelatedList` probes).
|
|
58
|
-
*/
|
|
59
|
-
const captured: { props: Record<string, any> | null; node: unknown } = {
|
|
60
|
-
props: null,
|
|
61
|
-
node: null,
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
function CarrierProbe(props: any) {
|
|
65
|
-
captured.props = props;
|
|
66
|
-
return <div data-testid="carrier-probe">{props.field?.label ?? 'NO FIELD'}</div>;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const AdaptedProbe = withFieldCarrier(CarrierProbe);
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Sits exactly where the registry entry sits, so it sees the host's props
|
|
73
|
-
* verbatim, then forwards them to the adapter under test. This is what makes
|
|
74
|
-
* the identity assertion possible: `captured.node` and the widget's `field`
|
|
75
|
-
* are observed within a single render pass.
|
|
76
|
-
*/
|
|
77
|
-
function CarrierSpy(props: any) {
|
|
78
|
-
captured.node = props.schema;
|
|
79
|
-
return <AdaptedProbe {...props} />;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
beforeEach(() => {
|
|
83
|
-
captured.props = null;
|
|
84
|
-
captured.node = null;
|
|
85
|
-
ComponentRegistry.register('carrierprobe', CarrierSpy, {
|
|
86
|
-
namespace: 'field',
|
|
87
|
-
skipFallback: true,
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
afterEach(() => {
|
|
92
|
-
cleanup();
|
|
93
|
-
vi.restoreAllMocks();
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
describe('SDUI path — the authored node arrives as `field` (objectui#3233)', () => {
|
|
97
|
-
it('hands the widget the very object it used to read through `schema`', () => {
|
|
98
|
-
const node = {
|
|
99
|
-
type: 'field:carrierprobe',
|
|
100
|
-
name: 'city',
|
|
101
|
-
label: 'City',
|
|
102
|
-
placeholder: 'Where?',
|
|
103
|
-
options: [{ label: 'Beijing', value: 'bj' }],
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
render(<SchemaRenderer schema={node as any} />);
|
|
107
|
-
|
|
108
|
-
expect(screen.getByTestId('carrier-probe')).toHaveTextContent('City');
|
|
109
|
-
expect(captured.props).not.toBeNull();
|
|
110
|
-
expect(captured.node).toBeTruthy();
|
|
111
|
-
// The payload-equivalence proof: same object, by reference. A
|
|
112
|
-
// reconstruction would still render "City" while quietly dropping whatever
|
|
113
|
-
// the copy did not know to carry.
|
|
114
|
-
expect(captured.props!.field).toBe(captured.node);
|
|
115
|
-
// …and it really is the whole authored node, not a narrowed projection.
|
|
116
|
-
expect(captured.props!.field.name).toBe('city');
|
|
117
|
-
expect(captured.props!.field.placeholder).toBe('Where?');
|
|
118
|
-
expect(captured.props!.field.options).toEqual([{ label: 'Beijing', value: 'bj' }]);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it('consumes `schema` rather than forwarding it to the widget', () => {
|
|
122
|
-
// Key ABSENCE, not `undefined`. Two reasons it must not be forwarded: a
|
|
123
|
-
// widget could resolve `field || schema` again (the second contract
|
|
124
|
-
// survives in practice while the type claims it is gone), and widgets
|
|
125
|
-
// spread their leftover props onto DOM nodes, where an object-valued
|
|
126
|
-
// `schema` attribute is a React warning.
|
|
127
|
-
render(
|
|
128
|
-
<SchemaRenderer schema={{ type: 'field:carrierprobe', name: 'city', label: 'City' } as any} />,
|
|
129
|
-
);
|
|
130
|
-
|
|
131
|
-
expect(captured.props).not.toBeNull();
|
|
132
|
-
expect('schema' in captured.props!).toBe(false);
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it('lets an explicit `field` win when a host supplies both', () => {
|
|
136
|
-
// The form renderer passes `field` (resolved metadata) and never `schema`.
|
|
137
|
-
// If some host ever passed both, the resolved metadata is the better
|
|
138
|
-
// answer — the raw node is the fallback, not the override.
|
|
139
|
-
const node = { type: 'field:carrierprobe', name: 'from-node', label: 'From node' };
|
|
140
|
-
const metadata = { name: 'from-field', label: 'From field' };
|
|
141
|
-
|
|
142
|
-
render(<AdaptedProbe schema={node} field={metadata} />);
|
|
143
|
-
|
|
144
|
-
expect(captured.props!.field).toBe(metadata);
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it('applies the adapter on the REAL registration path (`registerField`)', async () => {
|
|
148
|
-
// The seam is only worth anything if `registerField` goes through it.
|
|
149
|
-
// `BooleanField` reads `widget` / `name` off its metadata carrier and
|
|
150
|
-
// nowhere else: `widget: 'checkbox'` selects the Checkbox over the Switch,
|
|
151
|
-
// and `name` becomes the control's id. Both are node keys, so seeing them
|
|
152
|
-
// honoured here proves the node reached `field` end to end — through the
|
|
153
|
-
// real registry entry, the real SchemaRenderer, and the real widget.
|
|
154
|
-
registerField('boolean');
|
|
155
|
-
|
|
156
|
-
render(
|
|
157
|
-
<Suspense fallback={<div>loading</div>}>
|
|
158
|
-
<SchemaRenderer
|
|
159
|
-
schema={
|
|
160
|
-
{ type: 'field:boolean', name: 'is_active', label: 'Active', widget: 'checkbox' } as any
|
|
161
|
-
}
|
|
162
|
-
/>
|
|
163
|
-
</Suspense>,
|
|
164
|
-
);
|
|
165
|
-
|
|
166
|
-
const control = await screen.findByRole('checkbox');
|
|
167
|
-
expect(control).toHaveAttribute('id', 'is_active');
|
|
168
|
-
});
|
|
169
|
-
});
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* [framework#2926 ⑧] LookupCellRenderer must honor the target object's
|
|
3
|
-
* configured display field. ObjectGrid forwards `display_field` on the
|
|
4
|
-
* column meta (RELATIONAL_META_KEYS) exactly like `reference`, but the read
|
|
5
|
-
* cell used to ignore it and always ran the hardcoded heuristics — `name`
|
|
6
|
-
* first — so a target object whose displayNameField is a localized/label
|
|
7
|
-
* field still rendered the raw API `name`.
|
|
8
|
-
*/
|
|
9
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
10
|
-
import { render, screen, waitFor } from '@testing-library/react';
|
|
11
|
-
import '@testing-library/jest-dom';
|
|
12
|
-
import React from 'react';
|
|
13
|
-
|
|
14
|
-
import { LookupCellRenderer } from '../index';
|
|
15
|
-
import { SchemaRendererProvider } from '@object-ui/react';
|
|
16
|
-
|
|
17
|
-
const ID_A = '01ARZ3NDEKTSV4RRFFQ69G5FAA';
|
|
18
|
-
const ID_B = '01ARZ3NDEKTSV4RRFFQ69G5FAB';
|
|
19
|
-
|
|
20
|
-
function makeDataSource() {
|
|
21
|
-
const findOne = vi.fn(async (object: string, id: string) => {
|
|
22
|
-
if (object === 'showcase_category') {
|
|
23
|
-
// Record carries BOTH the raw API name and a localized label field.
|
|
24
|
-
if (id === ID_A) return { id, name: 'cat_hardware', label_zh: '硬件' };
|
|
25
|
-
if (id === ID_B) return { id, name: 'cat_software', label_zh: '软件' };
|
|
26
|
-
}
|
|
27
|
-
return null;
|
|
28
|
-
});
|
|
29
|
-
return { findOne, find: vi.fn() } as any;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
describe('LookupCellRenderer — display_field resolution', () => {
|
|
33
|
-
it('prefers the configured display_field over the heuristic `name`', async () => {
|
|
34
|
-
const ds = makeDataSource();
|
|
35
|
-
render(
|
|
36
|
-
<SchemaRendererProvider dataSource={ds}>
|
|
37
|
-
<LookupCellRenderer
|
|
38
|
-
value={ID_A}
|
|
39
|
-
field={{ type: 'lookup', reference: 'showcase_category', display_field: 'label_zh' } as any}
|
|
40
|
-
/>
|
|
41
|
-
</SchemaRendererProvider>,
|
|
42
|
-
);
|
|
43
|
-
await waitFor(() => {
|
|
44
|
-
expect(screen.getByText('硬件')).toBeInTheDocument();
|
|
45
|
-
});
|
|
46
|
-
expect(screen.queryByText('cat_hardware')).not.toBeInTheDocument();
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('keeps the heuristic (`name` first) when no display_field is configured', async () => {
|
|
50
|
-
const ds = makeDataSource();
|
|
51
|
-
render(
|
|
52
|
-
<SchemaRendererProvider dataSource={ds}>
|
|
53
|
-
<LookupCellRenderer
|
|
54
|
-
value={ID_A}
|
|
55
|
-
field={{ type: 'lookup', reference: 'showcase_category' } as any}
|
|
56
|
-
/>
|
|
57
|
-
</SchemaRendererProvider>,
|
|
58
|
-
);
|
|
59
|
-
await waitFor(() => {
|
|
60
|
-
expect(screen.getByText('cat_hardware')).toBeInTheDocument();
|
|
61
|
-
});
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it('uses display_field on server-expanded nested objects (no fetch path)', () => {
|
|
65
|
-
const ds = makeDataSource();
|
|
66
|
-
render(
|
|
67
|
-
<SchemaRendererProvider dataSource={ds}>
|
|
68
|
-
<LookupCellRenderer
|
|
69
|
-
value={{ id: ID_A, name: 'cat_hardware', label_zh: '硬件' } as any}
|
|
70
|
-
field={{ type: 'lookup', reference: 'showcase_category', display_field: 'label_zh' } as any}
|
|
71
|
-
/>
|
|
72
|
-
</SchemaRendererProvider>,
|
|
73
|
-
);
|
|
74
|
-
expect(screen.getByText('硬件')).toBeInTheDocument();
|
|
75
|
-
expect(ds.findOne).not.toHaveBeenCalled();
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('does not serve a cached heuristic name to a display_field column (cache key isolation)', async () => {
|
|
79
|
-
const ds = makeDataSource();
|
|
80
|
-
// First: a column WITHOUT display_field resolves and caches the heuristic name.
|
|
81
|
-
const first = render(
|
|
82
|
-
<SchemaRendererProvider dataSource={ds}>
|
|
83
|
-
<LookupCellRenderer
|
|
84
|
-
value={ID_B}
|
|
85
|
-
field={{ type: 'lookup', reference: 'showcase_category' } as any}
|
|
86
|
-
/>
|
|
87
|
-
</SchemaRendererProvider>,
|
|
88
|
-
);
|
|
89
|
-
await waitFor(() => {
|
|
90
|
-
expect(screen.getByText('cat_software')).toBeInTheDocument();
|
|
91
|
-
});
|
|
92
|
-
first.unmount();
|
|
93
|
-
// Then: a column WITH display_field for the same record must show the
|
|
94
|
-
// configured field, not the previously cached heuristic name.
|
|
95
|
-
render(
|
|
96
|
-
<SchemaRendererProvider dataSource={ds}>
|
|
97
|
-
<LookupCellRenderer
|
|
98
|
-
value={ID_B}
|
|
99
|
-
field={{ type: 'lookup', reference: 'showcase_category', display_field: 'label_zh' } as any}
|
|
100
|
-
/>
|
|
101
|
-
</SchemaRendererProvider>,
|
|
102
|
-
);
|
|
103
|
-
await waitFor(() => {
|
|
104
|
-
expect(screen.getByText('软件')).toBeInTheDocument();
|
|
105
|
-
});
|
|
106
|
-
});
|
|
107
|
-
});
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Regression for objectstack-ai/objectui#2357: the lookup chip
|
|
3
|
-
* (LookupCellRenderer — record header highlight + detail sections) resolved
|
|
4
|
-
* display names via the local key heuristic (`pickRecordDisplayName`), whose
|
|
5
|
-
* `_number`/`_code` suffix scan surfaced an autonumber (`0001`) over the
|
|
6
|
-
* record's real name whenever the referenced object declared its title in
|
|
7
|
-
* `nameField`/`titleFormat`. The picker (LookupField) already resolved via the
|
|
8
|
-
* unified ADR-0079 resolver, so the two surfaces disagreed.
|
|
9
|
-
*
|
|
10
|
-
* The chip must now resolve through the referenced object's schema
|
|
11
|
-
* (`dataSource.getObjectSchema`) with the precedence
|
|
12
|
-
* `displayField → nameField/titleFormat → type-aware derivation`, keeping the
|
|
13
|
-
* key heuristic only as the no-schema fallback.
|
|
14
|
-
*/
|
|
15
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
16
|
-
import { render, screen, waitFor } from '@testing-library/react';
|
|
17
|
-
import '@testing-library/jest-dom';
|
|
18
|
-
import React from 'react';
|
|
19
|
-
|
|
20
|
-
import { LookupCellRenderer } from '../index';
|
|
21
|
-
import { SchemaRendererProvider } from '@object-ui/react';
|
|
22
|
-
|
|
23
|
-
// The expanded record: the autonumber (`req_number`) precedes the real name
|
|
24
|
-
// (`req_name`) in key order, so the OLD heuristic-only path rendered "0001".
|
|
25
|
-
const EXPANDED = { id: 'rec1', req_number: '0001', req_name: 'Annual Maintenance' };
|
|
26
|
-
|
|
27
|
-
function makeDataSource(objectName: string, schema: any, record?: any) {
|
|
28
|
-
return {
|
|
29
|
-
find: vi.fn(),
|
|
30
|
-
findOne: vi.fn(async (object: string, id: string) =>
|
|
31
|
-
object === objectName && record && id === record.id ? record : null,
|
|
32
|
-
),
|
|
33
|
-
getObjectSchema: vi.fn(async (object: string) =>
|
|
34
|
-
object === objectName ? schema : null,
|
|
35
|
-
),
|
|
36
|
-
} as any;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
describe('LookupCellRenderer — schema-aware display name (issue #2357)', () => {
|
|
40
|
-
it('expanded object: nameField beats the autonumber suffix heuristic', async () => {
|
|
41
|
-
const ds = makeDataSource('mtc_request_a', { nameField: 'req_name' });
|
|
42
|
-
render(
|
|
43
|
-
<SchemaRendererProvider dataSource={ds}>
|
|
44
|
-
<LookupCellRenderer
|
|
45
|
-
value={EXPANDED}
|
|
46
|
-
field={{ type: 'lookup', reference_to: 'mtc_request_a' } as any}
|
|
47
|
-
/>
|
|
48
|
-
</SchemaRendererProvider>,
|
|
49
|
-
);
|
|
50
|
-
await waitFor(() => {
|
|
51
|
-
expect(screen.getByText('Annual Maintenance')).toBeInTheDocument();
|
|
52
|
-
});
|
|
53
|
-
expect(screen.queryByText('0001')).not.toBeInTheDocument();
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('expanded object: titleFormat template resolves when no nameField', async () => {
|
|
57
|
-
const ds = makeDataSource('mtc_request_b', { titleFormat: '{req_name} - {req_number}' });
|
|
58
|
-
render(
|
|
59
|
-
<SchemaRendererProvider dataSource={ds}>
|
|
60
|
-
<LookupCellRenderer
|
|
61
|
-
value={{ ...EXPANDED }}
|
|
62
|
-
field={{ type: 'lookup', reference_to: 'mtc_request_b' } as any}
|
|
63
|
-
/>
|
|
64
|
-
</SchemaRendererProvider>,
|
|
65
|
-
);
|
|
66
|
-
await waitFor(() => {
|
|
67
|
-
expect(screen.getByText('Annual Maintenance - 0001')).toBeInTheDocument();
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('explicit displayField wins over the schema nameField', async () => {
|
|
72
|
-
const ds = makeDataSource('mtc_request_c', { nameField: 'req_name' });
|
|
73
|
-
render(
|
|
74
|
-
<SchemaRendererProvider dataSource={ds}>
|
|
75
|
-
<LookupCellRenderer
|
|
76
|
-
value={{ ...EXPANDED }}
|
|
77
|
-
field={{ type: 'lookup', reference_to: 'mtc_request_c', display_field: 'req_number' } as any}
|
|
78
|
-
/>
|
|
79
|
-
</SchemaRendererProvider>,
|
|
80
|
-
);
|
|
81
|
-
// Author explicitly asked for the number — honour it (also the documented
|
|
82
|
-
// workaround in the issue).
|
|
83
|
-
expect(await screen.findByText('0001')).toBeInTheDocument();
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it('fetch-on-demand primitive id resolves via the schema nameField too', async () => {
|
|
87
|
-
// Opaque ULID-ish id so the old path would have resolved the fetched
|
|
88
|
-
// record through the heuristic and shown its autonumber.
|
|
89
|
-
const id = '01ARZ3NDEKTSV4RRFFQ69G5FBB';
|
|
90
|
-
const record = { id, req_number: '0007', req_name: 'Pump Overhaul' };
|
|
91
|
-
const ds = makeDataSource('mtc_request_d', { nameField: 'req_name' }, record);
|
|
92
|
-
render(
|
|
93
|
-
<SchemaRendererProvider dataSource={ds}>
|
|
94
|
-
<LookupCellRenderer
|
|
95
|
-
value={id}
|
|
96
|
-
field={{ type: 'lookup', reference_to: 'mtc_request_d' } as any}
|
|
97
|
-
/>
|
|
98
|
-
</SchemaRendererProvider>,
|
|
99
|
-
);
|
|
100
|
-
await waitFor(() => {
|
|
101
|
-
expect(screen.getByText('Pump Overhaul')).toBeInTheDocument();
|
|
102
|
-
});
|
|
103
|
-
expect(screen.queryByText('0007')).not.toBeInTheDocument();
|
|
104
|
-
expect(ds.getObjectSchema).toHaveBeenCalledWith('mtc_request_d');
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it('falls back to the key heuristic when the data source has no schema API', () => {
|
|
108
|
-
const ds = { find: vi.fn(), findOne: vi.fn() } as any; // no getObjectSchema
|
|
109
|
-
render(
|
|
110
|
-
<SchemaRendererProvider dataSource={ds}>
|
|
111
|
-
<LookupCellRenderer
|
|
112
|
-
value={{ id: 'rec2', framework_name: 'SOX' }}
|
|
113
|
-
field={{ type: 'lookup', reference_to: 'mtc_request_e' } as any}
|
|
114
|
-
/>
|
|
115
|
-
</SchemaRendererProvider>,
|
|
116
|
-
);
|
|
117
|
-
expect(screen.getByText('SOX')).toBeInTheDocument();
|
|
118
|
-
});
|
|
119
|
-
});
|
|
@@ -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,91 +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
|
-
* Registry collision guard.
|
|
11
|
-
*
|
|
12
|
-
* `@object-ui/components` registers display widgets (`text`, `image`,
|
|
13
|
-
* `avatar`, `html`, `grid`) under their bare names. `@object-ui/fields`
|
|
14
|
-
* registers form-input widgets that — by name — collide with those.
|
|
15
|
-
*
|
|
16
|
-
* The fix: field registrations for these 5 types pass `skipFallback: true`,
|
|
17
|
-
* so the bare lookup keeps returning the display widget while the field
|
|
18
|
-
* input is still reachable via the `field:<type>` namespace.
|
|
19
|
-
*
|
|
20
|
-
* Regression contract:
|
|
21
|
-
* 1. `FIELD_TYPES_SKIP_FALLBACK` must contain every colliding name.
|
|
22
|
-
* 2. After `registerAllFields()`, bare lookups must NOT resolve to a
|
|
23
|
-
* field renderer for any of those names.
|
|
24
|
-
* 3. The `field:<type>` namespaced lookup must still work, so form
|
|
25
|
-
* schemas using `{ type: 'text' }` still find an input renderer
|
|
26
|
-
* when explicitly requested.
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
|
-
import { describe, it, expect, beforeAll } from 'vitest';
|
|
30
|
-
import { ComponentRegistry } from '@object-ui/core';
|
|
31
|
-
|
|
32
|
-
// Importing the fields entry runs `registerAllFields()` at module load.
|
|
33
|
-
// Importing the components entry runs display-widget registration the
|
|
34
|
-
// same way. Both must be loaded before we inspect the registry.
|
|
35
|
-
import '@object-ui/components';
|
|
36
|
-
import { registerAllFields } from '../index';
|
|
37
|
-
|
|
38
|
-
const COLLIDING_TYPES = ['text', 'html', 'image', 'avatar', 'grid'] as const;
|
|
39
|
-
|
|
40
|
-
describe('registry collision (fields ↔ components)', () => {
|
|
41
|
-
beforeAll(() => {
|
|
42
|
-
// Defensive: in case the auto-register at import time hasn't run yet
|
|
43
|
-
// under the test runner's module isolation.
|
|
44
|
-
registerAllFields();
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it.each(COLLIDING_TYPES)(
|
|
48
|
-
'bare "%s" lookup is NOT clobbered by the field renderer',
|
|
49
|
-
(type) => {
|
|
50
|
-
// The bare lookup must resolve, and it must resolve to a display widget
|
|
51
|
-
// (or a FallbackComponent / null) — never to the lazy field renderer.
|
|
52
|
-
// The field renderer is a React.lazy() result; the display widget is a
|
|
53
|
-
// plain function component. We don't pin to a specific identity, but we
|
|
54
|
-
// DO pin that whatever the bare lookup returns, the namespaced
|
|
55
|
-
// `field:<type>` lookup returns something different.
|
|
56
|
-
const bare = ComponentRegistry.get(type);
|
|
57
|
-
const namespaced = ComponentRegistry.get(`field:${type}`);
|
|
58
|
-
|
|
59
|
-
// Namespaced field renderer must exist for every colliding type —
|
|
60
|
-
// that's the whole point of keeping the field accessible.
|
|
61
|
-
expect(
|
|
62
|
-
namespaced,
|
|
63
|
-
`field:${type} must be registered so forms can still use it`,
|
|
64
|
-
).toBeTruthy();
|
|
65
|
-
|
|
66
|
-
// The bare and namespaced renderers must NOT be identical. If they
|
|
67
|
-
// are, it means the field renderer wrote to the bare fallback and
|
|
68
|
-
// would render an empty <input> instead of the display widget.
|
|
69
|
-
expect(
|
|
70
|
-
bare,
|
|
71
|
-
`bare "${type}" must not be the same component as field:${type}`,
|
|
72
|
-
).not.toBe(namespaced);
|
|
73
|
-
},
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
it('non-colliding field types DO populate the bare fallback', () => {
|
|
77
|
-
// Sanity check: a field type with no display-widget collision (e.g.
|
|
78
|
-
// `email`, `password`, `number`) must still register both at the
|
|
79
|
-
// namespaced AND bare keys, otherwise we'd silently break forms.
|
|
80
|
-
for (const type of ['email', 'password', 'number', 'date', 'select']) {
|
|
81
|
-
expect(
|
|
82
|
-
ComponentRegistry.get(`field:${type}`),
|
|
83
|
-
`field:${type} must be registered`,
|
|
84
|
-
).toBeTruthy();
|
|
85
|
-
expect(
|
|
86
|
-
ComponentRegistry.get(type),
|
|
87
|
-
`bare "${type}" must also be reachable (no collision)`,
|
|
88
|
-
).toBeTruthy();
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
});
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { resolveFieldCurrency } from '../index';
|
|
3
|
-
|
|
4
|
-
describe('resolveFieldCurrency', () => {
|
|
5
|
-
it('prefers the field explicit currency over everything', () => {
|
|
6
|
-
expect(resolveFieldCurrency({ currency: 'JPY', currencyConfig: { defaultCurrency: 'EUR' } }, 'USD')).toBe('JPY');
|
|
7
|
-
});
|
|
8
|
-
it('falls back to currencyConfig.defaultCurrency, then legacy defaultCurrency', () => {
|
|
9
|
-
expect(resolveFieldCurrency({ currencyConfig: { defaultCurrency: 'EUR' } }, 'USD')).toBe('EUR');
|
|
10
|
-
expect(resolveFieldCurrency({ defaultCurrency: 'GBP' } as any, 'USD')).toBe('GBP');
|
|
11
|
-
});
|
|
12
|
-
it('falls back to the tenant default when the field omits its own', () => {
|
|
13
|
-
expect(resolveFieldCurrency({}, 'CNY')).toBe('CNY');
|
|
14
|
-
expect(resolveFieldCurrency(null, 'CNY')).toBe('CNY');
|
|
15
|
-
});
|
|
16
|
-
it('returns undefined when nothing is known (→ renderer shows a plain number)', () => {
|
|
17
|
-
expect(resolveFieldCurrency({})).toBeUndefined();
|
|
18
|
-
expect(resolveFieldCurrency(undefined, undefined)).toBeUndefined();
|
|
19
|
-
});
|
|
20
|
-
});
|