@tinacms/app 1.2.7 → 1.2.9
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.
- package/CHANGELOG.md +18 -0
- package/package.json +6 -7
- package/src/lib/build-form.ts +49 -0
- package/src/lib/expand-query.ts +231 -0
- package/src/lib/graphql-reducer.ts +753 -0
- package/src/lib/types.ts +48 -0
- package/src/preview.tsx +3 -105
- package/src/assets/react.svg +0 -1
- package/src/lib/formify/index.ts +0 -325
- package/src/lib/machines/document-machine.ts +0 -338
- package/src/lib/machines/query-machine.ts +0 -701
- package/src/lib/machines/util.ts +0 -196
|
@@ -0,0 +1,753 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import * as G from 'graphql'
|
|
3
|
+
import { getIn } from 'final-form'
|
|
4
|
+
import { z, ZodError } from 'zod'
|
|
5
|
+
// @ts-expect-error
|
|
6
|
+
import schemaJson from 'SCHEMA_IMPORT'
|
|
7
|
+
import { expandQuery, isNodeType } from './expand-query'
|
|
8
|
+
import {
|
|
9
|
+
Form,
|
|
10
|
+
TinaCMS,
|
|
11
|
+
NAMER,
|
|
12
|
+
TinaSchema,
|
|
13
|
+
useCMS,
|
|
14
|
+
resolveField,
|
|
15
|
+
Collection,
|
|
16
|
+
Template,
|
|
17
|
+
TinaField,
|
|
18
|
+
Client,
|
|
19
|
+
FormOptions,
|
|
20
|
+
GlobalFormPlugin,
|
|
21
|
+
} from 'tinacms'
|
|
22
|
+
import { createForm, createGlobalForm, FormifyCallback } from './build-form'
|
|
23
|
+
import type {
|
|
24
|
+
PostMessage,
|
|
25
|
+
Payload,
|
|
26
|
+
SystemInfo,
|
|
27
|
+
Document,
|
|
28
|
+
ResolvedDocument,
|
|
29
|
+
} from './types'
|
|
30
|
+
|
|
31
|
+
const sysSchema = z.object({
|
|
32
|
+
breadcrumbs: z.array(z.string()),
|
|
33
|
+
basename: z.string(),
|
|
34
|
+
filename: z.string(),
|
|
35
|
+
path: z.string(),
|
|
36
|
+
extension: z.string(),
|
|
37
|
+
relativePath: z.string(),
|
|
38
|
+
title: z.string().optional().nullable(),
|
|
39
|
+
template: z.string(),
|
|
40
|
+
collection: z.object({
|
|
41
|
+
name: z.string(),
|
|
42
|
+
slug: z.string(),
|
|
43
|
+
label: z.string(),
|
|
44
|
+
path: z.string(),
|
|
45
|
+
format: z.string().optional().nullable(),
|
|
46
|
+
matches: z.string().optional().nullable(),
|
|
47
|
+
}),
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
const documentSchema: z.ZodType<ResolvedDocument> = z.object({
|
|
51
|
+
_internalValues: z.record(z.unknown()),
|
|
52
|
+
_internalSys: sysSchema,
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const astNode = schemaJson as G.DocumentNode
|
|
56
|
+
const astNodeWithMeta: G.DocumentNode = {
|
|
57
|
+
...astNode,
|
|
58
|
+
definitions: astNode.definitions.map((def) => {
|
|
59
|
+
if (def.kind === 'ObjectTypeDefinition') {
|
|
60
|
+
return {
|
|
61
|
+
...def,
|
|
62
|
+
fields: [
|
|
63
|
+
...(def.fields || []),
|
|
64
|
+
{
|
|
65
|
+
kind: 'FieldDefinition',
|
|
66
|
+
name: {
|
|
67
|
+
kind: 'Name',
|
|
68
|
+
value: '_tina_metadata',
|
|
69
|
+
},
|
|
70
|
+
arguments: [],
|
|
71
|
+
type: {
|
|
72
|
+
kind: 'NonNullType',
|
|
73
|
+
type: {
|
|
74
|
+
kind: 'NamedType',
|
|
75
|
+
name: {
|
|
76
|
+
kind: 'Name',
|
|
77
|
+
value: 'JSON',
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return def
|
|
86
|
+
}),
|
|
87
|
+
}
|
|
88
|
+
const schema = G.buildASTSchema(astNode)
|
|
89
|
+
const schemaForResolver = G.buildASTSchema(astNodeWithMeta)
|
|
90
|
+
|
|
91
|
+
export const useGraphQLReducer = (
|
|
92
|
+
iframe: React.MutableRefObject<HTMLIFrameElement>,
|
|
93
|
+
url: string
|
|
94
|
+
) => {
|
|
95
|
+
const cms = useCMS()
|
|
96
|
+
const tinaSchema = cms.api.tina.schema as TinaSchema
|
|
97
|
+
const [payloads, setPayloads] = React.useState<Payload[]>([])
|
|
98
|
+
const [documentsToResolve, setDocumentsToResolve] = React.useState<string[]>(
|
|
99
|
+
[]
|
|
100
|
+
)
|
|
101
|
+
const [resolvedDocuments, setResolvedDocuments] = React.useState<
|
|
102
|
+
ResolvedDocument[]
|
|
103
|
+
>([])
|
|
104
|
+
const [operationIndex, setOperationIndex] = React.useState(0)
|
|
105
|
+
|
|
106
|
+
React.useEffect(() => {
|
|
107
|
+
const run = async () => {
|
|
108
|
+
return Promise.all(
|
|
109
|
+
documentsToResolve.map(async (documentId) => {
|
|
110
|
+
return await getDocument(documentId, cms.api.tina)
|
|
111
|
+
})
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
if (documentsToResolve.length) {
|
|
115
|
+
run().then((docs) => {
|
|
116
|
+
setResolvedDocuments((resolvedDocs) => [...resolvedDocs, ...docs])
|
|
117
|
+
setDocumentsToResolve([])
|
|
118
|
+
setOperationIndex((i) => i + 1)
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
}, [documentsToResolve.join('.')])
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Note: since React runs effects twice in development this will run twice for a given query
|
|
125
|
+
* which results in duplicate network requests in quick succession
|
|
126
|
+
*/
|
|
127
|
+
React.useEffect(() => {
|
|
128
|
+
const run = async () => {
|
|
129
|
+
return Promise.all(
|
|
130
|
+
payloads.map(async (payload) => {
|
|
131
|
+
// This payload has already been expanded, skip it.
|
|
132
|
+
if (payload.expandedQuery) {
|
|
133
|
+
return payload
|
|
134
|
+
} else {
|
|
135
|
+
const expandedPayload = await expandPayload(payload, cms)
|
|
136
|
+
processPayload(expandedPayload)
|
|
137
|
+
return expandedPayload
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
if (payloads.length) {
|
|
143
|
+
run().then((updatedPayloads) => {
|
|
144
|
+
setPayloads(updatedPayloads)
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
}, [payloads.map(({ id }) => id).join('.'), cms])
|
|
148
|
+
|
|
149
|
+
const processPayload = React.useCallback(
|
|
150
|
+
(payload: Payload) => {
|
|
151
|
+
const { expandedQueryForResolver, variables, expandedData } = payload
|
|
152
|
+
if (!expandedQueryForResolver || !expandedData) {
|
|
153
|
+
throw new Error(`Unable to process payload which has not been expanded`)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const result = G.graphqlSync({
|
|
157
|
+
schema: schemaForResolver,
|
|
158
|
+
source: expandedQueryForResolver,
|
|
159
|
+
variableValues: variables,
|
|
160
|
+
rootValue: expandedData,
|
|
161
|
+
fieldResolver: (source, args, context, info) => {
|
|
162
|
+
const fieldName = info.fieldName
|
|
163
|
+
/**
|
|
164
|
+
* Since the `source` for this resolver is the query that
|
|
165
|
+
* ran before passing it into `useTina`, we need to take aliases
|
|
166
|
+
* into consideration, so if an alias is provided we try to
|
|
167
|
+
* see if that has the value we're looking for. This isn't a perfect
|
|
168
|
+
* solution as the `value` gets overwritten depending on the alias
|
|
169
|
+
* query.
|
|
170
|
+
*/
|
|
171
|
+
const aliases: string[] = []
|
|
172
|
+
info.fieldNodes.forEach((fieldNode) => {
|
|
173
|
+
if (fieldNode.alias) {
|
|
174
|
+
aliases.push(fieldNode.alias.value)
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
let value = source[fieldName] as unknown
|
|
178
|
+
if (!value) {
|
|
179
|
+
aliases.forEach((alias) => {
|
|
180
|
+
const aliasValue = source[alias]
|
|
181
|
+
if (aliasValue) {
|
|
182
|
+
value = aliasValue
|
|
183
|
+
}
|
|
184
|
+
})
|
|
185
|
+
}
|
|
186
|
+
if (fieldName === '_sys') {
|
|
187
|
+
return source._internalSys
|
|
188
|
+
}
|
|
189
|
+
if (fieldName === '_values') {
|
|
190
|
+
return source._internalValues
|
|
191
|
+
}
|
|
192
|
+
if (info.fieldName === '_tina_metadata') {
|
|
193
|
+
if (value) {
|
|
194
|
+
return value
|
|
195
|
+
}
|
|
196
|
+
// TODO: ensure all fields that have _tina_metadata
|
|
197
|
+
// actually need it
|
|
198
|
+
return {
|
|
199
|
+
id: null,
|
|
200
|
+
fields: [],
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (isNodeType(info.returnType)) {
|
|
204
|
+
if (!value) {
|
|
205
|
+
return
|
|
206
|
+
}
|
|
207
|
+
let resolvedDocument: ResolvedDocument
|
|
208
|
+
// This is a reference from another form
|
|
209
|
+
if (typeof value === 'string') {
|
|
210
|
+
const valueFromSetup = getIn(
|
|
211
|
+
expandedData,
|
|
212
|
+
G.responsePathAsArray(info.path).join('.')
|
|
213
|
+
)
|
|
214
|
+
const maybeResolvedDocument = resolvedDocuments.find(
|
|
215
|
+
(doc) => doc._internalSys.path === value
|
|
216
|
+
)
|
|
217
|
+
// If we already have this document, use it.
|
|
218
|
+
if (maybeResolvedDocument) {
|
|
219
|
+
resolvedDocument = maybeResolvedDocument
|
|
220
|
+
} else if (valueFromSetup) {
|
|
221
|
+
// Else, even though in this context the value is a string because it's
|
|
222
|
+
// resolved from a parent form, if the reference hasn't changed
|
|
223
|
+
// from when we ran the setup query, we can avoid a data fetch
|
|
224
|
+
// here and just grab it from the response
|
|
225
|
+
const maybeResolvedDocument =
|
|
226
|
+
documentSchema.parse(valueFromSetup)
|
|
227
|
+
if (maybeResolvedDocument._internalSys.path === value) {
|
|
228
|
+
resolvedDocument = maybeResolvedDocument
|
|
229
|
+
} else {
|
|
230
|
+
throw new NoFormError(`No form found`, value)
|
|
231
|
+
}
|
|
232
|
+
} else {
|
|
233
|
+
throw new NoFormError(`No form found`, value)
|
|
234
|
+
}
|
|
235
|
+
} else {
|
|
236
|
+
resolvedDocument = documentSchema.parse(value)
|
|
237
|
+
}
|
|
238
|
+
const id = resolvedDocument._internalSys.path
|
|
239
|
+
let existingForm = cms.forms.find(id)
|
|
240
|
+
if (!existingForm) {
|
|
241
|
+
cms.plugins
|
|
242
|
+
.getType('screen')
|
|
243
|
+
.all()
|
|
244
|
+
.forEach((plugin) => {
|
|
245
|
+
// @ts-ignore
|
|
246
|
+
if (plugin?.form && plugin.form?.id === id) {
|
|
247
|
+
// @ts-ignore
|
|
248
|
+
existingForm = plugin.form
|
|
249
|
+
}
|
|
250
|
+
})
|
|
251
|
+
}
|
|
252
|
+
if (!existingForm) {
|
|
253
|
+
const { form, template } = buildForm({
|
|
254
|
+
resolvedDocument,
|
|
255
|
+
tinaSchema,
|
|
256
|
+
payloadId: payload.id,
|
|
257
|
+
cms,
|
|
258
|
+
})
|
|
259
|
+
form.subscribe(
|
|
260
|
+
() => {
|
|
261
|
+
setOperationIndex((i) => i + 1)
|
|
262
|
+
},
|
|
263
|
+
{ values: true }
|
|
264
|
+
)
|
|
265
|
+
return resolveDocument(resolvedDocument, template, form)
|
|
266
|
+
} else {
|
|
267
|
+
existingForm.addQuery(payload.id)
|
|
268
|
+
const { template } = getTemplateForDocument(
|
|
269
|
+
resolvedDocument,
|
|
270
|
+
tinaSchema
|
|
271
|
+
)
|
|
272
|
+
existingForm.addQuery(payload.id)
|
|
273
|
+
return resolveDocument(resolvedDocument, template, existingForm)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return value
|
|
277
|
+
},
|
|
278
|
+
})
|
|
279
|
+
if (result.errors) {
|
|
280
|
+
result.errors.forEach((error) => {
|
|
281
|
+
if (
|
|
282
|
+
error instanceof G.GraphQLError &&
|
|
283
|
+
error.originalError instanceof NoFormError
|
|
284
|
+
) {
|
|
285
|
+
const id = error.originalError.id
|
|
286
|
+
setDocumentsToResolve((docs) => [
|
|
287
|
+
...docs.filter((doc) => doc !== id),
|
|
288
|
+
id,
|
|
289
|
+
])
|
|
290
|
+
} else {
|
|
291
|
+
console.log(error)
|
|
292
|
+
// throw new Error(
|
|
293
|
+
// `Error processing value change, please contact support`
|
|
294
|
+
// )
|
|
295
|
+
}
|
|
296
|
+
})
|
|
297
|
+
} else {
|
|
298
|
+
iframe.current?.contentWindow?.postMessage({
|
|
299
|
+
type: 'updateData',
|
|
300
|
+
id: payload.id,
|
|
301
|
+
data: result.data,
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
// This can be improved, for now we just need something to test with
|
|
305
|
+
const elements =
|
|
306
|
+
iframe.current?.contentWindow?.document.querySelectorAll<HTMLElement>(
|
|
307
|
+
`[data-tinafield]`
|
|
308
|
+
)
|
|
309
|
+
if (elements) {
|
|
310
|
+
for (let i = 0; i < elements.length; i++) {
|
|
311
|
+
const el = elements[i]
|
|
312
|
+
el.onclick = () => {
|
|
313
|
+
const tinafield = el.getAttribute('data-tinafield')
|
|
314
|
+
cms.events.dispatch({
|
|
315
|
+
type: 'field:selected',
|
|
316
|
+
value: tinafield,
|
|
317
|
+
})
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
[resolvedDocuments.map((doc) => doc._internalSys.path).join('.')]
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
const notifyEditMode = React.useCallback(
|
|
327
|
+
(event: MessageEvent<PostMessage>) => {
|
|
328
|
+
if (event?.data?.type === 'isEditMode') {
|
|
329
|
+
iframe?.current?.contentWindow?.postMessage({
|
|
330
|
+
type: 'tina:editMode',
|
|
331
|
+
})
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
[]
|
|
335
|
+
)
|
|
336
|
+
const handleOpenClose = React.useCallback(
|
|
337
|
+
(event: MessageEvent<PostMessage>) => {
|
|
338
|
+
if (event.data.type === 'close') {
|
|
339
|
+
const payloadSchema = z.object({ id: z.string() })
|
|
340
|
+
const { id } = payloadSchema.parse(event.data)
|
|
341
|
+
setPayloads((previous) =>
|
|
342
|
+
previous.filter((payload) => payload.id !== id)
|
|
343
|
+
)
|
|
344
|
+
cms.forms.all().map((form) => {
|
|
345
|
+
form.removeQuery(id)
|
|
346
|
+
})
|
|
347
|
+
cms.removeOrphanedForms()
|
|
348
|
+
}
|
|
349
|
+
if (event.data.type === 'open') {
|
|
350
|
+
const payloadSchema = z.object({
|
|
351
|
+
id: z.string(),
|
|
352
|
+
query: z.string(),
|
|
353
|
+
variables: z.record(z.unknown()),
|
|
354
|
+
data: z.record(z.unknown()),
|
|
355
|
+
})
|
|
356
|
+
const payload = payloadSchema.parse(event.data)
|
|
357
|
+
setPayloads((payloads) => [
|
|
358
|
+
...payloads.filter(({ id }) => id !== payload.id),
|
|
359
|
+
payload,
|
|
360
|
+
])
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
[cms]
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
React.useEffect(() => {
|
|
367
|
+
payloads.forEach((payload) => {
|
|
368
|
+
if (payload.expandedData) {
|
|
369
|
+
processPayload(payload)
|
|
370
|
+
}
|
|
371
|
+
})
|
|
372
|
+
}, [operationIndex])
|
|
373
|
+
|
|
374
|
+
React.useEffect(() => {
|
|
375
|
+
return () => {
|
|
376
|
+
setPayloads([])
|
|
377
|
+
cms.removeAllForms()
|
|
378
|
+
}
|
|
379
|
+
}, [url])
|
|
380
|
+
|
|
381
|
+
React.useEffect(() => {
|
|
382
|
+
if (iframe) {
|
|
383
|
+
window.addEventListener('message', handleOpenClose)
|
|
384
|
+
window.addEventListener('message', notifyEditMode)
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return () => {
|
|
388
|
+
window.removeEventListener('message', handleOpenClose)
|
|
389
|
+
window.removeEventListener('message', notifyEditMode)
|
|
390
|
+
cms.removeAllForms()
|
|
391
|
+
}
|
|
392
|
+
}, [iframe.current])
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const onSubmit = async (
|
|
396
|
+
collection: Collection<true>,
|
|
397
|
+
relativePath: string,
|
|
398
|
+
payload: Record<string, unknown>,
|
|
399
|
+
cms: TinaCMS
|
|
400
|
+
) => {
|
|
401
|
+
const tinaSchema = cms.api.tina.schema
|
|
402
|
+
try {
|
|
403
|
+
const mutationString = `#graphql
|
|
404
|
+
mutation UpdateDocument($collection: String!, $relativePath: String!, $params: DocumentUpdateMutation!) {
|
|
405
|
+
updateDocument(collection: $collection, relativePath: $relativePath, params: $params) {
|
|
406
|
+
__typename
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
`
|
|
410
|
+
|
|
411
|
+
await cms.api.tina.request(mutationString, {
|
|
412
|
+
variables: {
|
|
413
|
+
collection: collection.name,
|
|
414
|
+
relativePath: relativePath,
|
|
415
|
+
params: tinaSchema.transformPayload(collection.name, payload),
|
|
416
|
+
},
|
|
417
|
+
})
|
|
418
|
+
cms.alerts.success('Document saved!')
|
|
419
|
+
} catch (e) {
|
|
420
|
+
cms.alerts.error('There was a problem saving your document')
|
|
421
|
+
console.error(e)
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
type Path = (string | number)[]
|
|
426
|
+
|
|
427
|
+
const resolveDocument = (
|
|
428
|
+
doc: ResolvedDocument,
|
|
429
|
+
template: Template<true>,
|
|
430
|
+
form: Form
|
|
431
|
+
): ResolvedDocument => {
|
|
432
|
+
// @ts-ignore AnyField and TinaField don't mix
|
|
433
|
+
const fields = form.fields as TinaField<true>[]
|
|
434
|
+
const id = doc._internalSys.path
|
|
435
|
+
const path: Path = []
|
|
436
|
+
const formValues = resolveFormValue({
|
|
437
|
+
fields: fields,
|
|
438
|
+
values: form.values,
|
|
439
|
+
path,
|
|
440
|
+
id,
|
|
441
|
+
})
|
|
442
|
+
const metadataFields: Record<string, string> = {}
|
|
443
|
+
Object.keys(formValues).forEach((key) => {
|
|
444
|
+
metadataFields[key] = [...path, key].join('.')
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
return {
|
|
448
|
+
...formValues,
|
|
449
|
+
id,
|
|
450
|
+
sys: doc._internalSys,
|
|
451
|
+
values: form.values,
|
|
452
|
+
_tina_metadata: {
|
|
453
|
+
id: doc._internalSys.path,
|
|
454
|
+
fields: metadataFields,
|
|
455
|
+
},
|
|
456
|
+
_internalSys: doc._internalSys,
|
|
457
|
+
_internalValues: doc._internalValues,
|
|
458
|
+
__typename: NAMER.dataTypeName(template.namespace),
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const resolveFormValue = <T extends Record<string, unknown>>({
|
|
463
|
+
fields,
|
|
464
|
+
values,
|
|
465
|
+
path,
|
|
466
|
+
id,
|
|
467
|
+
}: // tinaSchema,
|
|
468
|
+
{
|
|
469
|
+
fields: TinaField<true>[]
|
|
470
|
+
values: T
|
|
471
|
+
path: Path
|
|
472
|
+
id: string
|
|
473
|
+
// tinaSchema: TinaSchema
|
|
474
|
+
}): T & { __typename?: string } => {
|
|
475
|
+
const accum: Record<string, unknown> = {}
|
|
476
|
+
fields.forEach((field) => {
|
|
477
|
+
const v = values[field.name]
|
|
478
|
+
if (typeof v === 'undefined') {
|
|
479
|
+
return
|
|
480
|
+
}
|
|
481
|
+
if (v === null) {
|
|
482
|
+
return
|
|
483
|
+
}
|
|
484
|
+
accum[field.name] = resolveFieldValue({
|
|
485
|
+
field,
|
|
486
|
+
value: v,
|
|
487
|
+
path,
|
|
488
|
+
id,
|
|
489
|
+
})
|
|
490
|
+
})
|
|
491
|
+
return accum as T & { __typename?: string }
|
|
492
|
+
}
|
|
493
|
+
const resolveFieldValue = ({
|
|
494
|
+
field,
|
|
495
|
+
value,
|
|
496
|
+
path,
|
|
497
|
+
id,
|
|
498
|
+
}: {
|
|
499
|
+
field: TinaField<true>
|
|
500
|
+
value: unknown
|
|
501
|
+
path: Path
|
|
502
|
+
id: string
|
|
503
|
+
}) => {
|
|
504
|
+
switch (field.type) {
|
|
505
|
+
case 'object': {
|
|
506
|
+
if (field.templates) {
|
|
507
|
+
if (field.list) {
|
|
508
|
+
if (Array.isArray(value)) {
|
|
509
|
+
return value.map((item, index) => {
|
|
510
|
+
const template = field.templates[item._template]
|
|
511
|
+
if (typeof template === 'string') {
|
|
512
|
+
throw new Error('Global templates not supported')
|
|
513
|
+
}
|
|
514
|
+
const nextPath = [...path, field.name, index]
|
|
515
|
+
const metadataFields: Record<string, string> = {}
|
|
516
|
+
template.fields.forEach((field) => {
|
|
517
|
+
metadataFields[field.name] = [...nextPath, field.name].join('.')
|
|
518
|
+
})
|
|
519
|
+
return {
|
|
520
|
+
__typename: NAMER.dataTypeName(template.namespace),
|
|
521
|
+
_tina_metadata: {
|
|
522
|
+
id,
|
|
523
|
+
fields: metadataFields,
|
|
524
|
+
},
|
|
525
|
+
...resolveFormValue({
|
|
526
|
+
fields: template.fields,
|
|
527
|
+
values: item,
|
|
528
|
+
path: nextPath,
|
|
529
|
+
id,
|
|
530
|
+
}),
|
|
531
|
+
}
|
|
532
|
+
})
|
|
533
|
+
}
|
|
534
|
+
} else {
|
|
535
|
+
// not implemented
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const templateFields = field.fields
|
|
540
|
+
if (typeof templateFields === 'string') {
|
|
541
|
+
throw new Error('Global templates not supported')
|
|
542
|
+
}
|
|
543
|
+
if (!templateFields) {
|
|
544
|
+
throw new Error(`Expected to find sub-fields on field ${field.name}`)
|
|
545
|
+
}
|
|
546
|
+
if (field.list) {
|
|
547
|
+
if (Array.isArray(value)) {
|
|
548
|
+
return value.map((item, index) => {
|
|
549
|
+
const nextPath = [...path, field.name, index]
|
|
550
|
+
const metadataFields: Record<string, string> = {}
|
|
551
|
+
templateFields.forEach((field) => {
|
|
552
|
+
metadataFields[field.name] = [...nextPath, field.name].join('.')
|
|
553
|
+
})
|
|
554
|
+
return {
|
|
555
|
+
__typename: NAMER.dataTypeName(field.namespace),
|
|
556
|
+
_tina_metadata: {
|
|
557
|
+
id,
|
|
558
|
+
fields: metadataFields,
|
|
559
|
+
},
|
|
560
|
+
...resolveFormValue({
|
|
561
|
+
fields: templateFields,
|
|
562
|
+
values: item,
|
|
563
|
+
path,
|
|
564
|
+
id,
|
|
565
|
+
}),
|
|
566
|
+
}
|
|
567
|
+
})
|
|
568
|
+
}
|
|
569
|
+
} else {
|
|
570
|
+
const nextPath = [...path, field.name]
|
|
571
|
+
const metadataFields: Record<string, string> = {}
|
|
572
|
+
templateFields.forEach((field) => {
|
|
573
|
+
metadataFields[field.name] = [...nextPath, field.name].join('.')
|
|
574
|
+
})
|
|
575
|
+
return {
|
|
576
|
+
__typename: NAMER.dataTypeName(field.namespace),
|
|
577
|
+
_tina_metadata: {
|
|
578
|
+
id,
|
|
579
|
+
fields: metadataFields,
|
|
580
|
+
},
|
|
581
|
+
...resolveFormValue({
|
|
582
|
+
fields: templateFields,
|
|
583
|
+
values: value as any,
|
|
584
|
+
path,
|
|
585
|
+
id,
|
|
586
|
+
}),
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
default: {
|
|
591
|
+
return value
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const getDocument = async (id: string, tina: Client) => {
|
|
597
|
+
const response = await tina.request<{
|
|
598
|
+
node: { _internalSys: SystemInfo; _internalValues: Record<string, unknown> }
|
|
599
|
+
}>(
|
|
600
|
+
`query GetNode($id: String!) {
|
|
601
|
+
node(id: $id) {
|
|
602
|
+
...on Document {
|
|
603
|
+
_internalValues: _values
|
|
604
|
+
_internalSys: _sys {
|
|
605
|
+
breadcrumbs
|
|
606
|
+
basename
|
|
607
|
+
filename
|
|
608
|
+
path
|
|
609
|
+
extension
|
|
610
|
+
relativePath
|
|
611
|
+
title
|
|
612
|
+
template
|
|
613
|
+
collection {
|
|
614
|
+
name
|
|
615
|
+
slug
|
|
616
|
+
label
|
|
617
|
+
path
|
|
618
|
+
format
|
|
619
|
+
matches
|
|
620
|
+
templates
|
|
621
|
+
fields
|
|
622
|
+
__typename
|
|
623
|
+
}
|
|
624
|
+
__typename
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}`,
|
|
629
|
+
{ variables: { id: id } }
|
|
630
|
+
)
|
|
631
|
+
return response.node
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const expandPayload = async (payload: Payload, cms: TinaCMS) => {
|
|
635
|
+
const { query, variables } = payload
|
|
636
|
+
const documentNode = G.parse(query)
|
|
637
|
+
const expandedDocumentNode = expandQuery({ schema, documentNode })
|
|
638
|
+
const expandedQuery = G.print(expandedDocumentNode)
|
|
639
|
+
const expandedData = await cms.api.tina.request(expandedQuery, {
|
|
640
|
+
variables,
|
|
641
|
+
})
|
|
642
|
+
|
|
643
|
+
const expandedDocumentNodeForResolver = expandQuery({
|
|
644
|
+
schema: schemaForResolver,
|
|
645
|
+
documentNode,
|
|
646
|
+
})
|
|
647
|
+
const expandedQueryForResolver = G.print(expandedDocumentNodeForResolver)
|
|
648
|
+
return { ...payload, expandQuery, expandedData, expandedQueryForResolver }
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* When we resolve the graphql data we check for these errors,
|
|
653
|
+
* if we find one we enqueue the document to be generated, and then
|
|
654
|
+
* process it once we have that document
|
|
655
|
+
*/
|
|
656
|
+
class NoFormError extends Error {
|
|
657
|
+
id: string
|
|
658
|
+
constructor(msg: string, id: string) {
|
|
659
|
+
super(msg)
|
|
660
|
+
this.id = id
|
|
661
|
+
Object.setPrototypeOf(this, NoFormError.prototype)
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
const getTemplateForDocument = (
|
|
666
|
+
resolvedDocument: ResolvedDocument,
|
|
667
|
+
tinaSchema: TinaSchema
|
|
668
|
+
) => {
|
|
669
|
+
const id = resolvedDocument._internalSys.path
|
|
670
|
+
const collection = tinaSchema.getCollectionByFullPath(id)
|
|
671
|
+
if (!collection) {
|
|
672
|
+
throw new Error(`Unable to determine collection for path ${id}`)
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
const template = tinaSchema.getTemplateForData({
|
|
676
|
+
data: resolvedDocument._internalValues,
|
|
677
|
+
collection,
|
|
678
|
+
})
|
|
679
|
+
return { template, collection }
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const buildForm = ({
|
|
683
|
+
resolvedDocument,
|
|
684
|
+
tinaSchema,
|
|
685
|
+
payloadId,
|
|
686
|
+
cms,
|
|
687
|
+
}: {
|
|
688
|
+
resolvedDocument: ResolvedDocument
|
|
689
|
+
tinaSchema: TinaSchema
|
|
690
|
+
payloadId: string
|
|
691
|
+
cms: TinaCMS
|
|
692
|
+
}) => {
|
|
693
|
+
const { template, collection } = getTemplateForDocument(
|
|
694
|
+
resolvedDocument,
|
|
695
|
+
tinaSchema
|
|
696
|
+
)
|
|
697
|
+
const id = resolvedDocument._internalSys.path
|
|
698
|
+
let form: Form | undefined
|
|
699
|
+
let shouldRegisterForm = true
|
|
700
|
+
const formConfig: FormOptions<any> = {
|
|
701
|
+
id,
|
|
702
|
+
initialValues: resolvedDocument._internalValues,
|
|
703
|
+
fields: template.fields.map((field) => resolveField(field, tinaSchema)),
|
|
704
|
+
onSubmit: (payload) =>
|
|
705
|
+
onSubmit(
|
|
706
|
+
collection,
|
|
707
|
+
resolvedDocument._internalSys.relativePath,
|
|
708
|
+
payload,
|
|
709
|
+
cms
|
|
710
|
+
),
|
|
711
|
+
label: collection.label || collection.name,
|
|
712
|
+
}
|
|
713
|
+
if (tinaSchema.config.config?.formifyCallback) {
|
|
714
|
+
const callback = tinaSchema.config.config
|
|
715
|
+
?.formifyCallback as FormifyCallback
|
|
716
|
+
form =
|
|
717
|
+
callback(
|
|
718
|
+
{
|
|
719
|
+
createForm: createForm,
|
|
720
|
+
createGlobalForm: createGlobalForm,
|
|
721
|
+
skip: () => {},
|
|
722
|
+
formConfig,
|
|
723
|
+
},
|
|
724
|
+
cms
|
|
725
|
+
) || undefined
|
|
726
|
+
if (!form) {
|
|
727
|
+
// If the form isn't created from formify, we still
|
|
728
|
+
// need it, just don't show it to the user.
|
|
729
|
+
shouldRegisterForm = false
|
|
730
|
+
form = new Form(formConfig)
|
|
731
|
+
}
|
|
732
|
+
} else {
|
|
733
|
+
if (collection.ui?.global) {
|
|
734
|
+
form = createGlobalForm(formConfig)
|
|
735
|
+
} else {
|
|
736
|
+
form = createForm(formConfig)
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
if (form) {
|
|
740
|
+
if (shouldRegisterForm) {
|
|
741
|
+
form.subscribe(() => {}, { values: true })
|
|
742
|
+
if (collection.ui?.global) {
|
|
743
|
+
cms.plugins.add(new GlobalFormPlugin(form))
|
|
744
|
+
} else {
|
|
745
|
+
cms.forms.add(form)
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
if (!form) {
|
|
750
|
+
throw new Error(`No form registered for ${id}.`)
|
|
751
|
+
}
|
|
752
|
+
return { template, form }
|
|
753
|
+
}
|