@tinacms/app 0.0.0-d9672bc-20250218033222 → 0.0.0-d9ccf29-20251222052725

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.
@@ -1,36 +1,36 @@
1
- import React from 'react'
2
- import * as G from 'graphql'
3
- import { getIn } from 'final-form'
4
- import { z } from 'zod'
5
1
  // @ts-expect-error
6
- import schemaJson from 'SCHEMA_IMPORT'
7
- import { expandQuery, isConnectionType, isNodeType } from './expand-query'
2
+ import schemaJson from 'SCHEMA_IMPORT';
3
+ import { getIn } from 'final-form';
4
+ import * as G from 'graphql';
5
+ import React from 'react';
6
+ import { useSearchParams } from 'react-router-dom';
8
7
  import {
8
+ Client,
9
+ Collection,
10
+ ErrorDialog,
9
11
  Form,
10
- TinaCMS,
12
+ FormOptions,
13
+ GlobalFormPlugin,
11
14
  NAMER,
12
- TinaSchema,
13
- useCMS,
14
- resolveField,
15
- Collection,
16
15
  Template,
16
+ TinaCMS,
17
17
  TinaField,
18
- Client,
19
- FormOptions,
20
- GlobalFormPlugin,
18
+ TinaSchema,
21
19
  TinaState,
22
- ErrorDialog,
23
- } from 'tinacms'
24
- import { createForm, createGlobalForm, FormifyCallback } from './build-form'
20
+ resolveField,
21
+ useCMS,
22
+ } from 'tinacms';
23
+ import { z } from 'zod';
24
+ import { FormifyCallback, createForm, createGlobalForm } from './build-form';
25
+ import { showErrorModal } from './errors';
26
+ import { expandQuery, isConnectionType, isNodeType } from './expand-query';
25
27
  import type {
26
- PostMessage,
27
28
  Payload,
28
- SystemInfo,
29
+ PostMessage,
29
30
  ResolvedDocument,
30
- } from './types'
31
- import { getFormAndFieldNameFromMetadata } from './util'
32
- import { useSearchParams } from 'react-router-dom'
33
- import { showErrorModal } from './errors'
31
+ SystemInfo,
32
+ } from './types';
33
+ import { getFormAndFieldNameFromMetadata } from './util';
34
34
 
35
35
  const sysSchema = z.object({
36
36
  breadcrumbs: z.array(z.string()),
@@ -45,19 +45,19 @@ const sysSchema = z.object({
45
45
  collection: z.object({
46
46
  name: z.string(),
47
47
  slug: z.string(),
48
- label: z.string(),
48
+ label: z.string().optional().nullable(),
49
49
  path: z.string(),
50
50
  format: z.string().optional().nullable(),
51
51
  matches: z.string().optional().nullable(),
52
52
  }),
53
- })
53
+ });
54
54
 
55
55
  const documentSchema: z.ZodType<ResolvedDocument> = z.object({
56
56
  _internalValues: z.record(z.unknown()),
57
57
  _internalSys: sysSchema,
58
- })
58
+ });
59
59
 
60
- const astNode = schemaJson as G.DocumentNode
60
+ const astNode = schemaJson as G.DocumentNode;
61
61
  const astNodeWithMeta: G.DocumentNode = {
62
62
  ...astNode,
63
63
  definitions: astNode.definitions.map((def) => {
@@ -103,7 +103,7 @@ const astNodeWithMeta: G.DocumentNode = {
103
103
  },
104
104
  },
105
105
  ],
106
- }
106
+ };
107
107
  }
108
108
  if (def.kind === 'ObjectTypeDefinition') {
109
109
  return {
@@ -147,68 +147,68 @@ const astNodeWithMeta: G.DocumentNode = {
147
147
  },
148
148
  },
149
149
  ],
150
- }
150
+ };
151
151
  }
152
- return def
152
+ return def;
153
153
  }),
154
- }
155
- const schema = G.buildASTSchema(astNode)
156
- const schemaForResolver = G.buildASTSchema(astNodeWithMeta)
154
+ };
155
+ const schema = G.buildASTSchema(astNode);
156
+ const schemaForResolver = G.buildASTSchema(astNodeWithMeta);
157
157
 
158
158
  const isRejected = (
159
159
  input: PromiseSettledResult<unknown>
160
- ): input is PromiseRejectedResult => input.status === 'rejected'
160
+ ): input is PromiseRejectedResult => input.status === 'rejected';
161
161
 
162
162
  const isFulfilled = <T>(
163
163
  input: PromiseSettledResult<T>
164
- ): input is PromiseFulfilledResult<T> => input.status === 'fulfilled'
164
+ ): input is PromiseFulfilledResult<T> => input.status === 'fulfilled';
165
165
 
166
166
  export const useGraphQLReducer = (
167
167
  iframe: React.MutableRefObject<HTMLIFrameElement>,
168
168
  url: string
169
169
  ) => {
170
- const cms = useCMS()
171
- const tinaSchema = cms.api.tina.schema as TinaSchema
172
- const [payloads, setPayloads] = React.useState<Payload[]>([])
173
- const [requestErrors, setRequestErrors] = React.useState<string[]>([])
174
- const [searchParams, setSearchParams] = useSearchParams()
170
+ const cms = useCMS();
171
+ const tinaSchema = cms.api.tina.schema as TinaSchema;
172
+ const [payloads, setPayloads] = React.useState<Payload[]>([]);
173
+ const [requestErrors, setRequestErrors] = React.useState<string[]>([]);
174
+ const [searchParams, setSearchParams] = useSearchParams();
175
175
  const [results, setResults] = React.useState<
176
176
  {
177
- id: string
177
+ id: string;
178
178
  data:
179
179
  | {
180
- [key: string]: any
180
+ [key: string]: any;
181
181
  }
182
182
  | null
183
- | undefined
183
+ | undefined;
184
184
  }[]
185
- >([])
185
+ >([]);
186
186
  const [documentsToResolve, setDocumentsToResolve] = React.useState<string[]>(
187
187
  []
188
- )
188
+ );
189
189
  const [resolvedDocuments, setResolvedDocuments] = React.useState<
190
190
  ResolvedDocument[]
191
- >([])
192
- const [operationIndex, setOperationIndex] = React.useState(0)
191
+ >([]);
192
+ const [operationIndex, setOperationIndex] = React.useState(0);
193
193
 
194
- const activeField = searchParams.get('active-field')
194
+ const activeField = searchParams.get('active-field');
195
195
 
196
196
  React.useEffect(() => {
197
197
  const run = async () => {
198
198
  return Promise.all(
199
199
  documentsToResolve.map(async (documentId) => {
200
- return await getDocument(documentId, cms.api.tina)
200
+ return await getDocument(documentId, cms.api.tina);
201
201
  })
202
- )
203
- }
202
+ );
203
+ };
204
204
  if (documentsToResolve.length) {
205
205
  run().then((docs) => {
206
- setResolvedDocuments((resolvedDocs) => [...resolvedDocs, ...docs])
207
- setDocumentsToResolve([])
208
- setOperationIndex((i) => i + 1)
209
- })
206
+ setResolvedDocuments((resolvedDocs) => [...resolvedDocs, ...docs]);
207
+ setDocumentsToResolve([]);
208
+ setOperationIndex((i) => i + 1);
209
+ });
210
210
  }
211
- }, [documentsToResolve.join('.')])
211
+ }, [documentsToResolve.join('.')]);
212
212
 
213
213
  /**
214
214
  * Note: since React runs effects twice in development this will run twice for a given query
@@ -216,39 +216,41 @@ export const useGraphQLReducer = (
216
216
  */
217
217
  React.useEffect(() => {
218
218
  const run = async () => {
219
- setRequestErrors([])
219
+ setRequestErrors([]);
220
220
  // gather the errors and display an error message containing each error unique message
221
221
  return Promise.allSettled(
222
222
  payloads.map(async (payload) => {
223
223
  // This payload has already been expanded, skip it.
224
224
  if (payload.expandedQuery) {
225
- return payload
225
+ return payload;
226
226
  } else {
227
- const expandedPayload = await expandPayload(payload, cms)
228
- processPayload(expandedPayload)
229
- return expandedPayload
227
+ const expandedPayload = await expandPayload(payload, cms);
228
+ processPayload(expandedPayload);
229
+ return expandedPayload;
230
230
  }
231
231
  })
232
- )
233
- }
232
+ );
233
+ };
234
234
  if (payloads.length) {
235
235
  run().then((updatedPayloads) => {
236
- setPayloads(updatedPayloads.filter(isFulfilled).map((p) => p.value))
236
+ setPayloads(updatedPayloads.filter(isFulfilled).map((p) => p.value));
237
237
  setRequestErrors(
238
238
  updatedPayloads.filter(isRejected).map((p) => String(p.reason))
239
- )
240
- })
239
+ );
240
+ });
241
241
  }
242
- }, [JSON.stringify(payloads), cms])
242
+ }, [JSON.stringify(payloads), cms]);
243
243
 
244
244
  const processPayload = React.useCallback(
245
245
  (payload: Payload) => {
246
- const { expandedQueryForResolver, variables, expandedData } = payload
246
+ const { expandedQueryForResolver, variables, expandedData } = payload;
247
247
  if (!expandedQueryForResolver || !expandedData) {
248
- throw new Error(`Unable to process payload which has not been expanded`)
248
+ throw new Error(
249
+ `Unable to process payload which has not been expanded`
250
+ );
249
251
  }
250
- const formListItems: TinaState['formLists'][number]['items'] = []
251
- const formIds: string[] = []
252
+ const formListItems: TinaState['formLists'][number]['items'] = [];
253
+ const formIds: string[] = [];
252
254
 
253
255
  const result = G.graphqlSync({
254
256
  schema: schemaForResolver,
@@ -256,7 +258,7 @@ export const useGraphQLReducer = (
256
258
  variableValues: variables,
257
259
  rootValue: expandedData,
258
260
  fieldResolver: (source, args, context, info) => {
259
- const fieldName = info.fieldName
261
+ const fieldName = info.fieldName;
260
262
  /**
261
263
  * Since the `source` for this resolver is the query that
262
264
  * ran before passing it into `useTina`, we need to take aliases
@@ -265,35 +267,35 @@ export const useGraphQLReducer = (
265
267
  * solution as the `value` gets overwritten depending on the alias
266
268
  * query.
267
269
  */
268
- const aliases: string[] = []
270
+ const aliases: string[] = [];
269
271
  info.fieldNodes.forEach((fieldNode) => {
270
272
  if (fieldNode.alias) {
271
- aliases.push(fieldNode.alias.value)
273
+ aliases.push(fieldNode.alias.value);
272
274
  }
273
- })
274
- let value = source[fieldName] as unknown
275
+ });
276
+ let value = source[fieldName] as unknown;
275
277
  aliases.forEach((alias) => {
276
- const aliasValue = source[alias]
278
+ const aliasValue = source[alias];
277
279
  if (aliasValue) {
278
- value = aliasValue
280
+ value = aliasValue;
279
281
  }
280
- })
282
+ });
281
283
  if (fieldName === '_sys') {
282
- return source._internalSys
284
+ return source._internalSys;
283
285
  }
284
286
  if (fieldName === '_values') {
285
- return source._internalValues
287
+ return source._internalValues;
286
288
  }
287
289
  if (info.fieldName === '_content_source') {
288
- const pathArray = G.responsePathAsArray(info.path)
290
+ const pathArray = G.responsePathAsArray(info.path);
289
291
  return {
290
292
  queryId: payload.id,
291
293
  path: pathArray.slice(0, pathArray.length - 1),
292
- }
294
+ };
293
295
  }
294
296
  if (info.fieldName === '_tina_metadata') {
295
297
  if (value) {
296
- return value
298
+ return value;
297
299
  }
298
300
  // TODO: ensure all fields that have _tina_metadata
299
301
  // actually need it
@@ -301,77 +303,80 @@ export const useGraphQLReducer = (
301
303
  id: null,
302
304
  fields: [],
303
305
  prefix: '',
304
- }
306
+ };
305
307
  }
308
+
306
309
  if (isConnectionType(info.returnType)) {
307
- const name = G.getNamedType(info.returnType).name
310
+ const name = G.getNamedType(info.returnType).name;
308
311
  const connectionCollection = tinaSchema
309
312
  .getCollections()
310
313
  .find((collection) => {
311
314
  const collectionName = NAMER.referenceConnectionType(
312
315
  collection.namespace
313
- )
316
+ );
314
317
  if (collectionName === name) {
315
- return true
318
+ return true;
316
319
  }
317
- return false
318
- })
320
+ return false;
321
+ });
319
322
  if (connectionCollection) {
320
323
  formListItems.push({
321
324
  type: 'list',
322
325
  label: connectionCollection.label || connectionCollection.name,
323
- })
326
+ });
324
327
  }
325
328
  }
326
329
  if (isNodeType(info.returnType)) {
327
330
  if (!value) {
328
- return
331
+ return;
329
332
  }
330
- let resolvedDocument: ResolvedDocument
333
+ let resolvedDocument: ResolvedDocument;
331
334
  // This is a reference from another form
332
335
  if (typeof value === 'string') {
333
336
  const valueFromSetup = getIn(
334
337
  expandedData,
335
338
  G.responsePathAsArray(info.path).join('.')
336
- )
339
+ );
337
340
  const maybeResolvedDocument = resolvedDocuments.find(
338
341
  (doc) => doc._internalSys.path === value
339
- )
342
+ );
343
+
340
344
  // If we already have this document, use it.
341
345
  if (maybeResolvedDocument) {
342
- resolvedDocument = maybeResolvedDocument
346
+ resolvedDocument = maybeResolvedDocument;
343
347
  } else if (valueFromSetup) {
344
348
  // Else, even though in this context the value is a string because it's
345
349
  // resolved from a parent form, if the reference hasn't changed
346
350
  // from when we ran the setup query, we can avoid a data fetch
347
351
  // here and just grab it from the response
348
352
  const maybeResolvedDocument =
349
- documentSchema.parse(valueFromSetup)
353
+ documentSchema.parse(valueFromSetup);
354
+
350
355
  if (maybeResolvedDocument._internalSys.path === value) {
351
- resolvedDocument = maybeResolvedDocument
356
+ resolvedDocument = maybeResolvedDocument;
352
357
  } else {
353
- throw new NoFormError(`No form found`, value)
358
+ throw new NoFormError(`No form found`, value);
354
359
  }
355
360
  } else {
356
- throw new NoFormError(`No form found`, value)
361
+ throw new NoFormError(`No form found`, value);
357
362
  }
358
363
  } else {
359
- resolvedDocument = documentSchema.parse(value)
364
+ resolvedDocument = documentSchema.parse(value);
360
365
  }
361
- const id = resolvedDocument._internalSys.path
362
- formIds.push(id)
366
+ const id = resolvedDocument._internalSys.path;
367
+ formIds.push(id);
363
368
  const existingForm = cms.state.forms.find(
364
369
  (f) => f.tinaForm.id === id
365
- )
370
+ );
366
371
 
367
- const pathArray = G.responsePathAsArray(info.path)
368
- const pathString = pathArray.join('.')
372
+ const pathArray = G.responsePathAsArray(info.path);
373
+ const pathString = pathArray.join('.');
369
374
  const ancestors = formListItems.filter((item) => {
370
375
  if (item.type === 'document') {
371
- return pathString.startsWith(item.path)
376
+ return pathString.startsWith(item.path);
372
377
  }
373
- })
374
- const parent = ancestors[ancestors.length - 1]
378
+ });
379
+ const parent = ancestors[ancestors.length - 1];
375
380
  if (parent) {
376
381
  if (parent.type === 'document') {
377
382
  parent.subItems.push({
@@ -379,7 +384,7 @@ export const useGraphQLReducer = (
379
384
  path: pathString,
380
385
  formId: id,
381
386
  subItems: [],
382
- })
387
+ });
383
388
  }
384
389
  } else {
385
390
  formListItems.push({
@@ -387,7 +392,7 @@ export const useGraphQLReducer = (
387
392
  path: pathString,
388
393
  formId: id,
389
394
  subItems: [],
390
- })
395
+ });
391
396
  }
392
397
 
393
398
  if (!existingForm) {
@@ -396,65 +401,65 @@ export const useGraphQLReducer = (
396
401
  tinaSchema,
397
402
  payloadId: payload.id,
398
403
  cms,
399
- })
404
+ });
400
405
  form.subscribe(
401
406
  () => {
402
- setOperationIndex((i) => i + 1)
407
+ setOperationIndex((i) => i + 1);
403
408
  },
404
409
  { values: true }
405
- )
410
+ );
406
411
  return resolveDocument(
407
412
  resolvedDocument,
408
413
  template,
409
414
  form,
410
415
  pathString
411
- )
416
+ );
412
417
  } else {
413
- existingForm.tinaForm.addQuery(payload.id)
418
+ existingForm.tinaForm.addQuery(payload.id);
414
419
  const { template } = getTemplateForDocument(
415
420
  resolvedDocument,
416
421
  tinaSchema
417
- )
418
- existingForm.tinaForm.addQuery(payload.id)
422
+ );
423
+ existingForm.tinaForm.addQuery(payload.id);
419
424
  return resolveDocument(
420
425
  resolvedDocument,
421
426
  template,
422
427
  existingForm.tinaForm,
423
428
  pathString
424
- )
429
+ );
425
430
  }
426
431
  }
427
- return value
432
+ return value;
428
433
  },
429
- })
434
+ });
430
435
  if (result.errors) {
431
436
  result.errors.forEach((error) => {
432
437
  if (
433
438
  error instanceof G.GraphQLError &&
434
439
  error.originalError instanceof NoFormError
435
440
  ) {
436
- const id = error.originalError.id
441
+ const id = error.originalError.id;
437
442
  setDocumentsToResolve((docs) => [
438
443
  ...docs.filter((doc) => doc !== id),
439
444
  id,
440
- ])
445
+ ]);
441
446
  } else {
442
- console.log(error)
447
+ console.log(error);
443
448
  // throw new Error(
444
449
  // `Error processing value change, please contact support`
445
450
  // )
446
451
  }
447
- })
452
+ });
448
453
  } else {
449
454
  if (result.data) {
450
455
  setResults((results) => [
451
456
  ...results.filter((result) => result.id !== payload.id),
452
457
  { id: payload.id, data: result.data },
453
- ])
458
+ ]);
454
459
  }
455
460
  if (activeField) {
456
- setSearchParams({})
457
- const [queryId, eventFieldName] = activeField.split('---')
461
+ setSearchParams({});
462
+ const [queryId, eventFieldName] = activeField.split('---');
458
463
  if (queryId === payload.id) {
459
464
  if (result?.data) {
460
465
  cms.dispatch({
@@ -463,19 +468,19 @@ export const useGraphQLReducer = (
463
468
  result.data,
464
469
  eventFieldName
465
470
  ),
466
- })
471
+ });
467
472
  }
468
473
  cms.dispatch({
469
474
  type: 'sidebar:set-display-state',
470
475
  value: 'openOrFull',
471
- })
476
+ });
472
477
  }
473
478
  }
474
479
  iframe.current?.contentWindow?.postMessage({
475
480
  type: 'updateData',
476
481
  id: payload.id,
477
482
  data: result.data,
478
- })
483
+ });
479
484
  }
480
485
  cms.dispatch({
481
486
  type: 'form-lists:add',
@@ -485,57 +490,96 @@ export const useGraphQLReducer = (
485
490
  items: formListItems,
486
491
  formIds,
487
492
  },
488
- })
493
+ });
489
494
  },
490
495
  [
491
496
  resolvedDocuments.map((doc) => doc._internalSys.path).join('.'),
492
497
  activeField,
493
498
  ]
494
- )
499
+ );
495
500
 
496
501
  const handleMessage = React.useCallback(
497
502
  (event: MessageEvent<PostMessage>) => {
503
+ if (event.data.type === 'user-select-form') {
504
+ cms.dispatch({
505
+ type: 'forms:set-active-form-id',
506
+ value: event.data.formId,
507
+ });
508
+ }
509
+
498
510
  if (event?.data?.type === 'quick-edit') {
499
511
  cms.dispatch({
500
512
  type: 'set-quick-editing-supported',
501
513
  value: event.data.value,
502
- })
514
+ });
503
515
  iframe.current?.contentWindow?.postMessage({
504
516
  type: 'quickEditEnabled',
505
517
  value: cms.state.sidebarDisplayState === 'open',
506
- })
518
+ });
507
519
  }
508
520
  if (event?.data?.type === 'isEditMode') {
509
521
  iframe?.current?.contentWindow?.postMessage({
510
522
  type: 'tina:editMode',
511
- })
523
+ });
512
524
  }
513
525
  if (event.data.type === 'field:selected') {
514
- const [queryId, eventFieldName] = event.data.fieldName.split('---')
515
- const result = results.find((res) => res.id === queryId)
526
+ const [queryId, eventFieldName] = event.data.fieldName.split('---');
527
+ const result = results.find((res) => res.id === queryId);
516
528
  if (result?.data) {
529
+ const { formId, fieldName } = getFormAndFieldNameFromMetadata(
530
+ result.data,
531
+ eventFieldName
532
+ );
517
533
  cms.dispatch({
518
534
  type: 'forms:set-active-field-name',
519
- value: getFormAndFieldNameFromMetadata(result.data, eventFieldName),
520
- })
535
+ value: { formId: formId, fieldName: fieldName },
536
+ });
537
+ cms.events.dispatch({
538
+ ...event.data,
539
+ type: 'field:focus',
540
+ });
521
541
  }
522
542
  cms.dispatch({
523
543
  type: 'sidebar:set-display-state',
524
544
  value: 'openOrFull',
525
- })
545
+ });
546
+ }
547
+ if (event.data.type === 'field:hovered') {
548
+ if (event.data.fieldName) {
549
+ const [queryId, eventFieldName] = event.data.fieldName.split('---');
550
+ const result = results.find((res) => res.id === queryId);
551
+ if (result?.data) {
552
+ const fieldData = getFormAndFieldNameFromMetadata(
553
+ result.data,
554
+ eventFieldName
555
+ );
556
+ cms.dispatch({
557
+ type: 'forms:set-hovered-field-name',
558
+ value: fieldData,
559
+ });
560
+ }
561
+ } else {
562
+ // Clear hover state when fieldName is null
563
+ cms.forms.all().forEach((form) => {
564
+ cms.dispatch({
565
+ type: 'forms:set-hovered-field-name',
566
+ value: { formId: form.id, fieldName: null },
567
+ });
568
+ });
569
+ }
526
570
  }
527
571
  if (event.data.type === 'close') {
528
- const payloadSchema = z.object({ id: z.string() })
529
- const { id } = payloadSchema.parse(event.data)
572
+ const payloadSchema = z.object({ id: z.string() });
573
+ const { id } = payloadSchema.parse(event.data);
530
574
  setPayloads((previous) =>
531
575
  previous.filter((payload) => payload.id !== id)
532
- )
533
- setResults((previous) => previous.filter((result) => result.id !== id))
576
+ );
577
+ setResults((previous) => previous.filter((result) => result.id !== id));
534
578
  cms.forms.all().map((form) => {
535
- form.removeQuery(id)
536
- })
537
- cms.removeOrphanedForms()
538
- cms.dispatch({ type: 'form-lists:remove', value: id })
579
+ form.removeQuery(id);
580
+ });
581
+ cms.removeOrphanedForms();
582
+ cms.dispatch({ type: 'form-lists:remove', value: id });
539
583
  }
540
584
  if (event.data.type === 'open') {
541
585
  const payloadSchema = z.object({
@@ -543,60 +587,95 @@ export const useGraphQLReducer = (
543
587
  query: z.string(),
544
588
  variables: z.record(z.unknown()),
545
589
  data: z.record(z.unknown()),
546
- })
547
- const payload = payloadSchema.parse(event.data)
590
+ });
591
+ const payload = payloadSchema.parse(event.data);
548
592
  setPayloads((payloads) => [
549
593
  ...payloads.filter(({ id }) => id !== payload.id),
550
594
  payload,
551
- ])
595
+ ]);
552
596
  }
597
+ // TODO: This is causing a webpack HMR issue - look into refactoring this logic
598
+ // if (event.data.type === 'url-changed') {
599
+ // console.log('[EVENT_TRIGGERED] url-changed: ', event);
600
+ // cms.dispatch({
601
+ // type: 'sidebar:set-loading-state',
602
+ // value: true,
603
+ // });
604
+ // }
553
605
  },
554
606
  [cms, JSON.stringify(results)]
555
- )
607
+ );
556
608
 
557
609
  React.useEffect(() => {
558
610
  payloads.forEach((payload) => {
559
611
  if (payload.expandedData) {
560
- processPayload(payload)
612
+ processPayload(payload);
561
613
  }
562
- })
563
- }, [operationIndex])
614
+ });
615
+ }, [operationIndex]);
564
616
 
565
617
  React.useEffect(() => {
566
618
  return () => {
567
- setPayloads([])
568
- setResults([])
569
- cms.removeAllForms()
570
- cms.dispatch({ type: 'form-lists:clear' })
571
- }
572
- }, [url])
619
+ setPayloads([]);
620
+ setResults([]);
621
+ cms.removeAllForms();
622
+ cms.dispatch({ type: 'form-lists:clear' });
623
+ };
624
+ }, [url]);
573
625
 
574
626
  React.useEffect(() => {
575
627
  iframe.current?.contentWindow?.postMessage({
576
628
  type: 'quickEditEnabled',
577
629
  value: cms.state.sidebarDisplayState === 'open',
578
- })
579
- }, [cms.state.sidebarDisplayState])
630
+ });
631
+ }, [cms.state.sidebarDisplayState]);
632
+
633
+ // Compute the active field name to send to iframe
634
+ const activeFieldName = React.useMemo(() => {
635
+ const activeForm = cms.state.forms.find(
636
+ (form: any) => form.tinaForm.id === cms.state.activeFormId
637
+ );
638
+ if (!activeForm) {
639
+ return null;
640
+ }
641
+ const fieldName = activeForm.activeFieldName;
642
+ if (fieldName === null) {
643
+ return null;
644
+ }
645
+ const queries = activeForm.tinaForm.queries;
646
+ if (queries && queries.length > 0) {
647
+ const queryId = queries[queries.length - 1];
648
+ return `${queryId}---${fieldName}`;
649
+ }
650
+ return null;
651
+ }, [cms.state.forms, cms.state.activeFormId]);
652
+
653
+ React.useEffect(() => {
654
+ iframe.current?.contentWindow?.postMessage({
655
+ type: 'field:set-focused',
656
+ fieldName: activeFieldName,
657
+ });
658
+ }, [activeFieldName, iframe]);
580
659
 
581
660
  React.useEffect(() => {
582
- cms.dispatch({ type: 'set-edit-mode', value: 'visual' })
661
+ cms.dispatch({ type: 'set-edit-mode', value: 'visual' });
583
662
  if (iframe) {
584
- window.addEventListener('message', handleMessage)
663
+ window.addEventListener('message', handleMessage);
585
664
  }
586
665
 
587
666
  return () => {
588
- window.removeEventListener('message', handleMessage)
589
- cms.removeAllForms()
590
- cms.dispatch({ type: 'set-edit-mode', value: 'basic' })
591
- }
592
- }, [iframe.current, JSON.stringify(results)])
667
+ window.removeEventListener('message', handleMessage);
668
+ cms.removeAllForms();
669
+ cms.dispatch({ type: 'set-edit-mode', value: 'basic' });
670
+ };
671
+ }, [iframe.current, JSON.stringify(results)]);
593
672
 
594
673
  React.useEffect(() => {
595
674
  if (requestErrors.length) {
596
- showErrorModal('Unexpected error querying content', requestErrors, cms)
675
+ showErrorModal('Unexpected error querying content', requestErrors, cms);
597
676
  }
598
- }, [requestErrors])
599
- }
677
+ }, [requestErrors]);
678
+ };
600
679
 
601
680
  const onSubmit = async (
602
681
  collection: Collection<true>,
@@ -604,7 +683,7 @@ const onSubmit = async (
604
683
  payload: Record<string, unknown>,
605
684
  cms: TinaCMS
606
685
  ) => {
607
- const tinaSchema = cms.api.tina.schema
686
+ const tinaSchema = cms.api.tina.schema;
608
687
  try {
609
688
  const mutationString = `#graphql
610
689
  mutation UpdateDocument($collection: String!, $relativePath: String!, $params: DocumentUpdateMutation!) {
@@ -612,7 +691,7 @@ const onSubmit = async (
612
691
  __typename
613
692
  }
614
693
  }
615
- `
694
+ `;
616
695
 
617
696
  await cms.api.tina.request(mutationString, {
618
697
  variables: {
@@ -620,8 +699,8 @@ const onSubmit = async (
620
699
  relativePath: relativePath,
621
700
  params: tinaSchema.transformPayload(collection.name, payload),
622
701
  },
623
- })
624
- cms.alerts.success('Document saved!')
702
+ });
703
+ cms.alerts.success('Document saved!');
625
704
  } catch (e) {
626
705
  cms.alerts.error(() =>
627
706
  ErrorDialog({
@@ -629,12 +708,12 @@ const onSubmit = async (
629
708
  message: 'Tina caught an error while updating the page',
630
709
  error: e,
631
710
  })
632
- )
633
- console.error(e)
711
+ );
712
+ console.error(e);
634
713
  }
635
- }
714
+ };
636
715
 
637
- type Path = (string | number)[]
716
+ type Path = (string | number)[];
638
717
 
639
718
  const resolveDocument = (
640
719
  doc: ResolvedDocument,
@@ -643,20 +722,20 @@ const resolveDocument = (
643
722
  pathToDocument: string
644
723
  ): ResolvedDocument => {
645
724
  // @ts-ignore AnyField and TinaField don't mix
646
- const fields = form.fields as TinaField<true>[]
647
- const id = doc._internalSys.path
648
- const path: Path = []
725
+ const fields = form.fields as TinaField<true>[];
726
+ const id = doc._internalSys.path;
727
+ const path: Path = [];
649
728
  const formValues = resolveFormValue({
650
729
  fields: fields,
651
730
  values: form.values,
652
731
  path,
653
732
  id,
654
733
  pathToDocument,
655
- })
656
- const metadataFields: Record<string, string> = {}
734
+ });
735
+ const metadataFields: Record<string, string> = {};
657
736
  Object.keys(formValues).forEach((key) => {
658
- metadataFields[key] = [...path, key].join('.')
659
- })
737
+ metadataFields[key] = [...path, key].join('.');
738
+ });
660
739
 
661
740
  return {
662
741
  ...formValues,
@@ -672,8 +751,8 @@ const resolveDocument = (
672
751
  _internalSys: doc._internalSys,
673
752
  _internalValues: doc._internalValues,
674
753
  __typename: NAMER.dataTypeName(template.namespace),
675
- }
676
- }
754
+ };
755
+ };
677
756
 
678
757
  const resolveFormValue = <T extends Record<string, unknown>>({
679
758
  fields,
@@ -683,21 +762,21 @@ const resolveFormValue = <T extends Record<string, unknown>>({
683
762
  pathToDocument,
684
763
  }: // tinaSchema,
685
764
  {
686
- fields: TinaField<true>[]
687
- values: T
688
- path: Path
689
- id: string
690
- pathToDocument: string
765
+ fields: TinaField<true>[];
766
+ values: T;
767
+ path: Path;
768
+ id: string;
769
+ pathToDocument: string;
691
770
  // tinaSchema: TinaSchema
692
771
  }): T & { __typename?: string } => {
693
- const accum: Record<string, unknown> = {}
772
+ const accum: Record<string, unknown> = {};
694
773
  fields.forEach((field) => {
695
- const v = values[field.name]
774
+ const v = values[field.name];
696
775
  if (typeof v === 'undefined') {
697
- return
776
+ return;
698
777
  }
699
778
  if (v === null) {
700
- return
779
+ return;
701
780
  }
702
781
  accum[field.name] = resolveFieldValue({
703
782
  field,
@@ -705,10 +784,10 @@ const resolveFormValue = <T extends Record<string, unknown>>({
705
784
  path,
706
785
  id,
707
786
  pathToDocument,
708
- })
709
- })
710
- return accum as T & { __typename?: string }
711
- }
787
+ });
788
+ });
789
+ return accum as T & { __typename?: string };
790
+ };
712
791
  const resolveFieldValue = ({
713
792
  field,
714
793
  value,
@@ -716,11 +795,11 @@ const resolveFieldValue = ({
716
795
  id,
717
796
  pathToDocument,
718
797
  }: {
719
- field: TinaField<true>
720
- value: unknown
721
- path: Path
722
- id: string
723
- pathToDocument: string
798
+ field: TinaField<true>;
799
+ value: unknown;
800
+ path: Path;
801
+ id: string;
802
+ pathToDocument: string;
724
803
  }) => {
725
804
  switch (field.type) {
726
805
  case 'object': {
@@ -728,15 +807,17 @@ const resolveFieldValue = ({
728
807
  if (field.list) {
729
808
  if (Array.isArray(value)) {
730
809
  return value.map((item, index) => {
731
- const template = field.templates[item._template]
810
+ const template = field.templates[item._template];
732
811
  if (typeof template === 'string') {
733
- throw new Error('Global templates not supported')
812
+ throw new Error('Global templates not supported');
734
813
  }
735
- const nextPath = [...path, field.name, index]
736
- const metadataFields: Record<string, string> = {}
814
+ const nextPath = [...path, field.name, index];
815
+ const metadataFields: Record<string, string> = {};
737
816
  template.fields.forEach((field) => {
738
- metadataFields[field.name] = [...nextPath, field.name].join('.')
739
- })
817
+ metadataFields[field.name] = [...nextPath, field.name].join(
818
+ '.'
819
+ );
820
+ });
740
821
  return {
741
822
  __typename: NAMER.dataTypeName(template.namespace),
742
823
  _tina_metadata: {
@@ -752,29 +833,29 @@ const resolveFieldValue = ({
752
833
  id,
753
834
  pathToDocument,
754
835
  }),
755
- }
756
- })
836
+ };
837
+ });
757
838
  }
758
839
  } else {
759
840
  // not implemented
760
841
  }
761
842
  }
762
843
 
763
- const templateFields = field.fields
844
+ const templateFields = field.fields;
764
845
  if (typeof templateFields === 'string') {
765
- throw new Error('Global templates not supported')
846
+ throw new Error('Global templates not supported');
766
847
  }
767
848
  if (!templateFields) {
768
- throw new Error(`Expected to find sub-fields on field ${field.name}`)
849
+ throw new Error(`Expected to find sub-fields on field ${field.name}`);
769
850
  }
770
851
  if (field.list) {
771
852
  if (Array.isArray(value)) {
772
853
  return value.map((item, index) => {
773
- const nextPath = [...path, field.name, index]
774
- const metadataFields: Record<string, string> = {}
854
+ const nextPath = [...path, field.name, index];
855
+ const metadataFields: Record<string, string> = {};
775
856
  templateFields.forEach((field) => {
776
- metadataFields[field.name] = [...nextPath, field.name].join('.')
777
- })
857
+ metadataFields[field.name] = [...nextPath, field.name].join('.');
858
+ });
778
859
  return {
779
860
  __typename: NAMER.dataTypeName(field.namespace),
780
861
  _tina_metadata: {
@@ -790,15 +871,15 @@ const resolveFieldValue = ({
790
871
  id,
791
872
  pathToDocument,
792
873
  }),
793
- }
794
- })
874
+ };
875
+ });
795
876
  }
796
877
  } else {
797
- const nextPath = [...path, field.name]
798
- const metadataFields: Record<string, string> = {}
878
+ const nextPath = [...path, field.name];
879
+ const metadataFields: Record<string, string> = {};
799
880
  templateFields.forEach((field) => {
800
- metadataFields[field.name] = [...nextPath, field.name].join('.')
801
- })
881
+ metadataFields[field.name] = [...nextPath, field.name].join('.');
882
+ });
802
883
  return {
803
884
  __typename: NAMER.dataTypeName(field.namespace),
804
885
  _tina_metadata: {
@@ -814,18 +895,21 @@ const resolveFieldValue = ({
814
895
  id,
815
896
  pathToDocument,
816
897
  }),
817
- }
898
+ };
818
899
  }
819
900
  }
820
901
  default: {
821
- return value
902
+ return value;
822
903
  }
823
904
  }
824
- }
905
+ };
825
906
 
826
907
  const getDocument = async (id: string, tina: Client) => {
827
908
  const response = await tina.request<{
828
- node: { _internalSys: SystemInfo; _internalValues: Record<string, unknown> }
909
+ node: {
910
+ _internalSys: SystemInfo;
911
+ _internalValues: Record<string, unknown>;
912
+ };
829
913
  }>(
830
914
  `query GetNode($id: String!) {
831
915
  node(id: $id) {
@@ -858,29 +942,29 @@ _internalSys: _sys {
858
942
  }
859
943
  }`,
860
944
  { variables: { id: id } }
861
- )
862
- return response.node
863
- }
945
+ );
946
+ return response.node;
947
+ };
864
948
 
865
949
  const expandPayload = async (
866
950
  payload: Payload,
867
951
  cms: TinaCMS
868
952
  ): Promise<Payload> => {
869
- const { query, variables } = payload
870
- const documentNode = G.parse(query)
871
- const expandedDocumentNode = expandQuery({ schema, documentNode })
872
- const expandedQuery = G.print(expandedDocumentNode)
953
+ const { query, variables } = payload;
954
+ const documentNode = G.parse(query);
955
+ const expandedDocumentNode = expandQuery({ schema, documentNode });
956
+ const expandedQuery = G.print(expandedDocumentNode);
873
957
  const expandedData = await cms.api.tina.request<object>(expandedQuery, {
874
958
  variables,
875
- })
959
+ });
876
960
 
877
961
  const expandedDocumentNodeForResolver = expandQuery({
878
962
  schema: schemaForResolver,
879
963
  documentNode,
880
- })
881
- const expandedQueryForResolver = G.print(expandedDocumentNodeForResolver)
882
- return { ...payload, expandedQuery, expandedData, expandedQueryForResolver }
883
- }
964
+ });
965
+ const expandedQueryForResolver = G.print(expandedDocumentNodeForResolver);
966
+ return { ...payload, expandedQuery, expandedData, expandedQueryForResolver };
967
+ };
884
968
 
885
969
  /**
886
970
  * When we resolve the graphql data we check for these errors,
@@ -888,11 +972,11 @@ const expandPayload = async (
888
972
  * process it once we have that document
889
973
  */
890
974
  class NoFormError extends Error {
891
- id: string
975
+ id: string;
892
976
  constructor(msg: string, id: string) {
893
- super(msg)
894
- this.id = id
895
- Object.setPrototypeOf(this, NoFormError.prototype)
977
+ super(msg);
978
+ this.id = id;
979
+ Object.setPrototypeOf(this, NoFormError.prototype);
896
980
  }
897
981
  }
898
982
 
@@ -900,22 +984,22 @@ const getTemplateForDocument = (
900
984
  resolvedDocument: ResolvedDocument,
901
985
  tinaSchema: TinaSchema
902
986
  ) => {
903
- const id = resolvedDocument._internalSys.path
904
- let collection: Collection<true> | undefined
987
+ const id = resolvedDocument._internalSys.path;
988
+ let collection: Collection<true> | undefined;
905
989
  try {
906
- collection = tinaSchema.getCollectionByFullPath(id)
990
+ collection = tinaSchema.getCollectionByFullPath(id);
907
991
  } catch (e) {}
908
992
 
909
993
  if (!collection) {
910
- throw new Error(`Unable to determine collection for path ${id}`)
994
+ throw new Error(`Unable to determine collection for path ${id}`);
911
995
  }
912
996
 
913
997
  const template = tinaSchema.getTemplateForData({
914
998
  data: resolvedDocument._internalValues,
915
999
  collection,
916
- })
917
- return { template, collection }
918
- }
1000
+ });
1001
+ return { template, collection };
1002
+ };
919
1003
 
920
1004
  const buildForm = ({
921
1005
  resolvedDocument,
@@ -923,18 +1007,18 @@ const buildForm = ({
923
1007
  payloadId,
924
1008
  cms,
925
1009
  }: {
926
- resolvedDocument: ResolvedDocument
927
- tinaSchema: TinaSchema
928
- payloadId: string
929
- cms: TinaCMS
1010
+ resolvedDocument: ResolvedDocument;
1011
+ tinaSchema: TinaSchema;
1012
+ payloadId: string;
1013
+ cms: TinaCMS;
930
1014
  }) => {
931
1015
  const { template, collection } = getTemplateForDocument(
932
1016
  resolvedDocument,
933
1017
  tinaSchema
934
- )
935
- const id = resolvedDocument._internalSys.path
936
- let form: Form | undefined
937
- let shouldRegisterForm = true
1018
+ );
1019
+ const id = resolvedDocument._internalSys.path;
1020
+ let form: Form | undefined;
1021
+ let shouldRegisterForm = true;
938
1022
  const formConfig: FormOptions<any> = {
939
1023
  id,
940
1024
  initialValues: resolvedDocument._internalValues,
@@ -947,10 +1031,10 @@ const buildForm = ({
947
1031
  cms
948
1032
  ),
949
1033
  label: collection.label || collection.name,
950
- }
1034
+ };
951
1035
  if (tinaSchema.config.config?.formifyCallback) {
952
1036
  const callback = tinaSchema.config.config
953
- ?.formifyCallback as FormifyCallback
1037
+ ?.formifyCallback as FormifyCallback;
954
1038
  form =
955
1039
  callback(
956
1040
  {
@@ -960,30 +1044,30 @@ const buildForm = ({
960
1044
  formConfig,
961
1045
  },
962
1046
  cms
963
- ) || undefined
1047
+ ) || undefined;
964
1048
  if (!form) {
965
1049
  // If the form isn't created from formify, we still
966
1050
  // need it, just don't show it to the user.
967
- shouldRegisterForm = false
968
- form = new Form(formConfig)
1051
+ shouldRegisterForm = false;
1052
+ form = new Form(formConfig);
969
1053
  }
970
1054
  } else {
971
1055
  if (collection.ui?.global) {
972
- form = createGlobalForm(formConfig)
1056
+ form = createGlobalForm(formConfig);
973
1057
  } else {
974
- form = createForm(formConfig)
1058
+ form = createForm(formConfig);
975
1059
  }
976
1060
  }
977
1061
  if (form) {
978
1062
  if (shouldRegisterForm) {
979
1063
  if (collection.ui?.global) {
980
- cms.plugins.add(new GlobalFormPlugin(form))
1064
+ cms.plugins.add(new GlobalFormPlugin(form));
981
1065
  }
982
- cms.dispatch({ type: 'forms:add', value: form })
1066
+ cms.dispatch({ type: 'forms:add', value: form });
983
1067
  }
984
1068
  }
985
1069
  if (!form) {
986
- throw new Error(`No form registered for ${id}.`)
1070
+ throw new Error(`No form registered for ${id}.`);
987
1071
  }
988
- return { template, form }
989
- }
1072
+ return { template, form };
1073
+ };