@tinacms/app 0.0.0-e0ddb8c-20241004065742 → 0.0.0-e1b6d05-20251114052813

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,64 @@ 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) {
516
529
  cms.dispatch({
517
530
  type: 'forms:set-active-field-name',
518
531
  value: getFormAndFieldNameFromMetadata(result.data, eventFieldName),
519
- })
532
+ });
520
533
  }
521
534
  cms.dispatch({
522
535
  type: 'sidebar:set-display-state',
523
536
  value: 'openOrFull',
524
- })
537
+ });
525
538
  }
526
539
  if (event.data.type === 'close') {
527
- const payloadSchema = z.object({ id: z.string() })
528
- const { id } = payloadSchema.parse(event.data)
540
+ const payloadSchema = z.object({ id: z.string() });
541
+ const { id } = payloadSchema.parse(event.data);
529
542
  setPayloads((previous) =>
530
543
  previous.filter((payload) => payload.id !== id)
531
- )
532
- setResults((previous) => previous.filter((result) => result.id !== id))
544
+ );
545
+ setResults((previous) => previous.filter((result) => result.id !== id));
533
546
  cms.forms.all().map((form) => {
534
- form.removeQuery(id)
535
- })
536
- cms.removeOrphanedForms()
537
- cms.dispatch({ type: 'form-lists:remove', value: id })
547
+ form.removeQuery(id);
548
+ });
549
+ cms.removeOrphanedForms();
550
+ cms.dispatch({ type: 'form-lists:remove', value: id });
538
551
  }
539
552
  if (event.data.type === 'open') {
540
553
  const payloadSchema = z.object({
@@ -542,60 +555,68 @@ export const useGraphQLReducer = (
542
555
  query: z.string(),
543
556
  variables: z.record(z.unknown()),
544
557
  data: z.record(z.unknown()),
545
- })
546
- const payload = payloadSchema.parse(event.data)
558
+ });
559
+ const payload = payloadSchema.parse(event.data);
547
560
  setPayloads((payloads) => [
548
561
  ...payloads.filter(({ id }) => id !== payload.id),
549
562
  payload,
550
- ])
563
+ ]);
551
564
  }
565
+ // TODO: This is causing a webpack HMR issue - look into refactoring this logic
566
+ // if (event.data.type === 'url-changed') {
567
+ // console.log('[EVENT_TRIGGERED] url-changed: ', event);
568
+ // cms.dispatch({
569
+ // type: 'sidebar:set-loading-state',
570
+ // value: true,
571
+ // });
572
+ // }
552
573
  },
553
574
  [cms, JSON.stringify(results)]
554
- )
575
+ );
555
576
 
556
577
  React.useEffect(() => {
557
578
  payloads.forEach((payload) => {
558
579
  if (payload.expandedData) {
559
- processPayload(payload)
580
+ processPayload(payload);
560
581
  }
561
- })
562
- }, [operationIndex])
582
+ });
583
+ }, [operationIndex]);
563
584
 
564
585
  React.useEffect(() => {
565
586
  return () => {
566
- setPayloads([])
567
- setResults([])
568
- cms.removeAllForms()
569
- cms.dispatch({ type: 'form-lists:clear' })
570
- }
571
- }, [url])
587
+ setPayloads([]);
588
+ setResults([]);
589
+ cms.removeAllForms();
590
+ cms.dispatch({ type: 'form-lists:clear' });
591
+ };
592
+ }, [url]);
572
593
 
573
594
  React.useEffect(() => {
574
595
  iframe.current?.contentWindow?.postMessage({
575
596
  type: 'quickEditEnabled',
576
597
  value: cms.state.sidebarDisplayState === 'open',
577
- })
578
- }, [cms.state.sidebarDisplayState])
598
+ });
599
+ }, [cms.state.sidebarDisplayState]);
579
600
 
580
601
  React.useEffect(() => {
581
- cms.dispatch({ type: 'set-edit-mode', value: 'visual' })
602
+ cms.dispatch({ type: 'set-edit-mode', value: 'visual' });
582
603
  if (iframe) {
583
- window.addEventListener('message', handleMessage)
604
+ window.addEventListener('message', handleMessage);
584
605
  }
585
606
 
586
607
  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)])
608
+ window.removeEventListener('message', handleMessage);
609
+ cms.removeAllForms();
610
+ cms.dispatch({ type: 'set-edit-mode', value: 'basic' });
611
+ };
612
+ }, [iframe.current, JSON.stringify(results)]);
592
613
 
593
614
  React.useEffect(() => {
594
615
  if (requestErrors.length) {
595
- showErrorModal('Unexpected error querying content', requestErrors, cms)
616
+ showErrorModal('Unexpected error querying content', requestErrors, cms);
596
617
  }
597
- }, [requestErrors])
598
- }
618
+ }, [requestErrors]);
619
+ };
599
620
 
600
621
  const onSubmit = async (
601
622
  collection: Collection<true>,
@@ -603,7 +624,7 @@ const onSubmit = async (
603
624
  payload: Record<string, unknown>,
604
625
  cms: TinaCMS
605
626
  ) => {
606
- const tinaSchema = cms.api.tina.schema
627
+ const tinaSchema = cms.api.tina.schema;
607
628
  try {
608
629
  const mutationString = `#graphql
609
630
  mutation UpdateDocument($collection: String!, $relativePath: String!, $params: DocumentUpdateMutation!) {
@@ -611,7 +632,7 @@ const onSubmit = async (
611
632
  __typename
612
633
  }
613
634
  }
614
- `
635
+ `;
615
636
 
616
637
  await cms.api.tina.request(mutationString, {
617
638
  variables: {
@@ -619,8 +640,8 @@ const onSubmit = async (
619
640
  relativePath: relativePath,
620
641
  params: tinaSchema.transformPayload(collection.name, payload),
621
642
  },
622
- })
623
- cms.alerts.success('Document saved!')
643
+ });
644
+ cms.alerts.success('Document saved!');
624
645
  } catch (e) {
625
646
  cms.alerts.error(() =>
626
647
  ErrorDialog({
@@ -628,12 +649,12 @@ const onSubmit = async (
628
649
  message: 'Tina caught an error while updating the page',
629
650
  error: e,
630
651
  })
631
- )
632
- console.error(e)
652
+ );
653
+ console.error(e);
633
654
  }
634
- }
655
+ };
635
656
 
636
- type Path = (string | number)[]
657
+ type Path = (string | number)[];
637
658
 
638
659
  const resolveDocument = (
639
660
  doc: ResolvedDocument,
@@ -642,20 +663,20 @@ const resolveDocument = (
642
663
  pathToDocument: string
643
664
  ): ResolvedDocument => {
644
665
  // @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 = []
666
+ const fields = form.fields as TinaField<true>[];
667
+ const id = doc._internalSys.path;
668
+ const path: Path = [];
648
669
  const formValues = resolveFormValue({
649
670
  fields: fields,
650
671
  values: form.values,
651
672
  path,
652
673
  id,
653
674
  pathToDocument,
654
- })
655
- const metadataFields: Record<string, string> = {}
675
+ });
676
+ const metadataFields: Record<string, string> = {};
656
677
  Object.keys(formValues).forEach((key) => {
657
- metadataFields[key] = [...path, key].join('.')
658
- })
678
+ metadataFields[key] = [...path, key].join('.');
679
+ });
659
680
 
660
681
  return {
661
682
  ...formValues,
@@ -671,8 +692,8 @@ const resolveDocument = (
671
692
  _internalSys: doc._internalSys,
672
693
  _internalValues: doc._internalValues,
673
694
  __typename: NAMER.dataTypeName(template.namespace),
674
- }
675
- }
695
+ };
696
+ };
676
697
 
677
698
  const resolveFormValue = <T extends Record<string, unknown>>({
678
699
  fields,
@@ -682,21 +703,21 @@ const resolveFormValue = <T extends Record<string, unknown>>({
682
703
  pathToDocument,
683
704
  }: // tinaSchema,
684
705
  {
685
- fields: TinaField<true>[]
686
- values: T
687
- path: Path
688
- id: string
689
- pathToDocument: string
706
+ fields: TinaField<true>[];
707
+ values: T;
708
+ path: Path;
709
+ id: string;
710
+ pathToDocument: string;
690
711
  // tinaSchema: TinaSchema
691
712
  }): T & { __typename?: string } => {
692
- const accum: Record<string, unknown> = {}
713
+ const accum: Record<string, unknown> = {};
693
714
  fields.forEach((field) => {
694
- const v = values[field.name]
715
+ const v = values[field.name];
695
716
  if (typeof v === 'undefined') {
696
- return
717
+ return;
697
718
  }
698
719
  if (v === null) {
699
- return
720
+ return;
700
721
  }
701
722
  accum[field.name] = resolveFieldValue({
702
723
  field,
@@ -704,10 +725,10 @@ const resolveFormValue = <T extends Record<string, unknown>>({
704
725
  path,
705
726
  id,
706
727
  pathToDocument,
707
- })
708
- })
709
- return accum as T & { __typename?: string }
710
- }
728
+ });
729
+ });
730
+ return accum as T & { __typename?: string };
731
+ };
711
732
  const resolveFieldValue = ({
712
733
  field,
713
734
  value,
@@ -715,11 +736,11 @@ const resolveFieldValue = ({
715
736
  id,
716
737
  pathToDocument,
717
738
  }: {
718
- field: TinaField<true>
719
- value: unknown
720
- path: Path
721
- id: string
722
- pathToDocument: string
739
+ field: TinaField<true>;
740
+ value: unknown;
741
+ path: Path;
742
+ id: string;
743
+ pathToDocument: string;
723
744
  }) => {
724
745
  switch (field.type) {
725
746
  case 'object': {
@@ -727,15 +748,17 @@ const resolveFieldValue = ({
727
748
  if (field.list) {
728
749
  if (Array.isArray(value)) {
729
750
  return value.map((item, index) => {
730
- const template = field.templates[item._template]
751
+ const template = field.templates[item._template];
731
752
  if (typeof template === 'string') {
732
- throw new Error('Global templates not supported')
753
+ throw new Error('Global templates not supported');
733
754
  }
734
- const nextPath = [...path, field.name, index]
735
- const metadataFields: Record<string, string> = {}
755
+ const nextPath = [...path, field.name, index];
756
+ const metadataFields: Record<string, string> = {};
736
757
  template.fields.forEach((field) => {
737
- metadataFields[field.name] = [...nextPath, field.name].join('.')
738
- })
758
+ metadataFields[field.name] = [...nextPath, field.name].join(
759
+ '.'
760
+ );
761
+ });
739
762
  return {
740
763
  __typename: NAMER.dataTypeName(template.namespace),
741
764
  _tina_metadata: {
@@ -751,29 +774,29 @@ const resolveFieldValue = ({
751
774
  id,
752
775
  pathToDocument,
753
776
  }),
754
- }
755
- })
777
+ };
778
+ });
756
779
  }
757
780
  } else {
758
781
  // not implemented
759
782
  }
760
783
  }
761
784
 
762
- const templateFields = field.fields
785
+ const templateFields = field.fields;
763
786
  if (typeof templateFields === 'string') {
764
- throw new Error('Global templates not supported')
787
+ throw new Error('Global templates not supported');
765
788
  }
766
789
  if (!templateFields) {
767
- throw new Error(`Expected to find sub-fields on field ${field.name}`)
790
+ throw new Error(`Expected to find sub-fields on field ${field.name}`);
768
791
  }
769
792
  if (field.list) {
770
793
  if (Array.isArray(value)) {
771
794
  return value.map((item, index) => {
772
- const nextPath = [...path, field.name, index]
773
- const metadataFields: Record<string, string> = {}
795
+ const nextPath = [...path, field.name, index];
796
+ const metadataFields: Record<string, string> = {};
774
797
  templateFields.forEach((field) => {
775
- metadataFields[field.name] = [...nextPath, field.name].join('.')
776
- })
798
+ metadataFields[field.name] = [...nextPath, field.name].join('.');
799
+ });
777
800
  return {
778
801
  __typename: NAMER.dataTypeName(field.namespace),
779
802
  _tina_metadata: {
@@ -789,15 +812,15 @@ const resolveFieldValue = ({
789
812
  id,
790
813
  pathToDocument,
791
814
  }),
792
- }
793
- })
815
+ };
816
+ });
794
817
  }
795
818
  } else {
796
- const nextPath = [...path, field.name]
797
- const metadataFields: Record<string, string> = {}
819
+ const nextPath = [...path, field.name];
820
+ const metadataFields: Record<string, string> = {};
798
821
  templateFields.forEach((field) => {
799
- metadataFields[field.name] = [...nextPath, field.name].join('.')
800
- })
822
+ metadataFields[field.name] = [...nextPath, field.name].join('.');
823
+ });
801
824
  return {
802
825
  __typename: NAMER.dataTypeName(field.namespace),
803
826
  _tina_metadata: {
@@ -813,18 +836,21 @@ const resolveFieldValue = ({
813
836
  id,
814
837
  pathToDocument,
815
838
  }),
816
- }
839
+ };
817
840
  }
818
841
  }
819
842
  default: {
820
- return value
843
+ return value;
821
844
  }
822
845
  }
823
- }
846
+ };
824
847
 
825
848
  const getDocument = async (id: string, tina: Client) => {
826
849
  const response = await tina.request<{
827
- node: { _internalSys: SystemInfo; _internalValues: Record<string, unknown> }
850
+ node: {
851
+ _internalSys: SystemInfo;
852
+ _internalValues: Record<string, unknown>;
853
+ };
828
854
  }>(
829
855
  `query GetNode($id: String!) {
830
856
  node(id: $id) {
@@ -838,6 +864,7 @@ _internalSys: _sys {
838
864
  extension
839
865
  relativePath
840
866
  title
867
+ hasReferences
841
868
  template
842
869
  collection {
843
870
  name
@@ -856,29 +883,29 @@ _internalSys: _sys {
856
883
  }
857
884
  }`,
858
885
  { variables: { id: id } }
859
- )
860
- return response.node
861
- }
886
+ );
887
+ return response.node;
888
+ };
862
889
 
863
890
  const expandPayload = async (
864
891
  payload: Payload,
865
892
  cms: TinaCMS
866
893
  ): 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)
894
+ const { query, variables } = payload;
895
+ const documentNode = G.parse(query);
896
+ const expandedDocumentNode = expandQuery({ schema, documentNode });
897
+ const expandedQuery = G.print(expandedDocumentNode);
871
898
  const expandedData = await cms.api.tina.request<object>(expandedQuery, {
872
899
  variables,
873
- })
900
+ });
874
901
 
875
902
  const expandedDocumentNodeForResolver = expandQuery({
876
903
  schema: schemaForResolver,
877
904
  documentNode,
878
- })
879
- const expandedQueryForResolver = G.print(expandedDocumentNodeForResolver)
880
- return { ...payload, expandedQuery, expandedData, expandedQueryForResolver }
881
- }
905
+ });
906
+ const expandedQueryForResolver = G.print(expandedDocumentNodeForResolver);
907
+ return { ...payload, expandedQuery, expandedData, expandedQueryForResolver };
908
+ };
882
909
 
883
910
  /**
884
911
  * When we resolve the graphql data we check for these errors,
@@ -886,11 +913,11 @@ const expandPayload = async (
886
913
  * process it once we have that document
887
914
  */
888
915
  class NoFormError extends Error {
889
- id: string
916
+ id: string;
890
917
  constructor(msg: string, id: string) {
891
- super(msg)
892
- this.id = id
893
- Object.setPrototypeOf(this, NoFormError.prototype)
918
+ super(msg);
919
+ this.id = id;
920
+ Object.setPrototypeOf(this, NoFormError.prototype);
894
921
  }
895
922
  }
896
923
 
@@ -898,22 +925,22 @@ const getTemplateForDocument = (
898
925
  resolvedDocument: ResolvedDocument,
899
926
  tinaSchema: TinaSchema
900
927
  ) => {
901
- const id = resolvedDocument._internalSys.path
902
- let collection: Collection<true> | undefined
928
+ const id = resolvedDocument._internalSys.path;
929
+ let collection: Collection<true> | undefined;
903
930
  try {
904
- collection = tinaSchema.getCollectionByFullPath(id)
931
+ collection = tinaSchema.getCollectionByFullPath(id);
905
932
  } catch (e) {}
906
933
 
907
934
  if (!collection) {
908
- throw new Error(`Unable to determine collection for path ${id}`)
935
+ throw new Error(`Unable to determine collection for path ${id}`);
909
936
  }
910
937
 
911
938
  const template = tinaSchema.getTemplateForData({
912
939
  data: resolvedDocument._internalValues,
913
940
  collection,
914
- })
915
- return { template, collection }
916
- }
941
+ });
942
+ return { template, collection };
943
+ };
917
944
 
918
945
  const buildForm = ({
919
946
  resolvedDocument,
@@ -921,18 +948,18 @@ const buildForm = ({
921
948
  payloadId,
922
949
  cms,
923
950
  }: {
924
- resolvedDocument: ResolvedDocument
925
- tinaSchema: TinaSchema
926
- payloadId: string
927
- cms: TinaCMS
951
+ resolvedDocument: ResolvedDocument;
952
+ tinaSchema: TinaSchema;
953
+ payloadId: string;
954
+ cms: TinaCMS;
928
955
  }) => {
929
956
  const { template, collection } = getTemplateForDocument(
930
957
  resolvedDocument,
931
958
  tinaSchema
932
- )
933
- const id = resolvedDocument._internalSys.path
934
- let form: Form | undefined
935
- let shouldRegisterForm = true
959
+ );
960
+ const id = resolvedDocument._internalSys.path;
961
+ let form: Form | undefined;
962
+ let shouldRegisterForm = true;
936
963
  const formConfig: FormOptions<any> = {
937
964
  id,
938
965
  initialValues: resolvedDocument._internalValues,
@@ -945,10 +972,10 @@ const buildForm = ({
945
972
  cms
946
973
  ),
947
974
  label: collection.label || collection.name,
948
- }
975
+ };
949
976
  if (tinaSchema.config.config?.formifyCallback) {
950
977
  const callback = tinaSchema.config.config
951
- ?.formifyCallback as FormifyCallback
978
+ ?.formifyCallback as FormifyCallback;
952
979
  form =
953
980
  callback(
954
981
  {
@@ -958,30 +985,30 @@ const buildForm = ({
958
985
  formConfig,
959
986
  },
960
987
  cms
961
- ) || undefined
988
+ ) || undefined;
962
989
  if (!form) {
963
990
  // If the form isn't created from formify, we still
964
991
  // need it, just don't show it to the user.
965
- shouldRegisterForm = false
966
- form = new Form(formConfig)
992
+ shouldRegisterForm = false;
993
+ form = new Form(formConfig);
967
994
  }
968
995
  } else {
969
996
  if (collection.ui?.global) {
970
- form = createGlobalForm(formConfig)
997
+ form = createGlobalForm(formConfig);
971
998
  } else {
972
- form = createForm(formConfig)
999
+ form = createForm(formConfig);
973
1000
  }
974
1001
  }
975
1002
  if (form) {
976
1003
  if (shouldRegisterForm) {
977
1004
  if (collection.ui?.global) {
978
- cms.plugins.add(new GlobalFormPlugin(form))
1005
+ cms.plugins.add(new GlobalFormPlugin(form));
979
1006
  }
980
- cms.dispatch({ type: 'forms:add', value: form })
1007
+ cms.dispatch({ type: 'forms:add', value: form });
981
1008
  }
982
1009
  }
983
1010
  if (!form) {
984
- throw new Error(`No form registered for ${id}.`)
1011
+ throw new Error(`No form registered for ${id}.`);
985
1012
  }
986
- return { template, form }
987
- }
1013
+ return { template, form };
1014
+ };