@object-ui/fields 11.0.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@object-ui/fields",
3
- "version": "11.0.0",
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.21.0",
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.0.0",
34
- "@object-ui/core": "11.0.0",
35
- "@object-ui/i18n": "11.0.0",
36
- "@object-ui/providers": "11.0.0",
37
- "@object-ui/react": "11.0.0",
38
- "@object-ui/types": "11.0.0"
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.2",
47
+ "@vitejs/plugin-react": "^6.0.3",
48
48
  "typescript": "^6.0.3",
49
- "vite": "^8.0.16",
50
- "vite-plugin-dts": "^5.0.2"
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
- * When `titleFormat` is supplied (typically derived from the referenced
73
- * object's schema), the label is rendered via the template so users see a
74
- * human-readable name (e.g. `"Acme - John Doe"`) instead of falling all the
75
- * way through to the raw record id.
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) {