@object-ui/fields 17.1.0 → 17.2.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.
Files changed (105) hide show
  1. package/CHANGELOG.md +194 -0
  2. package/dist/FieldEditWidget.d.ts +2 -2
  3. package/dist/__tests__/spec-symbol-batch7.test.d.ts +8 -0
  4. package/dist/__tests__/widget-props-contract.test.d.ts +8 -0
  5. package/dist/index.cjs +3 -3
  6. package/dist/index.js +290 -292
  7. package/dist/widgets/AddressField.d.ts +2 -2
  8. package/dist/widgets/AutoNumberField.d.ts +2 -2
  9. package/dist/widgets/AvatarField.d.ts +2 -2
  10. package/dist/widgets/BooleanField.d.ts +2 -2
  11. package/dist/widgets/CapabilityMultiSelectField.d.ts +2 -2
  12. package/dist/widgets/CheckboxesField.d.ts +2 -2
  13. package/dist/widgets/CodeField.d.ts +2 -2
  14. package/dist/widgets/ColorField.d.ts +2 -2
  15. package/dist/widgets/CurrencyField.d.ts +2 -2
  16. package/dist/widgets/DateField.d.ts +2 -2
  17. package/dist/widgets/DateTimeField.d.ts +2 -2
  18. package/dist/widgets/EmailField.d.ts +2 -2
  19. package/dist/widgets/FileField.d.ts +2 -2
  20. package/dist/widgets/FilterConditionField.d.ts +2 -2
  21. package/dist/widgets/FormulaField.d.ts +2 -2
  22. package/dist/widgets/GeolocationField.d.ts +2 -2
  23. package/dist/widgets/GridField.d.ts +2 -2
  24. package/dist/widgets/ImageField.d.ts +2 -2
  25. package/dist/widgets/LocationField.d.ts +2 -2
  26. package/dist/widgets/LookupField.d.ts +2 -2
  27. package/dist/widgets/MasterDetailField.d.ts +2 -2
  28. package/dist/widgets/MultiSelectField.d.ts +2 -2
  29. package/dist/widgets/NumberField.d.ts +2 -2
  30. package/dist/widgets/ObjectField.d.ts +2 -2
  31. package/dist/widgets/ObjectRefField.d.ts +2 -2
  32. package/dist/widgets/PasswordField.d.ts +2 -2
  33. package/dist/widgets/PercentField.d.ts +2 -2
  34. package/dist/widgets/PhoneField.d.ts +2 -2
  35. package/dist/widgets/QRCodeField.d.ts +2 -2
  36. package/dist/widgets/RadioField.d.ts +2 -2
  37. package/dist/widgets/RatingField.d.ts +2 -2
  38. package/dist/widgets/RecipientPickerField.d.ts +2 -2
  39. package/dist/widgets/RichTextField.d.ts +2 -2
  40. package/dist/widgets/SelectField.d.ts +2 -2
  41. package/dist/widgets/SignatureField.d.ts +2 -2
  42. package/dist/widgets/SliderField.d.ts +2 -2
  43. package/dist/widgets/SummaryField.d.ts +2 -2
  44. package/dist/widgets/TagsField.d.ts +2 -2
  45. package/dist/widgets/TextAreaField.d.ts +2 -2
  46. package/dist/widgets/TextField.d.ts +2 -2
  47. package/dist/widgets/TimeField.d.ts +2 -2
  48. package/dist/widgets/UrlField.d.ts +2 -2
  49. package/dist/widgets/UserField.d.ts +2 -2
  50. package/dist/widgets/VectorField.d.ts +2 -2
  51. package/dist/widgets/file-value.d.ts +20 -33
  52. package/dist/widgets/types.d.ts +119 -3
  53. package/package.json +8 -8
  54. package/src/FieldEditWidget.tsx +6 -4
  55. package/src/__tests__/spec-symbol-batch7.test.ts +112 -0
  56. package/src/__tests__/widget-props-contract.test.tsx +129 -0
  57. package/src/complex-widgets.test.tsx +3 -3
  58. package/src/datetime-widgets.test.tsx +2 -2
  59. package/src/index.tsx +3 -1
  60. package/src/widgets/AddressField.tsx +2 -2
  61. package/src/widgets/AutoNumberField.tsx +2 -2
  62. package/src/widgets/AvatarField.tsx +2 -2
  63. package/src/widgets/BooleanField.tsx +2 -2
  64. package/src/widgets/CapabilityMultiSelectField.tsx +4 -4
  65. package/src/widgets/CheckboxesField.tsx +4 -4
  66. package/src/widgets/CodeField.tsx +2 -2
  67. package/src/widgets/ColorField.tsx +2 -2
  68. package/src/widgets/CurrencyField.tsx +2 -2
  69. package/src/widgets/DateField.tsx +2 -2
  70. package/src/widgets/DateTimeField.tsx +2 -2
  71. package/src/widgets/EmailField.tsx +2 -2
  72. package/src/widgets/FileField.tsx +2 -2
  73. package/src/widgets/FilterConditionField.tsx +3 -3
  74. package/src/widgets/FormulaField.tsx +2 -2
  75. package/src/widgets/GeolocationField.tsx +2 -2
  76. package/src/widgets/GridField.tsx +3 -3
  77. package/src/widgets/ImageField.tsx +2 -2
  78. package/src/widgets/LocationField.tsx +2 -2
  79. package/src/widgets/LookupField.tsx +10 -10
  80. package/src/widgets/MasterDetailField.tsx +2 -2
  81. package/src/widgets/MultiSelectField.tsx +4 -4
  82. package/src/widgets/NumberField.tsx +2 -2
  83. package/src/widgets/ObjectField.tsx +2 -2
  84. package/src/widgets/ObjectRefField.tsx +4 -4
  85. package/src/widgets/PasswordField.tsx +2 -2
  86. package/src/widgets/PercentField.tsx +2 -2
  87. package/src/widgets/PhoneField.tsx +2 -2
  88. package/src/widgets/QRCodeField.tsx +2 -2
  89. package/src/widgets/RadioField.tsx +4 -4
  90. package/src/widgets/RatingField.tsx +2 -2
  91. package/src/widgets/RecipientPickerField.tsx +4 -4
  92. package/src/widgets/RichTextField.tsx +2 -2
  93. package/src/widgets/SelectField.tsx +5 -5
  94. package/src/widgets/SignatureField.tsx +2 -2
  95. package/src/widgets/SliderField.tsx +2 -2
  96. package/src/widgets/SummaryField.tsx +2 -2
  97. package/src/widgets/TagsField.tsx +3 -3
  98. package/src/widgets/TextAreaField.tsx +2 -2
  99. package/src/widgets/TextField.tsx +2 -2
  100. package/src/widgets/TimeField.tsx +2 -2
  101. package/src/widgets/UrlField.tsx +2 -2
  102. package/src/widgets/UserField.tsx +2 -2
  103. package/src/widgets/VectorField.tsx +2 -2
  104. package/src/widgets/file-value.ts +21 -5
  105. package/src/widgets/types.ts +129 -6
@@ -1,12 +1,12 @@
1
1
  import React from 'react';
2
2
  import { Slider } from '@object-ui/components';
3
- import { FieldWidgetProps } from './types';
3
+ import { FieldWidgetComponentProps } from './types';
4
4
 
5
5
  /**
6
6
  * Slider field widget - provides a range slider input
7
7
  * Supports numeric values with configurable min, max, and step
8
8
  */
9
- export function SliderField({ value, onChange, field, readonly, ...props }: FieldWidgetProps<number>) {
9
+ export function SliderField({ value, onChange, field, readonly, ...props }: FieldWidgetComponentProps<number>) {
10
10
  // Get slider-specific configuration from field metadata
11
11
  const sliderField = (field || (props as any).schema) as any;
12
12
  const min = sliderField?.min ?? 0;
@@ -1,12 +1,12 @@
1
1
  import React from 'react';
2
2
  import { EmptyValue } from '@object-ui/components';
3
- import { FieldWidgetProps } from './types';
3
+ import { FieldWidgetComponentProps } from './types';
4
4
 
5
5
  /**
6
6
  * SummaryField - Read-only aggregation field
7
7
  * Values are aggregated from related records and cannot be edited
8
8
  */
9
- export function SummaryField({ value, field, ...props }: FieldWidgetProps<any>) {
9
+ export function SummaryField({ value, field, ...props }: FieldWidgetComponentProps<any>) {
10
10
  const summaryField = (field || (props as any).schema) as any;
11
11
  const summaryType = summaryField?.summary_type || 'count';
12
12
 
@@ -1,13 +1,13 @@
1
1
  import React from 'react';
2
2
  import { Badge, Input, EmptyValue, cn } from '@object-ui/components';
3
- import { FieldWidgetProps } from './types';
3
+ import { FieldWidgetComponentProps } from './types';
4
4
 
5
5
  /**
6
6
  * TagsField - free-form list of string tags. Type a value and press Enter (or
7
7
  * comma) to add it; click a tag's × to remove it. The stored value is a
8
8
  * string[]. Used for the `tags` field type.
9
9
  */
10
- export function TagsField({ value, onChange, field, readonly, className, ...props }: FieldWidgetProps<string[]>) {
10
+ export function TagsField({ value, onChange, field, readonly, className, ...props }: FieldWidgetComponentProps<string[]>) {
11
11
  const tags: string[] = Array.isArray(value) ? value : value == null ? [] : [value as unknown as string];
12
12
  const [draft, setDraft] = React.useState('');
13
13
 
@@ -58,7 +58,7 @@ export function TagsField({ value, onChange, field, readonly, className, ...prop
58
58
  onChange={(e) => setDraft(e.target.value)}
59
59
  onKeyDown={onKeyDown}
60
60
  onBlur={() => addTag(draft)}
61
- disabled={(props as any).disabled}
61
+ disabled={props.disabled}
62
62
  placeholder={tags.length === 0 ? '输入后回车添加…' : ''}
63
63
  className="h-7 flex-1 border-0 bg-transparent p-0 px-1 shadow-none focus-visible:ring-0 min-w-[8ch]"
64
64
  />
@@ -10,7 +10,7 @@ import {
10
10
  Button,
11
11
  } from '@object-ui/components';
12
12
  import { Maximize2, Check, X } from 'lucide-react';
13
- import { FieldWidgetProps } from './types';
13
+ import { FieldWidgetComponentProps } from './types';
14
14
 
15
15
  /**
16
16
  * TextAreaField - Multi-line text input widget
@@ -21,7 +21,7 @@ import { FieldWidgetProps } from './types';
21
21
  * a fullscreen edit dialog — much easier on phones than tapping a 4-row
22
22
  * textarea trapped between other fields.
23
23
  */
24
- export function TextAreaField({ value, onChange, field, readonly, errorMessage, ...props }: FieldWidgetProps<string>) {
24
+ export function TextAreaField({ value, onChange, field, readonly, errorMessage, ...props }: FieldWidgetComponentProps<string>) {
25
25
  // Hooks must run before any early return (readonly) to keep hook order stable.
26
26
  const [fullscreenOpen, setFullscreenOpen] = useState(false);
27
27
  const [draft, setDraft] = useState(value ?? '');
@@ -1,13 +1,13 @@
1
1
  import React from 'react';
2
2
  import { Input, Textarea, EmptyValue } from '@object-ui/components';
3
3
  import { TextareaFieldMetadata } from '@object-ui/types';
4
- import { FieldWidgetProps } from './types';
4
+ import { FieldWidgetComponentProps } from './types';
5
5
 
6
6
  /**
7
7
  * TextField - Standard single-line or multi-line text input
8
8
  * Automatically renders as a textarea when rows are configured in field metadata
9
9
  */
10
- export function TextField({ value, onChange, field, readonly, ...props }: FieldWidgetProps<string>) {
10
+ export function TextField({ value, onChange, field, readonly, ...props }: FieldWidgetComponentProps<string>) {
11
11
  const fieldData = field || (props as any).schema;
12
12
 
13
13
  if (readonly) {
@@ -1,13 +1,13 @@
1
1
  import React from 'react';
2
2
  import { Input, EmptyValue } from '@object-ui/components';
3
- import { FieldWidgetProps } from './types';
3
+ import { FieldWidgetComponentProps } from './types';
4
4
  import { openNativePicker } from './openNativePicker';
5
5
 
6
6
  /**
7
7
  * TimeField - Time picker input widget
8
8
  * Uses native time input for hour and minute selection
9
9
  */
10
- export function TimeField({ value, onChange, field, readonly, ...props }: FieldWidgetProps<string>) {
10
+ export function TimeField({ value, onChange, field, readonly, ...props }: FieldWidgetComponentProps<string>) {
11
11
  if (readonly) {
12
12
  return <span className="text-sm">{value || <EmptyValue />}</span>;
13
13
  }
@@ -1,12 +1,12 @@
1
1
  import React from 'react';
2
2
  import { Input, EmptyValue } from '@object-ui/components';
3
- import { FieldWidgetProps } from './types';
3
+ import { FieldWidgetComponentProps } from './types';
4
4
 
5
5
  /**
6
6
  * UrlField - URL input with clickable link in readonly mode
7
7
  * Validates URLs to only render http/https links for security
8
8
  */
9
- export function UrlField({ value, onChange, field, readonly, errorMessage, ...props }: FieldWidgetProps<string>) {
9
+ export function UrlField({ value, onChange, field, readonly, errorMessage, ...props }: FieldWidgetComponentProps<string>) {
10
10
  const config = field || (props as any).schema;
11
11
  if (readonly) {
12
12
  if (!value) return <EmptyValue />;
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { FieldWidgetProps } from './types';
2
+ import { FieldWidgetComponentProps } from './types';
3
3
  import { LookupField } from './LookupField';
4
4
 
5
5
  /**
@@ -29,7 +29,7 @@ function withBannedFilter(filters?: any[]): any[] {
29
29
  : [...base, { field: 'banned', operator: 'ne', value: true }];
30
30
  }
31
31
 
32
- export function UserField(props: FieldWidgetProps<any>) {
32
+ export function UserField(props: FieldWidgetComponentProps<any>) {
33
33
  const raw = (props.field || (props as any).schema) as any;
34
34
 
35
35
  // The objectSchema field metadata may live directly on `field`, or nested at
@@ -1,12 +1,12 @@
1
1
  import React from 'react';
2
2
  import { EmptyValue } from '@object-ui/components';
3
- import { FieldWidgetProps } from './types';
3
+ import { FieldWidgetComponentProps } from './types';
4
4
 
5
5
  /**
6
6
  * VectorField - Vector/embedding data display
7
7
  * Shows vector embeddings in a read-only format
8
8
  */
9
- export function VectorField({ value, field, ...props }: FieldWidgetProps<number[]>) {
9
+ export function VectorField({ value, field, ...props }: FieldWidgetComponentProps<number[]>) {
10
10
  const vectorField = (field || (props as any).schema) as any;
11
11
  const dimensions = vectorField?.dimensions || (Array.isArray(value) ? value.length : 0);
12
12
 
@@ -29,6 +29,8 @@
29
29
  * onto the same contract.
30
30
  */
31
31
 
32
+ import { isFileIdToken } from '@objectstack/spec/data';
33
+
32
34
  /** A file value normalised for rendering, whatever form it arrived in. */
33
35
  export interface FileValueView {
34
36
  /** `sys_file` id, when the value carries one. */
@@ -45,12 +47,26 @@ export interface FileValueView {
45
47
 
46
48
  /**
47
49
  * A minted file id: uuid/nanoid-shaped, and crucially not a URL — a URL always
48
- * carries `:`, `/` or `.`, so it can never match. Mirrors the platform's
49
- * `isFileIdToken`, which is the arbiter on the server side of the same question.
50
+ * carries `:`, `/` or `.`, so it can never match.
51
+ *
52
+ * This IS the platform's arbiter now, not a mirror of it (objectui#3161,
53
+ * objectstack#4115 ledger batch 7). The declaration that stood here said it
54
+ * "mirrors the platform's `isFileIdToken`" while being a character-for-character
55
+ * copy of that function's body under that function's own name — the shape
56
+ * objectui#3003 argued about and objectui#3169 caught again in
57
+ * `isAggregatedViewContainer`: every value test and every behaviour test passes
58
+ * against a faithful copy, so reference identity is the only check that can tell
59
+ * a re-export from a fork. It is asserted in
60
+ * `__tests__/spec-symbol-batch7.test.ts`.
61
+ *
62
+ * Why it matters that this one is shared rather than duplicated: the regex is a
63
+ * WIRE decision. Widening it server-side (say, ids grow past 64 chars) while a
64
+ * copy here keeps the old bound turns every new id into "not a reference", and
65
+ * the widget then submits the legacy inline blob against a backend that expects
66
+ * a reference. That failure surfaces as a broken thumbnail, nowhere near a
67
+ * regex.
50
68
  */
51
- export function isFileIdToken(value: unknown): value is string {
52
- return typeof value === 'string' && /^[A-Za-z0-9_-]{1,64}$/.test(value);
53
- }
69
+ export { isFileIdToken };
54
70
 
55
71
  /**
56
72
  * The `sys_file` id a value refers to, or `undefined`.
@@ -1,10 +1,57 @@
1
- import { FieldMetadata } from '@object-ui/types';
1
+ import type { AriaAttributes, FocusEventHandler, MouseEventHandler } from 'react';
2
+ import type { DependsOnInput, FieldMetadata } from '@object-ui/types';
3
+
4
+ /**
5
+ * Props every field widget in this package receives at RUNTIME.
6
+ *
7
+ * Named `FieldWidgetComponentProps`, not `FieldWidgetProps` (objectui#3161,
8
+ * objectstack#4115 ledger batch 7), adopting the name `@object-ui/app-shell`
9
+ * settled on for the same split in objectui#3169: `@objectstack/spec/ui` owns
10
+ * `FieldWidgetProps` for the DECLARED widget-plugin contract — a zod object
11
+ * (`FieldWidgetPropsSchema`) a plugin manifest is validated against, with
12
+ * `field` narrowed to `{ name?, label?, type: <the FieldType enum> }` and
13
+ * `readonly` / `required` carrying `.default()`s. This is the React interface
14
+ * the widgets in this directory actually implement, against
15
+ * `@object-ui/types`'s much richer `FieldMetadata`.
16
+ *
17
+ * The two are NOT interchangeable, and the divergence is not cosmetic: the
18
+ * spec's contract names the error slot `error`, this one names it
19
+ * `errorMessage`, so a widget written to the spec's declared props renders no
20
+ * validation message here. That divergence is tracked in objectui#3222 and is
21
+ * deliberately NOT resolved here — but it is now *visible*: reading
22
+ * `props.error` or `props.required` off this type is a compile error rather
23
+ * than a silent `any`.
24
+ *
25
+ * ## Why there is no `[key: string]: any` (objectui#3221)
26
+ *
27
+ * This type used to end in an index signature, "load-bearing" for the widgets'
28
+ * `...props` spreads. It was also why every drift above was invisible: **a type
29
+ * that claims to have every key can never be reported as missing one** (the
30
+ * objectstack#4075 mechanism). `props.required` and `props.error` were legal
31
+ * reads typed `any` and always `undefined` at runtime; a misspelled prop
32
+ * (`readOnly` for `readonly`, `onchange` for `onChange`) compiled; and any
33
+ * structural/parity comparison against the type was useless *in principle*,
34
+ * which is why the batch-7 symbol guard was the only detector that could see
35
+ * the collision at all.
36
+ *
37
+ * It is replaced by the CLOSED set below: the controlled-input contract, the
38
+ * host plumbing the form renderer genuinely forwards, and a DOM pass-through
39
+ * allowance. `data-*` is an open family by design (it is open in HTML too) and
40
+ * is expressed as a template-literal index signature, which — unlike
41
+ * `[key: string]` — leaves `keyof` finite, so an undeclared prop still fails.
42
+ *
43
+ * Adding a key here is a contract change: say who produces it and who reads it.
44
+ */
45
+ export type FieldWidgetComponentProps<T = any> = {
46
+ /* ── The controlled-input contract every widget implements ─────────────── */
2
47
 
3
- export type FieldWidgetProps<T = any> = {
4
48
  value: T;
5
49
  onChange: (val: T) => void;
6
- // Use a looser type for field to avoid complex circular dependencies for now
7
- field: FieldMetadata;
50
+ /**
51
+ * The field's metadata. Deliberately the looser `@object-ui/types` shape
52
+ * rather than the spec's, to avoid a circular dependency for now.
53
+ */
54
+ field: FieldMetadata;
8
55
  readonly?: boolean;
9
56
  disabled?: boolean;
10
57
  className?: string;
@@ -15,5 +62,81 @@ export type FieldWidgetProps<T = any> = {
15
62
  * widgets ignore it.
16
63
  */
17
64
  onUploadingChange?: (uploading: boolean) => void;
18
- [key: string]: any;
19
- }
65
+
66
+ /* ── Host plumbing: what a rendering host actually forwards ─────────────── */
67
+
68
+ /**
69
+ * The raw authored node the widget was rendered from. Two hosts supply it:
70
+ * `SchemaRenderer` (`<Component schema={schema} {...schema} />`) and the
71
+ * form renderer's `renderFieldComponent` (`schema={props.field || ...}`),
72
+ * which is why ~25 widgets read `field || schema` for their config.
73
+ *
74
+ * This is a second carrier for what `field` already means, i.e. a de-facto
75
+ * second contract (AGENTS.md #0.1) — declared here so it is at least
76
+ * visible; converging the two is tracked separately.
77
+ */
78
+ schema?: FieldMetadata | Record<string, unknown>;
79
+ /**
80
+ * DataSource for widgets that query records (lookup / user / object-ref /
81
+ * recipient-picker / grid). Injected by the form renderer for the field
82
+ * types that need it, and passed directly by inline-edit hosts. Option
83
+ * widgets destructure it purely to keep it off their DOM spread.
84
+ *
85
+ * Left structural (`unknown`): `@object-ui/fields` must not depend on a
86
+ * concrete adapter — every consumer narrows it itself.
87
+ */
88
+ dataSource?: unknown;
89
+ /**
90
+ * Live sibling-field values driving cascading / role-gated options and
91
+ * dependent lookups (ADR-0058, #2215/#2284). The form renderer passes the
92
+ * in-progress record; widgets fall back to `SchemaRendererContext`.
93
+ */
94
+ dependentValues?: Record<string, unknown>;
95
+ /**
96
+ * Controlling field(s) that gate this field's option list. Normally declared
97
+ * on the field metadata; accepted as a prop so a host can drive the gate for
98
+ * a field it synthesised. Field metadata wins when both are present.
99
+ */
100
+ dependsOn?: DependsOnInput;
101
+ /**
102
+ * Hint shown when a dependency-gated option list is still waiting on its
103
+ * controlling field. Forwarded by the form renderer; the option widgets in
104
+ * this package currently discard it and render their own message instead.
105
+ */
106
+ emptyHint?: string;
107
+ /**
108
+ * Render as a single-line, borderless control for a grid cell (the inline
109
+ * editor and the line-item grid set it) instead of the full form layout.
110
+ */
111
+ compact?: boolean;
112
+ /**
113
+ * Receive the FULL selected record rather than just its id, so a host can
114
+ * auto-fill sibling fields from it (a line-item grid copying a product's
115
+ * price). When provided, the host owns the resulting value change.
116
+ */
117
+ onSelectRecord?: (record: Record<string, any>) => void;
118
+ /**
119
+ * Offer a "create new" affordance in a record picker, carrying whatever the
120
+ * user had typed. Also declarable on the field metadata.
121
+ */
122
+ onCreateNew?: (searchQuery: string) => void;
123
+
124
+ /* ── DOM pass-through: what `...props` may legitimately reach an input ──── */
125
+
126
+ id?: string;
127
+ /** react-hook-form's field name, spread in by the form renderer. */
128
+ name?: string;
129
+ autoFocus?: boolean;
130
+ tabIndex?: number;
131
+ onBlur?: FocusEventHandler<HTMLElement>;
132
+ onFocus?: FocusEventHandler<HTMLElement>;
133
+ onClick?: MouseEventHandler<HTMLElement>;
134
+ } & AriaAttributes & {
135
+ /**
136
+ * Arbitrary `data-*` attributes (test ids, analytics hooks). Open by design,
137
+ * but a template-literal key — `keyof` stays finite, so this does NOT
138
+ * reintroduce the index signature objectui#3221 removed: `props.required`
139
+ * is still an error, `props['data-testid']` is still fine.
140
+ */
141
+ [dataAttribute: `data-${string}`]: string | number | boolean | undefined;
142
+ };