-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathFieldEditWidget.tsx
More file actions
189 lines (179 loc) · 8.54 KB
/
Copy pathFieldEditWidget.tsx
File metadata and controls
189 lines (179 loc) · 8.54 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
/**
* ObjectUI
* Copyright (c) 2024-present ObjectStack Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import type { FieldWidgetProps } from './widgets/types';
// The SAME dedicated widgets the form renders — reused for in-place editing
// (e.g. the data grid's inline cell editor) so a select edits as a dropdown, a
// boolean as a checkbox, a date as a date picker, etc. — never a bare text box.
import { TextField } from './widgets/TextField';
import { TextAreaField } from './widgets/TextAreaField';
import { NumberField } from './widgets/NumberField';
import { CurrencyField } from './widgets/CurrencyField';
import { PercentField } from './widgets/PercentField';
import { SliderField } from './widgets/SliderField';
import { RatingField } from './widgets/RatingField';
import { BooleanField } from './widgets/BooleanField';
import { SelectField } from './widgets/SelectField';
import { MultiSelectField } from './widgets/MultiSelectField';
import { RadioField } from './widgets/RadioField';
import { CheckboxesField } from './widgets/CheckboxesField';
import { TagsField } from './widgets/TagsField';
import { DateField } from './widgets/DateField';
import { DateTimeField } from './widgets/DateTimeField';
import { TimeField } from './widgets/TimeField';
import { EmailField } from './widgets/EmailField';
import { PhoneField } from './widgets/PhoneField';
import { UrlField } from './widgets/UrlField';
// Relational pickers — the SAME standard widgets the form uses. They read the
// related-object dataSource from SchemaRendererContext (which the grid already
// provides), so they drop straight into an inline cell.
import { LookupField } from './widgets/LookupField';
import { UserField } from './widgets/UserField';
// Structured-value editors — lightweight (no map/code-editor deps), same
// widgets the form uses. Drop into a cell like the rest.
import { ColorField } from './widgets/ColorField';
import { AddressField } from './widgets/AddressField';
import { LocationField } from './widgets/LocationField';
import { GeolocationField } from './widgets/GeolocationField';
import { CodeField } from './widgets/CodeField';
import { QRCodeField } from './widgets/QRCodeField';
// The FORM's spec-alias table (`json` → `field:code`, `tree` → `field:lookup`,
// …) — inline resolution reuses it so spec spellings get the form's decision.
import { mapFieldTypeToFormType } from './field-type-alias';
/**
* Field types that edit in place with a dedicated widget. Keyed by the raw
* field `type` (the widget map mirrors the form's `mapFieldTypeToFormType`).
* Rich/heavy types (file, image, lookup, richtext, …) are intentionally absent
* so callers fall back to their own simpler editor.
*/
const EDIT_WIDGETS: Record<string, React.ComponentType<FieldWidgetProps<any>>> = {
text: TextField,
textarea: TextAreaField,
number: NumberField,
currency: CurrencyField,
percent: PercentField,
slider: SliderField,
progress: SliderField,
rating: RatingField,
boolean: BooleanField,
toggle: BooleanField,
select: SelectField,
status: SelectField,
multiselect: MultiSelectField,
radio: RadioField,
checkboxes: CheckboxesField,
tags: TagsField,
date: DateField,
datetime: DateTimeField,
time: TimeField,
email: EmailField,
phone: PhoneField,
url: UrlField,
// Relational — the record/user pickers, same as the form (the form maps
// master_detail to the single-value LookupField too, see fieldWidgetMap).
lookup: LookupField,
master_detail: LookupField,
user: UserField,
owner: UserField,
// Structured-value editors — same widgets the form uses.
color: ColorField,
address: AddressField,
location: LocationField,
geolocation: GeolocationField,
code: CodeField,
qrcode: QRCodeField,
};
/**
* Form field types that are deliberately NOT given an inline editor — every
* other form widget type must be in {@link EDIT_WIDGETS}. This pairing is
* enforced by a test against the form's widget map (`FORM_FIELD_TYPES`) so the
* two can't silently drift again (which is how `lookup` was missed). To add a
* type to inline editing, move it from here into EDIT_WIDGETS.
*/
export const INLINE_EXCLUDED_FIELD_TYPES = new Set<string>([
// Computed / read-only — the grid keeps these non-editable (no value to author).
'formula', 'summary', 'auto_number',
// Binary / attachment — edited from the record form, shown read-only in the grid.
'file', 'image', 'avatar', 'signature',
// Heavy / full editors — better in the record form than a cell.
'markdown', 'html', 'richtext',
// Credentials — never inline-editable. Both are masked on read, so the cell has
// no real value to seed an editor with, and the grid's fallback is a PLAIN TEXT
// input: it would render the mask as if it were the value and write it straight
// back. `secret` additionally round-trips through an encrypted store (ADR-0100,
// data/field.zod.ts) — the cell holds an opaque ref, not the secret.
'password', 'secret',
// Containers / non-authorable — a sub-form / sub-grid / embedding vector
// doesn't belong in a single cell.
'object', 'grid', 'vector',
// Widget-hint-only pickers — authored in the record form (they depend on
// sibling fields / a loaded object catalog), not inline in a grid cell.
'object-ref', 'filter-condition', 'recipient-picker',
]);
/** Field types whose value is chosen in one discrete gesture (no free typing). */
export const DISCRETE_EDIT_TYPES = new Set<string>([
'boolean', 'toggle', 'select', 'status', 'radio', 'rating',
]);
/**
* Resolve a field type to its inline-editing key: the raw type when the
* widget/exclusion tables name it directly, otherwise its form-alias target
* (`mapFieldTypeToFormType`, `field:` prefix stripped).
*
* The tables are keyed by the FORM's spellings, but spec field types reach
* this layer in the SPEC's spellings — `json`, `tree`, `composite`, `record`,
* `repeater`, `video`, `audio`, `autonumber` are aliases in the form map and
* were keys in NEITHER table here, so the grid silently fell back to a plain
* text input for all of them: typing over structured JSON or a hierarchy ref
* is a value-corruption path (#2942). Resolving through the same alias table
* the form uses gives each spec type the form's own decision: `json` → the
* code editor, `tree` → the lookup picker, and the container/computed/binary
* families → their documented exclusions.
*/
function resolveInlineEditType(type: string): string {
if (type in EDIT_WIDGETS || INLINE_EXCLUDED_FIELD_TYPES.has(type)) return type;
return mapFieldTypeToFormType(type).replace(/^field:/, '');
}
/** True when a field type has a dedicated in-place edit widget. */
export function hasFieldEditWidget(type: string | undefined): boolean {
return !!type && resolveInlineEditType(type) in EDIT_WIDGETS;
}
/**
* True when a field type is deliberately NOT inline-editable (alias-aware:
* `composite` resolves to the excluded `object`, `video` to `file`, …).
* Grid hosts must treat these as read-only cells — falling back to a plain
* text editor is exactly the corruption path the exclusion exists to close.
*/
export function isInlineExcludedFieldType(type: string | undefined): boolean {
return !!type && INLINE_EXCLUDED_FIELD_TYPES.has(resolveInlineEditType(type));
}
/**
* Relational picker widgets (lookup / master_detail / user / owner) render best
* in a grid cell as a single-line, borderless trigger — so the selected
* record's NAME shows inside the trigger instead of a chip stacked above a
* separate "Select…" button (which double-stacks and wastes the row height).
* This mirrors how the line-item grid (`GridField`) renders its lookup cells.
*/
const COMPACT_EDIT_TYPES = new Set<string>(['lookup', 'master_detail', 'user', 'owner']);
/**
* Render the dedicated edit widget for a field's type — the SAME control the
* form uses — as a controlled `{ value, onChange, field }` input. Returns
* `null` for types without a registered widget so the caller can fall back to
* a plain editor.
*/
export function FieldEditWidget({
field,
value,
onChange,
readonly,
}: FieldWidgetProps<any>): React.ReactElement | null {
const resolved = field?.type ? resolveInlineEditType(field.type) : undefined;
const Widget = resolved ? EDIT_WIDGETS[resolved] : undefined;
if (!Widget) return null;
const compactProps = resolved && COMPACT_EDIT_TYPES.has(resolved) ? { compact: true } : {};
return <Widget field={field} value={value} onChange={onChange} readonly={readonly} {...(compactProps as any)} />;
}