@tinacms/app 0.0.0-e70425b-20241028042614 → 0.0.0-ea204c9-20250318044256

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