@object-ui/fields 11.2.0 → 11.3.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 +14 -0
- package/dist/FieldEditWidget.d.ts +13 -0
- package/dist/index.cjs +2 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1058 -1002
- package/package.json +11 -11
- package/src/FieldEditWidget.tsx +91 -0
- package/src/index.tsx +1 -0
- package/src/widgets/LookupField.tsx +33 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@object-ui/fields",
|
|
3
|
-
"version": "11.
|
|
3
|
+
"version": "11.3.0",
|
|
4
4
|
"description": "Field renderers and registry for Object UI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -25,17 +25,17 @@
|
|
|
25
25
|
],
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"clsx": "^2.1.1",
|
|
28
|
-
"lucide-react": "^1.
|
|
28
|
+
"lucide-react": "^1.22.0",
|
|
29
29
|
"react-markdown": "^10.1.0",
|
|
30
30
|
"rehype-sanitize": "^6.0.0",
|
|
31
31
|
"remark-gfm": "^4.0.1",
|
|
32
32
|
"tailwind-merge": "^3.6.0",
|
|
33
|
-
"@object-ui/components": "11.
|
|
34
|
-
"@object-ui/core": "11.
|
|
35
|
-
"@object-ui/i18n": "11.
|
|
36
|
-
"@object-ui/providers": "11.
|
|
37
|
-
"@object-ui/react": "11.
|
|
38
|
-
"@object-ui/types": "11.
|
|
33
|
+
"@object-ui/components": "11.3.0",
|
|
34
|
+
"@object-ui/core": "11.3.0",
|
|
35
|
+
"@object-ui/i18n": "11.3.0",
|
|
36
|
+
"@object-ui/providers": "11.3.0",
|
|
37
|
+
"@object-ui/react": "11.3.0",
|
|
38
|
+
"@object-ui/types": "11.3.0"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"react": "^18.0.0 || ^19.0.0",
|
|
@@ -44,10 +44,10 @@
|
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@types/react": "19.2.17",
|
|
46
46
|
"@types/react-dom": "19.2.3",
|
|
47
|
-
"@vitejs/plugin-react": "^6.0.
|
|
47
|
+
"@vitejs/plugin-react": "^6.0.3",
|
|
48
48
|
"typescript": "^6.0.3",
|
|
49
|
-
"vite": "^8.0
|
|
50
|
-
"vite-plugin-dts": "^5.0.
|
|
49
|
+
"vite": "^8.1.0",
|
|
50
|
+
"vite-plugin-dts": "^5.0.3"
|
|
51
51
|
},
|
|
52
52
|
"publishConfig": {
|
|
53
53
|
"access": "public"
|
|
@@ -0,0 +1,91 @@
|
|
|
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 type { FieldWidgetProps } from './widgets/types';
|
|
11
|
+
|
|
12
|
+
// The SAME dedicated widgets the form renders — reused for in-place editing
|
|
13
|
+
// (e.g. the data grid's inline cell editor) so a select edits as a dropdown, a
|
|
14
|
+
// boolean as a checkbox, a date as a date picker, etc. — never a bare text box.
|
|
15
|
+
import { TextField } from './widgets/TextField';
|
|
16
|
+
import { TextAreaField } from './widgets/TextAreaField';
|
|
17
|
+
import { NumberField } from './widgets/NumberField';
|
|
18
|
+
import { CurrencyField } from './widgets/CurrencyField';
|
|
19
|
+
import { PercentField } from './widgets/PercentField';
|
|
20
|
+
import { SliderField } from './widgets/SliderField';
|
|
21
|
+
import { RatingField } from './widgets/RatingField';
|
|
22
|
+
import { BooleanField } from './widgets/BooleanField';
|
|
23
|
+
import { SelectField } from './widgets/SelectField';
|
|
24
|
+
import { MultiSelectField } from './widgets/MultiSelectField';
|
|
25
|
+
import { RadioField } from './widgets/RadioField';
|
|
26
|
+
import { CheckboxesField } from './widgets/CheckboxesField';
|
|
27
|
+
import { TagsField } from './widgets/TagsField';
|
|
28
|
+
import { DateField } from './widgets/DateField';
|
|
29
|
+
import { DateTimeField } from './widgets/DateTimeField';
|
|
30
|
+
import { TimeField } from './widgets/TimeField';
|
|
31
|
+
import { EmailField } from './widgets/EmailField';
|
|
32
|
+
import { PhoneField } from './widgets/PhoneField';
|
|
33
|
+
import { UrlField } from './widgets/UrlField';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Field types that edit in place with a dedicated widget. Keyed by the raw
|
|
37
|
+
* field `type` (the widget map mirrors the form's `mapFieldTypeToFormType`).
|
|
38
|
+
* Rich/heavy types (file, image, lookup, richtext, …) are intentionally absent
|
|
39
|
+
* so callers fall back to their own simpler editor.
|
|
40
|
+
*/
|
|
41
|
+
const EDIT_WIDGETS: Record<string, React.ComponentType<FieldWidgetProps<any>>> = {
|
|
42
|
+
text: TextField,
|
|
43
|
+
textarea: TextAreaField,
|
|
44
|
+
number: NumberField,
|
|
45
|
+
currency: CurrencyField,
|
|
46
|
+
percent: PercentField,
|
|
47
|
+
slider: SliderField,
|
|
48
|
+
progress: SliderField,
|
|
49
|
+
rating: RatingField,
|
|
50
|
+
boolean: BooleanField,
|
|
51
|
+
toggle: BooleanField,
|
|
52
|
+
select: SelectField,
|
|
53
|
+
status: SelectField,
|
|
54
|
+
multiselect: MultiSelectField,
|
|
55
|
+
radio: RadioField,
|
|
56
|
+
checkboxes: CheckboxesField,
|
|
57
|
+
tags: TagsField,
|
|
58
|
+
date: DateField,
|
|
59
|
+
datetime: DateTimeField,
|
|
60
|
+
time: TimeField,
|
|
61
|
+
email: EmailField,
|
|
62
|
+
phone: PhoneField,
|
|
63
|
+
url: UrlField,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/** Field types whose value is chosen in one discrete gesture (no free typing). */
|
|
67
|
+
export const DISCRETE_EDIT_TYPES = new Set<string>([
|
|
68
|
+
'boolean', 'toggle', 'select', 'status', 'radio', 'rating',
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
/** True when a field type has a dedicated in-place edit widget. */
|
|
72
|
+
export function hasFieldEditWidget(type: string | undefined): boolean {
|
|
73
|
+
return !!type && type in EDIT_WIDGETS;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Render the dedicated edit widget for a field's type — the SAME control the
|
|
78
|
+
* form uses — as a controlled `{ value, onChange, field }` input. Returns
|
|
79
|
+
* `null` for types without a registered widget so the caller can fall back to
|
|
80
|
+
* a plain editor.
|
|
81
|
+
*/
|
|
82
|
+
export function FieldEditWidget({
|
|
83
|
+
field,
|
|
84
|
+
value,
|
|
85
|
+
onChange,
|
|
86
|
+
readonly,
|
|
87
|
+
}: FieldWidgetProps<any>): React.ReactElement | null {
|
|
88
|
+
const Widget = field?.type ? EDIT_WIDGETS[field.type] : undefined;
|
|
89
|
+
if (!Widget) return null;
|
|
90
|
+
return <Widget field={field} value={value} onChange={onChange} readonly={readonly} />;
|
|
91
|
+
}
|
package/src/index.tsx
CHANGED
|
@@ -2130,6 +2130,7 @@ export function registerFields() {
|
|
|
2130
2130
|
}
|
|
2131
2131
|
|
|
2132
2132
|
export * from './widgets/types';
|
|
2133
|
+
export * from './FieldEditWidget';
|
|
2133
2134
|
export * from './widgets/TextField';
|
|
2134
2135
|
export * from './widgets/NumberField';
|
|
2135
2136
|
export * from './widgets/BooleanField';
|
|
@@ -12,6 +12,7 @@ import type { DataSource, QueryParams, LookupColumnDef } from '@object-ui/types'
|
|
|
12
12
|
import { RecordPickerDialog, lookupFiltersToRecord } from './RecordPickerDialog';
|
|
13
13
|
import type { RecordPickerFilterColumn } from './RecordPickerDialog';
|
|
14
14
|
import { deriveLookupColumns } from './deriveLookupColumns';
|
|
15
|
+
import { getRecordDisplayName } from '@object-ui/core';
|
|
15
16
|
import { getRecentLookupIds, pushRecentLookupId } from './recentLookups';
|
|
16
17
|
import { getCellRendererResolver } from './_cell-renderer-bridge';
|
|
17
18
|
import { SchemaRendererContext as ImportedSchemaRendererContext } from '@object-ui/react';
|
|
@@ -69,10 +70,17 @@ function formatRecordTitle(record: any, titleFormat: string): string | null {
|
|
|
69
70
|
/**
|
|
70
71
|
* Map a raw record to a LookupOption using a display field and an id field.
|
|
71
72
|
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
73
|
+
* Label precedence (ADR-0079):
|
|
74
|
+
* 1. `titleFormat` template (when supplied, derived from the referenced
|
|
75
|
+
* object's schema) — e.g. `"Acme - John Doe"`.
|
|
76
|
+
* 2. the explicit `displayField` value on the record.
|
|
77
|
+
* 3. the unified `@object-ui/core#getRecordDisplayName` against the referenced
|
|
78
|
+
* object's schema (`objectDef`) — adds `displayNameField` + type-aware
|
|
79
|
+
* field derivation, so a lookup to an object whose name lives in e.g.
|
|
80
|
+
* `activity_name` resolves a real name instead of the raw id. We stop short
|
|
81
|
+
* of the resolver's `Record #<id>` floor here so the chip still falls
|
|
82
|
+
* through to the bare id when nothing nameable exists.
|
|
83
|
+
* 4. the legacy hard-coded name list, then the raw id.
|
|
76
84
|
*/
|
|
77
85
|
function recordToOption(
|
|
78
86
|
record: any,
|
|
@@ -80,12 +88,27 @@ function recordToOption(
|
|
|
80
88
|
idField: string,
|
|
81
89
|
descriptionField?: string,
|
|
82
90
|
titleFormat?: string | null,
|
|
91
|
+
objectDef?: any,
|
|
83
92
|
): LookupOption {
|
|
84
93
|
const val = record[idField] ?? record.id ?? record._id;
|
|
85
94
|
const templated = titleFormat ? formatRecordTitle(record, titleFormat) : null;
|
|
95
|
+
|
|
96
|
+
// Object-level resolver fallback (displayNameField + derivation), excluding
|
|
97
|
+
// its id floor so we don't shadow the explicit `String(val)` tail.
|
|
98
|
+
let unified: string | undefined;
|
|
99
|
+
if (objectDef) {
|
|
100
|
+
const resolved = getRecordDisplayName(objectDef, record);
|
|
101
|
+
const id = record?.id ?? record?._id;
|
|
102
|
+
const isFloor =
|
|
103
|
+
resolved === 'Untitled' ||
|
|
104
|
+
(id !== null && id !== undefined && resolved === `Record #${id}`);
|
|
105
|
+
if (!isFloor) unified = resolved;
|
|
106
|
+
}
|
|
107
|
+
|
|
86
108
|
const label =
|
|
87
109
|
templated ??
|
|
88
110
|
record[displayField] ??
|
|
111
|
+
unified ??
|
|
89
112
|
record.label ??
|
|
90
113
|
record.name ??
|
|
91
114
|
record.full_name ??
|
|
@@ -358,7 +381,7 @@ export function LookupField({ value, onChange, field, readonly, ...props }: Fiel
|
|
|
358
381
|
|
|
359
382
|
const result = await dataSource.find(referenceTo, params);
|
|
360
383
|
const records: any[] = result?.data ?? result ?? [];
|
|
361
|
-
const mapped = records.map(r => recordToOption(r, displayField, idField, effectiveDescriptionField, refTitleFormat));
|
|
384
|
+
const mapped = records.map(r => recordToOption(r, displayField, idField, effectiveDescriptionField, refTitleFormat, refObjectSchema));
|
|
362
385
|
|
|
363
386
|
setFetchedOptions(mapped);
|
|
364
387
|
setTotalCount(result?.total ?? records.length);
|
|
@@ -455,14 +478,14 @@ export function LookupField({ value, onChange, field, readonly, ...props }: Fiel
|
|
|
455
478
|
for (const id of unresolved) {
|
|
456
479
|
if (typeof (dataSource as any).findOne === 'function') {
|
|
457
480
|
const rec = await (dataSource as any).findOne(referenceTo, id);
|
|
458
|
-
if (rec) fetched.push(recordToOption(rec, displayField, idField, effectiveDescriptionField, refTitleFormat));
|
|
481
|
+
if (rec) fetched.push(recordToOption(rec, displayField, idField, effectiveDescriptionField, refTitleFormat, refObjectSchema));
|
|
459
482
|
} else {
|
|
460
483
|
const res = await dataSource.find(referenceTo, {
|
|
461
484
|
$filter: { [idField]: id },
|
|
462
485
|
$top: 1,
|
|
463
486
|
} as QueryParams);
|
|
464
487
|
const rows = res?.data ?? res ?? [];
|
|
465
|
-
if (rows[0]) fetched.push(recordToOption(rows[0], displayField, idField, effectiveDescriptionField, refTitleFormat));
|
|
488
|
+
if (rows[0]) fetched.push(recordToOption(rows[0], displayField, idField, effectiveDescriptionField, refTitleFormat, refObjectSchema));
|
|
466
489
|
}
|
|
467
490
|
}
|
|
468
491
|
if (!cancelled && fetched.length) {
|
|
@@ -539,7 +562,7 @@ export function LookupField({ value, onChange, field, readonly, ...props }: Fiel
|
|
|
539
562
|
// findOption can resolve display labels after the dialog closes.
|
|
540
563
|
const handlePickerSelectRecords = useCallback(
|
|
541
564
|
(records: any[]) => {
|
|
542
|
-
const mapped = records.map(r => recordToOption(r, displayField, idField, effectiveDescriptionField, refTitleFormat));
|
|
565
|
+
const mapped = records.map(r => recordToOption(r, displayField, idField, effectiveDescriptionField, refTitleFormat, refObjectSchema));
|
|
543
566
|
if (referenceTo) mapped.forEach((o) => pushRecentLookupId(referenceTo, o.value));
|
|
544
567
|
setPickerResolvedRecords(mapped);
|
|
545
568
|
},
|
|
@@ -561,7 +584,7 @@ export function LookupField({ value, onChange, field, readonly, ...props }: Fiel
|
|
|
561
584
|
if (cached) { recs.push(cached); continue; }
|
|
562
585
|
if (typeof (dataSource as any).findOne === 'function') {
|
|
563
586
|
const r = await (dataSource as any).findOne(referenceTo, id);
|
|
564
|
-
if (r) recs.push(recordToOption(r, displayField, idField, effectiveDescriptionField, refTitleFormat));
|
|
587
|
+
if (r) recs.push(recordToOption(r, displayField, idField, effectiveDescriptionField, refTitleFormat, refObjectSchema));
|
|
565
588
|
}
|
|
566
589
|
}
|
|
567
590
|
if (!cancelled) setRecentOptions(recs);
|
|
@@ -592,7 +615,7 @@ export function LookupField({ value, onChange, field, readonly, ...props }: Fiel
|
|
|
592
615
|
setError(null);
|
|
593
616
|
try {
|
|
594
617
|
const created = await (dataSource as any).create(referenceTo, { [displayField]: label });
|
|
595
|
-
const opt = recordToOption(created, displayField, idField, effectiveDescriptionField, refTitleFormat);
|
|
618
|
+
const opt = recordToOption(created, displayField, idField, effectiveDescriptionField, refTitleFormat, refObjectSchema);
|
|
596
619
|
setPickerResolvedRecords((prev) => [opt, ...prev.filter((o) => o.value !== opt.value)]);
|
|
597
620
|
handleSelect(opt);
|
|
598
621
|
} catch (err) {
|