@tinacms/app 0.0.9 → 0.0.12
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/appFiles/index.dev.html +23 -0
- package/appFiles/lib/formify/formify-utils.ts +299 -0
- package/appFiles/lib/formify/formify.ts +574 -0
- package/appFiles/lib/formify/index.ts +67 -0
- package/appFiles/lib/formify/types.ts +130 -0
- package/appFiles/lib/formify/util.ts +598 -0
- package/appFiles/lib/machines/document-machine.ts +351 -0
- package/appFiles/lib/machines/query-machine.ts +728 -0
- package/appFiles/lib/machines/util.ts +205 -0
- package/appFiles/src/App.tsx +12 -3
- package/appFiles/src/index.css +3 -0
- package/appFiles/src/main.tsx +1 -0
- package/appFiles/src/preview/index.tsx +150 -0
- package/appFiles/src/vite-env.d.ts +2 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +332 -10
- package/package.json +24 -10
|
@@ -0,0 +1,728 @@
|
|
|
1
|
+
/**
|
|
2
|
+
Copyright 2021 Forestry.io Holdings, Inc.
|
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
you may not use this file except in compliance with the License.
|
|
5
|
+
You may obtain a copy of the License at
|
|
6
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
7
|
+
Unless required by applicable law or agreed to in writing, software
|
|
8
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
9
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
10
|
+
See the License for the specific language governing permissions and
|
|
11
|
+
limitations under the License.
|
|
12
|
+
*/
|
|
13
|
+
import { assign, ContextFrom, createMachine, spawn } from 'xstate'
|
|
14
|
+
import { DocumentBlueprint } from '../formify/types'
|
|
15
|
+
import { Form, GlobalFormPlugin, TinaCMS, TinaField } from 'tinacms'
|
|
16
|
+
import { setIn } from 'final-form'
|
|
17
|
+
import * as G from 'graphql'
|
|
18
|
+
import * as util from './util'
|
|
19
|
+
import { formify } from '../formify'
|
|
20
|
+
import { spliceLocation } from '../formify/util'
|
|
21
|
+
import { documentMachine } from './document-machine'
|
|
22
|
+
import type { ActorRefFrom } from 'xstate'
|
|
23
|
+
import { NAMER } from '@tinacms/schema-tools'
|
|
24
|
+
|
|
25
|
+
export type DataType = Record<string, unknown>
|
|
26
|
+
type DocumentInfo = {
|
|
27
|
+
ref: ActorRefFrom<typeof documentMachine>
|
|
28
|
+
}
|
|
29
|
+
type DocumentMap = {
|
|
30
|
+
[documentId: string]: DocumentInfo
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type ContextType = {
|
|
34
|
+
id: null | string
|
|
35
|
+
data: null | DataType
|
|
36
|
+
cms: TinaCMS
|
|
37
|
+
selectedDocument: string | null
|
|
38
|
+
url: string
|
|
39
|
+
inputURL: null | string
|
|
40
|
+
displayURL: null | string
|
|
41
|
+
iframe: null | HTMLIFrameElement
|
|
42
|
+
iframeWidth: string
|
|
43
|
+
formifyCallback: (args: any) => Form
|
|
44
|
+
documentMap: DocumentMap
|
|
45
|
+
blueprints: DocumentBlueprint[]
|
|
46
|
+
documentsToResolve: { id: string; location: string }[]
|
|
47
|
+
}
|
|
48
|
+
export const initialContext: ContextType = {
|
|
49
|
+
id: null,
|
|
50
|
+
data: null,
|
|
51
|
+
selectedDocument: null,
|
|
52
|
+
blueprints: [],
|
|
53
|
+
// @ts-ignore
|
|
54
|
+
cms: null,
|
|
55
|
+
url: '/',
|
|
56
|
+
iframeWidth: '200px',
|
|
57
|
+
inputURL: null,
|
|
58
|
+
displayURL: null,
|
|
59
|
+
documentMap: {},
|
|
60
|
+
// @ts-ignore
|
|
61
|
+
formifyCallback: null,
|
|
62
|
+
iframe: null,
|
|
63
|
+
documentsToResolve: [],
|
|
64
|
+
}
|
|
65
|
+
export const queryMachine = createMachine(
|
|
66
|
+
{
|
|
67
|
+
tsTypes: {} as import('./query-machine.typegen').Typegen0,
|
|
68
|
+
// Breaks stuff:
|
|
69
|
+
// predictableActionArguments: true,
|
|
70
|
+
schema: {
|
|
71
|
+
context: {} as ContextType,
|
|
72
|
+
services: {} as {
|
|
73
|
+
initializer: {
|
|
74
|
+
data: {
|
|
75
|
+
data: DataType
|
|
76
|
+
blueprints: DocumentBlueprint[]
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
setter: {
|
|
80
|
+
data: { data: DataType }
|
|
81
|
+
}
|
|
82
|
+
subDocumentResolver: {
|
|
83
|
+
data: {
|
|
84
|
+
id: string
|
|
85
|
+
location: string
|
|
86
|
+
}[]
|
|
87
|
+
}
|
|
88
|
+
onChangeCallback: {
|
|
89
|
+
data: undefined
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
events: {} as
|
|
93
|
+
| {
|
|
94
|
+
type: 'IFRAME_MOUNTED'
|
|
95
|
+
value: HTMLIFrameElement
|
|
96
|
+
}
|
|
97
|
+
| {
|
|
98
|
+
type: 'SET_URL'
|
|
99
|
+
value: string
|
|
100
|
+
}
|
|
101
|
+
| {
|
|
102
|
+
type: 'SET_INPUT_URL'
|
|
103
|
+
value: string
|
|
104
|
+
}
|
|
105
|
+
| {
|
|
106
|
+
type: 'SET_DISPLAY_URL'
|
|
107
|
+
value: string
|
|
108
|
+
}
|
|
109
|
+
| {
|
|
110
|
+
type: 'UPDATE_URL'
|
|
111
|
+
}
|
|
112
|
+
| {
|
|
113
|
+
type: 'SELECT_DOCUMENT'
|
|
114
|
+
value: string
|
|
115
|
+
}
|
|
116
|
+
| {
|
|
117
|
+
type: 'DOCUMENT_READY'
|
|
118
|
+
value: string
|
|
119
|
+
}
|
|
120
|
+
| {
|
|
121
|
+
type: 'ADD_QUERY'
|
|
122
|
+
value: {
|
|
123
|
+
id: string
|
|
124
|
+
type: 'open' | 'close'
|
|
125
|
+
query: string
|
|
126
|
+
data: object
|
|
127
|
+
variables: object
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
| {
|
|
131
|
+
type: 'REMOVE_QUERY'
|
|
132
|
+
value: string
|
|
133
|
+
}
|
|
134
|
+
| {
|
|
135
|
+
type: 'SUBDOCUMENTS'
|
|
136
|
+
value: { id: string; location: string }[]
|
|
137
|
+
}
|
|
138
|
+
| {
|
|
139
|
+
type: 'FIELD_CHANGE'
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
type: 'parallel',
|
|
143
|
+
states: {
|
|
144
|
+
url: {
|
|
145
|
+
initial: 'idle',
|
|
146
|
+
states: {
|
|
147
|
+
idle: {
|
|
148
|
+
on: {
|
|
149
|
+
SET_URL: { actions: 'setUrl' },
|
|
150
|
+
SET_INPUT_URL: { actions: 'setInputUrl' },
|
|
151
|
+
SET_DISPLAY_URL: { actions: 'setDisplayUrl' },
|
|
152
|
+
UPDATE_URL: { actions: 'updateUrl' },
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
pipeline: {
|
|
158
|
+
initial: 'idle',
|
|
159
|
+
states: {
|
|
160
|
+
idle: {
|
|
161
|
+
entry: 'clear',
|
|
162
|
+
on: {
|
|
163
|
+
ADD_QUERY: 'initializing',
|
|
164
|
+
SUBDOCUMENTS: 'pending',
|
|
165
|
+
IFRAME_MOUNTED: {
|
|
166
|
+
actions: 'setIframe',
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
initializing: {
|
|
171
|
+
invoke: {
|
|
172
|
+
src: 'initializer',
|
|
173
|
+
onDone: {
|
|
174
|
+
actions: ['storeInitialValues', 'scanForInitialDocuments'],
|
|
175
|
+
target: 'pending',
|
|
176
|
+
},
|
|
177
|
+
onError: {
|
|
178
|
+
target: 'error',
|
|
179
|
+
actions: 'handleError',
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
waiting: {
|
|
184
|
+
on: {
|
|
185
|
+
DOCUMENT_READY: 'pending',
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
pending: {
|
|
189
|
+
invoke: {
|
|
190
|
+
src: 'setter',
|
|
191
|
+
onDone: 'ready',
|
|
192
|
+
onError: {
|
|
193
|
+
target: 'waiting',
|
|
194
|
+
actions: 'handleMissingDocument',
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
ready: {
|
|
199
|
+
entry: 'resolveData',
|
|
200
|
+
invoke: {
|
|
201
|
+
src: 'onChangeCallback',
|
|
202
|
+
},
|
|
203
|
+
on: {
|
|
204
|
+
UPDATE_URL: 'idle',
|
|
205
|
+
REMOVE_QUERY: 'idle',
|
|
206
|
+
SELECT_DOCUMENT: {
|
|
207
|
+
actions: 'selectDocument',
|
|
208
|
+
},
|
|
209
|
+
// TODO: for most _change_ events we could probably
|
|
210
|
+
// optimize this and not go through the pending
|
|
211
|
+
// process, but for now it keeps things simple and totally works
|
|
212
|
+
// to just totally restart the process on each change
|
|
213
|
+
FIELD_CHANGE: {
|
|
214
|
+
target: 'pending',
|
|
215
|
+
// actions: 'rescanForInitialDocuments',
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
error: {},
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
actions: {
|
|
226
|
+
handleError: (_context, event) => console.error(event.data),
|
|
227
|
+
handleMissingDocument: assign((context, event) => {
|
|
228
|
+
count = count + 1
|
|
229
|
+
if (count > 50) {
|
|
230
|
+
throw new Error('infinite loop')
|
|
231
|
+
}
|
|
232
|
+
if (event.data instanceof QueryError) {
|
|
233
|
+
if (context.documentMap[event.data.id]) {
|
|
234
|
+
// Already exists
|
|
235
|
+
return context
|
|
236
|
+
}
|
|
237
|
+
const doc = {
|
|
238
|
+
ref: spawn(
|
|
239
|
+
documentMachine.withContext({
|
|
240
|
+
id: event.data.id,
|
|
241
|
+
locations: [],
|
|
242
|
+
cms: context.cms,
|
|
243
|
+
formifyCallback: context.formifyCallback,
|
|
244
|
+
form: null,
|
|
245
|
+
data: null,
|
|
246
|
+
subDocuments: [],
|
|
247
|
+
allBlueprints: context.blueprints,
|
|
248
|
+
})
|
|
249
|
+
),
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
...context,
|
|
254
|
+
documentMap: {
|
|
255
|
+
...context.documentMap,
|
|
256
|
+
[event.data.id]: doc,
|
|
257
|
+
},
|
|
258
|
+
}
|
|
259
|
+
} else {
|
|
260
|
+
console.error(event.data)
|
|
261
|
+
return context
|
|
262
|
+
}
|
|
263
|
+
}),
|
|
264
|
+
clear: assign((context) => {
|
|
265
|
+
Object.values(context.documentMap).forEach((doc) => {
|
|
266
|
+
const form = doc.ref.getSnapshot()?.context?.form
|
|
267
|
+
if (form) {
|
|
268
|
+
context.cms.forms.remove(form.id)
|
|
269
|
+
}
|
|
270
|
+
})
|
|
271
|
+
return {
|
|
272
|
+
...initialContext,
|
|
273
|
+
formifyCallback: context.formifyCallback,
|
|
274
|
+
cms: context.cms,
|
|
275
|
+
// documentMap: context.documentMap, // to preserve docs across pages
|
|
276
|
+
iframe: context.iframe,
|
|
277
|
+
url: context.url,
|
|
278
|
+
}
|
|
279
|
+
}),
|
|
280
|
+
setUrl: assign((context, event) => {
|
|
281
|
+
return {
|
|
282
|
+
...context,
|
|
283
|
+
url: event.value,
|
|
284
|
+
}
|
|
285
|
+
}),
|
|
286
|
+
setDisplayUrl: assign((context, event) => {
|
|
287
|
+
localStorage.setItem('tina-url', event.value)
|
|
288
|
+
return {
|
|
289
|
+
...context,
|
|
290
|
+
displayURL: event.value,
|
|
291
|
+
}
|
|
292
|
+
}),
|
|
293
|
+
setInputUrl: assign((context, event) => {
|
|
294
|
+
return {
|
|
295
|
+
...context,
|
|
296
|
+
inputURL: event.value.startsWith('/')
|
|
297
|
+
? event.value
|
|
298
|
+
: `/${event.value}`,
|
|
299
|
+
}
|
|
300
|
+
}),
|
|
301
|
+
updateUrl: assign((context) => {
|
|
302
|
+
if (context.inputURL) {
|
|
303
|
+
Object.values(context.documentMap).forEach((doc) => {
|
|
304
|
+
const form = doc.ref.getSnapshot()?.context?.form
|
|
305
|
+
if (form) {
|
|
306
|
+
context.cms.forms.remove(form.id)
|
|
307
|
+
}
|
|
308
|
+
})
|
|
309
|
+
return {
|
|
310
|
+
...context,
|
|
311
|
+
selectedDocument: initialContext.selectedDocument,
|
|
312
|
+
documentMap: initialContext.documentMap,
|
|
313
|
+
blueprints: initialContext.blueprints,
|
|
314
|
+
data: initialContext.data,
|
|
315
|
+
inputURL: null,
|
|
316
|
+
displayURL: context.inputURL,
|
|
317
|
+
url: context.inputURL,
|
|
318
|
+
}
|
|
319
|
+
} else {
|
|
320
|
+
return context
|
|
321
|
+
}
|
|
322
|
+
}),
|
|
323
|
+
storeInitialValues: assign((context, event) => {
|
|
324
|
+
return {
|
|
325
|
+
...context,
|
|
326
|
+
...event.data,
|
|
327
|
+
}
|
|
328
|
+
}),
|
|
329
|
+
selectDocument: assign((context, event) => {
|
|
330
|
+
return {
|
|
331
|
+
...context,
|
|
332
|
+
selectedDocument: event.value,
|
|
333
|
+
}
|
|
334
|
+
}),
|
|
335
|
+
setIframe: assign((context, event) => {
|
|
336
|
+
return {
|
|
337
|
+
...context,
|
|
338
|
+
iframe: event.value,
|
|
339
|
+
}
|
|
340
|
+
}),
|
|
341
|
+
resolveData: assign((context, event) => {
|
|
342
|
+
if (context.iframe) {
|
|
343
|
+
context.iframe?.contentWindow?.postMessage({
|
|
344
|
+
id: context.id,
|
|
345
|
+
data: event.data.data,
|
|
346
|
+
})
|
|
347
|
+
}
|
|
348
|
+
return {
|
|
349
|
+
...context,
|
|
350
|
+
}
|
|
351
|
+
}),
|
|
352
|
+
scanForInitialDocuments: assign((context, event) => {
|
|
353
|
+
const blueprints = event.data.blueprints.filter(
|
|
354
|
+
(blueprint) => blueprint.isTopLevel
|
|
355
|
+
)
|
|
356
|
+
const newDocuments: DocumentMap = {}
|
|
357
|
+
blueprints.forEach((blueprint) => {
|
|
358
|
+
const values = util.getAllIn(context.data, blueprint.id)
|
|
359
|
+
|
|
360
|
+
values?.forEach((value) => {
|
|
361
|
+
const location = spliceLocation(blueprint.id, value.location || [])
|
|
362
|
+
const existing = context.documentMap[value.value.id]
|
|
363
|
+
if (existing) {
|
|
364
|
+
existing.ref.send({ type: 'ADD_LOCATION', value: location })
|
|
365
|
+
} else {
|
|
366
|
+
newDocuments[value.value.id] = {
|
|
367
|
+
ref: spawn(
|
|
368
|
+
documentMachine.withContext({
|
|
369
|
+
id: value.value.id,
|
|
370
|
+
form: null,
|
|
371
|
+
cms: context.cms,
|
|
372
|
+
formifyCallback: context.formifyCallback,
|
|
373
|
+
data: null,
|
|
374
|
+
locations: [location],
|
|
375
|
+
subDocuments: [],
|
|
376
|
+
allBlueprints: context.blueprints,
|
|
377
|
+
})
|
|
378
|
+
),
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
})
|
|
382
|
+
})
|
|
383
|
+
const nextDocumentMap = { ...context.documentMap, ...newDocuments }
|
|
384
|
+
return {
|
|
385
|
+
...context,
|
|
386
|
+
documentMap: nextDocumentMap,
|
|
387
|
+
}
|
|
388
|
+
}),
|
|
389
|
+
},
|
|
390
|
+
services: {
|
|
391
|
+
setter: async (context) => {
|
|
392
|
+
let newData = {}
|
|
393
|
+
const initialBlueprints = context.blueprints.filter(
|
|
394
|
+
(blueprint) => blueprint.isTopLevel
|
|
395
|
+
)
|
|
396
|
+
initialBlueprints.forEach((blueprint) => {
|
|
397
|
+
const values = util.getAllInBlueprint(context.data, blueprint.id)
|
|
398
|
+
|
|
399
|
+
values?.forEach((value) => {
|
|
400
|
+
const location = spliceLocation(blueprint.id, value.location || [])
|
|
401
|
+
if (!value.value) {
|
|
402
|
+
return
|
|
403
|
+
}
|
|
404
|
+
const doc = context.documentMap[value.value._internalSys.path]
|
|
405
|
+
const docContext = doc.ref.getSnapshot()?.context
|
|
406
|
+
const form = docContext?.form
|
|
407
|
+
if (!form) {
|
|
408
|
+
throw new QueryError(
|
|
409
|
+
`Unable to resolve form for initial document`,
|
|
410
|
+
value.value._internalSys.path
|
|
411
|
+
)
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* This section can be removed when we support forms for list
|
|
416
|
+
* and nested items.
|
|
417
|
+
*/
|
|
418
|
+
if (blueprint.path.some((item) => item.list)) {
|
|
419
|
+
// do nothing
|
|
420
|
+
} else {
|
|
421
|
+
if (form.global) {
|
|
422
|
+
context.cms.plugins.add(
|
|
423
|
+
new GlobalFormPlugin(
|
|
424
|
+
form,
|
|
425
|
+
form.global?.icon,
|
|
426
|
+
form.global?.layout
|
|
427
|
+
)
|
|
428
|
+
)
|
|
429
|
+
} else {
|
|
430
|
+
context.cms.forms.add(form)
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (docContext.data) {
|
|
435
|
+
const nextData: Record<string, unknown> = setData({
|
|
436
|
+
id: docContext.id,
|
|
437
|
+
data: { ...docContext.data, ...form.values },
|
|
438
|
+
// @ts-ignore form.fields is Field
|
|
439
|
+
fields: form.fields,
|
|
440
|
+
namespace: [docContext.data._internalSys.collection.name],
|
|
441
|
+
path: [],
|
|
442
|
+
blueprint,
|
|
443
|
+
context,
|
|
444
|
+
})
|
|
445
|
+
newData = setIn(newData, location, nextData)
|
|
446
|
+
}
|
|
447
|
+
})
|
|
448
|
+
})
|
|
449
|
+
return { data: newData }
|
|
450
|
+
},
|
|
451
|
+
initializer: async (context, event) => {
|
|
452
|
+
const schema = await context.cms.api.tina.getSchema()
|
|
453
|
+
const documentNode = G.parse(event.value.query)
|
|
454
|
+
const optimizedQuery = await context.cms.api.tina.getOptimizedQuery(
|
|
455
|
+
documentNode
|
|
456
|
+
)
|
|
457
|
+
if (!optimizedQuery) {
|
|
458
|
+
throw new Error(`Unable to optimize query`)
|
|
459
|
+
}
|
|
460
|
+
const { blueprints, formifiedQuery } = await formify({
|
|
461
|
+
schema,
|
|
462
|
+
optimizedDocumentNode: optimizedQuery,
|
|
463
|
+
})
|
|
464
|
+
const data = await context.cms.api.tina.request<DataType>(
|
|
465
|
+
G.print(formifiedQuery),
|
|
466
|
+
{
|
|
467
|
+
variables: event.value.variables,
|
|
468
|
+
}
|
|
469
|
+
)
|
|
470
|
+
return { data, blueprints, id: event.value.id }
|
|
471
|
+
},
|
|
472
|
+
onChangeCallback: (context) => (callback, _onReceive) => {
|
|
473
|
+
if (context.cms) {
|
|
474
|
+
context.cms.events.subscribe(`forms:fields:onChange`, () => {
|
|
475
|
+
callback({ type: 'FIELD_CHANGE' })
|
|
476
|
+
})
|
|
477
|
+
context.cms.events.subscribe(`forms:reset`, () => {
|
|
478
|
+
callback({ type: 'FIELD_CHANGE' })
|
|
479
|
+
})
|
|
480
|
+
}
|
|
481
|
+
},
|
|
482
|
+
},
|
|
483
|
+
}
|
|
484
|
+
)
|
|
485
|
+
class QueryError extends Error {
|
|
486
|
+
public id: string
|
|
487
|
+
constructor(message: string, id: string) {
|
|
488
|
+
super(message) // (1)
|
|
489
|
+
this.name = 'QueryError' // (2)
|
|
490
|
+
this.id = id
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
let count = 0
|
|
494
|
+
|
|
495
|
+
// https://github.com/oleics/node-is-scalar/blob/master/index.js
|
|
496
|
+
const withSymbol = typeof Symbol !== 'undefined'
|
|
497
|
+
function isScalar(value) {
|
|
498
|
+
const type = typeof value
|
|
499
|
+
if (type === 'string') return true
|
|
500
|
+
if (type === 'number') return true
|
|
501
|
+
if (type === 'boolean') return true
|
|
502
|
+
if (withSymbol === true && type === 'symbol') return true
|
|
503
|
+
|
|
504
|
+
if (value == null) return true
|
|
505
|
+
if (withSymbol === true && value instanceof Symbol) return true
|
|
506
|
+
if (value instanceof String) return true
|
|
507
|
+
if (value instanceof Number) return true
|
|
508
|
+
if (value instanceof Boolean) return true
|
|
509
|
+
|
|
510
|
+
return false
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const excludedValues = ['_internalValues', '_collection', '_template']
|
|
514
|
+
const excludedTinaFieldValues = [
|
|
515
|
+
'_sys',
|
|
516
|
+
'_internalSys',
|
|
517
|
+
'_internalValues',
|
|
518
|
+
'_values',
|
|
519
|
+
'_collection',
|
|
520
|
+
'_template',
|
|
521
|
+
]
|
|
522
|
+
|
|
523
|
+
const setData = ({
|
|
524
|
+
id,
|
|
525
|
+
data,
|
|
526
|
+
path,
|
|
527
|
+
fields,
|
|
528
|
+
namespace,
|
|
529
|
+
blueprint,
|
|
530
|
+
context,
|
|
531
|
+
}: {
|
|
532
|
+
id: string
|
|
533
|
+
data: Record<string, unknown>
|
|
534
|
+
path: (string | number)[]
|
|
535
|
+
fields: TinaField[]
|
|
536
|
+
namespace: string[]
|
|
537
|
+
blueprint: DocumentBlueprint
|
|
538
|
+
context: ContextFrom<typeof queryMachine>
|
|
539
|
+
}) => {
|
|
540
|
+
const nextData: Record<string, unknown> = {}
|
|
541
|
+
nextData['__typename'] = NAMER.dataTypeName(namespace)
|
|
542
|
+
nextData['_tinaField'] = {
|
|
543
|
+
id,
|
|
544
|
+
keys: Object.keys(data)
|
|
545
|
+
.filter((key) => !excludedTinaFieldValues.includes(key))
|
|
546
|
+
.map((item) => [...path, item].join('.')),
|
|
547
|
+
}
|
|
548
|
+
Object.entries(data).forEach(([key, value]) => {
|
|
549
|
+
const field = fields.find((field) => field.name === key)
|
|
550
|
+
const nextPath = [...path, key]
|
|
551
|
+
if (!value) {
|
|
552
|
+
nextData[key] = null
|
|
553
|
+
}
|
|
554
|
+
// This is a property not controlled by the form, so it
|
|
555
|
+
// cannot change. Eg. _sys
|
|
556
|
+
if (!field) {
|
|
557
|
+
if (!excludedValues.includes(key)) {
|
|
558
|
+
nextData[key] = value
|
|
559
|
+
}
|
|
560
|
+
return
|
|
561
|
+
}
|
|
562
|
+
if (Array.isArray(value)) {
|
|
563
|
+
if (field) {
|
|
564
|
+
if (!field.list) {
|
|
565
|
+
throw new Error(
|
|
566
|
+
`Expected field for array value to be have property list: true`
|
|
567
|
+
)
|
|
568
|
+
} else {
|
|
569
|
+
if (field.type === 'object') {
|
|
570
|
+
if (field.templates) {
|
|
571
|
+
nextData[key] = value.map((item, index) => {
|
|
572
|
+
const template = Object.values(field.templates).find(
|
|
573
|
+
(template) => {
|
|
574
|
+
// @ts-ignore FIXME: template is transformed to an
|
|
575
|
+
// object that the `blocks` field plugin expects
|
|
576
|
+
return template.key === item._template
|
|
577
|
+
}
|
|
578
|
+
)
|
|
579
|
+
if (!template) {
|
|
580
|
+
throw new Error(
|
|
581
|
+
`Unable to find template for field ${field.name}`
|
|
582
|
+
)
|
|
583
|
+
}
|
|
584
|
+
return setData({
|
|
585
|
+
id,
|
|
586
|
+
data: item,
|
|
587
|
+
path: [...nextPath, index],
|
|
588
|
+
// @ts-ignore form.fields is Field
|
|
589
|
+
fields: template.fields,
|
|
590
|
+
namespace: template.namespace,
|
|
591
|
+
blueprint,
|
|
592
|
+
context,
|
|
593
|
+
})
|
|
594
|
+
})
|
|
595
|
+
} else {
|
|
596
|
+
if (typeof field?.fields === 'string') {
|
|
597
|
+
throw new Error('Global templates not supported')
|
|
598
|
+
}
|
|
599
|
+
nextData[key] = value.map((item, index) =>
|
|
600
|
+
setData({
|
|
601
|
+
id,
|
|
602
|
+
data: item,
|
|
603
|
+
path: [...nextPath, index],
|
|
604
|
+
// @ts-ignore form.fields is Field
|
|
605
|
+
fields: field.fields,
|
|
606
|
+
namespace: field.namespace,
|
|
607
|
+
blueprint,
|
|
608
|
+
context,
|
|
609
|
+
})
|
|
610
|
+
)
|
|
611
|
+
}
|
|
612
|
+
} else {
|
|
613
|
+
nextData[key] = value
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
} else {
|
|
617
|
+
nextData[key] = value.map((item, index) =>
|
|
618
|
+
isScalar(item)
|
|
619
|
+
? item
|
|
620
|
+
: setData({
|
|
621
|
+
id,
|
|
622
|
+
data: item,
|
|
623
|
+
path: [...nextPath, index],
|
|
624
|
+
namespace: field.namespace,
|
|
625
|
+
fields: [],
|
|
626
|
+
blueprint,
|
|
627
|
+
context,
|
|
628
|
+
})
|
|
629
|
+
)
|
|
630
|
+
}
|
|
631
|
+
} else {
|
|
632
|
+
const fieldBlueprintPath = `${blueprint.id}.${nextPath
|
|
633
|
+
.map((item) => (isNaN(Number(item)) ? item : '[]'))
|
|
634
|
+
.join('.')}`
|
|
635
|
+
|
|
636
|
+
const childBlueprint = context.blueprints.find(
|
|
637
|
+
({ id }) => id === fieldBlueprintPath
|
|
638
|
+
)
|
|
639
|
+
const blueprintField = blueprint.fields.find(
|
|
640
|
+
({ id }) => id === fieldBlueprintPath
|
|
641
|
+
)
|
|
642
|
+
// If the query isn't requesting this data, don't populate it
|
|
643
|
+
if (!blueprintField && !childBlueprint) {
|
|
644
|
+
return
|
|
645
|
+
}
|
|
646
|
+
if (isScalar(value)) {
|
|
647
|
+
// This value is a reference (eg. "content/authors/pedro.md")
|
|
648
|
+
if (childBlueprint) {
|
|
649
|
+
if (typeof value === 'string') {
|
|
650
|
+
if (!value) {
|
|
651
|
+
nextData[key] = null
|
|
652
|
+
return
|
|
653
|
+
}
|
|
654
|
+
const doc = context.documentMap[value]
|
|
655
|
+
const docContext = doc?.ref?.getSnapshot()?.context
|
|
656
|
+
const form = docContext?.form
|
|
657
|
+
if (!form) {
|
|
658
|
+
throw new QueryError(`Unable to resolve form for document`, value)
|
|
659
|
+
}
|
|
660
|
+
nextData[key] = {
|
|
661
|
+
id: docContext.id,
|
|
662
|
+
...setData({
|
|
663
|
+
id: docContext.id,
|
|
664
|
+
data: { ...docContext.data, ...form.values },
|
|
665
|
+
// @ts-ignore form.fields is Field
|
|
666
|
+
fields: form.fields,
|
|
667
|
+
namespace: [],
|
|
668
|
+
path: [],
|
|
669
|
+
blueprint: childBlueprint,
|
|
670
|
+
context,
|
|
671
|
+
}),
|
|
672
|
+
}
|
|
673
|
+
} else {
|
|
674
|
+
// The reference value is null
|
|
675
|
+
nextData[key] = null
|
|
676
|
+
}
|
|
677
|
+
} else {
|
|
678
|
+
// This is a reference that's not formified.
|
|
679
|
+
// That is - we don't generate form for it because the query didn't ask us to
|
|
680
|
+
if (field && field.type === 'reference') {
|
|
681
|
+
if (value) {
|
|
682
|
+
nextData[key] = { id: value }
|
|
683
|
+
} else {
|
|
684
|
+
nextData[key] = null
|
|
685
|
+
}
|
|
686
|
+
} else {
|
|
687
|
+
nextData[key] = value
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
} else {
|
|
691
|
+
// TODO: when rich-text is {json: {}, embeds: {}[]} we'll need to resolve the embeds
|
|
692
|
+
if (field.type === 'rich-text') {
|
|
693
|
+
nextData[key] = value
|
|
694
|
+
return
|
|
695
|
+
}
|
|
696
|
+
if (field.type !== 'object') {
|
|
697
|
+
throw new Error(
|
|
698
|
+
`Expected field for object values to be of type "object", but got ${field.type}`
|
|
699
|
+
)
|
|
700
|
+
}
|
|
701
|
+
if (field?.templates) {
|
|
702
|
+
throw new Error(`Unexpected path ${field.name}`)
|
|
703
|
+
} else {
|
|
704
|
+
if (typeof field?.fields === 'string') {
|
|
705
|
+
throw new Error('Global templates not supported')
|
|
706
|
+
}
|
|
707
|
+
const nextValue = value as Record<string, unknown>
|
|
708
|
+
const nextDataResult = setData({
|
|
709
|
+
id,
|
|
710
|
+
data: nextValue,
|
|
711
|
+
path: nextPath,
|
|
712
|
+
namespace: field.namespace,
|
|
713
|
+
fields: field.fields,
|
|
714
|
+
blueprint,
|
|
715
|
+
context,
|
|
716
|
+
})
|
|
717
|
+
// Don't populate an empty key {someValue: {}}
|
|
718
|
+
if (Object.keys(nextDataResult).length > 0) {
|
|
719
|
+
nextData[key] = nextDataResult
|
|
720
|
+
} else {
|
|
721
|
+
nextData[key] = null
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
})
|
|
727
|
+
return nextData
|
|
728
|
+
}
|