@byline/admin 4.2.0 → 4.4.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 (36) hide show
  1. package/dist/fields/array/array-field.d.ts +12 -9
  2. package/dist/fields/array/array-field.js +46 -34
  3. package/dist/fields/blocks/blocks-field.js +1 -0
  4. package/dist/fields/draggable-context-menu.js +2 -1
  5. package/dist/fields/field-admin.d.ts +15 -0
  6. package/dist/fields/field-admin.js +11 -0
  7. package/dist/fields/field-admin.test.node.d.ts +8 -0
  8. package/dist/fields/field-renderer.d.ts +11 -4
  9. package/dist/fields/field-renderer.js +8 -9
  10. package/dist/fields/file/file-field.d.ts +1 -3
  11. package/dist/fields/file/file-field.js +2 -2
  12. package/dist/fields/group/group-field.d.ts +16 -9
  13. package/dist/fields/group/group-field.js +5 -4
  14. package/dist/fields/image/image-field.d.ts +1 -3
  15. package/dist/fields/image/image-field.js +2 -2
  16. package/dist/fields/sortable-item.d.ts +6 -0
  17. package/dist/fields/sortable-item.js +44 -25
  18. package/dist/forms/form-context.d.ts +20 -1
  19. package/dist/forms/form-context.js +3 -2
  20. package/dist/forms/form-renderer.js +4 -2
  21. package/dist/forms/upload-executor.js +50 -29
  22. package/package.json +10 -10
  23. package/src/fields/array/array-field.tsx +74 -43
  24. package/src/fields/blocks/blocks-field.tsx +5 -0
  25. package/src/fields/draggable-context-menu.tsx +9 -1
  26. package/src/fields/field-admin.test.node.ts +49 -0
  27. package/src/fields/field-admin.ts +39 -0
  28. package/src/fields/field-renderer.tsx +14 -7
  29. package/src/fields/file/file-field.tsx +4 -4
  30. package/src/fields/group/group-field.tsx +19 -11
  31. package/src/fields/image/image-field.tsx +4 -4
  32. package/src/fields/sortable-item.tsx +115 -34
  33. package/src/forms/form-context.tsx +30 -1
  34. package/src/forms/form-renderer.tsx +3 -1
  35. package/src/forms/upload-executor.test.node.ts +187 -0
  36. package/src/forms/upload-executor.ts +90 -29
@@ -15,6 +15,7 @@
15
15
  */
16
16
 
17
17
  import type { Field, StoredFileValue, UploadConfig } from '@byline/core'
18
+ import { type PathSegment, parseInstancePath } from '@byline/core'
18
19
 
19
20
  import { get as getNestedValue } from './nested-path'
20
21
  import type { UploadFieldFn } from '../fields/field-services-types'
@@ -149,13 +150,16 @@ function buildUploadFormData(
149
150
  formData.append('documentId', executionContext.documentId)
150
151
  }
151
152
 
153
+ // Resolved once per upload, before locating the field: a `blocks` hop in
154
+ // the path can only be resolved by reading the addressed item's `_type`.
155
+ const formValues = executionContext?.getFormValues?.()
156
+
152
157
  const contextPaths =
153
158
  executionContext?.fields != null
154
- ? findUploadFieldByPath(executionContext.fields, fieldPath)?.context
159
+ ? findUploadFieldByPath(executionContext.fields, fieldPath, formValues)?.context
155
160
  : undefined
156
161
 
157
- if (contextPaths && contextPaths.length > 0 && executionContext?.getFormValues) {
158
- const formValues = executionContext.getFormValues()
162
+ if (contextPaths && contextPaths.length > 0 && formValues) {
159
163
  for (const contextPath of contextPaths) {
160
164
  const resolvedPath = resolveContextPath(fieldPath, contextPath)
161
165
  if (resolvedPath === undefined) continue
@@ -263,43 +267,100 @@ function isRelationEnvelope(value: unknown): value is { targetDocumentId: string
263
267
 
264
268
  /**
265
269
  * Locate the upload-capable `image | file` schema field addressed by a form
266
- * field path, walking `group` / `array` / `blocks` structures. Array indices
267
- * in the path (`files[2]`) map onto the schema's repeating field (`files`);
268
- * blocks are matched by trying each block's field set (block-type info is
269
- * not encoded in the path, so the first block containing the remaining path
270
- * wins upload leaf names must be unique among a collection's
271
- * upload-capable fields anyway, per the server-side resolver's contract).
270
+ * field path, walking `group` / `array` / `blocks` structures.
271
+ *
272
+ * The path is an *instance* path (`content[1].gallery[0].poster`), so it
273
+ * carries no block type a block item's type lives on the item itself, as
274
+ * `_type`. Passing `formValues` therefore lets a `blocks` hop be resolved
275
+ * exactly: read the addressed item, take its `_type`, descend into that
276
+ * block. Without form values the block type is genuinely unknowable from the
277
+ * path, so every block is tried and the result is accepted only when exactly
278
+ * one resolves — ambiguity returns `undefined` rather than a guess.
272
279
  */
273
280
  function findUploadFieldByPath(
274
281
  fields: readonly Field[],
275
- fieldPath: string
282
+ fieldPath: string,
283
+ formValues?: Record<string, any>
276
284
  ): UploadConfig | undefined {
277
- const segments = fieldPath.split('.').map((s) => s.replace(/\[\d+\]$/, ''))
285
+ const parsed = parseInstancePath(fieldPath)
286
+ if (!parsed.ok) return undefined
287
+ return resolveUploadConfig(fields, parsed.segments, formValues)
288
+ }
278
289
 
279
- let currentFields: readonly Field[] = fields
280
- for (let i = 0; i < segments.length; i++) {
281
- const name = segments[i]
282
- const isLeaf = i === segments.length - 1
283
- const field = currentFields.find((f) => f.name === name)
284
- if (!field) return undefined
290
+ /** Select the item an index / id segment addresses, when data is available. */
291
+ function selectItem(value: unknown, segment: PathSegment): unknown {
292
+ if (!Array.isArray(value)) return undefined
293
+ if (segment.kind === 'index') return value[segment.index]
294
+ if (segment.kind === 'id') {
295
+ return value.find((item) => (item as { _id?: unknown } | null)?._id === segment.id)
296
+ }
297
+ return undefined
298
+ }
299
+
300
+ /**
301
+ * Walk instance-path segments against the schema, carrying the corresponding
302
+ * slice of form data alongside so `blocks` hops can read `_type`.
303
+ */
304
+ function resolveUploadConfig(
305
+ fields: readonly Field[],
306
+ segments: readonly PathSegment[],
307
+ value: unknown
308
+ ): UploadConfig | undefined {
309
+ const head = segments[0]
310
+ if (head == null || head.kind !== 'field') return undefined
285
311
 
286
- if (isLeaf) {
287
- return field.type === 'image' || field.type === 'file' ? field.upload : undefined
312
+ const field = fields.find((candidate) => candidate.name === head.name)
313
+ if (field == null) return undefined
314
+
315
+ const fieldValue = (value as Record<string, unknown> | undefined)?.[head.name]
316
+ const rest = segments.slice(1)
317
+
318
+ if (rest.length === 0) {
319
+ return field.type === 'image' || field.type === 'file' ? field.upload : undefined
320
+ }
321
+
322
+ if (field.type === 'group') {
323
+ return resolveUploadConfig(field.fields, rest, fieldValue)
324
+ }
325
+
326
+ if (field.type === 'array') {
327
+ // An item selector may be absent when the caller addresses the array's
328
+ // child declaration rather than one item; the child fields are the same.
329
+ const selector = rest[0]
330
+ if (selector?.kind === 'index' || selector?.kind === 'id') {
331
+ return resolveUploadConfig(field.fields, rest.slice(1), selectItem(fieldValue, selector))
288
332
  }
333
+ return resolveUploadConfig(field.fields, rest, undefined)
334
+ }
289
335
 
290
- if (field.type === 'group' || field.type === 'array') {
291
- currentFields = field.fields
292
- continue
336
+ if (field.type === 'blocks') {
337
+ const selector = rest[0]
338
+ const remainder = selector?.kind === 'index' || selector?.kind === 'id' ? rest.slice(1) : rest
339
+ const item =
340
+ selector?.kind === 'index' || selector?.kind === 'id'
341
+ ? selectItem(fieldValue, selector)
342
+ : undefined
343
+
344
+ const blockType = (item as { _type?: unknown } | undefined)?._type
345
+ if (typeof blockType === 'string') {
346
+ const block = field.blocks.find((candidate) => candidate.blockType === blockType)
347
+ return block == null ? undefined : resolveUploadConfig(block.fields, remainder, item)
293
348
  }
294
- if (field.type === 'blocks') {
295
- const remaining = segments[i + 1]
296
- const block = field.blocks.find((b) => b.fields.some((f) => f.name === remaining))
297
- if (!block) return undefined
298
- currentFields = block.fields
299
- continue
349
+
350
+ // No data to disambiguate. Try every block and accept a unique answer;
351
+ // two matches mean the path genuinely does not identify one declaration.
352
+ let found: UploadConfig | undefined
353
+ let matches = 0
354
+ for (const block of field.blocks) {
355
+ const result = resolveUploadConfig(block.fields, remainder, undefined)
356
+ if (result !== undefined) {
357
+ matches += 1
358
+ found = result
359
+ }
300
360
  }
301
- return undefined
361
+ return matches === 1 ? found : undefined
302
362
  }
363
+
303
364
  return undefined
304
365
  }
305
366