@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,454 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { FilterBuilder, 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
|
-
* FilterConditionField — visual criteria builder for a stored FilterCondition
|
|
10
|
-
* (e.g. `sys_sharing_rule.criteria_json`), scoped to the object chosen in a
|
|
11
|
-
* sibling `object_name` field.
|
|
12
|
-
*
|
|
13
|
-
* Reached via the field `widget: 'filter-condition'` hint (resolves as
|
|
14
|
-
* `field:filter-condition`). Reads the live `object_name` from
|
|
15
|
-
* `dependentValues`, loads that object's fields via
|
|
16
|
-
* `dataSource.getObjectSchema(...)`, and renders `<FilterBuilder>` over them —
|
|
17
|
-
* so an admin builds `type == "customer" AND is_active == true` by picking
|
|
18
|
-
* fields/operators instead of hand-writing JSON.
|
|
19
|
-
*
|
|
20
|
-
* Storage contract: the value round-trips as a **MongoDB-style object filter**
|
|
21
|
-
* (`{ field: value }`, `{ field: { $gt: n } }`, `{ $or: [...] }`), JSON-encoded
|
|
22
|
-
* — the exact shape the sharing evaluator spreads into `engine.find(object,
|
|
23
|
-
* { filter })`. Criteria that can't be represented in the builder (nested
|
|
24
|
-
* mixes, unknown operators) fall back to a raw-JSON editor so nothing is hidden
|
|
25
|
-
* or lost; an "Edit as JSON" toggle is always available.
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
interface FilterFieldDef {
|
|
29
|
-
value: string;
|
|
30
|
-
label: string;
|
|
31
|
-
type?: string;
|
|
32
|
-
options?: Array<{ value: string; label: string }>;
|
|
33
|
-
referenceTo?: string;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
interface BuilderCondition {
|
|
37
|
-
id: string;
|
|
38
|
-
field: string;
|
|
39
|
-
operator: string;
|
|
40
|
-
value: any;
|
|
41
|
-
}
|
|
42
|
-
interface BuilderGroup {
|
|
43
|
-
id: string;
|
|
44
|
-
logic: 'and' | 'or';
|
|
45
|
-
conditions: BuilderCondition[];
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const EMPTY_GROUP: BuilderGroup = { id: 'root', logic: 'and', conditions: [] };
|
|
49
|
-
|
|
50
|
-
/** Field types that are not meaningfully filterable in a simple builder. */
|
|
51
|
-
const NON_FILTERABLE = new Set([
|
|
52
|
-
'object', 'vector', 'file', 'image', 'avatar', 'signature',
|
|
53
|
-
'richtext', 'html', 'markdown', 'location', 'grid', 'json', 'code',
|
|
54
|
-
]);
|
|
55
|
-
|
|
56
|
-
function deriveFilterFields(schema: any): FilterFieldDef[] {
|
|
57
|
-
const raw = schema?.fields;
|
|
58
|
-
const entries: Array<[string, any]> = Array.isArray(raw)
|
|
59
|
-
? raw.map((f: any) => [f?.name, f])
|
|
60
|
-
: raw && typeof raw === 'object'
|
|
61
|
-
? Object.entries(raw)
|
|
62
|
-
: [];
|
|
63
|
-
const out: FilterFieldDef[] = [];
|
|
64
|
-
for (const [name, f] of entries) {
|
|
65
|
-
if (!name || !f || f.hidden) continue;
|
|
66
|
-
const type = f.type as string | undefined;
|
|
67
|
-
if (type && NON_FILTERABLE.has(type)) continue;
|
|
68
|
-
out.push({
|
|
69
|
-
value: name,
|
|
70
|
-
label: f.label || name,
|
|
71
|
-
type,
|
|
72
|
-
options: Array.isArray(f.options)
|
|
73
|
-
? f.options.map((o: any) =>
|
|
74
|
-
typeof o === 'string'
|
|
75
|
-
? { value: o, label: o }
|
|
76
|
-
: { value: String(o?.value), label: String(o?.label ?? o?.value) },
|
|
77
|
-
)
|
|
78
|
-
: undefined,
|
|
79
|
-
referenceTo: f.reference_to || f.reference,
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
return out;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function coerceByType(value: any, type?: string): any {
|
|
86
|
-
if (value == null) return value;
|
|
87
|
-
if (type === 'boolean' || type === 'toggle') {
|
|
88
|
-
if (typeof value === 'boolean') return value;
|
|
89
|
-
if (value === 'true') return true;
|
|
90
|
-
if (value === 'false') return false;
|
|
91
|
-
return value;
|
|
92
|
-
}
|
|
93
|
-
if (type === 'number' || type === 'currency' || type === 'percent' || type === 'rating' || type === 'slider') {
|
|
94
|
-
const n = Number(value);
|
|
95
|
-
return Number.isFinite(n) && value !== '' ? n : value;
|
|
96
|
-
}
|
|
97
|
-
return value;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function toArray(value: any): any[] {
|
|
101
|
-
if (Array.isArray(value)) return value;
|
|
102
|
-
if (typeof value === 'string') return value.split(',').map((s) => s.trim()).filter(Boolean);
|
|
103
|
-
return value == null ? [] : [value];
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Builder condition → `$`-operator criteria. Exported for tests: this is the
|
|
108
|
-
* chokepoint where a builder token becomes a spec `FieldOperatorsSchema` key,
|
|
109
|
-
* and a wrong spelling here is rejected downstream by `convertFiltersToAST`
|
|
110
|
-
* rather than at authoring time. @internal
|
|
111
|
-
*/
|
|
112
|
-
export function condToMongo(c: BuilderCondition, typeOf: (f: string) => string | undefined): Record<string, any> | null {
|
|
113
|
-
const { field, operator, value } = c || ({} as BuilderCondition);
|
|
114
|
-
if (!field) return null;
|
|
115
|
-
const t = typeOf(field);
|
|
116
|
-
const cv = coerceByType(value, t);
|
|
117
|
-
switch (operator) {
|
|
118
|
-
case 'equals': return { [field]: cv };
|
|
119
|
-
case 'notEquals': return { [field]: { $ne: cv } };
|
|
120
|
-
case 'contains': return { [field]: { $contains: value } };
|
|
121
|
-
// `$notContains` is the spec spelling (FieldOperatorsSchema, data/filter.zod.ts).
|
|
122
|
-
// This emitted `$ncontains` — a token that appears nowhere in @objectstack/spec and
|
|
123
|
-
// that convertFiltersToAST throws on, so every "does not contain" rule authored here
|
|
124
|
-
// was rejected downstream. See kvToCondition for reading the old spelling back.
|
|
125
|
-
case 'notContains': return { [field]: { $notContains: value } };
|
|
126
|
-
// String-specific spec operators — previously unreachable from the
|
|
127
|
-
// builder UI even though FieldOperatorsSchema accepts them (#2942).
|
|
128
|
-
case 'startsWith': return { [field]: { $startsWith: value } };
|
|
129
|
-
case 'endsWith': return { [field]: { $endsWith: value } };
|
|
130
|
-
case 'isEmpty': return { [field]: { $in: [null, ''] } };
|
|
131
|
-
case 'isNotEmpty': return { [field]: { $nin: [null, ''] } };
|
|
132
|
-
// Null / existence spec operators. Distinct from isEmpty/isNotEmpty,
|
|
133
|
-
// which also treat '' as empty.
|
|
134
|
-
case 'isNull': return { [field]: { $null: true } };
|
|
135
|
-
case 'isNotNull': return { [field]: { $null: false } };
|
|
136
|
-
case 'exists': return { [field]: { $exists: true } };
|
|
137
|
-
case 'notExists': return { [field]: { $exists: false } };
|
|
138
|
-
case 'greaterThan':
|
|
139
|
-
case 'after': return { [field]: { $gt: cv } };
|
|
140
|
-
case 'lessThan':
|
|
141
|
-
case 'before': return { [field]: { $lt: cv } };
|
|
142
|
-
case 'greaterOrEqual': return { [field]: { $gte: cv } };
|
|
143
|
-
case 'lessOrEqual': return { [field]: { $lte: cv } };
|
|
144
|
-
case 'between': {
|
|
145
|
-
const [a, b] = Array.isArray(value) ? value : [undefined, undefined];
|
|
146
|
-
return { [field]: { $gte: coerceByType(a, t), $lte: coerceByType(b, t) } };
|
|
147
|
-
}
|
|
148
|
-
case 'in': return { [field]: { $in: toArray(value).map((v) => coerceByType(v, t)) } };
|
|
149
|
-
case 'notIn': return { [field]: { $nin: toArray(value).map((v) => coerceByType(v, t)) } };
|
|
150
|
-
default: return { [field]: cv };
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function filterGroupToMongo(group: BuilderGroup, typeOf: (f: string) => string | undefined): Record<string, any> | null {
|
|
155
|
-
const frags = (group?.conditions ?? [])
|
|
156
|
-
.map((c) => condToMongo(c, typeOf))
|
|
157
|
-
.filter((x): x is Record<string, any> => !!x);
|
|
158
|
-
// No conditions → no predicate. NOT "match all": a sharing rule with an
|
|
159
|
-
// empty criteria is refused on save, and one already stored shares nothing
|
|
160
|
-
// (objectstack#3896 / ADR-0049). `null` becomes an empty stored value, which
|
|
161
|
-
// is what the required-criteria messaging below keys off.
|
|
162
|
-
if (frags.length === 0) return null;
|
|
163
|
-
if (group.logic === 'or') return { $or: frags };
|
|
164
|
-
if (frags.length === 1) return frags[0];
|
|
165
|
-
const keys = frags.flatMap((f) => Object.keys(f));
|
|
166
|
-
const noCollision = new Set(keys).size === keys.length;
|
|
167
|
-
return noCollision ? Object.assign({}, ...frags) : { $and: frags };
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function arraysEqual(a: any, b: any[]): boolean {
|
|
171
|
-
return Array.isArray(a) && a.length === b.length && a.every((v, i) => v === b[i]);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Criteria → builder condition (the reverse of {@link condToMongo}). Returning
|
|
176
|
-
* `null` makes the builder refuse to load the rule ("criteria can't be
|
|
177
|
-
* represented"), so this must keep accepting spellings previously written.
|
|
178
|
-
* @internal
|
|
179
|
-
*/
|
|
180
|
-
export function kvToCondition(field: string, v: any, idx: number): BuilderCondition | null {
|
|
181
|
-
const id = `c_${idx}_${field}`;
|
|
182
|
-
if (v === null || typeof v !== 'object' || Array.isArray(v)) {
|
|
183
|
-
return { id, field, operator: 'equals', value: v };
|
|
184
|
-
}
|
|
185
|
-
const opKeys = Object.keys(v);
|
|
186
|
-
if (opKeys.length === 1) {
|
|
187
|
-
const op = opKeys[0];
|
|
188
|
-
const val = v[op];
|
|
189
|
-
switch (op) {
|
|
190
|
-
case '$ne': return { id, field, operator: 'notEquals', value: val };
|
|
191
|
-
case '$contains': return { id, field, operator: 'contains', value: val };
|
|
192
|
-
// `$ncontains` is the pre-fix spelling this widget used to emit. Criteria saved
|
|
193
|
-
// before the fix still carry it, so keep reading it — dropping it here would make
|
|
194
|
-
// those rules fail to load ("criteria can't be represented") instead of migrating.
|
|
195
|
-
case '$notContains':
|
|
196
|
-
case '$ncontains': return { id, field, operator: 'notContains', value: val };
|
|
197
|
-
case '$gt': return { id, field, operator: 'greaterThan', value: val };
|
|
198
|
-
case '$lt': return { id, field, operator: 'lessThan', value: val };
|
|
199
|
-
case '$gte': return { id, field, operator: 'greaterOrEqual', value: val };
|
|
200
|
-
case '$lte': return { id, field, operator: 'lessOrEqual', value: val };
|
|
201
|
-
case '$startsWith': return { id, field, operator: 'startsWith', value: val };
|
|
202
|
-
case '$endsWith': return { id, field, operator: 'endsWith', value: val };
|
|
203
|
-
case '$null': return { id, field, operator: val === false ? 'isNotNull' : 'isNull', value: '' };
|
|
204
|
-
case '$exists': return { id, field, operator: val === false ? 'notExists' : 'exists', value: '' };
|
|
205
|
-
case '$in':
|
|
206
|
-
return arraysEqual(val, [null, ''])
|
|
207
|
-
? { id, field, operator: 'isEmpty', value: '' }
|
|
208
|
-
: { id, field, operator: 'in', value: val };
|
|
209
|
-
case '$nin':
|
|
210
|
-
return arraysEqual(val, [null, ''])
|
|
211
|
-
? { id, field, operator: 'isNotEmpty', value: '' }
|
|
212
|
-
: { id, field, operator: 'notIn', value: val };
|
|
213
|
-
default: return null;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
if (opKeys.length === 2 && '$gte' in v && '$lte' in v) {
|
|
217
|
-
return { id, field, operator: 'between', value: [v.$gte, v.$lte] };
|
|
218
|
-
}
|
|
219
|
-
return null;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/** Returns a BuilderGroup, or `null` when the criteria can't be represented. */
|
|
223
|
-
function mongoToFilterGroup(mongo: any): BuilderGroup | null {
|
|
224
|
-
if (mongo == null) return { ...EMPTY_GROUP, conditions: [] };
|
|
225
|
-
if (typeof mongo !== 'object' || Array.isArray(mongo)) return null;
|
|
226
|
-
const entries = Object.entries(mongo);
|
|
227
|
-
if (entries.length === 0) return { ...EMPTY_GROUP, conditions: [] };
|
|
228
|
-
if (entries.length === 1 && (mongo.$or || mongo.$and)) {
|
|
229
|
-
const logic: 'and' | 'or' = mongo.$or ? 'or' : 'and';
|
|
230
|
-
const arr = mongo.$or || mongo.$and;
|
|
231
|
-
if (!Array.isArray(arr)) return null;
|
|
232
|
-
const conditions: BuilderCondition[] = [];
|
|
233
|
-
for (let i = 0; i < arr.length; i++) {
|
|
234
|
-
const frag = arr[i];
|
|
235
|
-
if (!frag || typeof frag !== 'object' || Object.keys(frag).length !== 1) return null;
|
|
236
|
-
const field = Object.keys(frag)[0];
|
|
237
|
-
if (field.startsWith('$')) return null;
|
|
238
|
-
const c = kvToCondition(field, frag[field], i);
|
|
239
|
-
if (!c) return null;
|
|
240
|
-
conditions.push(c);
|
|
241
|
-
}
|
|
242
|
-
return { id: 'root', logic, conditions };
|
|
243
|
-
}
|
|
244
|
-
const conditions: BuilderCondition[] = [];
|
|
245
|
-
let i = 0;
|
|
246
|
-
for (const [field, v] of entries) {
|
|
247
|
-
if (field.startsWith('$')) return null; // mixed logical + field → raw
|
|
248
|
-
const c = kvToCondition(field, v, i++);
|
|
249
|
-
if (!c) return null;
|
|
250
|
-
conditions.push(c);
|
|
251
|
-
}
|
|
252
|
-
return { id: 'root', logic: 'and', conditions };
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* Would this criteria select EVERY record of the object?
|
|
257
|
-
*
|
|
258
|
-
* Mirrors the server's `isMatchAllCriteria` (objectstack `plugin-sharing`,
|
|
259
|
-
* #3896) closely enough to warn before the round-trip: blank, `{}`, `[]`, and
|
|
260
|
-
* the vacuous combinators. Deliberately conservative in the same direction —
|
|
261
|
-
* the cost of a false positive is one extra hint, the cost of a false negative
|
|
262
|
-
* is a save that fails with a toast. The server stays authoritative.
|
|
263
|
-
*
|
|
264
|
-
* @internal exported for tests
|
|
265
|
-
*/
|
|
266
|
-
export function isMatchAllCriteria(parsed: any): boolean {
|
|
267
|
-
if (parsed == null) return true;
|
|
268
|
-
if (Array.isArray(parsed)) return parsed.every(isMatchAllCriteria);
|
|
269
|
-
if (typeof parsed !== 'object') return true;
|
|
270
|
-
const entries = Object.entries(parsed);
|
|
271
|
-
if (entries.length === 0) return true;
|
|
272
|
-
for (const [key, value] of entries) {
|
|
273
|
-
if (key === '$and') {
|
|
274
|
-
if (!Array.isArray(value) || value.every(isMatchAllCriteria)) continue;
|
|
275
|
-
return false;
|
|
276
|
-
}
|
|
277
|
-
if (key === '$or') {
|
|
278
|
-
if (!Array.isArray(value) || value.length === 0 || value.some(isMatchAllCriteria)) continue;
|
|
279
|
-
return false;
|
|
280
|
-
}
|
|
281
|
-
return false;
|
|
282
|
-
}
|
|
283
|
-
return true;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
function stringifyValue(value: string | object | undefined | null): string {
|
|
287
|
-
if (value == null || value === '') return '';
|
|
288
|
-
if (typeof value === 'string') return value;
|
|
289
|
-
try {
|
|
290
|
-
return JSON.stringify(value);
|
|
291
|
-
} catch {
|
|
292
|
-
return '';
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
export function FilterConditionField({
|
|
297
|
-
value,
|
|
298
|
-
onChange,
|
|
299
|
-
readonly,
|
|
300
|
-
className,
|
|
301
|
-
error,
|
|
302
|
-
...props
|
|
303
|
-
}: FieldWidgetComponentProps<string | object>) {
|
|
304
|
-
const ctx = React.useContext(SchemaRendererContext);
|
|
305
|
-
const { t } = useFieldTranslation();
|
|
306
|
-
const dataSource: any = props.dataSource ?? (ctx as any)?.dataSource ?? null;
|
|
307
|
-
const dependentValues: Record<string, any> = (props as any).dependentValues ?? {};
|
|
308
|
-
const objectName = String(dependentValues.object_name ?? '');
|
|
309
|
-
|
|
310
|
-
const [fields, setFields] = React.useState<FilterFieldDef[] | null>(null);
|
|
311
|
-
|
|
312
|
-
React.useEffect(() => {
|
|
313
|
-
setFields(null);
|
|
314
|
-
if (!dataSource || !objectName || typeof dataSource.getObjectSchema !== 'function') return;
|
|
315
|
-
let cancelled = false;
|
|
316
|
-
(async () => {
|
|
317
|
-
try {
|
|
318
|
-
const schema = await dataSource.getObjectSchema(objectName);
|
|
319
|
-
if (!cancelled) setFields(deriveFilterFields(schema));
|
|
320
|
-
} catch {
|
|
321
|
-
if (!cancelled) setFields([]);
|
|
322
|
-
}
|
|
323
|
-
})();
|
|
324
|
-
return () => {
|
|
325
|
-
cancelled = true;
|
|
326
|
-
};
|
|
327
|
-
}, [dataSource, objectName]);
|
|
328
|
-
|
|
329
|
-
const rawValue = React.useMemo(() => stringifyValue(value), [value]);
|
|
330
|
-
|
|
331
|
-
const parsed = React.useMemo(() => {
|
|
332
|
-
if (!rawValue.trim()) return { mongo: {}, ok: true };
|
|
333
|
-
try {
|
|
334
|
-
return { mongo: JSON.parse(rawValue), ok: true };
|
|
335
|
-
} catch {
|
|
336
|
-
return { mongo: null, ok: false };
|
|
337
|
-
}
|
|
338
|
-
}, [rawValue]);
|
|
339
|
-
|
|
340
|
-
const group = React.useMemo(
|
|
341
|
-
() => (parsed.ok ? mongoToFilterGroup(parsed.mongo) : null),
|
|
342
|
-
[parsed],
|
|
343
|
-
);
|
|
344
|
-
|
|
345
|
-
// Only flag a criteria that PARSES to match-all. Unparsable JSON has its own
|
|
346
|
-
// message (`invalidJson`) and must not collect a second, contradictory one.
|
|
347
|
-
const isEmptyCriteria = React.useMemo(
|
|
348
|
-
() => parsed.ok && isMatchAllCriteria(parsed.mongo),
|
|
349
|
-
[parsed],
|
|
350
|
-
);
|
|
351
|
-
|
|
352
|
-
// Raw JSON mode: forced when the stored value can't be represented in the
|
|
353
|
-
// builder; otherwise opt-in via the toggle.
|
|
354
|
-
const representable = parsed.ok && group !== null;
|
|
355
|
-
const [rawMode, setRawMode] = React.useState<boolean>(!representable);
|
|
356
|
-
React.useEffect(() => {
|
|
357
|
-
if (!representable) setRawMode(true);
|
|
358
|
-
}, [representable]);
|
|
359
|
-
|
|
360
|
-
const typeOf = React.useMemo(() => {
|
|
361
|
-
const map = new Map((fields ?? []).map((f) => [f.value, f.type]));
|
|
362
|
-
return (f: string) => map.get(f);
|
|
363
|
-
}, [fields]);
|
|
364
|
-
|
|
365
|
-
const handleBuilderChange = (g: BuilderGroup) => {
|
|
366
|
-
const mongo = filterGroupToMongo(g, typeOf);
|
|
367
|
-
onChange((mongo == null ? '' : JSON.stringify(mongo)) as any);
|
|
368
|
-
};
|
|
369
|
-
|
|
370
|
-
if (!objectName) {
|
|
371
|
-
return (
|
|
372
|
-
<p className={cn('text-sm text-muted-foreground', className)}>
|
|
373
|
-
{t('fields.filterCondition.selectObjectFirst')}
|
|
374
|
-
</p>
|
|
375
|
-
);
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
if (readonly) {
|
|
379
|
-
if (!rawValue.trim()) {
|
|
380
|
-
// Used to read "All records" — which was both wrong and the most
|
|
381
|
-
// dangerous thing this widget could say (objectstack#3896). A rule with
|
|
382
|
-
// no criteria has never usefully shared everything; it now shares
|
|
383
|
-
// nothing and is refused on save, so name that instead.
|
|
384
|
-
return (
|
|
385
|
-
<span className={cn('text-sm text-destructive', className)}>
|
|
386
|
-
{t('fields.filterCondition.noCriteria')}
|
|
387
|
-
</span>
|
|
388
|
-
);
|
|
389
|
-
}
|
|
390
|
-
return (
|
|
391
|
-
<pre className={cn('overflow-x-auto rounded bg-muted/40 p-2 text-xs', className)}>
|
|
392
|
-
{rawValue}
|
|
393
|
-
</pre>
|
|
394
|
-
);
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
return (
|
|
398
|
-
<div className={cn('flex flex-col gap-2', className)}>
|
|
399
|
-
{rawMode ? (
|
|
400
|
-
<>
|
|
401
|
-
<textarea
|
|
402
|
-
// DOM pass-through onto the raw-JSON editing surface
|
|
403
|
-
// (objectui#3318). NOTE this widget stays on the #3318 ledger
|
|
404
|
-
// regardless: its dependency-gated state (no `object_name` chosen
|
|
405
|
-
// yet — the state a fresh form and the registry sweep render) is a
|
|
406
|
-
// plain hint paragraph with no focusable control.
|
|
407
|
-
{...toDomProps(props)}
|
|
408
|
-
className="min-h-[96px] w-full rounded border bg-background px-2 py-1 font-mono text-xs"
|
|
409
|
-
value={rawValue}
|
|
410
|
-
placeholder='{ "type": "customer", "is_active": true }'
|
|
411
|
-
onChange={(e) => onChange(e.target.value as any)}
|
|
412
|
-
// The form's validation slot (#3222) OR this widget's own
|
|
413
|
-
// unparsable-JSON state, which already renders its red message.
|
|
414
|
-
aria-invalid={!!error || !parsed.ok}
|
|
415
|
-
/>
|
|
416
|
-
{!parsed.ok && (
|
|
417
|
-
<span className="text-xs text-destructive">{t('fields.filterCondition.invalidJson')}</span>
|
|
418
|
-
)}
|
|
419
|
-
</>
|
|
420
|
-
) : (
|
|
421
|
-
<FilterBuilder
|
|
422
|
-
fields={fields ?? []}
|
|
423
|
-
value={(group ?? EMPTY_GROUP) as any}
|
|
424
|
-
onChange={handleBuilderChange as any}
|
|
425
|
-
/>
|
|
426
|
-
)}
|
|
427
|
-
{/*
|
|
428
|
-
The server refuses to save a criteria that would select every record
|
|
429
|
-
(objectstack#3896), but that rejection only arrives as a toast after
|
|
430
|
-
the admin hits Save. Say it here, while they are still looking at the
|
|
431
|
-
empty builder — and never imply that leaving it empty means "share
|
|
432
|
-
everything", which is what this widget used to do.
|
|
433
|
-
*/}
|
|
434
|
-
{isEmptyCriteria && (
|
|
435
|
-
<span className="text-xs text-destructive">
|
|
436
|
-
{t('fields.filterCondition.criteriaRequired')}
|
|
437
|
-
</span>
|
|
438
|
-
)}
|
|
439
|
-
<button
|
|
440
|
-
type="button"
|
|
441
|
-
className="self-start text-xs text-muted-foreground underline-offset-2 hover:underline"
|
|
442
|
-
onClick={() => setRawMode((m) => !m)}
|
|
443
|
-
disabled={!representable && !rawMode}
|
|
444
|
-
title={!representable ? t('fields.filterCondition.jsonOnly') : undefined}
|
|
445
|
-
>
|
|
446
|
-
{rawMode
|
|
447
|
-
? t('fields.filterCondition.useVisualBuilder')
|
|
448
|
-
: t('fields.filterCondition.editAsJson')}
|
|
449
|
-
</button>
|
|
450
|
-
</div>
|
|
451
|
-
);
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
export default FilterConditionField;
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { EmptyValue } from '@object-ui/components';
|
|
3
|
-
import { useDisplayLocale } from '@object-ui/i18n';
|
|
4
|
-
import { FieldWidgetComponentProps } from './types';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* FormulaField - Read-only computed field
|
|
8
|
-
* Values are computed on the backend and cannot be edited
|
|
9
|
-
*/
|
|
10
|
-
export function FormulaField({ value, field, ...props }: FieldWidgetComponentProps<any>) {
|
|
11
|
-
// Before the empty-value early return — the hook count must not change when
|
|
12
|
-
// a value flips between null and set. A `date` return type used to format
|
|
13
|
-
// through the MACHINE's locale (objectui#4468).
|
|
14
|
-
const locale = useDisplayLocale();
|
|
15
|
-
const formulaField = field as any;
|
|
16
|
-
const returnType = formulaField?.return_type || 'text';
|
|
17
|
-
|
|
18
|
-
if (value == null) {
|
|
19
|
-
return <EmptyValue className={props.className} />;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
let displayValue: string;
|
|
23
|
-
if (returnType === 'number' || returnType === 'currency') {
|
|
24
|
-
displayValue = typeof value === 'number' ? value.toFixed(2) : String(value);
|
|
25
|
-
} else if (returnType === 'boolean') {
|
|
26
|
-
displayValue = value ? 'Yes' : 'No';
|
|
27
|
-
} else if (returnType === 'date') {
|
|
28
|
-
displayValue = new Date(value).toLocaleDateString(locale);
|
|
29
|
-
} else {
|
|
30
|
-
displayValue = String(value);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return (
|
|
34
|
-
<span className={`text-sm font-mono text-gray-700 ${props.className || ''}`}>
|
|
35
|
-
{displayValue}
|
|
36
|
-
</span>
|
|
37
|
-
);
|
|
38
|
-
}
|
|
@@ -1,60 +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 React from 'react';
|
|
10
|
-
import { FullscreenEditor, type FullscreenEditorProps } from '@object-ui/components';
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* The fullscreen-edit affordance shared by every long-text widget in this
|
|
14
|
-
* package (`field:textarea` → `TextAreaField`, `field:markdown` / `field:html`
|
|
15
|
-
* → `RichTextField`).
|
|
16
|
-
*
|
|
17
|
-
* ## This is now a thin wrapper (objectui#3398)
|
|
18
|
-
*
|
|
19
|
-
* The affordance, the dialog, the draft/commit state machine and the
|
|
20
|
-
* `readOnly` / `disabled` semantics all live in `FullscreenEditor`
|
|
21
|
-
* (`@object-ui/components`). They used to live here AND, separately, in the
|
|
22
|
-
* form renderer's built-in `textarea` branch — one form-level promise
|
|
23
|
-
* (`ObjectFormSchema.mobile.fullscreenLongText`, projected onto every long-text
|
|
24
|
-
* field as `mobile_fullscreen`) answered twice, on the two render paths.
|
|
25
|
-
*
|
|
26
|
-
* Two copies of a state machine drift, and these did: objectui#3400 measured a
|
|
27
|
-
* read-only field editable through the built-in branch's dialog, objectui#3402
|
|
28
|
-
* measured the same hole for `disabled` here, and #3393 (label in the dialog
|
|
29
|
-
* title) and #3272 (i18n) each landed on one side before the other. The
|
|
30
|
-
* maintainer's ruling of 2026-08-10 was one implementation hoisted to the
|
|
31
|
-
* dependency-legal side — and the measured import graph allows exactly that
|
|
32
|
-
* direction: this package depends on `@object-ui/components`, which declares no
|
|
33
|
-
* dependency on `@object-ui/fields` in `dependencies` or `peerDependencies`.
|
|
34
|
-
*
|
|
35
|
-
* ## Why the wrapper survives the merge rather than being deleted
|
|
36
|
-
*
|
|
37
|
-
* It carries the one thing that is genuinely about THIS package: `readOnly` is
|
|
38
|
-
* not part of the contract here. Both hosts early-return a read-only DISPLAY
|
|
39
|
-
* before they compute the affordance (`TextAreaField.tsx`, `RichTextField.tsx`),
|
|
40
|
-
* so a `readOnly` reaching this component would be a prop with no producer on
|
|
41
|
-
* this path — the shape this package keeps deleting (objectui#3232/#3233). The
|
|
42
|
-
* `Omit` below is that statement in the type system, and it is why the hoist did
|
|
43
|
-
* not simply re-export the primitive. A future host that renders an editor for
|
|
44
|
-
* read-only fields must widen this type AND pass the prop together; the
|
|
45
|
-
* primitive already answers it (no affordance at all).
|
|
46
|
-
*
|
|
47
|
-
* Everything else — `value` / `onCommit` / `label` / `testIdPrefix` /
|
|
48
|
-
* `disabled` / `children` / `footer` — passes straight through, so the hosts and
|
|
49
|
-
* their pins are unchanged. `testIdPrefix` still namespaces this package's ids
|
|
50
|
-
* (`textarea-`, `richtext-`), distinct from the built-in branch's
|
|
51
|
-
* `form-textarea-`, so a test can still say which render path it found.
|
|
52
|
-
*/
|
|
53
|
-
export type FullscreenFieldEditorProps = Omit<FullscreenEditorProps, 'readOnly'>;
|
|
54
|
-
|
|
55
|
-
export function FullscreenFieldEditor(props: FullscreenFieldEditorProps) {
|
|
56
|
-
// `readOnly` is deliberately not forwarded — it is not in the props type, and
|
|
57
|
-
// the primitive defaults it to false, which is the behaviour both hosts rely
|
|
58
|
-
// on: they have already returned a read-only display before reaching here.
|
|
59
|
-
return <FullscreenEditor {...props} />;
|
|
60
|
-
}
|