@tinacms/app 0.0.0-20230516164752 → 0.0.0-20230516170903

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,14 +1,14 @@
1
1
  # @tinacms/app
2
2
 
3
- ## 0.0.0-20230516164752
3
+ ## 0.0.0-20230516170903
4
4
 
5
5
  ### Patch Changes
6
6
 
7
7
  - 7f95c1ce5: Reorganize the way fields are presented on the form to allow for deep-linking
8
8
  - Updated dependencies [b3d98d159]
9
9
  - Updated dependencies [7f95c1ce5]
10
- - @tinacms/toolkit@0.0.0-20230516164752
11
- - tinacms@0.0.0-20230516164752
10
+ - @tinacms/toolkit@0.0.0-20230516170903
11
+ - tinacms@0.0.0-20230516170903
12
12
 
13
13
  ## 1.2.12
14
14
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tinacms/app",
3
- "version": "0.0.0-20230516164752",
3
+ "version": "0.0.0-20230516170903",
4
4
  "main": "src/main.tsx",
5
5
  "license": "Apache-2.0",
6
6
  "devDependencies": {
@@ -13,8 +13,6 @@
13
13
  "@headlessui/react": "1.6.6",
14
14
  "@heroicons/react": "1.0.6",
15
15
  "@monaco-editor/react": "4.4.5",
16
- "@tinacms/mdx": "1.3.10",
17
- "@tinacms/toolkit": "0.0.0-20230516164752",
18
16
  "@xstate/react": "3.0.0",
19
17
  "final-form": "4.20.7",
20
18
  "graphiql": "^2.4.0",
@@ -27,8 +25,10 @@
27
25
  "react-is": "17.0.2",
28
26
  "react-router-dom": "6.3.0",
29
27
  "tailwindcss": "^3.2.7",
30
- "tinacms": "0.0.0-20230516164752",
31
28
  "typescript": "^4.6.4",
32
- "zod": "^3.14.3"
29
+ "zod": "^3.14.3",
30
+ "@tinacms/mdx": "1.3.10",
31
+ "@tinacms/toolkit": "0.0.0-20230516170903",
32
+ "tinacms": "0.0.0-20230516170903"
33
33
  }
34
34
  }
@@ -67,6 +67,13 @@ const addTypenameToDocument = (doc: G.DocumentNode) => {
67
67
  })
68
68
  }
69
69
 
70
+ const CONTENT_SOURCE_FIELD: G.FieldNode = {
71
+ kind: G.Kind.FIELD,
72
+ name: {
73
+ kind: G.Kind.NAME,
74
+ value: '_content_source',
75
+ },
76
+ }
70
77
  const METADATA_FIELD: G.FieldNode = {
71
78
  kind: G.Kind.FIELD,
72
79
  name: {
@@ -94,7 +101,11 @@ const addMetadataField = (
94
101
  selections: [],
95
102
  }),
96
103
  selections:
97
- [...(node.selectionSet?.selections || []), METADATA_FIELD] || [],
104
+ [
105
+ ...(node.selectionSet?.selections || []),
106
+ METADATA_FIELD,
107
+ CONTENT_SOURCE_FIELD,
108
+ ] || [],
98
109
  },
99
110
  }
100
111
  }
@@ -1,7 +1,7 @@
1
1
  import React from 'react'
2
2
  import * as G from 'graphql'
3
3
  import { getIn } from 'final-form'
4
- import { z, ZodError } from 'zod'
4
+ import { z } from 'zod'
5
5
  // @ts-expect-error
6
6
  import schemaJson from 'SCHEMA_IMPORT'
7
7
  import { expandQuery, isConnectionType, isNodeType } from './expand-query'
@@ -25,9 +25,10 @@ import type {
25
25
  PostMessage,
26
26
  Payload,
27
27
  SystemInfo,
28
- Document,
29
28
  ResolvedDocument,
30
29
  } from './types'
30
+ import { getFormAndFieldNameFromMetadata } from './util'
31
+ import { useSearchParams } from 'react-router-dom'
31
32
 
32
33
  const sysSchema = z.object({
33
34
  breadcrumbs: z.array(z.string()),
@@ -57,6 +58,50 @@ const astNode = schemaJson as G.DocumentNode
57
58
  const astNodeWithMeta: G.DocumentNode = {
58
59
  ...astNode,
59
60
  definitions: astNode.definitions.map((def) => {
61
+ if (def.kind === 'InterfaceTypeDefinition') {
62
+ return {
63
+ ...def,
64
+ fields: [
65
+ ...(def.fields || []),
66
+ {
67
+ kind: 'FieldDefinition',
68
+ name: {
69
+ kind: 'Name',
70
+ value: '_tina_metadata',
71
+ },
72
+ arguments: [],
73
+ type: {
74
+ kind: 'NonNullType',
75
+ type: {
76
+ kind: 'NamedType',
77
+ name: {
78
+ kind: 'Name',
79
+ value: 'JSON',
80
+ },
81
+ },
82
+ },
83
+ },
84
+ {
85
+ kind: 'FieldDefinition',
86
+ name: {
87
+ kind: 'Name',
88
+ value: '_content_source',
89
+ },
90
+ arguments: [],
91
+ type: {
92
+ kind: 'NonNullType',
93
+ type: {
94
+ kind: 'NamedType',
95
+ name: {
96
+ kind: 'Name',
97
+ value: 'JSON',
98
+ },
99
+ },
100
+ },
101
+ },
102
+ ],
103
+ }
104
+ }
60
105
  if (def.kind === 'ObjectTypeDefinition') {
61
106
  return {
62
107
  ...def,
@@ -80,6 +125,24 @@ const astNodeWithMeta: G.DocumentNode = {
80
125
  },
81
126
  },
82
127
  },
128
+ {
129
+ kind: 'FieldDefinition',
130
+ name: {
131
+ kind: 'Name',
132
+ value: '_content_source',
133
+ },
134
+ arguments: [],
135
+ type: {
136
+ kind: 'NonNullType',
137
+ type: {
138
+ kind: 'NamedType',
139
+ name: {
140
+ kind: 'Name',
141
+ value: 'JSON',
142
+ },
143
+ },
144
+ },
145
+ },
83
146
  ],
84
147
  }
85
148
  }
@@ -96,6 +159,18 @@ export const useGraphQLReducer = (
96
159
  const cms = useCMS()
97
160
  const tinaSchema = cms.api.tina.schema as TinaSchema
98
161
  const [payloads, setPayloads] = React.useState<Payload[]>([])
162
+ const [searchParams, setSearchParams] = useSearchParams()
163
+ const [results, setResults] = React.useState<
164
+ {
165
+ id: string
166
+ data:
167
+ | {
168
+ [key: string]: any
169
+ }
170
+ | null
171
+ | undefined
172
+ }[]
173
+ >([])
99
174
  const [documentsToResolve, setDocumentsToResolve] = React.useState<string[]>(
100
175
  []
101
176
  )
@@ -104,6 +179,11 @@ export const useGraphQLReducer = (
104
179
  >([])
105
180
  const [operationIndex, setOperationIndex] = React.useState(0)
106
181
 
182
+ const helper = React.useMemo(() => {
183
+ const previewPlugins = cms.plugins.getType('preview-helper')
184
+ return previewPlugins.find('preview-helper')
185
+ }, [cms])
186
+
107
187
  React.useEffect(() => {
108
188
  const run = async () => {
109
189
  return Promise.all(
@@ -145,7 +225,7 @@ export const useGraphQLReducer = (
145
225
  setPayloads(updatedPayloads)
146
226
  })
147
227
  }
148
- }, [payloads.map(({ id }) => id).join('.'), cms])
228
+ }, [JSON.stringify(payloads), cms])
149
229
 
150
230
  const processPayload = React.useCallback(
151
231
  (payload: Payload) => {
@@ -192,6 +272,13 @@ export const useGraphQLReducer = (
192
272
  if (fieldName === '_values') {
193
273
  return source._internalValues
194
274
  }
275
+ if (info.fieldName === '_content_source') {
276
+ const pathArray = G.responsePathAsArray(info.path)
277
+ return {
278
+ queryId: payload.id,
279
+ path: pathArray.slice(0, pathArray.length - 1),
280
+ }
281
+ }
195
282
  if (info.fieldName === '_tina_metadata') {
196
283
  if (value) {
197
284
  return value
@@ -201,6 +288,7 @@ export const useGraphQLReducer = (
201
288
  return {
202
289
  id: null,
203
290
  fields: [],
291
+ prefix: '',
204
292
  }
205
293
  }
206
294
  if (isConnectionType(info.returnType)) {
@@ -271,13 +359,9 @@ export const useGraphQLReducer = (
271
359
  return pathString.startsWith(item.path)
272
360
  }
273
361
  })
274
- let parent = ancestors[ancestors.length - 1]
362
+ const parent = ancestors[ancestors.length - 1]
275
363
  if (parent) {
276
364
  if (parent.type === 'document') {
277
- // console.log(
278
- // 'its a parent',
279
- // JSON.stringify({ id, value, pathString, parent }, null, 2)
280
- // )
281
365
  parent.subItems.push({
282
366
  type: 'document',
283
367
  path: pathString,
@@ -307,7 +391,12 @@ export const useGraphQLReducer = (
307
391
  },
308
392
  { values: true }
309
393
  )
310
- return resolveDocument(resolvedDocument, template, form)
394
+ return resolveDocument(
395
+ resolvedDocument,
396
+ template,
397
+ form,
398
+ pathString
399
+ )
311
400
  } else {
312
401
  existingForm.tinaForm.addQuery(payload.id)
313
402
  const { template } = getTemplateForDocument(
@@ -318,10 +407,19 @@ export const useGraphQLReducer = (
318
407
  return resolveDocument(
319
408
  resolvedDocument,
320
409
  template,
321
- existingForm.tinaForm
410
+ existingForm.tinaForm,
411
+ pathString
322
412
  )
323
413
  }
324
414
  }
415
+ if (typeof value === 'string' && source?._tina_metadata) {
416
+ // FIXME: hack to prevent breaking images
417
+ if (helper) {
418
+ const pathArray = G.responsePathAsArray(info.path)
419
+
420
+ return helper.encode(pathArray.join('.'), value, payload.id)
421
+ }
422
+ }
325
423
  return value
326
424
  },
327
425
  })
@@ -344,6 +442,32 @@ export const useGraphQLReducer = (
344
442
  }
345
443
  })
346
444
  } else {
445
+ if (result.data) {
446
+ setResults((results) => [
447
+ ...results,
448
+ { id: payload.id, data: result.data },
449
+ ])
450
+ }
451
+ const activeField = searchParams.get('active-field')
452
+ if (activeField) {
453
+ setSearchParams({})
454
+ const [queryId, eventFieldName] = activeField.split('---')
455
+ if (queryId === payload.id) {
456
+ if (result?.data) {
457
+ cms.dispatch({
458
+ type: 'forms:set-active-field-name',
459
+ value: getFormAndFieldNameFromMetadata(
460
+ result.data,
461
+ eventFieldName
462
+ ),
463
+ })
464
+ }
465
+ cms.dispatch({
466
+ type: 'sidebar:set-display-state',
467
+ value: 'openOrFull',
468
+ })
469
+ }
470
+ }
347
471
  iframe.current?.contentWindow?.postMessage({
348
472
  type: 'updateData',
349
473
  id: payload.id,
@@ -363,24 +487,40 @@ export const useGraphQLReducer = (
363
487
  [resolvedDocuments.map((doc) => doc._internalSys.path).join('.')]
364
488
  )
365
489
 
366
- const notifyEditMode = React.useCallback(
490
+ const handleMessage = React.useCallback(
367
491
  (event: MessageEvent<PostMessage>) => {
492
+ if (event?.data?.type === 'quick-edit') {
493
+ cms.dispatch({
494
+ type: 'set-quick-editing-supported',
495
+ value: event.data.value,
496
+ })
497
+ }
368
498
  if (event?.data?.type === 'isEditMode') {
369
499
  iframe?.current?.contentWindow?.postMessage({
370
500
  type: 'tina:editMode',
371
501
  })
372
502
  }
373
- },
374
- []
375
- )
376
- const handleOpenClose = React.useCallback(
377
- (event: MessageEvent<PostMessage>) => {
503
+ if (event.data.type === 'field:selected') {
504
+ const [queryId, eventFieldName] = event.data.fieldName.split('---')
505
+ const result = results.find((res) => res.id === queryId)
506
+ if (result?.data) {
507
+ cms.dispatch({
508
+ type: 'forms:set-active-field-name',
509
+ value: getFormAndFieldNameFromMetadata(result.data, eventFieldName),
510
+ })
511
+ }
512
+ cms.dispatch({
513
+ type: 'sidebar:set-display-state',
514
+ value: 'openOrFull',
515
+ })
516
+ }
378
517
  if (event.data.type === 'close') {
379
518
  const payloadSchema = z.object({ id: z.string() })
380
519
  const { id } = payloadSchema.parse(event.data)
381
520
  setPayloads((previous) =>
382
521
  previous.filter((payload) => payload.id !== id)
383
522
  )
523
+ setResults((previous) => previous.filter((result) => result.id !== id))
384
524
  cms.forms.all().map((form) => {
385
525
  form.removeQuery(id)
386
526
  })
@@ -401,7 +541,7 @@ export const useGraphQLReducer = (
401
541
  ])
402
542
  }
403
543
  },
404
- [cms]
544
+ [cms, JSON.stringify(results)]
405
545
  )
406
546
 
407
547
  React.useEffect(() => {
@@ -415,23 +555,31 @@ export const useGraphQLReducer = (
415
555
  React.useEffect(() => {
416
556
  return () => {
417
557
  setPayloads([])
558
+ setResults([])
418
559
  cms.removeAllForms()
419
560
  cms.dispatch({ type: 'form-lists:clear' })
420
561
  }
421
562
  }, [url])
422
563
 
423
564
  React.useEffect(() => {
565
+ iframe.current?.contentWindow?.postMessage({
566
+ type: 'quickEditEnabled',
567
+ value: cms.state.quickEditEnabled,
568
+ })
569
+ }, [cms.state.quickEditEnabled])
570
+
571
+ React.useEffect(() => {
572
+ cms.dispatch({ type: 'set-edit-mode', value: 'visual' })
424
573
  if (iframe) {
425
- window.addEventListener('message', handleOpenClose)
426
- window.addEventListener('message', notifyEditMode)
574
+ window.addEventListener('message', handleMessage)
427
575
  }
428
576
 
429
577
  return () => {
430
- window.removeEventListener('message', handleOpenClose)
431
- window.removeEventListener('message', notifyEditMode)
578
+ window.removeEventListener('message', handleMessage)
432
579
  cms.removeAllForms()
580
+ cms.dispatch({ type: 'set-edit-mode', value: 'basic' })
433
581
  }
434
- }, [iframe.current])
582
+ }, [iframe.current, JSON.stringify(results)])
435
583
  }
436
584
 
437
585
  const onSubmit = async (
@@ -469,7 +617,8 @@ type Path = (string | number)[]
469
617
  const resolveDocument = (
470
618
  doc: ResolvedDocument,
471
619
  template: Template<true>,
472
- form: Form
620
+ form: Form,
621
+ pathToDocument: string
473
622
  ): ResolvedDocument => {
474
623
  // @ts-ignore AnyField and TinaField don't mix
475
624
  const fields = form.fields as TinaField<true>[]
@@ -480,6 +629,7 @@ const resolveDocument = (
480
629
  values: form.values,
481
630
  path,
482
631
  id,
632
+ pathToDocument,
483
633
  })
484
634
  const metadataFields: Record<string, string> = {}
485
635
  Object.keys(formValues).forEach((key) => {
@@ -492,7 +642,9 @@ const resolveDocument = (
492
642
  sys: doc._internalSys,
493
643
  values: form.values,
494
644
  _tina_metadata: {
645
+ prefix: pathToDocument,
495
646
  id: doc._internalSys.path,
647
+ name: '',
496
648
  fields: metadataFields,
497
649
  },
498
650
  _internalSys: doc._internalSys,
@@ -506,12 +658,14 @@ const resolveFormValue = <T extends Record<string, unknown>>({
506
658
  values,
507
659
  path,
508
660
  id,
661
+ pathToDocument,
509
662
  }: // tinaSchema,
510
663
  {
511
664
  fields: TinaField<true>[]
512
665
  values: T
513
666
  path: Path
514
667
  id: string
668
+ pathToDocument: string
515
669
  // tinaSchema: TinaSchema
516
670
  }): T & { __typename?: string } => {
517
671
  const accum: Record<string, unknown> = {}
@@ -528,6 +682,7 @@ const resolveFormValue = <T extends Record<string, unknown>>({
528
682
  value: v,
529
683
  path,
530
684
  id,
685
+ pathToDocument,
531
686
  })
532
687
  })
533
688
  return accum as T & { __typename?: string }
@@ -537,11 +692,13 @@ const resolveFieldValue = ({
537
692
  value,
538
693
  path,
539
694
  id,
695
+ pathToDocument,
540
696
  }: {
541
697
  field: TinaField<true>
542
698
  value: unknown
543
699
  path: Path
544
700
  id: string
701
+ pathToDocument: string
545
702
  }) => {
546
703
  switch (field.type) {
547
704
  case 'object': {
@@ -562,13 +719,16 @@ const resolveFieldValue = ({
562
719
  __typename: NAMER.dataTypeName(template.namespace),
563
720
  _tina_metadata: {
564
721
  id,
722
+ name: nextPath.join('.'),
565
723
  fields: metadataFields,
724
+ prefix: pathToDocument,
566
725
  },
567
726
  ...resolveFormValue({
568
727
  fields: template.fields,
569
728
  values: item,
570
729
  path: nextPath,
571
730
  id,
731
+ pathToDocument,
572
732
  }),
573
733
  }
574
734
  })
@@ -597,13 +757,16 @@ const resolveFieldValue = ({
597
757
  __typename: NAMER.dataTypeName(field.namespace),
598
758
  _tina_metadata: {
599
759
  id,
760
+ name: nextPath.join('.'),
600
761
  fields: metadataFields,
762
+ prefix: pathToDocument,
601
763
  },
602
764
  ...resolveFormValue({
603
765
  fields: templateFields,
604
766
  values: item,
605
- path,
767
+ path: nextPath,
606
768
  id,
769
+ pathToDocument,
607
770
  }),
608
771
  }
609
772
  })
@@ -618,13 +781,16 @@ const resolveFieldValue = ({
618
781
  __typename: NAMER.dataTypeName(field.namespace),
619
782
  _tina_metadata: {
620
783
  id,
784
+ name: nextPath.join('.'),
621
785
  fields: metadataFields,
786
+ prefix: pathToDocument,
622
787
  },
623
788
  ...resolveFormValue({
624
789
  fields: templateFields,
625
790
  values: value as any,
626
- path,
791
+ path: nextPath,
627
792
  id,
793
+ pathToDocument,
628
794
  }),
629
795
  }
630
796
  }
@@ -687,7 +853,7 @@ const expandPayload = async (payload: Payload, cms: TinaCMS) => {
687
853
  documentNode,
688
854
  })
689
855
  const expandedQueryForResolver = G.print(expandedDocumentNodeForResolver)
690
- return { ...payload, expandQuery, expandedData, expandedQueryForResolver }
856
+ return { ...payload, expandedQuery, expandedData, expandedQueryForResolver }
691
857
  }
692
858
 
693
859
  /**
package/src/lib/types.ts CHANGED
@@ -1,8 +1,11 @@
1
- export type PostMessage = {
2
- type: 'open' | 'close' | 'isEditMode'
3
- id: string
4
- data: object
5
- }
1
+ export type PostMessage =
2
+ | {
3
+ type: 'open' | 'close' | 'isEditMode'
4
+ id: string
5
+ data: object
6
+ }
7
+ | { type: 'field:selected'; fieldName: string }
8
+ | { type: 'quick-edit'; value: boolean }
6
9
 
7
10
  export type Payload = {
8
11
  id: string
@@ -0,0 +1,124 @@
1
+ const charCodeOfDot = '.'.charCodeAt(0)
2
+ const reEscapeChar = /\\(\\)?/g
3
+ const rePropName = RegExp(
4
+ // Match anything that isn't a dot or bracket.
5
+ '[^.[\\]]+' +
6
+ '|' +
7
+ // Or match property names within brackets.
8
+ '\\[(?:' +
9
+ // Match a non-string expression.
10
+ '([^"\'][^[]*)' +
11
+ '|' +
12
+ // Or match strings (supports escaping characters).
13
+ '(["\'])((?:(?!\\2)[^\\\\]|\\\\.)*?)\\2' +
14
+ ')\\]' +
15
+ '|' +
16
+ // Or match "" as the space between consecutive dots or empty brackets.
17
+ '(?=(?:\\.|\\[\\])(?:\\.|\\[\\]|$))',
18
+ 'g'
19
+ )
20
+
21
+ /**
22
+ * Converts `string` to a property path array.
23
+ *
24
+ * @private
25
+ * @param {string} string The string to convert.
26
+ * @returns {Array} Returns the property path array.
27
+ */
28
+ const stringToPath = (string: string) => {
29
+ const result = []
30
+ if (string.charCodeAt(0) === charCodeOfDot) {
31
+ result.push('')
32
+ }
33
+ string.replace(rePropName, (match, expression, quote, subString) => {
34
+ let key = match
35
+ if (quote) {
36
+ key = subString.replace(reEscapeChar, '$1')
37
+ } else if (expression) {
38
+ key = expression.trim()
39
+ }
40
+ result.push(key)
41
+ })
42
+ return result
43
+ }
44
+
45
+ const keysCache: { [key: string]: string[] } = {}
46
+ const keysRegex = /[.[\]]+/
47
+
48
+ const toPath = (key: string): string[] => {
49
+ if (key === null || key === undefined || !key.length) {
50
+ return []
51
+ }
52
+ if (typeof key !== 'string') {
53
+ throw new Error('toPath() expects a string')
54
+ }
55
+ if (keysCache[key] == null) {
56
+ /**
57
+ * The following patch fixes issue 456, introduced since v4.20.3:
58
+ *
59
+ * Before v4.20.3, i.e. in v4.20.2, a `key` like 'choices[]' would map to ['choices']
60
+ * (e.g. an array of choices used where 'choices[]' is name attribute of an input of type checkbox).
61
+ *
62
+ * Since v4.20.3, a `key` like 'choices[]' would map to ['choices', ''] which is wrong and breaks
63
+ * this kind of inputs e.g. in React.
64
+ *
65
+ * v4.20.3 introduced an unwanted breaking change, this patch fixes it, see the issue at the link below.
66
+ *
67
+ * @see https://github.com/final-form/final-form/issues/456
68
+ */
69
+ if (key.endsWith('[]')) {
70
+ // v4.20.2 (a `key` like 'choices[]' should map to ['choices'], which is fine).
71
+ keysCache[key] = key.split(keysRegex).filter(Boolean)
72
+ } else {
73
+ // v4.20.3 (a `key` like 'choices[]' maps to ['choices', ''], which breaks applications relying on inputs like `<input type="checkbox" name="choices[]" />`).
74
+ keysCache[key] = stringToPath(key)
75
+ }
76
+ }
77
+ return keysCache[key]
78
+ }
79
+ export const getDeepestMetadata = (state: Object, complexKey: string): any => {
80
+ // Intentionally using iteration rather than recursion
81
+ const path = toPath(complexKey)
82
+ let current: any = state
83
+ let metadata: any
84
+ for (let i = 0; i < path.length; i++) {
85
+ const key = path[i]
86
+ if (
87
+ current === undefined ||
88
+ current === null ||
89
+ typeof current !== 'object' ||
90
+ (Array.isArray(current) && isNaN(Number(key)))
91
+ ) {
92
+ return undefined
93
+ }
94
+ const value = current[key]
95
+ if (value?._tina_metadata) {
96
+ metadata = value._tina_metadata
97
+ }
98
+ current = value
99
+ }
100
+ return metadata
101
+ }
102
+ export const getFormAndFieldNameFromMetadata = (
103
+ object: object,
104
+ eventFieldName: string
105
+ ) => {
106
+ let formId
107
+ let n
108
+ const value = getDeepestMetadata(object, eventFieldName)
109
+ if (value) {
110
+ if (value.prefix) {
111
+ const fieldName = eventFieldName.slice(value?.prefix?.length + 1)
112
+ const localFieldName = value.name
113
+ ? fieldName.slice(value?.name?.length + 1)
114
+ : fieldName
115
+ if (localFieldName) {
116
+ n = value.fields[localFieldName]
117
+ } else {
118
+ n = value.name
119
+ }
120
+ formId = value.id
121
+ }
122
+ }
123
+ return { formId, fieldName: n }
124
+ }