@tinacms/app 0.0.0-c1132cd-20241024060747 → 0.0.0-c19d29e-20251224001156

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