@asteby/metacore-runtime-react 18.27.0 → 18.28.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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # @asteby/metacore-runtime-react
2
2
 
3
+ ## 18.28.0
4
+
5
+ ### Minor Changes
6
+
7
+ - d3b7060: feat(line-items): a PrefillSpec can `lock` item-field columns — locked dynamic_select cells render as a resolved, read-only NAME (eager option fetch, never the raw id) instead of an editable picker. Used for receive-goods/partial-reception lines whose product is dictated by the source document; the create flow (no prefill) stays fully editable.
8
+
3
9
  ## 18.27.0
4
10
 
5
11
  ### Minor Changes
@@ -1 +1 @@
1
- {"version":3,"file":"action-modal-dispatcher.d.ts","sourceRoot":"","sources":["../src/action-modal-dispatcher.tsx"],"names":[],"mappings":"AA+CA,OAAO,EACH,KAAK,cAAc,EACnB,KAAK,gBAAgB,EAExB,MAAM,sBAAsB,CAAA;AAE7B,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,CAAA;AAkEhD,wBAAgB,qBAAqB,CAAC,EAClC,IAAI,EACJ,YAAY,EACZ,MAAM,EACN,KAAK,EACL,MAAM,EACN,QAAQ,EACR,SAAS,GACZ,EAAE,gBAAgB,sCAiDlB"}
1
+ {"version":3,"file":"action-modal-dispatcher.d.ts","sourceRoot":"","sources":["../src/action-modal-dispatcher.tsx"],"names":[],"mappings":"AA+CA,OAAO,EACH,KAAK,cAAc,EACnB,KAAK,gBAAgB,EAExB,MAAM,sBAAsB,CAAA;AAE7B,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,CAAA;AA0FhD,wBAAgB,qBAAqB,CAAC,EAClC,IAAI,EACJ,YAAY,EACZ,MAAM,EACN,KAAK,EACL,MAAM,EACN,QAAQ,EACR,SAAS,GACZ,EAAE,gBAAgB,sCAiDlB"}
@@ -38,6 +38,24 @@ function toNum(v) {
38
38
  const n = typeof v === 'number' ? v : parseFloat(String(v ?? ''));
39
39
  return Number.isFinite(n) ? n : 0;
40
40
  }
41
+ // applyPrefillLock marks the item-field columns named in a line-items field's
42
+ // PrefillSpec.lock as read-only, so the prefilled cells (e.g. the product of a
43
+ // receive line) render as a resolved, non-editable name. Returns the field
44
+ // untouched when there is no prefill spec or no lock list (the create flow,
45
+ // which carries no prefill, stays fully editable). The readonly flag is set on
46
+ // BOTH itemFields aliases the renderers tolerate.
47
+ function applyPrefillLock(field) {
48
+ const spec = lineItemsDefault(field);
49
+ if (!isPrefillSpec(spec) || !spec.lock || spec.lock.length === 0)
50
+ return field;
51
+ const lock = new Set(spec.lock);
52
+ const f = field;
53
+ const items = f.itemFields ?? f.item_fields;
54
+ if (!Array.isArray(items))
55
+ return field;
56
+ const patched = items.map((c) => (c && c.key && lock.has(c.key) ? { ...c, readonly: true } : c));
57
+ return { ...field, itemFields: patched, item_fields: patched };
58
+ }
41
59
  // buildPrefillRows projects record[spec.$prefillFromRecord] into modal rows.
42
60
  function buildPrefillRows(spec, record) {
43
61
  const src = record?.[spec.$prefillFromRecord];
@@ -207,7 +225,7 @@ formValues) {
207
225
  // Repeatable line-items group → row grid (value is an array of row objects).
208
226
  // The header form values flow in so a cell can depend on a header field.
209
227
  if (isLineItemsField(field)) {
210
- return _jsx(DynamicLineItems, { field: field, value: value, onChange: onChange, formValues: formValues });
228
+ return _jsx(DynamicLineItems, { field: applyPrefillLock(field), value: value, onChange: onChange, formValues: formValues });
211
229
  }
212
230
  // Resolve the widget the same way DynamicForm does (explicit widget wins,
213
231
  // else inferred from type) so action modals and the standalone form stay in
@@ -91,7 +91,9 @@ function CellRenderer({ field, value, onChange, disabled, formValues, rowValues
91
91
  // Async searchable picker per row cell — e.g. the account_id column of a
92
92
  // journal entry's debit/credit lines. Same widget as the flat form.
93
93
  if (widget === 'dynamic_select') {
94
- return _jsx(DynamicSelectField, { field: field, value: value, onChange: onChange, dependsValue: dependsValue });
94
+ const ro = !!field.readonly ||
95
+ !!field.read_only;
96
+ return _jsx(DynamicSelectField, { field: field, value: value, onChange: onChange, dependsValue: dependsValue, readonly: ro });
95
97
  }
96
98
  if (widget === 'select' && (field.ref || getOptionsConfig(field)?.source)) {
97
99
  return (_jsx(RefCell, { field: field, value: value, onChange: onChange, disabled: disabled, formValues: formValues, rowValues: rowValues }));
@@ -47,7 +47,15 @@ export interface DynamicSelectFieldProps {
47
47
  dependsValue?: string;
48
48
  /** Overrides the disabled-state hint shown while `dependsValue` is empty. */
49
49
  dependsHint?: string;
50
+ /**
51
+ * Renders the picker as a locked, non-interactive display of the current
52
+ * value's resolved label (no popover, no inline-create). Used when the value
53
+ * is fixed by context — e.g. the product of a receive-goods line is dictated
54
+ * by the source document, not chosen. Options are fetched eagerly (not just
55
+ * on open) so the label resolves to the NAME instead of showing the raw id.
56
+ */
57
+ readonly?: boolean;
50
58
  }
51
- export declare function DynamicSelectField({ field, value, onChange, seedOption, dependsValue, dependsHint, }: DynamicSelectFieldProps): import("react").JSX.Element;
59
+ export declare function DynamicSelectField({ field, value, onChange, seedOption, dependsValue, dependsHint, readonly, }: DynamicSelectFieldProps): import("react").JSX.Element;
52
60
  export default DynamicSelectField;
53
61
  //# sourceMappingURL=dynamic-select-field.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-select-field.d.ts","sourceRoot":"","sources":["../src/dynamic-select-field.tsx"],"names":[],"mappings":"AAuCA,OAAO,EAAsB,KAAK,cAAc,EAAE,MAAM,wBAAwB,CAAA;AAEhF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAE7C;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,gDAAgD,CAAA;AAEjF;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,EAAE,KAAK,EAAE,IAAS,EAAE,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,+BA4BzF;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,EACvB,MAAM,EACN,IAAS,GACZ,EAAE;IACC,MAAM,CAAC,EAAE,IAAI,CAAC,cAAc,EAAE,OAAO,GAAG,OAAO,GAAG,MAAM,CAAC,GAAG,IAAI,CAAA;IAChE,IAAI,CAAC,EAAE,MAAM,CAAA;CAChB,sCAwBA;AAqBD,MAAM,WAAW,uBAAuB;IACpC,KAAK,EAAE,cAAc,CAAA;IACrB,KAAK,EAAE,GAAG,CAAA;IACV,QAAQ,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,IAAI,CAAA;IAC1B;;;;;OAKG;IACH,UAAU,CAAC,EAAE,cAAc,GAAG,IAAI,CAAA;IAClC;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,6EAA6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,wBAAgB,kBAAkB,CAAC,EAC/B,KAAK,EACL,KAAK,EACL,QAAQ,EACR,UAAU,EACV,YAAY,EACZ,WAAW,GACd,EAAE,uBAAuB,+BA4MzB;AAED,eAAe,kBAAkB,CAAA"}
1
+ {"version":3,"file":"dynamic-select-field.d.ts","sourceRoot":"","sources":["../src/dynamic-select-field.tsx"],"names":[],"mappings":"AAuCA,OAAO,EAAsB,KAAK,cAAc,EAAE,MAAM,wBAAwB,CAAA;AAEhF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAE7C;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,gDAAgD,CAAA;AAEjF;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,EAAE,KAAK,EAAE,IAAS,EAAE,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,+BA4BzF;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,EACvB,MAAM,EACN,IAAS,GACZ,EAAE;IACC,MAAM,CAAC,EAAE,IAAI,CAAC,cAAc,EAAE,OAAO,GAAG,OAAO,GAAG,MAAM,CAAC,GAAG,IAAI,CAAA;IAChE,IAAI,CAAC,EAAE,MAAM,CAAA;CAChB,sCAwBA;AAqBD,MAAM,WAAW,uBAAuB;IACpC,KAAK,EAAE,cAAc,CAAA;IACrB,KAAK,EAAE,GAAG,CAAA;IACV,QAAQ,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,IAAI,CAAA;IAC1B;;;;;OAKG;IACH,UAAU,CAAC,EAAE,cAAc,GAAG,IAAI,CAAA;IAClC;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,6EAA6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;CACrB;AAED,wBAAgB,kBAAkB,CAAC,EAC/B,KAAK,EACL,KAAK,EACL,QAAQ,EACR,UAAU,EACV,YAAY,EACZ,WAAW,EACX,QAAgB,GACnB,EAAE,uBAAuB,+BAsOzB;AAED,eAAe,kBAAkB,CAAA"}
@@ -85,7 +85,7 @@ function useDebounced(value, ms) {
85
85
  }, [value, ms]);
86
86
  return debounced;
87
87
  }
88
- export function DynamicSelectField({ field, value, onChange, seedOption, dependsValue, dependsHint, }) {
88
+ export function DynamicSelectField({ field, value, onChange, seedOption, dependsValue, dependsHint, readonly = false, }) {
89
89
  const [open, setOpen] = useState(false);
90
90
  const [search, setSearch] = useState('');
91
91
  const debounced = useDebounced(search, 250);
@@ -120,8 +120,9 @@ export function DynamicSelectField({ field, value, onChange, seedOption, depends
120
120
  filterValue: dependsOn ? scope : undefined,
121
121
  // Don't fetch until the popover opens (and keep fetching as the query
122
122
  // changes while open). A picker blocked by an unset dependency never
123
- // fetches.
124
- enabled: open && !blockedByDependency,
123
+ // fetches. A readonly cell fetches eagerly so its value's label resolves
124
+ // to the name without the user ever opening it.
125
+ enabled: (open || readonly) && !blockedByDependency,
125
126
  });
126
127
  // When the depended-on value changes, the previously-picked option no longer
127
128
  // belongs to the new scope, so clear the selection (skip the initial mount).
@@ -175,6 +176,13 @@ export function DynamicSelectField({ field, value, onChange, seedOption, depends
175
176
  },
176
177
  }));
177
178
  };
179
+ // Locked display: the value is fixed by context, so render the resolved
180
+ // label (name) as a disabled control — no popover, no inline-create. While
181
+ // the eager fetch is in flight the label falls back to the seed/raw value,
182
+ // then snaps to the name once options arrive.
183
+ if (readonly) {
184
+ return (_jsx(Button, { type: "button", variant: "outline", role: "combobox", id: field.key, disabled: true, "aria-readonly": "true", className: "w-full min-w-0 cursor-default justify-start font-normal opacity-100", children: _jsxs("span", { className: "flex min-w-0 flex-1 items-center gap-2 text-left", children: [hasVisual && value ? _jsx(OptionLead, { option: selectedOption, size: 20 }) : null, _jsx("span", { className: 'min-w-0 flex-1 truncate ' + (selectedLabel ? '' : 'text-muted-foreground'), children: selectedLabel || (loading ? 'Cargando…' : field.placeholder || '—') })] }) }));
185
+ }
178
186
  // w-full + min-w-0: as a grid cell child, the row must be allowed to shrink
179
187
  // to the cell. Without min-w-0 the combobox+button row sizes to its content
180
188
  // (the long empty-state placeholder) and overflows the column, pushing the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asteby/metacore-runtime-react",
3
- "version": "18.27.0",
3
+ "version": "18.28.0",
4
4
  "description": "React runtime for metacore hosts — renders addon contributions dynamically",
5
5
  "repository": {
6
6
  "type": "git",
@@ -72,6 +72,13 @@ interface PrefillSpec {
72
72
  $prefillFromRecord: string
73
73
  map?: Record<string, string>
74
74
  remaining?: { target: string; of: string; minus?: string }
75
+ /**
76
+ * Item-field keys to lock (render read-only) in the prefilled rows — e.g. the
77
+ * product of a receive-goods line is dictated by the source document, so it
78
+ * is shown as a resolved name but cannot be changed. Editable in the create
79
+ * flow (which carries no prefill spec); only the prefilled action locks them.
80
+ */
81
+ lock?: string[]
75
82
  }
76
83
 
77
84
  function isPrefillSpec(v: unknown): v is PrefillSpec {
@@ -96,6 +103,23 @@ function toNum(v: unknown): number {
96
103
  return Number.isFinite(n) ? n : 0
97
104
  }
98
105
 
106
+ // applyPrefillLock marks the item-field columns named in a line-items field's
107
+ // PrefillSpec.lock as read-only, so the prefilled cells (e.g. the product of a
108
+ // receive line) render as a resolved, non-editable name. Returns the field
109
+ // untouched when there is no prefill spec or no lock list (the create flow,
110
+ // which carries no prefill, stays fully editable). The readonly flag is set on
111
+ // BOTH itemFields aliases the renderers tolerate.
112
+ function applyPrefillLock(field: ActionFieldDef): ActionFieldDef {
113
+ const spec = lineItemsDefault(field)
114
+ if (!isPrefillSpec(spec) || !spec.lock || spec.lock.length === 0) return field
115
+ const lock = new Set(spec.lock)
116
+ const f = field as ActionFieldDef & { itemFields?: any[]; item_fields?: any[] }
117
+ const items: any[] | undefined = f.itemFields ?? f.item_fields
118
+ if (!Array.isArray(items)) return field
119
+ const patched: any[] = items.map((c) => (c && c.key && lock.has(c.key) ? { ...c, readonly: true } : c))
120
+ return { ...(field as any), itemFields: patched, item_fields: patched } as ActionFieldDef
121
+ }
122
+
99
123
  // buildPrefillRows projects record[spec.$prefillFromRecord] into modal rows.
100
124
  function buildPrefillRows(spec: PrefillSpec, record: any): Array<Record<string, any>> {
101
125
  const src = record?.[spec.$prefillFromRecord]
@@ -397,7 +421,7 @@ function renderField(
397
421
  // Repeatable line-items group → row grid (value is an array of row objects).
398
422
  // The header form values flow in so a cell can depend on a header field.
399
423
  if (isLineItemsField(field)) {
400
- return <DynamicLineItems field={field} value={value} onChange={onChange} formValues={formValues} />
424
+ return <DynamicLineItems field={applyPrefillLock(field)} value={value} onChange={onChange} formValues={formValues} />
401
425
  }
402
426
  // Resolve the widget the same way DynamicForm does (explicit widget wins,
403
427
  // else inferred from type) so action modals and the standalone form stay in
@@ -267,7 +267,9 @@ function CellRenderer({ field, value, onChange, disabled, formValues, rowValues
267
267
  // Async searchable picker per row cell — e.g. the account_id column of a
268
268
  // journal entry's debit/credit lines. Same widget as the flat form.
269
269
  if (widget === 'dynamic_select') {
270
- return <DynamicSelectField field={field} value={value} onChange={onChange} dependsValue={dependsValue} />
270
+ const ro = !!(field as { readonly?: boolean; read_only?: boolean }).readonly ||
271
+ !!(field as { readonly?: boolean; read_only?: boolean }).read_only
272
+ return <DynamicSelectField field={field} value={value} onChange={onChange} dependsValue={dependsValue} readonly={ro} />
271
273
  }
272
274
  if (widget === 'select' && (field.ref || getOptionsConfig(field)?.source)) {
273
275
  return (
@@ -162,6 +162,14 @@ export interface DynamicSelectFieldProps {
162
162
  dependsValue?: string
163
163
  /** Overrides the disabled-state hint shown while `dependsValue` is empty. */
164
164
  dependsHint?: string
165
+ /**
166
+ * Renders the picker as a locked, non-interactive display of the current
167
+ * value's resolved label (no popover, no inline-create). Used when the value
168
+ * is fixed by context — e.g. the product of a receive-goods line is dictated
169
+ * by the source document, not chosen. Options are fetched eagerly (not just
170
+ * on open) so the label resolves to the NAME instead of showing the raw id.
171
+ */
172
+ readonly?: boolean
165
173
  }
166
174
 
167
175
  export function DynamicSelectField({
@@ -171,6 +179,7 @@ export function DynamicSelectField({
171
179
  seedOption,
172
180
  dependsValue,
173
181
  dependsHint,
182
+ readonly = false,
174
183
  }: DynamicSelectFieldProps) {
175
184
  const [open, setOpen] = useState(false)
176
185
  const [search, setSearch] = useState('')
@@ -210,8 +219,9 @@ export function DynamicSelectField({
210
219
  filterValue: dependsOn ? scope : undefined,
211
220
  // Don't fetch until the popover opens (and keep fetching as the query
212
221
  // changes while open). A picker blocked by an unset dependency never
213
- // fetches.
214
- enabled: open && !blockedByDependency,
222
+ // fetches. A readonly cell fetches eagerly so its value's label resolves
223
+ // to the name without the user ever opening it.
224
+ enabled: (open || readonly) && !blockedByDependency,
215
225
  })
216
226
 
217
227
  // When the depended-on value changes, the previously-picked option no longer
@@ -272,6 +282,31 @@ export function DynamicSelectField({
272
282
  )
273
283
  }
274
284
 
285
+ // Locked display: the value is fixed by context, so render the resolved
286
+ // label (name) as a disabled control — no popover, no inline-create. While
287
+ // the eager fetch is in flight the label falls back to the seed/raw value,
288
+ // then snaps to the name once options arrive.
289
+ if (readonly) {
290
+ return (
291
+ <Button
292
+ type="button"
293
+ variant="outline"
294
+ role="combobox"
295
+ id={field.key}
296
+ disabled
297
+ aria-readonly="true"
298
+ className="w-full min-w-0 cursor-default justify-start font-normal opacity-100"
299
+ >
300
+ <span className="flex min-w-0 flex-1 items-center gap-2 text-left">
301
+ {hasVisual && value ? <OptionLead option={selectedOption} size={20} /> : null}
302
+ <span className={'min-w-0 flex-1 truncate ' + (selectedLabel ? '' : 'text-muted-foreground')}>
303
+ {selectedLabel || (loading ? 'Cargando…' : field.placeholder || '—')}
304
+ </span>
305
+ </span>
306
+ </Button>
307
+ )
308
+ }
309
+
275
310
  // w-full + min-w-0: as a grid cell child, the row must be allowed to shrink
276
311
  // to the cell. Without min-w-0 the combobox+button row sizes to its content
277
312
  // (the long empty-state placeholder) and overflows the column, pushing the