@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,157 +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
|
-
* RecipientPickerField — the sharing-rule recipient picker (objectstack#3821).
|
|
11
|
-
*
|
|
12
|
-
* Two regressions are pinned here:
|
|
13
|
-
* 1. the candidate query must use the STRUCTURED `$orderby` form. It used to
|
|
14
|
-
* pass the clause string `'name asc'`, which ApiDataSource walked
|
|
15
|
-
* character by character into `0 n,1 a,2 m,…`; the server then sorted by
|
|
16
|
-
* columns that don't exist and every recipient list came back empty, so
|
|
17
|
-
* the control read "No matches" forever and no rule could be saved.
|
|
18
|
-
* 2. the placeholder must come from a per-type i18n key, not from
|
|
19
|
-
* interpolating the enum value into an English sentence.
|
|
20
|
-
*/
|
|
21
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
22
|
-
import React from 'react';
|
|
23
|
-
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
|
|
24
|
-
import '@testing-library/jest-dom';
|
|
25
|
-
import { RecipientPickerField } from './RecipientPickerField';
|
|
26
|
-
|
|
27
|
-
const USERS = [
|
|
28
|
-
{ id: 'usr_1', name: 'Ada Auditor' },
|
|
29
|
-
{ id: 'usr_2', name: 'Bob Builder' },
|
|
30
|
-
];
|
|
31
|
-
|
|
32
|
-
function mockDataSource(rows: any[] = USERS) {
|
|
33
|
-
return { find: vi.fn().mockResolvedValue({ data: rows }) } as any;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function renderPicker(recipientType: string, dataSource: any, value = '') {
|
|
37
|
-
return render(
|
|
38
|
-
<RecipientPickerField
|
|
39
|
-
value={value}
|
|
40
|
-
onChange={vi.fn()}
|
|
41
|
-
field={{ name: 'recipient_id' } as any}
|
|
42
|
-
dataSource={dataSource}
|
|
43
|
-
dependentValues={{ recipient_type: recipientType }}
|
|
44
|
-
/>,
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
describe('RecipientPickerField — candidate query', () => {
|
|
49
|
-
it('orders candidates with the structured $orderby form, never a clause string', async () => {
|
|
50
|
-
const ds = mockDataSource();
|
|
51
|
-
renderPicker('user', ds);
|
|
52
|
-
|
|
53
|
-
await waitFor(() => expect(ds.find).toHaveBeenCalled());
|
|
54
|
-
const [object, params] = ds.find.mock.calls[0];
|
|
55
|
-
expect(object).toBe('sys_user');
|
|
56
|
-
expect(params.$orderby).toEqual({ name: 'asc' });
|
|
57
|
-
// A bare string is what regressed; assert the shape explicitly.
|
|
58
|
-
expect(typeof params.$orderby).not.toBe('string');
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('queries the object mapped to each recipient type', async () => {
|
|
62
|
-
for (const [type, object] of [
|
|
63
|
-
['user', 'sys_user'],
|
|
64
|
-
['team', 'sys_team'],
|
|
65
|
-
['business_unit', 'sys_business_unit'],
|
|
66
|
-
['unit_and_subordinates', 'sys_business_unit'],
|
|
67
|
-
['position', 'sys_position'],
|
|
68
|
-
] as const) {
|
|
69
|
-
const ds = mockDataSource([]);
|
|
70
|
-
const { unmount } = renderPicker(type, ds);
|
|
71
|
-
await waitFor(() => expect(ds.find).toHaveBeenCalled());
|
|
72
|
-
expect(ds.find.mock.calls[0][0]).toBe(object);
|
|
73
|
-
unmount();
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('shows the loaded records as options once the list is opened', async () => {
|
|
78
|
-
const ds = mockDataSource();
|
|
79
|
-
renderPicker('user', ds);
|
|
80
|
-
await waitFor(() => expect(ds.find).toHaveBeenCalled());
|
|
81
|
-
|
|
82
|
-
fireEvent.click(screen.getByRole('combobox'));
|
|
83
|
-
expect(await screen.findByText('Ada Auditor')).toBeInTheDocument();
|
|
84
|
-
expect(screen.getByText('Bob Builder')).toBeInTheDocument();
|
|
85
|
-
});
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
describe('RecipientPickerField — keeping the stored recipient', () => {
|
|
89
|
-
function renderControlled(initialType: string, value: string, onChange: any) {
|
|
90
|
-
const ds = mockDataSource();
|
|
91
|
-
const view = render(
|
|
92
|
-
<RecipientPickerField
|
|
93
|
-
value={value}
|
|
94
|
-
onChange={onChange}
|
|
95
|
-
field={{ name: 'recipient_id' } as any}
|
|
96
|
-
dataSource={ds}
|
|
97
|
-
dependentValues={{ recipient_type: initialType }}
|
|
98
|
-
/>,
|
|
99
|
-
);
|
|
100
|
-
const rerenderWith = (type: string) =>
|
|
101
|
-
view.rerender(
|
|
102
|
-
<RecipientPickerField
|
|
103
|
-
value={value}
|
|
104
|
-
onChange={onChange}
|
|
105
|
-
field={{ name: 'recipient_id' } as any}
|
|
106
|
-
dataSource={ds}
|
|
107
|
-
dependentValues={{ recipient_type: type }}
|
|
108
|
-
/>,
|
|
109
|
-
);
|
|
110
|
-
return { ...view, rerenderWith };
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
it('keeps the saved recipient when the type arrives late (edit form hydrating)', async () => {
|
|
114
|
-
// The edit dialog mounts the widget before the record lands, so
|
|
115
|
-
// recipient_type is '' on the first render and 'user' on the next. That is
|
|
116
|
-
// hydration, not the admin changing the type — clearing there wiped the
|
|
117
|
-
// saved recipient and the blank got persisted on save.
|
|
118
|
-
const onChange = vi.fn();
|
|
119
|
-
const { rerenderWith } = renderControlled('', 'usr_1', onChange);
|
|
120
|
-
rerenderWith('user');
|
|
121
|
-
await waitFor(() => expect(onChange).not.toHaveBeenCalled());
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it('clears the recipient when the admin actually switches type', async () => {
|
|
125
|
-
const onChange = vi.fn();
|
|
126
|
-
const { rerenderWith } = renderControlled('user', 'usr_1', onChange);
|
|
127
|
-
rerenderWith('team');
|
|
128
|
-
await waitFor(() => expect(onChange).toHaveBeenCalledWith(''));
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it('does not clear when the type is unset back to empty', async () => {
|
|
132
|
-
const onChange = vi.fn();
|
|
133
|
-
const { rerenderWith } = renderControlled('user', 'usr_1', onChange);
|
|
134
|
-
rerenderWith('');
|
|
135
|
-
await waitFor(() => expect(onChange).not.toHaveBeenCalled());
|
|
136
|
-
});
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
describe('RecipientPickerField — localizable copy', () => {
|
|
140
|
-
it('takes the placeholder from a per-type key rather than the raw enum value', async () => {
|
|
141
|
-
// With no I18nProvider the widget falls back to the English defaults, so
|
|
142
|
-
// the assertion is on the *wording*: 'Select a business unit' can only come
|
|
143
|
-
// from the keyed string — interpolating the enum produced
|
|
144
|
-
// 'Select a business unit' only by accident and 'Select a unit and
|
|
145
|
-
// subordinates' for the next type over.
|
|
146
|
-
const ds = mockDataSource([]);
|
|
147
|
-
renderPicker('unit_and_subordinates', ds);
|
|
148
|
-
await waitFor(() => expect(ds.find).toHaveBeenCalled());
|
|
149
|
-
expect(await screen.findByText('Select a business unit')).toBeInTheDocument();
|
|
150
|
-
expect(screen.queryByText(/unit and subordinates/i)).not.toBeInTheDocument();
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it('prompts for a recipient type before one is chosen', () => {
|
|
154
|
-
renderPicker('', mockDataSource([]));
|
|
155
|
-
expect(screen.getByText('Select a recipient type first.')).toBeInTheDocument();
|
|
156
|
-
});
|
|
157
|
-
});
|
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { Combobox, EmptyValue, cn } from '@object-ui/components';
|
|
3
|
-
import { SchemaRendererContext } from '@object-ui/react';
|
|
4
|
-
import type { FieldWidgetComponentProps } from './types';
|
|
5
|
-
import { toDomProps } from './toDomProps';
|
|
6
|
-
import { useFieldTranslation } from './useFieldTranslation';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* RecipientPickerField — dependent record picker for a polymorphic recipient
|
|
10
|
-
* reference (e.g. `sys_sharing_rule.recipient_id`), whose target object is
|
|
11
|
-
* decided by a sibling `recipient_type` select.
|
|
12
|
-
*
|
|
13
|
-
* Reached via the field `widget: 'recipient-picker'` hint (resolves as
|
|
14
|
-
* `field:recipient-picker`). Reads the live `recipient_type` from
|
|
15
|
-
* `dependentValues` (the form record), loads candidate records of the mapped
|
|
16
|
-
* object via `dataSource.find(...)`, and stores the value the sharing evaluator
|
|
17
|
-
* expects for that type:
|
|
18
|
-
*
|
|
19
|
-
* user → sys_user, store `id`
|
|
20
|
-
* team → sys_team, store `id`
|
|
21
|
-
* business_unit → sys_business_unit, store `id`
|
|
22
|
-
* unit_and_subordinates → sys_business_unit, store `id`
|
|
23
|
-
* position → sys_position, store `name` (matched against
|
|
24
|
-
* sys_user_position.position at evaluation time)
|
|
25
|
-
*
|
|
26
|
-
* When `recipient_type` changes after mount the stored id is reset (an id valid
|
|
27
|
-
* for one type is meaningless for another). Unknown types degrade to a plain
|
|
28
|
-
* text input so nothing breaks.
|
|
29
|
-
*/
|
|
30
|
-
interface RecipientMapping {
|
|
31
|
-
object: string;
|
|
32
|
-
/** Which record field to persist into recipient_id. */
|
|
33
|
-
storeField: 'id' | 'name';
|
|
34
|
-
/** Candidate display-label fields, in preference order. */
|
|
35
|
-
labelFields: string[];
|
|
36
|
-
/**
|
|
37
|
-
* i18n key for the "choose one" placeholder. Keyed per type rather than
|
|
38
|
-
* interpolating the enum value into an English sentence — "Select a
|
|
39
|
-
* business unit" and "选择业务单元" share no structure (objectstack#3821).
|
|
40
|
-
*/
|
|
41
|
-
placeholderKey: string;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const TYPE_TO_OBJECT: Record<string, RecipientMapping> = {
|
|
45
|
-
user: { object: 'sys_user', storeField: 'id', labelFields: ['name', 'full_name', 'email'], placeholderKey: 'fields.recipient.selectUser' },
|
|
46
|
-
team: { object: 'sys_team', storeField: 'id', labelFields: ['name', 'label'], placeholderKey: 'fields.recipient.selectTeam' },
|
|
47
|
-
business_unit: { object: 'sys_business_unit', storeField: 'id', labelFields: ['name', 'label'], placeholderKey: 'fields.recipient.selectBusinessUnit' },
|
|
48
|
-
unit_and_subordinates: { object: 'sys_business_unit', storeField: 'id', labelFields: ['name', 'label'], placeholderKey: 'fields.recipient.selectUnitAndSubordinates' },
|
|
49
|
-
position: { object: 'sys_position', storeField: 'name', labelFields: ['label', 'name'], placeholderKey: 'fields.recipient.selectPosition' },
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
export function RecipientPickerField({
|
|
53
|
-
value,
|
|
54
|
-
onChange,
|
|
55
|
-
readonly,
|
|
56
|
-
className,
|
|
57
|
-
error,
|
|
58
|
-
...props
|
|
59
|
-
}: FieldWidgetComponentProps<string>) {
|
|
60
|
-
const ctx = React.useContext(SchemaRendererContext);
|
|
61
|
-
const { t } = useFieldTranslation();
|
|
62
|
-
const dataSource: any = props.dataSource ?? (ctx as any)?.dataSource ?? null;
|
|
63
|
-
const disabled = props.disabled;
|
|
64
|
-
const dependentValues: Record<string, any> = (props as any).dependentValues ?? {};
|
|
65
|
-
const recipientType = String(dependentValues.recipient_type ?? '');
|
|
66
|
-
const mapping = TYPE_TO_OBJECT[recipientType];
|
|
67
|
-
|
|
68
|
-
const [records, setRecords] = React.useState<any[] | null>(null);
|
|
69
|
-
|
|
70
|
-
// Reset the stored recipient when the admin PICKS a different type (an id for
|
|
71
|
-
// a user is not a valid team/business-unit id).
|
|
72
|
-
//
|
|
73
|
-
// "Different type" means one non-empty type replacing another. The empty
|
|
74
|
-
// string is not a type — it is the edit form before the record has hydrated,
|
|
75
|
-
// and treating `'' → 'user'` as a change wiped the saved recipient the moment
|
|
76
|
-
// an existing rule was opened for editing (objectstack#3821): the picker went
|
|
77
|
-
// blank, and saving persisted the blank. Only a non-empty predecessor can
|
|
78
|
-
// invalidate the stored id.
|
|
79
|
-
const prevType = React.useRef<string>('');
|
|
80
|
-
React.useEffect(() => {
|
|
81
|
-
if (prevType.current && recipientType && prevType.current !== recipientType && value) {
|
|
82
|
-
onChange('' as any);
|
|
83
|
-
}
|
|
84
|
-
prevType.current = recipientType;
|
|
85
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
86
|
-
}, [recipientType]);
|
|
87
|
-
|
|
88
|
-
React.useEffect(() => {
|
|
89
|
-
setRecords(null);
|
|
90
|
-
if (!dataSource || !mapping || typeof dataSource.find !== 'function') return;
|
|
91
|
-
let cancelled = false;
|
|
92
|
-
(async () => {
|
|
93
|
-
try {
|
|
94
|
-
// Object form, not the 'name asc' clause string: the clause string is
|
|
95
|
-
// supported again (objectstack#3821 fixed ApiDataSource walking it
|
|
96
|
-
// character by character), but the structured form can't regress that
|
|
97
|
-
// way for any data source.
|
|
98
|
-
const res = await dataSource.find(mapping.object, { $top: 500, $orderby: { name: 'asc' } });
|
|
99
|
-
const list: any[] = res?.data ?? res?.records ?? (Array.isArray(res) ? res : []);
|
|
100
|
-
if (!cancelled) setRecords(Array.isArray(list) ? list : []);
|
|
101
|
-
} catch {
|
|
102
|
-
if (!cancelled) setRecords([]);
|
|
103
|
-
}
|
|
104
|
-
})();
|
|
105
|
-
return () => {
|
|
106
|
-
cancelled = true;
|
|
107
|
-
};
|
|
108
|
-
}, [dataSource, mapping?.object]);
|
|
109
|
-
|
|
110
|
-
const labelOf = (r: any): string => {
|
|
111
|
-
for (const f of mapping?.labelFields ?? ['name']) if (r?.[f]) return String(r[f]);
|
|
112
|
-
return String(r?.id ?? '');
|
|
113
|
-
};
|
|
114
|
-
const valueOf = (r: any): string => String(r?.[mapping?.storeField ?? 'id'] ?? '');
|
|
115
|
-
|
|
116
|
-
const options = React.useMemo(() => {
|
|
117
|
-
const opts = (records ?? [])
|
|
118
|
-
.map((r) => ({ value: valueOf(r), label: labelOf(r) }))
|
|
119
|
-
.filter((o) => o.value);
|
|
120
|
-
if (value && !opts.some((o) => o.value === value)) opts.unshift({ value, label: value });
|
|
121
|
-
return opts;
|
|
122
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
123
|
-
}, [records, value, mapping]);
|
|
124
|
-
|
|
125
|
-
if (!recipientType) {
|
|
126
|
-
return (
|
|
127
|
-
<p className={cn('text-sm text-muted-foreground', className)}>
|
|
128
|
-
{t('fields.recipient.selectTypeFirst')}
|
|
129
|
-
</p>
|
|
130
|
-
);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (!mapping) {
|
|
134
|
-
// Unknown / unsupported recipient type — keep a plain text input so the
|
|
135
|
-
// field is never un-editable.
|
|
136
|
-
return (
|
|
137
|
-
<input
|
|
138
|
-
// DOM pass-through onto the real focusable control (objectui#3318).
|
|
139
|
-
{...toDomProps(props)}
|
|
140
|
-
className={cn(
|
|
141
|
-
'w-full rounded border bg-background px-2 py-1 text-sm',
|
|
142
|
-
className,
|
|
143
|
-
)}
|
|
144
|
-
value={value ?? ''}
|
|
145
|
-
disabled={disabled || readonly}
|
|
146
|
-
onChange={(e) => onChange(e.target.value as any)}
|
|
147
|
-
aria-invalid={!!error}
|
|
148
|
-
/>
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
if (readonly) {
|
|
153
|
-
if (!value) return <EmptyValue />;
|
|
154
|
-
return <span className={className}>{options.find((o) => o.value === value)?.label ?? value}</span>;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// DOM pass-through onto the combobox trigger — the widget's real focusable
|
|
158
|
-
// control (objectui#3318). `name` is withheld: the trigger is a button, not
|
|
159
|
-
// a submission control (same reasoning as #3306's SelectTrigger).
|
|
160
|
-
//
|
|
161
|
-
// NOTE this widget stays on the #3318 ledger regardless: its dependency-
|
|
162
|
-
// gated state (no `recipient_type` chosen yet — the state a fresh form and
|
|
163
|
-
// the registry sweep render) is a plain hint paragraph with no focusable
|
|
164
|
-
// control, so there is nothing there to carry the attribute.
|
|
165
|
-
const { name: _domName, ...triggerDomProps } = toDomProps(props);
|
|
166
|
-
|
|
167
|
-
return (
|
|
168
|
-
<Combobox
|
|
169
|
-
{...triggerDomProps}
|
|
170
|
-
options={options}
|
|
171
|
-
value={value ?? ''}
|
|
172
|
-
onValueChange={(v) => onChange(v as any)}
|
|
173
|
-
placeholder={
|
|
174
|
-
records === null
|
|
175
|
-
? t('fields.recipient.loading')
|
|
176
|
-
: t(mapping.placeholderKey ?? 'fields.recipient.select')
|
|
177
|
-
}
|
|
178
|
-
searchPlaceholder={t('fields.recipient.search')}
|
|
179
|
-
emptyText={records === null ? t('fields.recipient.loading') : t('fields.recipient.empty')}
|
|
180
|
-
disabled={disabled}
|
|
181
|
-
className={cn('w-full', className)}
|
|
182
|
-
// AFTER the spread so this widget's own computation wins (#3222).
|
|
183
|
-
aria-invalid={!!error}
|
|
184
|
-
/>
|
|
185
|
-
);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
export default RecipientPickerField;
|