Skip to content

Commit

Permalink
[structure] Allow specifying a default document node resolver (#1637)
Browse files Browse the repository at this point in the history
* [structure] Allow specifying a default document fragment resolver

* [desk-tool] Warn when structure exports unknown properties

* [test-studio] Add example of default document fragment resolver

* [structure] Rename to getDefaultDocumentNode
  • Loading branch information
rexxars committed Dec 6, 2019
1 parent a0f2866 commit 6eb4406
Show file tree
Hide file tree
Showing 15 changed files with 150 additions and 30 deletions.
1 change: 1 addition & 0 deletions packages/@sanity/desk-tool/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"diff": "^3.2.0",
"element-resize-detector": "^1.1.14",
"hashlru": "^2.1.0",
"leven": "^2.1.0",
"lodash": "^4.17.15",
"promise-latest": "^1.0.4",
"react-click-outside": "^3.0.0",
Expand Down
30 changes: 30 additions & 0 deletions packages/@sanity/desk-tool/src/utils/resolvePanes.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ import {useEffect, useState} from 'react'
import shallowEquals from 'shallow-equals'
import {Observable, defer, throwError, from, of as observableOf} from 'rxjs'
import {map, switchMap, distinctUntilChanged} from 'rxjs/operators'
import leven from 'leven'
import {LOADING_PANE} from '../index'
import defaultStructure from '../defaultStructure'
import isSubscribable from './isSubscribable'
import validateStructure from './validateStructure'
import serializeStructure from './serializeStructure'
import generateHelpUrl from '@sanity/generate-help-url'

const KNOWN_STRUCTURE_EXPORTS = ['getDefaultDocumentNode']

let prevStructureError = null
if (__DEV__) {
if (module.hot && module.hot.data) {
Expand Down Expand Up @@ -223,6 +226,8 @@ export const loadStructure = () => {
const mod = require('part:@sanity/desk-tool/structure?') || defaultStructure
structure = mod && mod.__esModule ? mod.default : mod

warnOnUnknownExports(mod)

// On invalid modules, when HMR kicks in, we sometimes get an empty object back when the
// source has changed without fixing the problem. In this case, keep showing the error
if (
Expand Down Expand Up @@ -297,3 +302,28 @@ function isStructure(structure) {
typeof structure.type !== 'string')
)
}

function warnOnUnknownExports(mod) {
if (!mod) {
return
}

const known = KNOWN_STRUCTURE_EXPORTS.concat('default')
const keys = Object.keys(mod)
keys
.filter(key => !known.includes(key))
.forEach(key => {
const {closest} = known.reduce(
(acc, current) => {
const distance = leven(current, key)
return distance < 3 && distance < acc.distance ? {closest: current, distance} : acc
},
{closest: null, distance: +Infinity}
)

const hint = closest ? ` - did you mean "${closest}"` : ''

// eslint-disable-next-line
console.warn(`Unknown structure export "${key}"${hint}`)
})
}
1 change: 1 addition & 0 deletions packages/@sanity/structure/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
"moduleNameMapper": {
"^part:@sanity/base/schema$": "<rootDir>/test/mocks/schema.js",
"^part:@sanity/base/client$": "<rootDir>/test/mocks/client.js",
"^part:@sanity/desk-tool/structure\\??$": "<rootDir>/test/mocks/userStructure.js",
"^part:@sanity/data-aspects/resolver$": "<rootDir>/test/mocks/dataAspects.js",
"^part:@sanity/base/.*?-icon$": "<rootDir>/test/mocks/icon.js",
"^part:@sanity/base/util/document-action-utils": "<rootDir>/test/mocks/documentActionUtils.js"
Expand Down
43 changes: 35 additions & 8 deletions packages/@sanity/structure/src/Document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import {SchemaType} from './parts/Schema'
import {validateId} from './util/validateId'
import {View, ViewBuilder, maybeSerializeView} from './views/View'
import {form} from './views'
import {
getUserDefinedDefaultDocumentBuilder,
DocumentFragmentResolveOptions
} from './userDefinedStructure'
import {getTemplateById} from '@sanity/initial-value-templates'

interface DocumentOptions {
Expand Down Expand Up @@ -141,10 +145,9 @@ export class DocumentBuilder implements Serializable {
).withHelpUrl(HELP_URL.DOCUMENT_ID_REQUIRED)
}

const views = (this.spec.views && this.spec.views.length > 0
? this.spec.views
: [form()]
).map((item, i) => maybeSerializeView(item, i, path))
const views = (this.spec.views && this.spec.views.length > 0 ? this.spec.views : [form()]).map(
(item, i) => maybeSerializeView(item, i, path)
)

const viewIds = views.map(view => view.id)
const dupes = uniq(viewIds.filter((id, i) => viewIds.includes(id, i + 1)))
Expand Down Expand Up @@ -193,7 +196,12 @@ function getDocumentOptions(spec: Partial<DocumentOptions>): DocumentOptions {
}

export function documentFromEditor(spec?: EditorNode) {
let doc = new DocumentBuilder()
let doc =
spec && spec.type
? // Use user-defined document fragment as base if possible
getDefaultDocumentNode({schemaType: spec.type})
: // Fall back to plain old document builder
new DocumentBuilder()

if (spec) {
const {id, type, template, templateParameters} = spec.options
Expand Down Expand Up @@ -225,7 +233,26 @@ export function documentFromEditorWithInitialValue(
throw new Error(`Template with ID "${templateId}" not defined`)
}

return documentFromEditor()
.schemaType(template.schemaType)
.initialValueTemplate(templateId, parameters)
return getDefaultDocumentNode({schemaType: template.schemaType}).initialValueTemplate(
templateId,
parameters
)
}

export function getDefaultDocumentNode(
options: DocumentFragmentResolveOptions
): DocumentBuilder {
const {documentId, schemaType} = options
const userDefined = getUserDefinedDefaultDocumentBuilder(options)

let builder = userDefined || new DocumentBuilder()
if (!builder.getId()) {
builder = builder.id('documentEditor')
}

if (documentId) {
builder = builder.documentId(documentId.replace(/^drafts\./, ''))
}

return builder.schemaType(schemaType)
}
14 changes: 8 additions & 6 deletions packages/@sanity/structure/src/DocumentList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
GenericList,
GenericListInput
} from './GenericList'
import {DocumentBuilder} from './Document'
import {DocumentBuilder, getDefaultDocumentNode} from './Document'

const resolveTypeForDocument = (id: string): Promise<string | undefined> => {
const query = '*[_id in [$documentId, $draftId]]._type'
Expand Down Expand Up @@ -43,11 +43,13 @@ const resolveDocumentChildForItem: ChildResolver = (
): ItemChild | Promise<ItemChild> | undefined => {
const parentItem = options.parent as DocumentList
const schemaType = parentItem.schemaTypeName || resolveTypeForDocument(itemId)
return Promise.resolve(schemaType).then(type =>
new DocumentBuilder()
.id('editor')
.documentId(itemId)
.schemaType(type || '')
return Promise.resolve(schemaType).then(schemaType =>
schemaType
? getDefaultDocumentNode({schemaType, documentId: itemId})
: new DocumentBuilder()
.id('editor')
.documentId(itemId)
.schemaType('')
)
}

Expand Down
13 changes: 7 additions & 6 deletions packages/@sanity/structure/src/DocumentListItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {Partial} from './Partial'
import {ListItemBuilder, ListItem, UnserializedListItem, ListItemInput} from './ListItem'
import {SchemaType} from './parts/Schema'
import {SerializeError, HELP_URL} from './SerializeError'
import {DocumentBuilder} from './Document'
import {DocumentBuilder, getDefaultDocumentNode} from './Document'

export interface DocumentListItemInput extends ListItemInput {
schemaType: SchemaType | string
Expand All @@ -17,12 +17,13 @@ export interface DocumentListItem extends ListItem {
type PartialDocumentListItem = Partial<UnserializedListItem>

const getDefaultChildResolver = (spec: PartialDocumentListItem) => (documentId: string) => {
let editor = new DocumentBuilder().id('editor').documentId(documentId)
if (spec.schemaType) {
editor = editor.schemaType(spec.schemaType)
}
const schemaType =
spec.schemaType &&
(typeof spec.schemaType === 'string' ? spec.schemaType : spec.schemaType.name)

return editor
return schemaType
? getDefaultDocumentNode({schemaType, documentId})
: new DocumentBuilder().id('documentEditor').documentId(documentId)
}

export class DocumentListItemBuilder extends ListItemBuilder {
Expand Down
8 changes: 2 additions & 6 deletions packages/@sanity/structure/src/documentTypeListItems.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {DocumentListBuilder} from './DocumentList'
import {ListItemBuilder, ListItem} from './ListItem'
import {DocumentTypeListBuilder, DocumentTypeListInput} from './DocumentTypeList'
import {defaultIntentChecker} from './Intent'
import {DocumentBuilder} from './Document'
import {getDefaultDocumentNode} from './Document'
import {isList} from './List'

const ListIcon = getListIcon()
Expand Down Expand Up @@ -90,11 +90,7 @@ export function getDocumentTypeList(
)
.child(
spec.child ||
((documentId: string) =>
new DocumentBuilder()
.id('editor')
.schemaType(type)
.documentId(documentId))
((documentId: string) => getDefaultDocumentNode({schemaType: typeName, documentId}))
)
.canHandleIntent(spec.canHandleIntent || defaultIntentChecker)
.menuItems(
Expand Down
4 changes: 3 additions & 1 deletion packages/@sanity/structure/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ import {
DocumentBuilder,
PartialDocumentNode,
documentFromEditor,
documentFromEditorWithInitialValue
documentFromEditorWithInitialValue,
getDefaultDocumentNode
} from './Document'
import {ComponentInput, ComponentBuilder} from './Component'
import {DocumentListItemBuilder, DocumentListItemInput} from './DocumentListItem'
Expand All @@ -40,6 +41,7 @@ const StructureBuilder = {
documentTypeListItems: getDocumentTypeListItems,
document: (spec?: PartialDocumentNode) => new DocumentBuilder(spec),
documentWithInitialValueTemplate: documentFromEditorWithInitialValue,
defaultDocument: getDefaultDocumentNode,

list: (spec?: ListInput) => new ListBuilder(spec),
listItem: (spec?: ListItemInput) => new ListItemBuilder(spec),
Expand Down
13 changes: 13 additions & 0 deletions packages/@sanity/structure/src/parts/userStructure.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {DocumentBuilder} from '../Document'
import {DocumentNode} from '../StructureNodes'
import {DocumentFragmentResolveOptions} from '../userDefinedStructure'

interface UserDefinedStructure {
getDefaultDocumentNode?: (
options: DocumentFragmentResolveOptions
) => DocumentNode | DocumentBuilder | null
}

export function getUserDefinedStructure(): UserDefinedStructure | undefined {
return require('part:@sanity/desk-tool/structure?')
}
36 changes: 36 additions & 0 deletions packages/@sanity/structure/src/userDefinedStructure.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {DocumentBuilder} from './Document'
import {DocumentNode} from './StructureNodes'
import {getUserDefinedStructure} from './parts/userStructure'

export interface DocumentFragmentResolveOptions {
documentId?: string
schemaType: string
}

export const getUserDefinedDefaultDocumentBuilder = (
options: DocumentFragmentResolveOptions
): DocumentBuilder | null => {
const structure = getUserDefinedStructure()
if (!structure || !structure.getDefaultDocumentNode) {
return null
}

if (typeof structure.getDefaultDocumentNode !== 'function') {
throw new Error('Structure export `getDefaultDocumentNode` must be a function')
}

const documentNode = structure.getDefaultDocumentNode(options)

if (!documentNode) {
return null
}

const isBuilder = typeof (documentNode as DocumentBuilder).serialize === 'function'
if (!isBuilder && (documentNode as DocumentNode).type !== 'document') {
throw new Error('`getDefaultDocumentNode` must return a document or a document builder')
}

return isBuilder
? (documentNode as DocumentBuilder)
: new DocumentBuilder(documentNode as DocumentNode)
}
2 changes: 1 addition & 1 deletion packages/@sanity/structure/test/DocumentList.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ test('default child resolver resolves to editor', done => {
serializeStructure(list.child, context, ['asoiaf-wow', context]).subscribe(child => {
expect(child).toEqual({
child: undefined,
id: 'editor',
id: 'documentEditor',
type: 'document',
options: {
id: 'asoiaf-wow',
Expand Down
2 changes: 1 addition & 1 deletion packages/@sanity/structure/test/DocumentTypeList.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ test('default child resolver resolves to editor', done => {
serializeStructure(list.child, context, ['asoiaf-wow', context]).subscribe(child => {
expect(child).toEqual({
child: undefined,
id: 'editor',
id: 'documentEditor',
type: 'document',
options: {
id: 'asoiaf-wow',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {DocumentTypeListBuilder} from '../src/DocumentTypeList'

const nope = () => 'NOPE'
const editor = {
id: 'editor',
id: 'documentEditor',
options: {
type: 'author',
id: 'grrm'
Expand Down
1 change: 1 addition & 0 deletions packages/@sanity/structure/test/mocks/userStructure.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = {}
10 changes: 10 additions & 0 deletions packages/test-studio/src/deskStructure.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,16 @@ import S from '@sanity/desk-tool/structure-builder'
// For testing. Bump the timeout to introduce som lag
const delay = (val, ms = 10) => new Promise(resolve => setTimeout(resolve, ms, val))

export const getDefaultDocumentNode = () => {
return S.document().views([
S.view.form().icon(EditIcon),
S.view
.component(DeveloperPreview)
.icon(EyeIcon)
.title('Preview')
])
}

export default () =>
S.list()
.id('root')
Expand Down

0 comments on commit 6eb4406

Please sign in to comment.