@object-ui/fields 17.5.0 → 17.7.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 +6150 -0
- package/README.md +108 -8
- package/dist/FieldEditWidget.d.ts +70 -8
- package/dist/coerceToSafeValue.d.ts +54 -0
- package/dist/currency.d.ts +1 -1
- package/dist/field-type-alias.d.ts +16 -0
- package/dist/index.cjs +5 -5
- package/dist/index.css +53 -11
- package/dist/index.d.ts +653 -128
- package/dist/index.js +4275 -3171
- package/dist/widgets/AddressField.d.ts +3 -3
- package/dist/widgets/AutoNumberField.d.ts +1 -1
- package/dist/widgets/AvatarField.d.ts +20 -4
- package/dist/widgets/BooleanField.d.ts +2 -2
- 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 +2 -2
- package/dist/widgets/DateTimeField.d.ts +2 -2
- package/dist/widgets/EmailField.d.ts +1 -1
- package/dist/widgets/FileField.d.ts +18 -2
- package/dist/widgets/FilterConditionField.d.ts +43 -2
- package/dist/widgets/FormulaField.d.ts +1 -1
- package/dist/widgets/FullscreenFieldEditor.d.ts +9 -2
- package/dist/widgets/GeolocationField.d.ts +10 -4
- package/dist/widgets/GridField.d.ts +102 -86
- package/dist/widgets/ImageField.d.ts +1 -1
- package/dist/widgets/LocationField.d.ts +31 -4
- package/dist/widgets/LookupField.d.ts +9 -1
- package/dist/widgets/MasterDetailField.d.ts +1 -1
- package/dist/widgets/MultiSelectField.d.ts +1 -1
- package/dist/widgets/NumberField.d.ts +2 -2
- package/dist/widgets/ObjectField.d.ts +10 -3
- package/dist/widgets/ObjectRefField.d.ts +1 -1
- package/dist/widgets/OptionsEmptyState.d.ts +38 -8
- package/dist/widgets/PasswordField.d.ts +15 -1
- package/dist/widgets/PeoplePicker.d.ts +17 -2
- package/dist/widgets/PercentField.d.ts +3 -2
- package/dist/widgets/PersonRow.d.ts +13 -2
- 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 +44 -1
- package/dist/widgets/RecordPickerDialog.d.ts +33 -16
- package/dist/widgets/RichTextField.d.ts +37 -1
- package/dist/widgets/SelectField.d.ts +1 -1
- package/dist/widgets/SelectionTray.d.ts +13 -2
- 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 +2 -2
- package/dist/widgets/TimeField.d.ts +2 -2
- 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/file-affordance.d.ts +33 -0
- package/dist/widgets/file-size-guard.d.ts +5 -23
- package/dist/widgets/file-value.d.ts +38 -9
- package/dist/widgets/lookupColumnDisplay.d.ts +110 -0
- package/dist/widgets/nativeDateValue.d.ts +9 -27
- package/dist/widgets/number-format.d.ts +137 -0
- package/dist/widgets/numberBadInput.d.ts +135 -0
- package/dist/widgets/personDisplay.d.ts +22 -4
- package/dist/widgets/richTextDisplay.d.ts +116 -0
- package/dist/widgets/toDomProps.d.ts +10 -1
- package/dist/widgets/toHostGroupProps.d.ts +172 -21
- package/dist/widgets/toHostProps.d.ts +105 -0
- package/dist/widgets/types.d.ts +19 -6
- package/dist/widgets/upload-error-message.d.ts +6 -0
- package/dist/widgets/uploadingScope.d.ts +142 -0
- package/dist/widgets/useCascadingOptions.d.ts +3 -2
- package/dist/widgets/useRecordQuery.d.ts +4 -2
- package/dist/widgets/useUploadingSignal.d.ts +8 -0
- package/package.json +11 -13
- 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,45 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { Input, Textarea, EmptyValue } from '@object-ui/components';
|
|
3
|
-
import { TextareaFieldMetadata } from '@object-ui/types';
|
|
4
|
-
import { FieldWidgetComponentProps } from './types';
|
|
5
|
-
import { toDomProps } from './toDomProps';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* TextField - Standard single-line or multi-line text input
|
|
9
|
-
* Automatically renders as a textarea when rows are configured in field metadata
|
|
10
|
-
*/
|
|
11
|
-
export function TextField({ value, onChange, field, readonly, ...props }: FieldWidgetComponentProps<string>) {
|
|
12
|
-
const fieldData = field;
|
|
13
|
-
|
|
14
|
-
if (readonly) {
|
|
15
|
-
return <span className="text-sm">{value || <EmptyValue />}</span>;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// Cast for rows property
|
|
19
|
-
const rows = (fieldData as unknown as TextareaFieldMetadata)?.rows;
|
|
20
|
-
|
|
21
|
-
const domProps = toDomProps(props);
|
|
22
|
-
|
|
23
|
-
if (rows && rows > 1) {
|
|
24
|
-
return (
|
|
25
|
-
<Textarea
|
|
26
|
-
{...domProps}
|
|
27
|
-
value={value || ''}
|
|
28
|
-
onChange={(e) => onChange(e.target.value)}
|
|
29
|
-
placeholder={fieldData?.placeholder}
|
|
30
|
-
disabled={readonly || domProps.disabled}
|
|
31
|
-
/>
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
return (
|
|
36
|
-
<Input
|
|
37
|
-
{...domProps}
|
|
38
|
-
type={fieldData?.type === 'password' ? 'password' : 'text'}
|
|
39
|
-
value={value || ''}
|
|
40
|
-
onChange={(e) => onChange(e.target.value)}
|
|
41
|
-
placeholder={fieldData?.placeholder}
|
|
42
|
-
disabled={readonly || domProps.disabled}
|
|
43
|
-
/>
|
|
44
|
-
);
|
|
45
|
-
}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { Input, EmptyValue } from '@object-ui/components';
|
|
3
|
-
import { FieldWidgetComponentProps } from './types';
|
|
4
|
-
import { toDomProps } from './toDomProps';
|
|
5
|
-
import { openNativePicker } from './openNativePicker';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* TimeField - Time picker input widget
|
|
9
|
-
* Uses native time input for hour and minute selection
|
|
10
|
-
*/
|
|
11
|
-
export function TimeField({ value, onChange, field, readonly, ...props }: FieldWidgetComponentProps<string>) {
|
|
12
|
-
if (readonly) {
|
|
13
|
-
return <span className="text-sm">{value || <EmptyValue />}</span>;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const domProps = toDomProps(props);
|
|
17
|
-
|
|
18
|
-
return (
|
|
19
|
-
<Input
|
|
20
|
-
{...domProps}
|
|
21
|
-
type="time"
|
|
22
|
-
value={value || ''}
|
|
23
|
-
onChange={(e) => onChange(e.target.value)}
|
|
24
|
-
onClick={(e) => {
|
|
25
|
-
openNativePicker(e.currentTarget);
|
|
26
|
-
domProps.onClick?.(e);
|
|
27
|
-
}}
|
|
28
|
-
disabled={readonly || domProps.disabled}
|
|
29
|
-
/>
|
|
30
|
-
);
|
|
31
|
-
}
|
package/src/widgets/UrlField.tsx
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { Input, EmptyValue } from '@object-ui/components';
|
|
3
|
-
import { FieldWidgetComponentProps } from './types';
|
|
4
|
-
import { toDomProps } from './toDomProps';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* UrlField - URL input with clickable link in readonly mode
|
|
8
|
-
* Validates URLs to only render http/https links for security
|
|
9
|
-
*/
|
|
10
|
-
export function UrlField({ value, onChange, field, readonly, error, ...props }: FieldWidgetComponentProps<string>) {
|
|
11
|
-
const config = field;
|
|
12
|
-
if (readonly) {
|
|
13
|
-
if (!value) return <EmptyValue />;
|
|
14
|
-
|
|
15
|
-
// Validate URL to prevent javascript: or data: URLs
|
|
16
|
-
const isValidUrl = value.startsWith('http://') || value.startsWith('https://');
|
|
17
|
-
if (!isValidUrl) {
|
|
18
|
-
return <span className="text-sm">{value}</span>;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
return (
|
|
22
|
-
<a
|
|
23
|
-
href={value}
|
|
24
|
-
target="_blank"
|
|
25
|
-
rel="noopener noreferrer"
|
|
26
|
-
className="text-sm text-blue-600 hover:text-blue-800 hover:underline"
|
|
27
|
-
>
|
|
28
|
-
{value}
|
|
29
|
-
</a>
|
|
30
|
-
);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const domProps = toDomProps(props);
|
|
34
|
-
|
|
35
|
-
return (
|
|
36
|
-
<Input
|
|
37
|
-
{...domProps}
|
|
38
|
-
type="url"
|
|
39
|
-
value={value || ''}
|
|
40
|
-
onChange={(e) => onChange(e.target.value)}
|
|
41
|
-
placeholder={config?.placeholder || 'https://example.com'}
|
|
42
|
-
disabled={readonly || domProps.disabled}
|
|
43
|
-
aria-invalid={!!error}
|
|
44
|
-
/>
|
|
45
|
-
);
|
|
46
|
-
}
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { FieldWidgetComponentProps } from './types';
|
|
3
|
-
import { LookupField } from './LookupField';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* UserField — person picker for the `user` field type.
|
|
7
|
-
*
|
|
8
|
-
* `user` is a lookup specialized to the `sys_user` system object (see framework
|
|
9
|
-
* "lookup → sys_user" specialization). Rather than re-implement candidate search,
|
|
10
|
-
* the searchable picker, the record-picker dialog and id resolution, this widget
|
|
11
|
-
* **delegates to the shared {@link LookupField}** with the reference fixed to
|
|
12
|
-
* `sys_user`. The author writes `Field.user({ multiple })`; we normalise the field
|
|
13
|
-
* metadata so the lookup machinery targets users with a sensible display field.
|
|
14
|
-
*
|
|
15
|
-
* Table-cell display (avatars / initials) is handled separately by
|
|
16
|
-
* `UserCellRenderer`; this is the form/editor widget.
|
|
17
|
-
*
|
|
18
|
-
* By default the user picker is the search-first {@link PeoplePicker}
|
|
19
|
-
* (`picker: 'search'`) with a department·email subtitle, avatar, and banned
|
|
20
|
-
* users excluded from candidates. Authors can override any of these, or opt
|
|
21
|
-
* back to the classic table dialog with `picker: 'default'`.
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
/** Exclude deactivated (`banned`) users unless the author already filters on it. */
|
|
25
|
-
function withBannedFilter(filters?: any[]): any[] {
|
|
26
|
-
const base = Array.isArray(filters) ? filters : [];
|
|
27
|
-
return base.some(f => f?.field === 'banned')
|
|
28
|
-
? base
|
|
29
|
-
: [...base, { field: 'banned', operator: 'ne', value: true }];
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function UserField(props: FieldWidgetComponentProps<any>) {
|
|
33
|
-
const raw = props.field as any;
|
|
34
|
-
|
|
35
|
-
// The objectSchema field metadata may live directly on `field`, or nested at
|
|
36
|
-
// `field.field` — the form renderer's declared metadata slot (objectui#3090).
|
|
37
|
-
// Mirror the unwrap LookupField itself performs. (Formerly attributed to the
|
|
38
|
-
// docs-demo `createFieldRenderer` wrapper, removed in objectui#3910.)
|
|
39
|
-
const metaIsNested = raw?.field && typeof raw.field === 'object'
|
|
40
|
-
&& ('reference' in raw.field || 'reference_to' in raw.field || 'type' in raw.field);
|
|
41
|
-
const meta = metaIsNested ? raw.field : raw;
|
|
42
|
-
|
|
43
|
-
// Ensure the picker always targets sys_user (even if the author omitted an
|
|
44
|
-
// explicit reference), presents user names by default, and defaults to the
|
|
45
|
-
// search-first PeoplePicker with sensible person display + candidate hygiene.
|
|
46
|
-
const normalized = {
|
|
47
|
-
...(meta || {}),
|
|
48
|
-
reference: meta?.reference || meta?.reference_to || 'sys_user',
|
|
49
|
-
display_field: meta?.display_field || meta?.displayField || meta?.reference_field || 'name',
|
|
50
|
-
picker: meta?.picker ?? 'search',
|
|
51
|
-
subtitle: meta?.subtitle ?? ['primary_business_unit_id.name', 'email'],
|
|
52
|
-
avatar_field: meta?.avatar_field ?? meta?.avatarField ?? 'image',
|
|
53
|
-
lookup_filters: withBannedFilter(meta?.lookup_filters ?? meta?.lookupFilters),
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
const fieldProp = metaIsNested ? { ...raw, field: normalized } : normalized;
|
|
57
|
-
|
|
58
|
-
return <LookupField {...(props as any)} field={fieldProp} />;
|
|
59
|
-
}
|
|
@@ -1,78 +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 { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
10
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
11
|
-
import { UserField } from './UserField';
|
|
12
|
-
|
|
13
|
-
const USERS = [
|
|
14
|
-
{ id: 'u1', name: 'Amy Lin', email: 'amy@x.io', image: 'http://x/amy.png', primary_business_unit_id: { name: 'Sales' } },
|
|
15
|
-
{ id: 'u2', name: 'Bob Wu', email: 'bob@x.io', primary_business_unit_id: { name: 'Eng' } },
|
|
16
|
-
];
|
|
17
|
-
|
|
18
|
-
function makeDataSource() {
|
|
19
|
-
return {
|
|
20
|
-
find: vi.fn(async (_obj: string, params: any = {}) => {
|
|
21
|
-
const idIn = params?.$filter?.id?.$in as any[] | undefined;
|
|
22
|
-
if (idIn) {
|
|
23
|
-
const s = new Set(idIn.map(String));
|
|
24
|
-
const data = USERS.filter(u => s.has(String(u.id)));
|
|
25
|
-
return { data, total: data.length };
|
|
26
|
-
}
|
|
27
|
-
const search: string | undefined = params?.$search;
|
|
28
|
-
const data = search
|
|
29
|
-
? USERS.filter(u => u.name.toLowerCase().includes(search.toLowerCase()))
|
|
30
|
-
: USERS;
|
|
31
|
-
return { data, total: data.length };
|
|
32
|
-
}),
|
|
33
|
-
} as any;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
beforeEach(() => {
|
|
37
|
-
Object.defineProperty(window, 'innerWidth', { writable: true, configurable: true, value: 1280 });
|
|
38
|
-
window.matchMedia = ((query: string) => ({
|
|
39
|
-
matches: window.innerWidth < 768,
|
|
40
|
-
media: query,
|
|
41
|
-
onchange: null,
|
|
42
|
-
addEventListener: () => {},
|
|
43
|
-
removeEventListener: () => {},
|
|
44
|
-
addListener: () => {},
|
|
45
|
-
removeListener: () => {},
|
|
46
|
-
dispatchEvent: () => false,
|
|
47
|
-
})) as any;
|
|
48
|
-
try {
|
|
49
|
-
window.localStorage.clear();
|
|
50
|
-
} catch {
|
|
51
|
-
/* ignore */
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
describe('UserField — search-first entry (P0)', () => {
|
|
56
|
-
it('opens the PeoplePicker from the primary trigger (no separate browse-all button)', async () => {
|
|
57
|
-
const ds = makeDataSource();
|
|
58
|
-
render(<UserField field={{ type: 'user' } as any} dataSource={ds} value={null} onChange={vi.fn()} />);
|
|
59
|
-
|
|
60
|
-
// The redundant "browse all" icon is not rendered for search-first fields.
|
|
61
|
-
expect(screen.queryByTestId('browse-all-records')).toBeNull();
|
|
62
|
-
|
|
63
|
-
// Primary click opens the rich PeoplePicker (its search box appears).
|
|
64
|
-
fireEvent.click(screen.getByTestId('lookup-trigger'));
|
|
65
|
-
await waitFor(() => expect(screen.getByTestId('people-picker-search')).toBeTruthy());
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it('renders the current selection as avatar chips', async () => {
|
|
69
|
-
const ds = makeDataSource();
|
|
70
|
-
render(
|
|
71
|
-
<UserField field={{ type: 'user', multiple: true } as any} dataSource={ds} value={['u1']} onChange={vi.fn()} />,
|
|
72
|
-
);
|
|
73
|
-
await waitFor(() => expect(screen.getByTestId('people-field-chip')).toBeTruthy());
|
|
74
|
-
expect(screen.getByText('Amy Lin')).toBeTruthy();
|
|
75
|
-
// Initials fallback renders (image does not load in jsdom).
|
|
76
|
-
expect(screen.getByText('AL')).toBeTruthy();
|
|
77
|
-
});
|
|
78
|
-
});
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { EmptyValue } from '@object-ui/components';
|
|
3
|
-
import { FieldWidgetComponentProps } from './types';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* VectorField - Vector/embedding data display
|
|
7
|
-
* Shows vector embeddings in a read-only format
|
|
8
|
-
*/
|
|
9
|
-
export function VectorField({ value, field, ...props }: FieldWidgetComponentProps<number[]>) {
|
|
10
|
-
const vectorField = field as any;
|
|
11
|
-
const dimensions = vectorField?.dimensions || (Array.isArray(value) ? value.length : 0);
|
|
12
|
-
|
|
13
|
-
if (!value || !Array.isArray(value)) {
|
|
14
|
-
return <EmptyValue />;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
// Show first few values and total dimensions
|
|
18
|
-
const preview = value.slice(0, 3).map(v => {
|
|
19
|
-
const num = Number(v);
|
|
20
|
-
return isNaN(num) ? '0' : num.toFixed(4);
|
|
21
|
-
}).join(', ');
|
|
22
|
-
|
|
23
|
-
return (
|
|
24
|
-
<div className={`text-sm ${props.className || ''}`}>
|
|
25
|
-
<span className="font-mono text-gray-700">[{preview}...]</span>
|
|
26
|
-
<span className="text-gray-500 ml-2">({dimensions}D)</span>
|
|
27
|
-
</div>
|
|
28
|
-
);
|
|
29
|
-
}
|
|
@@ -1,66 +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
|
-
* Empty-criteria guard for the sharing-rule criteria builder
|
|
11
|
-
* (objectstack#3896).
|
|
12
|
-
*
|
|
13
|
-
* This widget used to tell an admin that an empty criteria meant "All
|
|
14
|
-
* records" — the most dangerous thing it could say, because it was describing
|
|
15
|
-
* a bug as a feature. A sharing rule with no predicate was stored as
|
|
16
|
-
* `criteria_json: null` and then evaluated as `find(object, { filter: {} })`,
|
|
17
|
-
* i.e. every record of the object granted to the recipient. `SharingRuleSchema`
|
|
18
|
-
* had always forbidden that shape ("never seeded as a permissive match-all",
|
|
19
|
-
* ADR-0049); the REST and data-API entries just never checked.
|
|
20
|
-
*
|
|
21
|
-
* The server now refuses to save such a rule and shares nothing for one
|
|
22
|
-
* already stored. These tests pin the client-side mirror of that predicate, so
|
|
23
|
-
* the widget warns while the admin is still looking at the empty builder
|
|
24
|
-
* rather than after a rejected save.
|
|
25
|
-
*/
|
|
26
|
-
import { describe, it, expect } from 'vitest';
|
|
27
|
-
import { isMatchAllCriteria } from '../FilterConditionField';
|
|
28
|
-
|
|
29
|
-
describe('isMatchAllCriteria — shapes that would share every record', () => {
|
|
30
|
-
it('flags the empty and vacuous shapes', () => {
|
|
31
|
-
for (const shape of [
|
|
32
|
-
undefined,
|
|
33
|
-
null,
|
|
34
|
-
{}, // what a blank builder / blank textarea parses to
|
|
35
|
-
[],
|
|
36
|
-
true,
|
|
37
|
-
42,
|
|
38
|
-
{ $and: [] },
|
|
39
|
-
{ $or: [] },
|
|
40
|
-
{ $and: [{}] },
|
|
41
|
-
{ $or: [{ stage: 'won' }, {}] }, // one unconstrained branch opens the OR
|
|
42
|
-
]) {
|
|
43
|
-
expect(isMatchAllCriteria(shape), `${JSON.stringify(shape) ?? String(shape)}`).toBe(true);
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('passes anything that actually narrows the result set', () => {
|
|
48
|
-
for (const shape of [
|
|
49
|
-
{ stage: 'won' },
|
|
50
|
-
{ amount: { $gte: 100000 } },
|
|
51
|
-
[{ stage: 'won' }],
|
|
52
|
-
{ $and: [{ stage: 'won' }] },
|
|
53
|
-
{ $or: [{ a: 1 }, { b: 2 }] },
|
|
54
|
-
{ health: 'red', budget: { $gt: 1 } },
|
|
55
|
-
]) {
|
|
56
|
-
expect(isMatchAllCriteria(shape), JSON.stringify(shape)).toBe(false);
|
|
57
|
-
}
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('agrees with the server on the reported case: a rule saved with no criteria', () => {
|
|
61
|
-
// The builder emits '' for an empty group, which the widget parses to {}.
|
|
62
|
-
expect(isMatchAllCriteria({})).toBe(true);
|
|
63
|
-
// …and one real condition is enough to clear the gate.
|
|
64
|
-
expect(isMatchAllCriteria({ health: 'red' })).toBe(false);
|
|
65
|
-
});
|
|
66
|
-
});
|
|
@@ -1,166 +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
|
-
* Operator spelling guard for the sharing-rule criteria builder (#2901).
|
|
11
|
-
*
|
|
12
|
-
* `condToMongo` emitted `$ncontains` — a token that appears nowhere in
|
|
13
|
-
* `@objectstack/spec` and that objectui's own `convertFiltersToAST` throws on,
|
|
14
|
-
* naming the correct spelling (`$notContains`) in its error message. So every
|
|
15
|
-
* "does not contain" rule authored here validated in the UI and was then
|
|
16
|
-
* rejected downstream. Nothing caught it because the builder's operator list is
|
|
17
|
-
* hand-written and never compared against the spec vocabulary.
|
|
18
|
-
*
|
|
19
|
-
* These tests pin the emitted spellings against `FieldOperatorsSchema`'s keys
|
|
20
|
-
* and pin the round-trip, including the pre-fix spelling that stored criteria
|
|
21
|
-
* still carry.
|
|
22
|
-
*/
|
|
23
|
-
import { describe, it, expect } from 'vitest';
|
|
24
|
-
import { FieldOperatorsSchema } from '@objectstack/spec/data';
|
|
25
|
-
import { condToMongo, kvToCondition } from '../FilterConditionField';
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* The `$`-prefixed operator keys of the spec's `FieldOperatorsSchema` —
|
|
29
|
-
* DERIVED from the schema (#2942), so a 16th operator landing in the spec
|
|
30
|
-
* fails the reachability test below instead of drifting unnoticed.
|
|
31
|
-
*/
|
|
32
|
-
const SPEC_OPERATORS = new Set(
|
|
33
|
-
Object.keys((FieldOperatorsSchema as unknown as { shape?: Record<string, unknown> }).shape ?? {}),
|
|
34
|
-
);
|
|
35
|
-
|
|
36
|
-
const noTypes = () => undefined;
|
|
37
|
-
|
|
38
|
-
/** Pull the operator keys out of a `{ field: { $op: v } }` fragment. */
|
|
39
|
-
function operatorsOf(frag: Record<string, any> | null): string[] {
|
|
40
|
-
if (!frag) return [];
|
|
41
|
-
return Object.values(frag).flatMap((v) =>
|
|
42
|
-
v !== null && typeof v === 'object' && !Array.isArray(v) ? Object.keys(v) : [],
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
describe('condToMongo emits only spec operator spellings', () => {
|
|
47
|
-
const builderOperators = [
|
|
48
|
-
'equals', 'notEquals', 'contains', 'notContains', 'isEmpty', 'isNotEmpty',
|
|
49
|
-
'greaterThan', 'lessThan', 'greaterOrEqual', 'lessOrEqual', 'in', 'notIn',
|
|
50
|
-
'before', 'after',
|
|
51
|
-
'startsWith', 'endsWith', 'isNull', 'isNotNull', 'exists', 'notExists',
|
|
52
|
-
];
|
|
53
|
-
|
|
54
|
-
it.each(builderOperators)('%s emits a spec-defined operator', (operator) => {
|
|
55
|
-
const frag = condToMongo(
|
|
56
|
-
{ id: 'c1', field: 'name', operator, value: operator === 'in' || operator === 'notIn' ? ['a'] : 'a' } as any,
|
|
57
|
-
noTypes,
|
|
58
|
-
);
|
|
59
|
-
const unknown = operatorsOf(frag).filter((op) => !SPEC_OPERATORS.has(op));
|
|
60
|
-
expect(unknown, `${operator} emits an operator @objectstack/spec does not define`).toEqual([]);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('notContains emits $notContains, not the pre-fix $ncontains', () => {
|
|
64
|
-
const frag = condToMongo({ id: 'c1', field: 'name', operator: 'notContains', value: 'x' } as any, noTypes);
|
|
65
|
-
expect(frag).toEqual({ name: { $notContains: 'x' } });
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it('between emits a range the spec defines', () => {
|
|
69
|
-
const frag = condToMongo({ id: 'c1', field: 'age', operator: 'between', value: [1, 5] } as any, noTypes);
|
|
70
|
-
expect(operatorsOf(frag).every((op) => SPEC_OPERATORS.has(op))).toBe(true);
|
|
71
|
-
});
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
describe('every spec field operator is reachable from the builder (#2942)', () => {
|
|
75
|
-
it('reads a non-empty operator vocabulary from the spec', () => {
|
|
76
|
-
expect([...SPEC_OPERATORS].length, 'could not read FieldOperatorsSchema.shape').toBeGreaterThan(0);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('some builder operator emits every spec $-token', () => {
|
|
80
|
-
const builderOperators = [
|
|
81
|
-
'equals', 'notEquals', 'contains', 'notContains', 'isEmpty', 'isNotEmpty',
|
|
82
|
-
'greaterThan', 'lessThan', 'greaterOrEqual', 'lessOrEqual', 'in', 'notIn',
|
|
83
|
-
'before', 'after', 'between',
|
|
84
|
-
'startsWith', 'endsWith', 'isNull', 'isNotNull', 'exists', 'notExists',
|
|
85
|
-
];
|
|
86
|
-
const emitted = new Set<string>();
|
|
87
|
-
for (const operator of builderOperators) {
|
|
88
|
-
const value = operator === 'in' || operator === 'notIn' ? ['a'] : operator === 'between' ? [1, 5] : 'a';
|
|
89
|
-
const frag = condToMongo({ id: 'c1', field: 'f', operator, value } as any, noTypes);
|
|
90
|
-
for (const op of operatorsOf(frag)) emitted.add(op);
|
|
91
|
-
}
|
|
92
|
-
// `$eq` is the spec's IMPLICIT equality form — `equals` deliberately
|
|
93
|
-
// emits the bare `{ field: value }` shape, never an explicit `$eq`.
|
|
94
|
-
// `$between` stays covered by the `$gte`+`$lte` pair `between` emits
|
|
95
|
-
// (kvToCondition reads that pair back as `between`).
|
|
96
|
-
//
|
|
97
|
-
// `$icontains` is a GENUINE GAP, not a modelling nuance, and is excluded
|
|
98
|
-
// here rather than silently dropped from the vocabulary. `FieldOperatorsSchema`
|
|
99
|
-
// gained it between @objectstack/spec 17.0.0-rc.2 and rc.5 (it folds ASCII
|
|
100
|
-
// case, i.e. case-insensitive contains): the server accepts the token, and no
|
|
101
|
-
// builder operator can author it, so the capability is unreachable from the
|
|
102
|
-
// filter UI. Closing it needs a new builder operator with a user-visible
|
|
103
|
-
// label — a new key in all ten locale packs — which is feature work and
|
|
104
|
-
// deliberately NOT carried by the dependency bump that surfaced it
|
|
105
|
-
// (objectui#3560). Tracked as objectui#3567; delete this exclusion when it
|
|
106
|
-
// lands, and the assertion below will hold it honest.
|
|
107
|
-
const KNOWN_UNREACHABLE = new Set(['$eq', '$between', '$icontains']);
|
|
108
|
-
const unreachable = [...SPEC_OPERATORS].filter(
|
|
109
|
-
(op) => !KNOWN_UNREACHABLE.has(op) && !emitted.has(op),
|
|
110
|
-
);
|
|
111
|
-
expect(
|
|
112
|
-
unreachable,
|
|
113
|
-
'FieldOperatorsSchema accepts these but no builder operator can author them',
|
|
114
|
-
).toEqual([]);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it('every KNOWN_UNREACHABLE token is still a spec operator (the exclusion ratchet)', () => {
|
|
118
|
-
// A stale exclusion is how a parity test rots into a tautology: if the spec
|
|
119
|
-
// ever drops one of these, the entry must go too rather than sit there
|
|
120
|
-
// excusing a token nobody ships. `$eq` / `$between` / `$icontains` are all
|
|
121
|
-
// real `FieldOperatorsSchema` keys today.
|
|
122
|
-
for (const op of ['$eq', '$between', '$icontains']) {
|
|
123
|
-
expect(SPEC_OPERATORS.has(op), `'${op}' is excluded but no longer a spec operator`).toBe(
|
|
124
|
-
true,
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
});
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
describe('kvToCondition round-trips what condToMongo writes', () => {
|
|
131
|
-
const cases: Array<[string, unknown]> = [
|
|
132
|
-
['notEquals', 'a'],
|
|
133
|
-
['contains', 'a'],
|
|
134
|
-
['notContains', 'a'],
|
|
135
|
-
['greaterThan', 1],
|
|
136
|
-
['lessThan', 1],
|
|
137
|
-
['greaterOrEqual', 1],
|
|
138
|
-
['lessOrEqual', 1],
|
|
139
|
-
['startsWith', 'a'],
|
|
140
|
-
['endsWith', 'a'],
|
|
141
|
-
['isNull', ''],
|
|
142
|
-
['isNotNull', ''],
|
|
143
|
-
['exists', ''],
|
|
144
|
-
['notExists', ''],
|
|
145
|
-
];
|
|
146
|
-
|
|
147
|
-
it.each(cases)('%s survives the round trip', (operator, value) => {
|
|
148
|
-
const frag = condToMongo({ id: 'c1', field: 'f', operator, value } as any, noTypes)!;
|
|
149
|
-
const [[field, v]] = Object.entries(frag);
|
|
150
|
-
expect(kvToCondition(field, v, 0)).toMatchObject({ field: 'f', operator });
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it('still reads the pre-fix $ncontains so stored criteria keep loading', () => {
|
|
154
|
-
// Dropping this would make rules saved before the fix fail to load entirely
|
|
155
|
-
// ("criteria can't be represented") rather than migrate.
|
|
156
|
-
expect(kvToCondition('name', { $ncontains: 'x' }, 0)).toMatchObject({
|
|
157
|
-
field: 'name',
|
|
158
|
-
operator: 'notContains',
|
|
159
|
-
value: 'x',
|
|
160
|
-
});
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it('rejects an operator it cannot represent rather than guessing', () => {
|
|
164
|
-
expect(kvToCondition('name', { $nope: 'x' }, 0)).toBeNull();
|
|
165
|
-
});
|
|
166
|
-
});
|