diff --git a/.changeset/user-field-picker.md b/.changeset/user-field-picker.md new file mode 100644 index 000000000..55dc8b99a --- /dev/null +++ b/.changeset/user-field-picker.md @@ -0,0 +1,22 @@ +--- +'@object-ui/fields': minor +--- + +feat(fields): wire the `user` field picker to a real `sys_user` search + +The `user`/`owner` field widgets previously rendered a placeholder ("User +selection component requires integration with user management system") and the +form-type map fell through to `field:text`, so a `user` field rendered as a +plain text input. + +`UserField` now **delegates to the shared `LookupField`** with the reference +fixed to `sys_user` — reusing the existing debounced candidate search, the +record-picker dialog, and id resolution — so selecting a person works the same +way as any lookup, with zero bespoke data plumbing. `mapFieldTypeToFormType` +now maps `user`/`owner` to `field:user`/`field:owner`, satisfying the existing +`field-type-coverage` regression guard (which already listed both but had no +mapping wired — the widget map and cell renderers were registered, the form-type +map was the missing link). Table-cell display continues to use `UserCellRenderer` +(avatars/initials). + +Pairs with the framework `user` field type (a lookup specialized to `sys_user`). diff --git a/packages/fields/src/index.tsx b/packages/fields/src/index.tsx index 7a87ed5de..cb76e2d17 100644 --- a/packages/fields/src/index.tsx +++ b/packages/fields/src/index.tsx @@ -1656,6 +1656,11 @@ export function mapFieldTypeToFormType(fieldType: string): string { lookup: 'field:lookup', master_detail: 'field:master_detail', tree: 'field:lookup', // hierarchical reference — pick the parent via a lookup + // `user` is a lookup specialized to sys_user; `owner` mirrors it (record + // ownership). Both render via the UserField person-picker (delegates to the + // lookup picker). Without these they would fall through to `field:text`. + user: 'field:user', + owner: 'field:owner', // Contact fields email: 'field:email', diff --git a/packages/fields/src/widgets/UserField.tsx b/packages/fields/src/widgets/UserField.tsx index 2b42889a0..96f48ac93 100644 --- a/packages/fields/src/widgets/UserField.tsx +++ b/packages/fields/src/widgets/UserField.tsx @@ -1,88 +1,39 @@ import React from 'react'; -import { Avatar, AvatarFallback, Badge, EmptyValue } from '@object-ui/components'; -import { X } from 'lucide-react'; import { FieldWidgetProps } from './types'; +import { LookupField } from './LookupField'; /** - * UserField - User/Owner selector field - * Allows selecting one or multiple users + * UserField — person picker for the `user` field type. + * + * `user` is a lookup specialized to the `sys_user` system object (see framework + * "lookup → sys_user" specialization). Rather than re-implement candidate search, + * the searchable picker, the record-picker dialog and id resolution, this widget + * **delegates to the shared {@link LookupField}** with the reference fixed to + * `sys_user`. The author writes `Field.user({ multiple })`; we normalise the field + * metadata so the lookup machinery targets users with a sensible display field. + * + * Table-cell display (avatars / initials) is handled separately by + * `UserCellRenderer`; this is the form/editor widget. */ -export function UserField({ value, onChange, field, readonly, ...props }: FieldWidgetProps) { - const userField = (field || (props as any).schema) as any; - const multiple = userField?.multiple || false; +export function UserField(props: FieldWidgetProps) { + const raw = (props.field || (props as any).schema) as any; - if (readonly) { - if (!value) return ; - - const users = Array.isArray(value) ? value : [value]; - return ( -
- {users.slice(0, 3).map((user: any, idx: number) => { - const name = user.name || user.username || 'User'; - const initials = name.split(' ').map((n: string) => n[0]).join('').toUpperCase().slice(0, 2); - - return ( - - - {initials} - - - ); - })} - {users.length > 3 && ( - - - +{users.length - 3} - - - )} -
- ); - } + // The objectSchema field metadata may live directly on `field`, or nested at + // `field.field` when rendered via the createFieldRenderer wrapper — mirror the + // unwrap LookupField itself performs. + const metaIsNested = raw?.field && typeof raw.field === 'object' + && ('reference' in raw.field || 'reference_to' in raw.field || 'type' in raw.field); + const meta = metaIsNested ? raw.field : raw; - const users = value ? (Array.isArray(value) ? value : [value]) : []; - - const handleRemove = (index: number) => { - if (multiple) { - const newUsers = users.filter((_: any, i: number) => i !== index); - onChange(newUsers.length > 0 ? newUsers : null); - } else { - onChange(null); - } + // Ensure the picker always targets sys_user (even if the author omitted an + // explicit reference) and presents user names by default. + const normalized = { + ...(meta || {}), + reference: meta?.reference || meta?.reference_to || 'sys_user', + display_field: meta?.display_field || meta?.displayField || meta?.reference_field || 'name', }; - return ( -
- {users.length > 0 && ( -
- {users.map((user: any, idx: number) => { - const name = user.name || user.username || 'User'; - const initials = name.split(' ').map((n: string) => n[0]).join('').toUpperCase().slice(0, 2); - - return ( - - - - {initials} - - - {name} - - - ); - })} -
- )} - -
- User selection component requires integration with user management system -
-
- ); + const fieldProp = metaIsNested ? { ...raw, field: normalized } : normalized; + + return ; }