@tinacms/app 0.0.0-e0ddb8c-20241004065742 → 0.0.0-e5c0e91-20250421003142

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,6 +41,7 @@ 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(),
@@ -49,14 +50,14 @@ const sysSchema = z.object({
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,77 @@ export const useGraphQLReducer = (
300
303
  id: null,
301
304
  fields: [],
302
305
  prefix: '',
303
- }
306
+ };
304
307
  }
305
308
  if (isConnectionType(info.returnType)) {
306
- const name = G.getNamedType(info.returnType).name
309
+ const name = G.getNamedType(info.returnType).name;
307
310
  const connectionCollection = tinaSchema
308
311
  .getCollections()
309
312
  .find((collection) => {
310
313
  const collectionName = NAMER.referenceConnectionType(
311
314
  collection.namespace
312
- )
315
+ );
313
316
  if (collectionName === name) {
314
- return true
317
+ return true;
315
318
  }
316
- return false
317
- })
319
+ return false;
320
+ });
318
321
  if (connectionCollection) {
319
322
  formListItems.push({
320
323
  type: 'list',
321
324
  label: connectionCollection.label || connectionCollection.name,
322
- })
325
+ });
323
326
  }
324
327
  }
325
328
  if (isNodeType(info.returnType)) {
326
329
  if (!value) {
327
- return
330
+ return;
328
331
  }
329
- let resolvedDocument: ResolvedDocument
332
+ let resolvedDocument: ResolvedDocument;
330
333
  // This is a reference from another form
331
334
  if (typeof value === 'string') {
332
335
  const valueFromSetup = getIn(
333
336
  expandedData,
334
337
  G.responsePathAsArray(info.path).join('.')
335
- )
338
+ );
336
339
  const maybeResolvedDocument = resolvedDocuments.find(
337
340
  (doc) => doc._internalSys.path === value
338
- )
341
+ );
339
342
  // If we already have this document, use it.
340
343
  if (maybeResolvedDocument) {
341
- resolvedDocument = maybeResolvedDocument
344
+ resolvedDocument = maybeResolvedDocument;
342
345
  } else if (valueFromSetup) {
343
346
  // Else, even though in this context the value is a string because it's
344
347
  // resolved from a parent form, if the reference hasn't changed
345
348
  // from when we ran the setup query, we can avoid a data fetch
346
349
  // here and just grab it from the response
347
350
  const maybeResolvedDocument =
348
- documentSchema.parse(valueFromSetup)
351
+ documentSchema.parse(valueFromSetup);
349
352
  if (maybeResolvedDocument._internalSys.path === value) {
350
- resolvedDocument = maybeResolvedDocument
353
+ resolvedDocument = maybeResolvedDocument;
351
354
  } else {
352
- throw new NoFormError(`No form found`, value)
355
+ throw new NoFormError(`No form found`, value);
353
356
  }
354
357
  } else {
355
- throw new NoFormError(`No form found`, value)
358
+ throw new NoFormError(`No form found`, value);
356
359
  }
357
360
  } else {
358
- resolvedDocument = documentSchema.parse(value)
361
+ resolvedDocument = documentSchema.parse(value);
359
362
  }
360
- const id = resolvedDocument._internalSys.path
361
- formIds.push(id)
363
+ const id = resolvedDocument._internalSys.path;
364
+ formIds.push(id);
362
365
  const existingForm = cms.state.forms.find(
363
366
  (f) => f.tinaForm.id === id
364
- )
367
+ );
365
368
 
366
- const pathArray = G.responsePathAsArray(info.path)
367
- const pathString = pathArray.join('.')
369
+ const pathArray = G.responsePathAsArray(info.path);
370
+ const pathString = pathArray.join('.');
368
371
  const ancestors = formListItems.filter((item) => {
369
372
  if (item.type === 'document') {
370
- return pathString.startsWith(item.path)
373
+ return pathString.startsWith(item.path);
371
374
  }
372
- })
373
- const parent = ancestors[ancestors.length - 1]
375
+ });
376
+ const parent = ancestors[ancestors.length - 1];
374
377
  if (parent) {
375
378
  if (parent.type === 'document') {
376
379
  parent.subItems.push({
@@ -378,7 +381,7 @@ export const useGraphQLReducer = (
378
381
  path: pathString,
379
382
  formId: id,
380
383
  subItems: [],
381
- })
384
+ });
382
385
  }
383
386
  } else {
384
387
  formListItems.push({
@@ -386,7 +389,7 @@ export const useGraphQLReducer = (
386
389
  path: pathString,
387
390
  formId: id,
388
391
  subItems: [],
389
- })
392
+ });
390
393
  }
391
394
 
392
395
  if (!existingForm) {
@@ -395,65 +398,65 @@ export const useGraphQLReducer = (
395
398
  tinaSchema,
396
399
  payloadId: payload.id,
397
400
  cms,
398
- })
401
+ });
399
402
  form.subscribe(
400
403
  () => {
401
- setOperationIndex((i) => i + 1)
404
+ setOperationIndex((i) => i + 1);
402
405
  },
403
406
  { values: true }
404
- )
407
+ );
405
408
  return resolveDocument(
406
409
  resolvedDocument,
407
410
  template,
408
411
  form,
409
412
  pathString
410
- )
413
+ );
411
414
  } else {
412
- existingForm.tinaForm.addQuery(payload.id)
415
+ existingForm.tinaForm.addQuery(payload.id);
413
416
  const { template } = getTemplateForDocument(
414
417
  resolvedDocument,
415
418
  tinaSchema
416
- )
417
- existingForm.tinaForm.addQuery(payload.id)
419
+ );
420
+ existingForm.tinaForm.addQuery(payload.id);
418
421
  return resolveDocument(
419
422
  resolvedDocument,
420
423
  template,
421
424
  existingForm.tinaForm,
422
425
  pathString
423
- )
426
+ );
424
427
  }
425
428
  }
426
- return value
429
+ return value;
427
430
  },
428
- })
431
+ });
429
432
  if (result.errors) {
430
433
  result.errors.forEach((error) => {
431
434
  if (
432
435
  error instanceof G.GraphQLError &&
433
436
  error.originalError instanceof NoFormError
434
437
  ) {
435
- const id = error.originalError.id
438
+ const id = error.originalError.id;
436
439
  setDocumentsToResolve((docs) => [
437
440
  ...docs.filter((doc) => doc !== id),
438
441
  id,
439
- ])
442
+ ]);
440
443
  } else {
441
- console.log(error)
444
+ console.log(error);
442
445
  // throw new Error(
443
446
  // `Error processing value change, please contact support`
444
447
  // )
445
448
  }
446
- })
449
+ });
447
450
  } else {
448
451
  if (result.data) {
449
452
  setResults((results) => [
450
453
  ...results.filter((result) => result.id !== payload.id),
451
454
  { id: payload.id, data: result.data },
452
- ])
455
+ ]);
453
456
  }
454
457
  if (activeField) {
455
- setSearchParams({})
456
- const [queryId, eventFieldName] = activeField.split('---')
458
+ setSearchParams({});
459
+ const [queryId, eventFieldName] = activeField.split('---');
457
460
  if (queryId === payload.id) {
458
461
  if (result?.data) {
459
462
  cms.dispatch({
@@ -462,19 +465,19 @@ export const useGraphQLReducer = (
462
465
  result.data,
463
466
  eventFieldName
464
467
  ),
465
- })
468
+ });
466
469
  }
467
470
  cms.dispatch({
468
471
  type: 'sidebar:set-display-state',
469
472
  value: 'openOrFull',
470
- })
473
+ });
471
474
  }
472
475
  }
473
476
  iframe.current?.contentWindow?.postMessage({
474
477
  type: 'updateData',
475
478
  id: payload.id,
476
479
  data: result.data,
477
- })
480
+ });
478
481
  }
479
482
  cms.dispatch({
480
483
  type: 'form-lists:add',
@@ -484,57 +487,64 @@ export const useGraphQLReducer = (
484
487
  items: formListItems,
485
488
  formIds,
486
489
  },
487
- })
490
+ });
488
491
  },
489
492
  [
490
493
  resolvedDocuments.map((doc) => doc._internalSys.path).join('.'),
491
494
  activeField,
492
495
  ]
493
- )
496
+ );
494
497
 
495
498
  const handleMessage = React.useCallback(
496
499
  (event: MessageEvent<PostMessage>) => {
500
+ if (event.data.type === 'user-select-form') {
501
+ cms.dispatch({
502
+ type: 'forms:set-active-form-id',
503
+ value: event.data.formId,
504
+ });
505
+ }
506
+
497
507
  if (event?.data?.type === 'quick-edit') {
498
508
  cms.dispatch({
499
509
  type: 'set-quick-editing-supported',
500
510
  value: event.data.value,
501
- })
511
+ });
502
512
  iframe.current?.contentWindow?.postMessage({
503
513
  type: 'quickEditEnabled',
504
514
  value: cms.state.sidebarDisplayState === 'open',
505
- })
515
+ });
506
516
  }
507
517
  if (event?.data?.type === 'isEditMode') {
508
518
  iframe?.current?.contentWindow?.postMessage({
509
519
  type: 'tina:editMode',
510
- })
520
+ });
511
521
  }
512
522
  if (event.data.type === 'field:selected') {
513
- const [queryId, eventFieldName] = event.data.fieldName.split('---')
514
- const result = results.find((res) => res.id === queryId)
523
+ const [queryId, eventFieldName] = event.data.fieldName.split('---');
524
+ const result = results.find((res) => res.id === queryId);
515
525
  if (result?.data) {
516
526
  cms.dispatch({
517
527
  type: 'forms:set-active-field-name',
518
528
  value: getFormAndFieldNameFromMetadata(result.data, eventFieldName),
519
- })
529
+ });
520
530
  }
521
531
  cms.dispatch({
522
532
  type: 'sidebar:set-display-state',
523
533
  value: 'openOrFull',
524
- })
534
+ });
525
535
  }
526
536
  if (event.data.type === 'close') {
527
- const payloadSchema = z.object({ id: z.string() })
528
- const { id } = payloadSchema.parse(event.data)
537
+ const payloadSchema = z.object({ id: z.string() });
538
+ const { id } = payloadSchema.parse(event.data);
529
539
  setPayloads((previous) =>
530
540
  previous.filter((payload) => payload.id !== id)
531
- )
532
- setResults((previous) => previous.filter((result) => result.id !== id))
541
+ );
542
+ setResults((previous) => previous.filter((result) => result.id !== id));
533
543
  cms.forms.all().map((form) => {
534
- form.removeQuery(id)
535
- })
536
- cms.removeOrphanedForms()
537
- cms.dispatch({ type: 'form-lists:remove', value: id })
544
+ form.removeQuery(id);
545
+ });
546
+ cms.removeOrphanedForms();
547
+ cms.dispatch({ type: 'form-lists:remove', value: id });
538
548
  }
539
549
  if (event.data.type === 'open') {
540
550
  const payloadSchema = z.object({
@@ -542,60 +552,66 @@ export const useGraphQLReducer = (
542
552
  query: z.string(),
543
553
  variables: z.record(z.unknown()),
544
554
  data: z.record(z.unknown()),
545
- })
546
- const payload = payloadSchema.parse(event.data)
555
+ });
556
+ const payload = payloadSchema.parse(event.data);
547
557
  setPayloads((payloads) => [
548
558
  ...payloads.filter(({ id }) => id !== payload.id),
549
559
  payload,
550
- ])
560
+ ]);
561
+ }
562
+ if (event.data.type === 'url-changed') {
563
+ cms.dispatch({
564
+ type: 'sidebar:set-loading-state',
565
+ value: true,
566
+ });
551
567
  }
552
568
  },
553
569
  [cms, JSON.stringify(results)]
554
- )
570
+ );
555
571
 
556
572
  React.useEffect(() => {
557
573
  payloads.forEach((payload) => {
558
574
  if (payload.expandedData) {
559
- processPayload(payload)
575
+ processPayload(payload);
560
576
  }
561
- })
562
- }, [operationIndex])
577
+ });
578
+ }, [operationIndex]);
563
579
 
564
580
  React.useEffect(() => {
565
581
  return () => {
566
- setPayloads([])
567
- setResults([])
568
- cms.removeAllForms()
569
- cms.dispatch({ type: 'form-lists:clear' })
570
- }
571
- }, [url])
582
+ setPayloads([]);
583
+ setResults([]);
584
+ cms.removeAllForms();
585
+ cms.dispatch({ type: 'form-lists:clear' });
586
+ };
587
+ }, [url]);
572
588
 
573
589
  React.useEffect(() => {
574
590
  iframe.current?.contentWindow?.postMessage({
575
591
  type: 'quickEditEnabled',
576
592
  value: cms.state.sidebarDisplayState === 'open',
577
- })
578
- }, [cms.state.sidebarDisplayState])
593
+ });
594
+ }, [cms.state.sidebarDisplayState]);
579
595
 
580
596
  React.useEffect(() => {
581
- cms.dispatch({ type: 'set-edit-mode', value: 'visual' })
597
+ cms.dispatch({ type: 'set-edit-mode', value: 'visual' });
582
598
  if (iframe) {
583
- window.addEventListener('message', handleMessage)
599
+ window.addEventListener('message', handleMessage);
584
600
  }
585
601
 
586
602
  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)])
603
+ window.removeEventListener('message', handleMessage);
604
+ cms.removeAllForms();
605
+ cms.dispatch({ type: 'set-edit-mode', value: 'basic' });
606
+ };
607
+ }, [iframe.current, JSON.stringify(results)]);
592
608
 
593
609
  React.useEffect(() => {
594
610
  if (requestErrors.length) {
595
- showErrorModal('Unexpected error querying content', requestErrors, cms)
611
+ showErrorModal('Unexpected error querying content', requestErrors, cms);
596
612
  }
597
- }, [requestErrors])
598
- }
613
+ }, [requestErrors]);
614
+ };
599
615
 
600
616
  const onSubmit = async (
601
617
  collection: Collection<true>,
@@ -603,7 +619,7 @@ const onSubmit = async (
603
619
  payload: Record<string, unknown>,
604
620
  cms: TinaCMS
605
621
  ) => {
606
- const tinaSchema = cms.api.tina.schema
622
+ const tinaSchema = cms.api.tina.schema;
607
623
  try {
608
624
  const mutationString = `#graphql
609
625
  mutation UpdateDocument($collection: String!, $relativePath: String!, $params: DocumentUpdateMutation!) {
@@ -611,7 +627,7 @@ const onSubmit = async (
611
627
  __typename
612
628
  }
613
629
  }
614
- `
630
+ `;
615
631
 
616
632
  await cms.api.tina.request(mutationString, {
617
633
  variables: {
@@ -619,8 +635,8 @@ const onSubmit = async (
619
635
  relativePath: relativePath,
620
636
  params: tinaSchema.transformPayload(collection.name, payload),
621
637
  },
622
- })
623
- cms.alerts.success('Document saved!')
638
+ });
639
+ cms.alerts.success('Document saved!');
624
640
  } catch (e) {
625
641
  cms.alerts.error(() =>
626
642
  ErrorDialog({
@@ -628,12 +644,12 @@ const onSubmit = async (
628
644
  message: 'Tina caught an error while updating the page',
629
645
  error: e,
630
646
  })
631
- )
632
- console.error(e)
647
+ );
648
+ console.error(e);
633
649
  }
634
- }
650
+ };
635
651
 
636
- type Path = (string | number)[]
652
+ type Path = (string | number)[];
637
653
 
638
654
  const resolveDocument = (
639
655
  doc: ResolvedDocument,
@@ -642,20 +658,20 @@ const resolveDocument = (
642
658
  pathToDocument: string
643
659
  ): ResolvedDocument => {
644
660
  // @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 = []
661
+ const fields = form.fields as TinaField<true>[];
662
+ const id = doc._internalSys.path;
663
+ const path: Path = [];
648
664
  const formValues = resolveFormValue({
649
665
  fields: fields,
650
666
  values: form.values,
651
667
  path,
652
668
  id,
653
669
  pathToDocument,
654
- })
655
- const metadataFields: Record<string, string> = {}
670
+ });
671
+ const metadataFields: Record<string, string> = {};
656
672
  Object.keys(formValues).forEach((key) => {
657
- metadataFields[key] = [...path, key].join('.')
658
- })
673
+ metadataFields[key] = [...path, key].join('.');
674
+ });
659
675
 
660
676
  return {
661
677
  ...formValues,
@@ -671,8 +687,8 @@ const resolveDocument = (
671
687
  _internalSys: doc._internalSys,
672
688
  _internalValues: doc._internalValues,
673
689
  __typename: NAMER.dataTypeName(template.namespace),
674
- }
675
- }
690
+ };
691
+ };
676
692
 
677
693
  const resolveFormValue = <T extends Record<string, unknown>>({
678
694
  fields,
@@ -682,21 +698,21 @@ const resolveFormValue = <T extends Record<string, unknown>>({
682
698
  pathToDocument,
683
699
  }: // tinaSchema,
684
700
  {
685
- fields: TinaField<true>[]
686
- values: T
687
- path: Path
688
- id: string
689
- pathToDocument: string
701
+ fields: TinaField<true>[];
702
+ values: T;
703
+ path: Path;
704
+ id: string;
705
+ pathToDocument: string;
690
706
  // tinaSchema: TinaSchema
691
707
  }): T & { __typename?: string } => {
692
- const accum: Record<string, unknown> = {}
708
+ const accum: Record<string, unknown> = {};
693
709
  fields.forEach((field) => {
694
- const v = values[field.name]
710
+ const v = values[field.name];
695
711
  if (typeof v === 'undefined') {
696
- return
712
+ return;
697
713
  }
698
714
  if (v === null) {
699
- return
715
+ return;
700
716
  }
701
717
  accum[field.name] = resolveFieldValue({
702
718
  field,
@@ -704,10 +720,10 @@ const resolveFormValue = <T extends Record<string, unknown>>({
704
720
  path,
705
721
  id,
706
722
  pathToDocument,
707
- })
708
- })
709
- return accum as T & { __typename?: string }
710
- }
723
+ });
724
+ });
725
+ return accum as T & { __typename?: string };
726
+ };
711
727
  const resolveFieldValue = ({
712
728
  field,
713
729
  value,
@@ -715,11 +731,11 @@ const resolveFieldValue = ({
715
731
  id,
716
732
  pathToDocument,
717
733
  }: {
718
- field: TinaField<true>
719
- value: unknown
720
- path: Path
721
- id: string
722
- pathToDocument: string
734
+ field: TinaField<true>;
735
+ value: unknown;
736
+ path: Path;
737
+ id: string;
738
+ pathToDocument: string;
723
739
  }) => {
724
740
  switch (field.type) {
725
741
  case 'object': {
@@ -727,15 +743,17 @@ const resolveFieldValue = ({
727
743
  if (field.list) {
728
744
  if (Array.isArray(value)) {
729
745
  return value.map((item, index) => {
730
- const template = field.templates[item._template]
746
+ const template = field.templates[item._template];
731
747
  if (typeof template === 'string') {
732
- throw new Error('Global templates not supported')
748
+ throw new Error('Global templates not supported');
733
749
  }
734
- const nextPath = [...path, field.name, index]
735
- const metadataFields: Record<string, string> = {}
750
+ const nextPath = [...path, field.name, index];
751
+ const metadataFields: Record<string, string> = {};
736
752
  template.fields.forEach((field) => {
737
- metadataFields[field.name] = [...nextPath, field.name].join('.')
738
- })
753
+ metadataFields[field.name] = [...nextPath, field.name].join(
754
+ '.'
755
+ );
756
+ });
739
757
  return {
740
758
  __typename: NAMER.dataTypeName(template.namespace),
741
759
  _tina_metadata: {
@@ -751,29 +769,29 @@ const resolveFieldValue = ({
751
769
  id,
752
770
  pathToDocument,
753
771
  }),
754
- }
755
- })
772
+ };
773
+ });
756
774
  }
757
775
  } else {
758
776
  // not implemented
759
777
  }
760
778
  }
761
779
 
762
- const templateFields = field.fields
780
+ const templateFields = field.fields;
763
781
  if (typeof templateFields === 'string') {
764
- throw new Error('Global templates not supported')
782
+ throw new Error('Global templates not supported');
765
783
  }
766
784
  if (!templateFields) {
767
- throw new Error(`Expected to find sub-fields on field ${field.name}`)
785
+ throw new Error(`Expected to find sub-fields on field ${field.name}`);
768
786
  }
769
787
  if (field.list) {
770
788
  if (Array.isArray(value)) {
771
789
  return value.map((item, index) => {
772
- const nextPath = [...path, field.name, index]
773
- const metadataFields: Record<string, string> = {}
790
+ const nextPath = [...path, field.name, index];
791
+ const metadataFields: Record<string, string> = {};
774
792
  templateFields.forEach((field) => {
775
- metadataFields[field.name] = [...nextPath, field.name].join('.')
776
- })
793
+ metadataFields[field.name] = [...nextPath, field.name].join('.');
794
+ });
777
795
  return {
778
796
  __typename: NAMER.dataTypeName(field.namespace),
779
797
  _tina_metadata: {
@@ -789,15 +807,15 @@ const resolveFieldValue = ({
789
807
  id,
790
808
  pathToDocument,
791
809
  }),
792
- }
793
- })
810
+ };
811
+ });
794
812
  }
795
813
  } else {
796
- const nextPath = [...path, field.name]
797
- const metadataFields: Record<string, string> = {}
814
+ const nextPath = [...path, field.name];
815
+ const metadataFields: Record<string, string> = {};
798
816
  templateFields.forEach((field) => {
799
- metadataFields[field.name] = [...nextPath, field.name].join('.')
800
- })
817
+ metadataFields[field.name] = [...nextPath, field.name].join('.');
818
+ });
801
819
  return {
802
820
  __typename: NAMER.dataTypeName(field.namespace),
803
821
  _tina_metadata: {
@@ -813,18 +831,21 @@ const resolveFieldValue = ({
813
831
  id,
814
832
  pathToDocument,
815
833
  }),
816
- }
834
+ };
817
835
  }
818
836
  }
819
837
  default: {
820
- return value
838
+ return value;
821
839
  }
822
840
  }
823
- }
841
+ };
824
842
 
825
843
  const getDocument = async (id: string, tina: Client) => {
826
844
  const response = await tina.request<{
827
- node: { _internalSys: SystemInfo; _internalValues: Record<string, unknown> }
845
+ node: {
846
+ _internalSys: SystemInfo;
847
+ _internalValues: Record<string, unknown>;
848
+ };
828
849
  }>(
829
850
  `query GetNode($id: String!) {
830
851
  node(id: $id) {
@@ -838,6 +859,7 @@ _internalSys: _sys {
838
859
  extension
839
860
  relativePath
840
861
  title
862
+ hasReferences
841
863
  template
842
864
  collection {
843
865
  name
@@ -856,29 +878,29 @@ _internalSys: _sys {
856
878
  }
857
879
  }`,
858
880
  { variables: { id: id } }
859
- )
860
- return response.node
861
- }
881
+ );
882
+ return response.node;
883
+ };
862
884
 
863
885
  const expandPayload = async (
864
886
  payload: Payload,
865
887
  cms: TinaCMS
866
888
  ): 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)
889
+ const { query, variables } = payload;
890
+ const documentNode = G.parse(query);
891
+ const expandedDocumentNode = expandQuery({ schema, documentNode });
892
+ const expandedQuery = G.print(expandedDocumentNode);
871
893
  const expandedData = await cms.api.tina.request<object>(expandedQuery, {
872
894
  variables,
873
- })
895
+ });
874
896
 
875
897
  const expandedDocumentNodeForResolver = expandQuery({
876
898
  schema: schemaForResolver,
877
899
  documentNode,
878
- })
879
- const expandedQueryForResolver = G.print(expandedDocumentNodeForResolver)
880
- return { ...payload, expandedQuery, expandedData, expandedQueryForResolver }
881
- }
900
+ });
901
+ const expandedQueryForResolver = G.print(expandedDocumentNodeForResolver);
902
+ return { ...payload, expandedQuery, expandedData, expandedQueryForResolver };
903
+ };
882
904
 
883
905
  /**
884
906
  * When we resolve the graphql data we check for these errors,
@@ -886,11 +908,11 @@ const expandPayload = async (
886
908
  * process it once we have that document
887
909
  */
888
910
  class NoFormError extends Error {
889
- id: string
911
+ id: string;
890
912
  constructor(msg: string, id: string) {
891
- super(msg)
892
- this.id = id
893
- Object.setPrototypeOf(this, NoFormError.prototype)
913
+ super(msg);
914
+ this.id = id;
915
+ Object.setPrototypeOf(this, NoFormError.prototype);
894
916
  }
895
917
  }
896
918
 
@@ -898,22 +920,22 @@ const getTemplateForDocument = (
898
920
  resolvedDocument: ResolvedDocument,
899
921
  tinaSchema: TinaSchema
900
922
  ) => {
901
- const id = resolvedDocument._internalSys.path
902
- let collection: Collection<true> | undefined
923
+ const id = resolvedDocument._internalSys.path;
924
+ let collection: Collection<true> | undefined;
903
925
  try {
904
- collection = tinaSchema.getCollectionByFullPath(id)
926
+ collection = tinaSchema.getCollectionByFullPath(id);
905
927
  } catch (e) {}
906
928
 
907
929
  if (!collection) {
908
- throw new Error(`Unable to determine collection for path ${id}`)
930
+ throw new Error(`Unable to determine collection for path ${id}`);
909
931
  }
910
932
 
911
933
  const template = tinaSchema.getTemplateForData({
912
934
  data: resolvedDocument._internalValues,
913
935
  collection,
914
- })
915
- return { template, collection }
916
- }
936
+ });
937
+ return { template, collection };
938
+ };
917
939
 
918
940
  const buildForm = ({
919
941
  resolvedDocument,
@@ -921,18 +943,18 @@ const buildForm = ({
921
943
  payloadId,
922
944
  cms,
923
945
  }: {
924
- resolvedDocument: ResolvedDocument
925
- tinaSchema: TinaSchema
926
- payloadId: string
927
- cms: TinaCMS
946
+ resolvedDocument: ResolvedDocument;
947
+ tinaSchema: TinaSchema;
948
+ payloadId: string;
949
+ cms: TinaCMS;
928
950
  }) => {
929
951
  const { template, collection } = getTemplateForDocument(
930
952
  resolvedDocument,
931
953
  tinaSchema
932
- )
933
- const id = resolvedDocument._internalSys.path
934
- let form: Form | undefined
935
- let shouldRegisterForm = true
954
+ );
955
+ const id = resolvedDocument._internalSys.path;
956
+ let form: Form | undefined;
957
+ let shouldRegisterForm = true;
936
958
  const formConfig: FormOptions<any> = {
937
959
  id,
938
960
  initialValues: resolvedDocument._internalValues,
@@ -945,10 +967,10 @@ const buildForm = ({
945
967
  cms
946
968
  ),
947
969
  label: collection.label || collection.name,
948
- }
970
+ };
949
971
  if (tinaSchema.config.config?.formifyCallback) {
950
972
  const callback = tinaSchema.config.config
951
- ?.formifyCallback as FormifyCallback
973
+ ?.formifyCallback as FormifyCallback;
952
974
  form =
953
975
  callback(
954
976
  {
@@ -958,30 +980,30 @@ const buildForm = ({
958
980
  formConfig,
959
981
  },
960
982
  cms
961
- ) || undefined
983
+ ) || undefined;
962
984
  if (!form) {
963
985
  // If the form isn't created from formify, we still
964
986
  // need it, just don't show it to the user.
965
- shouldRegisterForm = false
966
- form = new Form(formConfig)
987
+ shouldRegisterForm = false;
988
+ form = new Form(formConfig);
967
989
  }
968
990
  } else {
969
991
  if (collection.ui?.global) {
970
- form = createGlobalForm(formConfig)
992
+ form = createGlobalForm(formConfig);
971
993
  } else {
972
- form = createForm(formConfig)
994
+ form = createForm(formConfig);
973
995
  }
974
996
  }
975
997
  if (form) {
976
998
  if (shouldRegisterForm) {
977
999
  if (collection.ui?.global) {
978
- cms.plugins.add(new GlobalFormPlugin(form))
1000
+ cms.plugins.add(new GlobalFormPlugin(form));
979
1001
  }
980
- cms.dispatch({ type: 'forms:add', value: form })
1002
+ cms.dispatch({ type: 'forms:add', value: form });
981
1003
  }
982
1004
  }
983
1005
  if (!form) {
984
- throw new Error(`No form registered for ${id}.`)
1006
+ throw new Error(`No form registered for ${id}.`);
985
1007
  }
986
- return { template, form }
987
- }
1008
+ return { template, form };
1009
+ };