@tinacms/app 0.0.0-8eeb2df-20241010045955 → 0.0.0-8f7f815-20250623000754

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