@tinacms/app 0.0.12 → 0.0.13

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,130 +0,0 @@
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
-
14
- import type * as G from 'graphql'
15
-
16
- export type FormifiedDocumentNode = {
17
- id: string
18
- _internalSys: {
19
- path: string
20
- relativePath: string
21
- collection: {
22
- name
23
- }
24
- }
25
- _values: object
26
- }
27
-
28
- export type ChangeMutation = { type: 'change' }
29
- export type ReferenceChangeMutation = { type: 'referenceChange' }
30
- export type InsertMutation = { type: 'insert'; at: number }
31
- export type MoveMutation = { type: 'move'; from: number; to: number }
32
- export type RemoveMutation = { type: 'remove'; at: number }
33
- export type ResetMutation = { type: 'reset' }
34
- export type GlobalMutation = { type: 'global' }
35
-
36
- type MutationType =
37
- | ChangeMutation
38
- | ReferenceChangeMutation
39
- | InsertMutation
40
- | MoveMutation
41
- | RemoveMutation
42
- | ResetMutation
43
- | GlobalMutation
44
-
45
- export type OnChangeEvent = {
46
- type: 'forms:fields:onChange' | 'forms:reset'
47
- value: unknown
48
- previousValue: unknown
49
- mutationType: MutationType
50
- formId: string
51
- field: {
52
- data: {
53
- tinaField: {
54
- name: string
55
- type: 'string' | 'reference' | 'object'
56
- list?: boolean
57
- parentTypename: string
58
- }
59
- }
60
- name: string
61
- }
62
- }
63
-
64
- export type ChangeSet = {
65
- path: string
66
- // event: OnChangeEvent
67
- value: unknown
68
- formId: string
69
- fieldDefinition: {
70
- name: string
71
- type: 'string' | 'reference' | 'object'
72
- list?: boolean
73
- }
74
- mutationType: MutationType
75
- name: string
76
- displaceIndex?: boolean // FIXME: this should be the index, not a boolean
77
- formNode: FormNode
78
- }
79
-
80
- export type BlueprintPath = {
81
- name: string
82
- alias: string
83
- parentTypename?: string
84
- list?: boolean
85
- isNode?: boolean
86
- }
87
-
88
- export type DocumentBlueprint = {
89
- /** The stringified representation of a path relative to root or it's parent document */
90
- id: string
91
- /** The path to a field node */
92
- path: BlueprintPath[]
93
- /** The GraphQL SelectionNode, useful for re-fetching the given node */
94
- selection: G.SelectionNode
95
- fields: FieldBlueprint[]
96
- /** For now, only top-level, non-list nodes will be shown in the sidebar */
97
- showInSidebar: boolean
98
- /** these 2 are not traditional GraphQL fields but need be kept in-sync regardless */
99
- hasDataJSONField: boolean
100
- hasValuesField: boolean
101
- /** this blueprint is not the result of a reference */
102
- isTopLevel: boolean
103
- }
104
- export type FieldBlueprint = {
105
- /** The stringified representation of a path relative to root or it's parent document */
106
- id: string
107
- documentBlueprintId: string
108
- /** The path to a field node */
109
- path: BlueprintPath[]
110
- }
111
- export type FormNode = {
112
- /** The stringified path with location values injected (eg. 'getBlockPageList.edges.0.node.data.social.1.relatedPage') */
113
- documentFormId: string
114
- documentBlueprintId: string
115
- /** Coordinates for the DocumentBlueprint's '[]' values */
116
- location: number[]
117
- }
118
- /** The document ID is the true ID 'content/pages/hello-world.md') */
119
-
120
- export type State = {
121
- schema: G.GraphQLSchema
122
- query: G.DocumentNode
123
- queryString: string
124
- status: 'idle' | 'initialized' | 'formified' | 'ready' | 'done'
125
- count: number
126
- data: object
127
- changeSets: ChangeSet[]
128
- blueprints: DocumentBlueprint[]
129
- formNodes: FormNode[]
130
- }
@@ -1,598 +0,0 @@
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
-
14
- import { Form, Field, FormOptions, TinaCMS, AnyField, TinaField } from 'tinacms'
15
- import { getIn } from 'final-form'
16
-
17
- import type {
18
- DocumentBlueprint,
19
- FieldBlueprint,
20
- OnChangeEvent,
21
- FormNode,
22
- State,
23
- ChangeSet,
24
- BlueprintPath,
25
- } from './types'
26
- import { TinaSchema, resolveForm } from '@tinacms/schema-tools'
27
-
28
- // import {
29
- // generateFormCreators,
30
- // formifyCallback,
31
- // transformDocumentIntoMutationRequestPayload,
32
- // onSubmitArgs,
33
- // } from '../use-graphql-forms'
34
-
35
- interface RecursiveFormifiedDocumentNode<T extends object>
36
- extends Array<RecursiveFormifiedDocumentNode<T> | T> {}
37
- export const getValueForBlueprint = <T extends object>(
38
- state: object,
39
- path: string
40
- ): RecursiveFormifiedDocumentNode<T> | T => {
41
- const pathArray = path.split('.')
42
- let latest: object | undefined = state
43
- pathArray.every((item, index) => {
44
- if (item === '[]') {
45
- const restOfItems = pathArray.slice(index + 1)
46
- if (latest) {
47
- const next: (RecursiveFormifiedDocumentNode<object> | object)[] = []
48
- if (Array.isArray(latest)) {
49
- latest.forEach((latest2) => {
50
- const res = getValueForBlueprint(latest2, restOfItems.join('.'))
51
- next.push(res)
52
- })
53
- } else {
54
- throw new Error(`Expected value to be an array for "[]" item`)
55
- }
56
- if (next.length > 0) {
57
- latest = next
58
- } else {
59
- latest = undefined
60
- }
61
- }
62
- return false
63
- } else {
64
- if (latest) {
65
- latest = latest[item]
66
- } else {
67
- latest = undefined
68
- }
69
- }
70
- return true
71
- })
72
- // @ts-ignore
73
- return latest
74
- }
75
-
76
- export const getNamePath = (path: BlueprintPath[]) => {
77
- const namePath: string[] = []
78
- path.forEach((p) => {
79
- namePath.push(p.name)
80
- if (p.list) {
81
- namePath.push('[]')
82
- }
83
- })
84
-
85
- return namePath.join('.')
86
- }
87
-
88
- export const getBlueprintNamePath = (
89
- blueprint: Pick<DocumentBlueprint, 'path'>,
90
- disambiguator?: boolean
91
- ) => {
92
- const namePath: string[] = []
93
- blueprint.path.forEach((p) => {
94
- if (disambiguator) {
95
- if (p.parentTypename) {
96
- namePath.push(p.parentTypename)
97
- }
98
- }
99
- namePath.push(p.name)
100
- if (p.list) {
101
- namePath.push('[]')
102
- }
103
- })
104
-
105
- return namePath.join('.')
106
- }
107
-
108
- interface RecursiveFormifiedDocumentNode<T extends object>
109
- extends Array<RecursiveFormifiedDocumentNode<T> | T> {}
110
-
111
- /**
112
- * Returns the name of the field. In the example query, `title` and `t` would both be blueprint fields
113
- *
114
- * ```graphql
115
- * {
116
- * getPostDocument(relativePath: $relativePath) {
117
- * data {
118
- * title,
119
- * t: title # here `t` is an alias for title
120
- * }
121
- * }
122
- * }
123
- * ```
124
- */
125
- export const getFieldNameOrAlias = (fieldBlueprint: FieldBlueprint) => {
126
- return fieldBlueprint.path[fieldBlueprint.path.length - 1].alias
127
- }
128
-
129
- export const spliceLocation = (string: string, location: number[] | null) => {
130
- const accum: (string | number)[] = []
131
- let counter = 0
132
-
133
- string.split('.').forEach((item) => {
134
- if (item === '[]') {
135
- if (!location) {
136
- throw new Error(`Error splicing without location`)
137
- }
138
- accum.push(location[counter])
139
- counter++
140
- } else {
141
- accum.push(item)
142
- }
143
- })
144
-
145
- return accum.join('.')
146
- }
147
-
148
- export const getPathToChange = (
149
- documentBlueprint: DocumentBlueprint | FieldBlueprint,
150
- formNode: FormNode,
151
- event: OnChangeEvent
152
- ) => {
153
- const fieldName = event.field.name
154
- const location = [...formNode.location, ...stripIndices(fieldName)]
155
- const accum: (string | number)[] = []
156
- let counter = 0
157
- documentBlueprint.path.forEach((item) => {
158
- accum.push(item.alias)
159
- if (item.list) {
160
- // If there's no match we're assuming it's a list field, and not an item within the list field
161
- // eg. blocks vs blocks.0
162
- if (location[counter] !== undefined) {
163
- accum.push(location[counter])
164
- counter++
165
- }
166
- }
167
- })
168
-
169
- return accum.join('.')
170
- }
171
-
172
- export const formNodeId = (formNode: FormNode) => {
173
- return (
174
- spliceLocation(formNode.documentBlueprintId, formNode.location) +
175
- formNode.documentFormId
176
- )
177
- }
178
- export const formNodePath = (formNode: FormNode) => {
179
- return spliceLocation(formNode.documentBlueprintId, formNode.location)
180
- }
181
-
182
- export const formNodeNotIn = (formNode: FormNode, formNodes: FormNode[]) => {
183
- return !formNodes.find((fn) => formNodeId(fn) === formNodeId(formNode))
184
- }
185
-
186
- export const sequential = async <A, B>(
187
- items: A[] | undefined,
188
- callback: (args: A, idx: number) => Promise<B>
189
- ) => {
190
- const accum: B[] = []
191
- if (!items) {
192
- return []
193
- }
194
-
195
- const reducePromises = async (previous: Promise<B>, endpoint: A) => {
196
- const prev = await previous
197
- // initial value will be undefined
198
- if (prev) {
199
- accum.push(prev)
200
- }
201
-
202
- return callback(endpoint, accum.length)
203
- }
204
-
205
- // @ts-ignore FIXME: this can be properly typed
206
- const result = await items.reduce(reducePromises, Promise.resolve())
207
- if (result) {
208
- // @ts-ignore FIXME: this can be properly typed
209
- accum.push(result)
210
- }
211
-
212
- return accum
213
- }
214
-
215
- const getFormNodesStartingWith = (string: string, state: State) => {
216
- return state.formNodes.filter((subFormNode) => {
217
- return subFormNode.documentBlueprintId.startsWith(string)
218
- })
219
- }
220
-
221
- export const getFormNodesForField = (
222
- fieldBlueprint: FieldBlueprint,
223
- formNode: FormNode,
224
- event: OnChangeEvent,
225
- state: State
226
- ) => {
227
- const pathToChange = getPathToChange(fieldBlueprint, formNode, event)
228
- const formNodes = getFormNodesStartingWith(fieldBlueprint.id, state)
229
- const eventLocation = [
230
- ...formNode.location,
231
- ...stripIndices(event.field.name),
232
- ]
233
- // const existing = getIn(state.data, pathToChange)
234
- return { pathToChange, formNodes, eventLocation, existing }
235
- }
236
-
237
- export const getBlueprintAliasPath = (blueprint: DocumentBlueprint) => {
238
- const namePath: string[] = []
239
- const aliasPath: string[] = []
240
- blueprint.path.forEach((p) => {
241
- namePath.push(p.name)
242
- aliasPath.push(p.alias)
243
- if (p.list) {
244
- namePath.push('[]')
245
- aliasPath.push('[]')
246
- }
247
- })
248
-
249
- return aliasPath.join('.')
250
- }
251
-
252
- export const getFieldAliasForBlueprint = (path: BlueprintPath[]) => {
253
- const reversePath = [...path].reverse()
254
- const accum: string[] = []
255
- reversePath.every((item, index) => {
256
- if (index === 0) {
257
- if (item.list) {
258
- accum.push('[]')
259
- }
260
- accum.push(item.alias)
261
- } else {
262
- if (item.isNode) {
263
- return false
264
- }
265
- if (item.list) {
266
- accum.push('[]')
267
- }
268
- accum.push(item.alias)
269
- }
270
- return true
271
- })
272
- return accum.reverse().join('.')
273
- }
274
-
275
- /**
276
- *
277
- * Determines the appropriate fields which should recieve an update from a form change
278
- *
279
- * In cases where there's polymorphic blocks, it's possible that an update would affect
280
- * multiple locations that it shouldn't.
281
- *
282
- * An OnChange event name can look like: `blocks.2.title`, but if there are 2 block elements
283
- * with a field of the same name, an event name it wouldn't be enough information for us.
284
- *
285
- * To get around this, the event sends the current `typename` along with it, and we use that
286
- * to determine where in our blueprint the value should be updated.
287
- *
288
- */
289
- export const getBlueprintFieldsForEvent = (
290
- blueprint: DocumentBlueprint,
291
- event: OnChangeEvent
292
- ) => {
293
- return blueprint.fields
294
- .filter((fbp) => {
295
- if (getBlueprintNamePath(fbp) === getEventPath(event, blueprint)) {
296
- return true
297
- }
298
- })
299
- .filter((fbp) => {
300
- return filterFieldBlueprintsByParentTypename(
301
- fbp,
302
- event.field.data.tinaField.parentTypename
303
- )
304
- })
305
- }
306
-
307
- export const filterFieldBlueprintsByParentTypename = (
308
- fbp: FieldBlueprint,
309
- typename
310
- ) => {
311
- let lastDisambiguator: string
312
-
313
- fbp.path.forEach((path) => {
314
- if (path.parentTypename) {
315
- lastDisambiguator = path.parentTypename
316
- }
317
- })
318
- if (lastDisambiguator) {
319
- return typename === lastDisambiguator
320
- } else {
321
- return true
322
- }
323
- }
324
-
325
- /**
326
- *
327
- * Returns the path for the event, which uses `data.tinaField` metadata to
328
- * determine the shape of the field. For polymorphic objects it's necessary
329
- * to build-in the name of the GraphQL type that's receiving the change.
330
- *
331
- * Eg. when the title of a blocks "cta" template is changed, we might see an
332
- * event path like:
333
- * ```
334
- * getPageDocument.data.blocks.0.PageBlocksCta.title
335
- * ```
336
- */
337
- const getEventPath = (
338
- event: OnChangeEvent,
339
- blueprint: DocumentBlueprint | FieldBlueprint
340
- ) => {
341
- const stringArray = event.field.name.split('.')
342
- const eventPath = stringArray
343
- .map((item) => {
344
- if (isNaN(Number(item))) {
345
- return item
346
- }
347
- return `[]`
348
- })
349
- .join('.')
350
- const items = [blueprint.id, eventPath]
351
- const isList = event.field.data.tinaField.list
352
- if (isList && !eventPath.endsWith('[]')) {
353
- items.push(`[]`)
354
- }
355
- return items.join('.')
356
- }
357
-
358
- export const stripIndices = (string) => {
359
- const accum: string[] = []
360
- const stringArray = string.split('.')
361
- stringArray.forEach((item) => {
362
- if (isNaN(item)) {
363
- } else {
364
- accum.push(item)
365
- }
366
- })
367
-
368
- return accum
369
- }
370
-
371
- export const replaceRealNum = (string) => {
372
- const stringArray = string.split('.')
373
- return stringArray
374
- .map((item) => {
375
- if (isNaN(item)) {
376
- return item
377
- }
378
- return '[]'
379
- })
380
- .join('.')
381
- }
382
-
383
- export const getMatchName = ({
384
- field,
385
- prefix,
386
- blueprint,
387
- }: {
388
- field: TinaField
389
- prefix?: string
390
- blueprint: DocumentBlueprint
391
- }) => {
392
- const fieldName = field.list ? `${field.name}.[]` : field.name
393
- const blueprintName = getBlueprintNamePath(blueprint)
394
- const extra: string[] = []
395
- if (prefix) {
396
- extra.push(prefix)
397
- }
398
- const matchName = [blueprintName, ...extra, fieldName].join('.')
399
- return { matchName, fieldName }
400
- }
401
-
402
- export const getFormNodesFromEvent = (state: State, event: OnChangeEvent) => {
403
- const formNodes = state.formNodes.filter(
404
- (formNode) => formNode.documentFormId === event.formId
405
- )
406
- return formNodes
407
- }
408
-
409
- export const getBlueprintFieldPath = (
410
- blueprint: DocumentBlueprint,
411
- blueprintField: FieldBlueprint
412
- ) => {
413
- return blueprintField.path.slice(blueprint.path.length)
414
- }
415
-
416
- export const printState = (state: State) => {
417
- let string = ''
418
- state.blueprints.forEach((blueprint) => {
419
- let bpString = `# Blueprint\n`
420
- bpString = bpString + `# ${blueprint.id}`
421
- bpString = bpString + `\n#`
422
- bpString = bpString + `\n# Documents for blueprint`
423
- bpString = bpString + `\n# ================`
424
- state.formNodes
425
- .filter((formNode) => formNode.documentBlueprintId === blueprint.id)
426
- .forEach((formNode) => {
427
- const newString = `# ${formNode.documentFormId}${
428
- blueprint.id.includes('[]')
429
- ? ` [${formNode.location.join(', ')}]`
430
- : ``
431
- }`
432
- bpString = bpString + `\n${newString}`
433
- })
434
-
435
- bpString = bpString + `\n#`
436
- bpString = bpString + `\n# Field blueprints`
437
- bpString = bpString + `\n# ================`
438
- blueprint.fields
439
- .filter((fbp) => fbp.documentBlueprintId === blueprint.id)
440
- .forEach((fbp) => {
441
- bpString = bpString + `\n# ${getFieldAliasForBlueprint(fbp.path)}`
442
- // bpString = bpString + `\n# ${getBlueprintAliasPath(fbp)}`
443
- // bpString = bpString + `\n# ${fbp.id}`
444
- })
445
- string = string + `${bpString}\n`
446
- string = string + '\n'
447
- })
448
- string = string + `\n${state.queryString}`
449
-
450
- return string
451
- }
452
-
453
- export const printEvent = (event: OnChangeEvent) => {
454
- return {
455
- type: event.type,
456
- value: event.value,
457
- previousValue: event.previousValue,
458
- mutationType: event.mutationType,
459
- formId: event.formId,
460
- field: {
461
- data: event.field?.data,
462
- name: event.field?.name,
463
- },
464
- }
465
- }
466
-
467
- export const getFormNodeBlueprint = (formNode: FormNode, state: State) => {
468
- return state.blueprints.find((d) => d.id === formNode.documentBlueprintId)
469
- }
470
-
471
- export const getMoveMapping = (existing, from, to) => {
472
- const newOrderObject: { [key: number]: number } = {}
473
- if (from < to) {
474
- existing.map((_, i) => {
475
- if (i === from) {
476
- newOrderObject[i] = to
477
- return
478
- }
479
- if (i > from) {
480
- if (i < to) {
481
- newOrderObject[i] = i - 1
482
- return
483
- } else {
484
- if (i === to) {
485
- newOrderObject[i] = i - 1
486
- return
487
- }
488
- newOrderObject[i] = i
489
- return
490
- }
491
- } else {
492
- newOrderObject[i] = i
493
- return
494
- }
495
- })
496
- } else {
497
- existing.map((_, i) => {
498
- if (i === from) {
499
- newOrderObject[i] = to
500
- return
501
- }
502
- if (i > to) {
503
- if (i < from) {
504
- newOrderObject[i] = i + 1
505
- return
506
- } else {
507
- newOrderObject[i] = i
508
- return
509
- }
510
- } else {
511
- if (i === to) {
512
- newOrderObject[i] = i + 1
513
- return
514
- }
515
- newOrderObject[i] = i
516
- return
517
- }
518
- })
519
- }
520
- return newOrderObject
521
- }
522
-
523
- export const matchLocation = (eventLocation: number[], formNode: FormNode) => {
524
- return eventLocation.every((item, index) => item === formNode.location[index])
525
- }
526
-
527
- export const bumpLocation = (location: number[]) => {
528
- return location.map((item, index) => {
529
- // Bump the last item in the location array by 1, assuming "at" is always 0
530
- if (index === location.length - 1) {
531
- return item + 1
532
- }
533
- return item
534
- })
535
- }
536
-
537
- export const maybeLowerLocation = (location: number[], at: number) => {
538
- return location.map((item, index) => {
539
- // Bump the last item in the location array by 1, assuming "at" is always 0
540
- if (index === location.length - 1) {
541
- return item < at ? item : item - 1
542
- }
543
- return item
544
- })
545
- }
546
-
547
- export const matchesAt = (location: number[], at: number) => {
548
- let matches = false
549
- location.map((item, index) => {
550
- // Bump the last item in the location array by 1, assuming "at" is always 0
551
- if (index === location.length - 1) {
552
- if (item === at) {
553
- matches = true
554
- }
555
- }
556
- })
557
- return matches
558
- }
559
-
560
- export const swapLocation = (
561
- location: number[],
562
- mapping: { [key: number]: number }
563
- ) => {
564
- return location.map((item, index) => {
565
- if (index === location.length - 1) {
566
- return mapping[item]
567
- }
568
- return item
569
- })
570
- }
571
-
572
- /**
573
- *
574
- * Gets the sub-fields for an object field, if it's a polymorphic
575
- * object then we also need to get the __typename, though
576
- * we should probably supply that regardless. The current downside
577
- * of this is that it needs to come from the server because we
578
- * have no way of knowing what it would be from the client-side
579
- */
580
- export const getSubFields = (
581
- changeSet: ChangeSet
582
- ): { fields: Field[]; __typename: string } => {
583
- // @ts-ignore FIXME: import types from newly-defined defineSchema
584
- const fields = changeSet.fieldDefinition.fields
585
- ? // @ts-ignore FIXME: import types from newly-defined defineSchema
586
- changeSet.fieldDefinition.fields
587
- : // @ts-ignore FIXME: import types from newly-defined defineSchema
588
- changeSet.fieldDefinition.templates[changeSet.value[0]._template].fields
589
-
590
- let __typename
591
- // @ts-ignore FIXME: import types from newly-defined defineSchema
592
- if (changeSet.fieldDefinition?.templates) {
593
- // @ts-ignore FIXME: import types from newly-defined defineSchema
594
- __typename = changeSet.fieldDefinition.typeMap[changeSet.value[0]._template]
595
- }
596
-
597
- return { fields, __typename }
598
- }