Skip to content

Commit

Permalink
[base] Add infrastructure for document-actions
Browse files Browse the repository at this point in the history
Co-authored-by: Per-Kristian Nordnes <per.kristian.nordnes@gmail.com>
  • Loading branch information
bjoerge and skogsmaskin committed Feb 19, 2020
1 parent 68a80c9 commit 87d9299
Show file tree
Hide file tree
Showing 36 changed files with 1,118 additions and 167 deletions.
1 change: 0 additions & 1 deletion packages/@sanity/base/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
"homepage": "https://www.sanity.io/",
"dependencies": {
"@sanity/client": "1.148.3",
"@sanity/document-store": "1.148.1",
"@sanity/image-url": "0.140.15",
"@sanity/initial-value-templates": "1.148.1",
"@sanity/mutator": "1.148.1",
Expand Down
28 changes: 24 additions & 4 deletions packages/@sanity/base/sanity.json
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,30 @@
"name": "part:@sanity/base/locale-messages",
"description": "Messages across all known locales"
},
{
"implements": "part:@sanity/base/util/document-action-utils",
"path": "actions/utils/legacy_documentActionUtils.js"
},
{
"implements": "part:@sanity/base/actions/utils",
"path": "actions/utils/index.js"
},
{
"name": "part:@sanity/base/document-actions/resolver",
"description": "A function that resolves current available actions for a document."
},
{
"name": "part:@sanity/base/document-actions",
"description": "The default document actions"
},
{
"name": "part:@sanity/base/document-badges/resolver",
"description": "A function that resolves current available badges for a document."
},
{
"name": "part:@sanity/base/document-badges",
"description": "The default document badges"
},
{
"name": "part:@sanity/base/language-resolver",
"description": "Figures out which locale the client should use"
Expand Down Expand Up @@ -278,10 +302,6 @@
"implements": "part:@sanity/base/util/search-utils",
"path": "util/searchUtils.js"
},
{
"implements": "part:@sanity/base/util/document-action-utils",
"path": "util/documentActionUtils.js"
},
{
"implements": "part:@sanity/base/theme/variables-style",
"path": "styles/variables.css"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/* eslint-disable react/no-multi-comp */
import * as React from 'react'
import {ActionDescription} from './types'

interface RenderActionCollectionProps {
actions: any[]
actionProps: any
onActionComplete: () => void
component: (args: {actionStates: ActionDescription[]}) => React.ReactNode
}

const actionIds = new WeakMap()

let counter = 0
const getActionId = action => {
if (actionIds.has(action)) {
return actionIds.get(action)
}
const id = `${action.name || action.displayName || '<anonymous>'}-${counter++}`
actionIds.set(action, id)
return id
}

export function RenderActionCollectionState(props: RenderActionCollectionProps) {
const [actionsWithStates, setActionsWithState] = React.useState([])
const [keys, setKey] = React.useState({})
const handleComplete = React.useCallback(
id => {
setKey(keys => ({...keys, [id]: (keys[id] || 0) + 1}))
props.onActionComplete()
},
[props.actions]
)

const onStateChange = React.useCallback(
stateUpdate => {
setActionsWithState(prevState => {
return props.actions.map((action: any) => {
const id = getActionId(action)
return stateUpdate[0] === id
? [id, stateUpdate[1]]
: prevState.find(prev => prev[0] === id) || [id]
})
})
},
[props.actions]
)

const {actions: _, actionProps, component, ...rest} = props

return (
<>
{component({
actionStates: actionsWithStates
.map(([id, state]) => state && {...state, actionId: id})
.filter(Boolean),
...rest
})}

{props.actions.map(action => {
const actionId = getActionId(action)
return (
<ActionStateContainer
key={`${actionId}-${keys[actionId] || '0'}`}
action={action}
id={actionId}
actionProps={props.actionProps}
onUpdate={onStateChange}
onComplete={handleComplete}
/>
)
})}
</>
)
}

const ActionStateContainer = React.memo(function ActionStateContainer(props: any) {
const {id, action, onUpdate, onComplete, actionProps} = props

const state = action({...actionProps, onComplete: () => onComplete(id)})
onUpdate([id, state ? state : null])
return null
})
1 change: 1 addition & 0 deletions packages/@sanity/base/src/actions/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {RenderActionCollectionState} from './RenderActionCollectionState'
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
import {difference} from 'lodash'
import helpUrl from '@sanity/generate-help-url'

const ACTIONS_FLAG = '__experimental_actions'

const DEFAULT_ACTIONS = ['create', 'update', 'delete', 'publish']
const VALID_ACTIONS = DEFAULT_ACTIONS

const readActions = schemaType =>
ACTIONS_FLAG in schemaType ? schemaType[ACTIONS_FLAG] : DEFAULT_ACTIONS
const hasWarned = {}
const readActions = schemaType => {
// todo: enable this when officially deprecating experimental actions
if (false && !(schemaType.name in hasWarned)) {
console.warn(`Heads up! Experimental actions is now deprecated and replaced by Document Actions. Read more about how to migrate on ${helpUrl(
'experimental-actions-replaced-by-document-actions'
)}".
`)
hasWarned[schemaType.name] = true
}
return ACTIONS_FLAG in schemaType ? schemaType[ACTIONS_FLAG] : DEFAULT_ACTIONS
}

const validateActions = (typeName, actions) => {
if (!Array.isArray(actions)) {
Expand Down
52 changes: 52 additions & 0 deletions packages/@sanity/base/src/actions/utils/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React from 'react'

export interface ActionComponent<ActionProps> {
(props: ActionProps): ActionDescription
}

interface Document {
_id: string
_type: string
}

interface DocumentActionProps {
id: string
type: string
draft: null | Document
published: null | Document
onComplete: () => void
}

export type DocumentActionComponent = ActionComponent<DocumentActionProps>

interface ConfirmDialogProps {
type: 'confirm'
color?: 'warning' | 'success' | 'danger' | 'info'
message: React.ReactNode
onConfirm: () => void
onCancel: () => void
}

// Todo: move these to action spec/core types
interface ModalDialogProps {
type: 'modal'
content: React.ReactNode
onClose: () => void
}

// Todo: move these to action spec/core types
interface PopOverDialogProps {
type: 'popover'
content: React.ReactNode
onClose: () => void
}

export interface ActionDescription {
label: string
icon?: React.ReactNode
disabled?: boolean
shortcut?: string
title?: string
dialog?: ConfirmDialogProps | PopOverDialogProps | ModalDialogProps
onHandle?: () => void
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import {Observable, defer, of as observableOf} from 'rxjs'
import {concatMap, map, share} from 'rxjs/operators'
import {createBufferedDocument} from './buffered-doc/createBufferedDocument'
import {doCommit} from './checkoutPair'
import {SanityDocument, WelcomeEvent} from './types'

function fetchDocumentSnapshot(client, id) {
Expand All @@ -21,8 +20,15 @@ export interface QuerySnapshotEvent {

type QueryEvent = WelcomeEvent | MutationEvent | QuerySnapshotEvent

function commitMutations(client, mutations) {
return client.dataRequest('mutate', mutations, {
visibility: 'async',
returnDocuments: false
})
}

function _createDeprecatedAPIs(client) {
const _doCommit = mutations => doCommit(client, mutations)
const _doCommit = mutations => commitMutations(client, mutations)

function patchDoc(documentId, patches) {
const doc = checkout(documentId)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,66 +1,62 @@
import {createObservableBufferedDocument} from './createObservableBufferedDocument'
import {filter} from 'rxjs/operators'
import {merge, Observable} from 'rxjs'
import {ReconnectEvent} from '../types'
import {Observable} from 'rxjs'
import {
CommitFunction,
SnapshotEvent,
CommittedEvent,
DocumentMutationEvent,
DocumentRebaseEvent,
DocumentMutationEvent
SnapshotEvent
} from './types'
import {ListenerEvent} from '../getPairListener'
import {Mutation} from '../types'

type BufferedDocumentEvent =
| ReconnectEvent
export type BufferedDocumentEvent =
| SnapshotEvent
| DocumentRebaseEvent
| DocumentMutationEvent
| CommittedEvent

const prepare = id => document => {
const {_id, _rev, _updatedAt, ...rest} = document
return {_id: id, ...rest}
}

export interface BufferedDocumentWrapper {
consistency$: Observable<boolean>
events: Observable<BufferedDocumentEvent>
patch: (patches) => void
create: (document) => void
createIfNotExists: (document) => void
createOrReplace: (document) => void
delete: () => void
// helper functions
patch: (patches) => Mutation[]
create: (document) => Mutation
createIfNotExists: (document) => Mutation
createOrReplace: (document) => Mutation
delete: () => Mutation

mutate: (mutations: Mutation[]) => void
commit: () => Observable<never>
}

function isReconnect(event: ListenerEvent): event is ReconnectEvent {
return event.type === 'reconnect'
}
export const createBufferedDocument = (
documentId: string,
serverEvents$: Observable<ListenerEvent>,
doCommit: CommitFunction
// consider naming it remoteEvent$
listenerEvent$: Observable<ListenerEvent>,
commitMutations: CommitFunction
): BufferedDocumentWrapper => {
const bufferedDocument = createObservableBufferedDocument(serverEvents$, doCommit)
const bufferedDocument = createObservableBufferedDocument(listenerEvent$, commitMutations)

const prepareDoc = prepare(documentId)

const reconnects$ = serverEvents$.pipe(filter(isReconnect))
const DELETE = {delete: {id: documentId}}

return {
events: merge(reconnects$, bufferedDocument.updates$),
patch(patches) {
bufferedDocument.addMutations(patches.map(patch => ({patch: {...patch, id: documentId}})))
},
create(document) {
bufferedDocument.addMutation({
create: Object.assign({id: documentId}, document)
})
},
createIfNotExists(document) {
bufferedDocument.addMutation({createIfNotExists: document})
},
createOrReplace(document) {
bufferedDocument.addMutation({createOrReplace: document})
},
delete() {
bufferedDocument.addMutation({delete: {id: documentId}})
},
commit() {
return bufferedDocument.commit()
}
events: bufferedDocument.updates$,
consistency$: bufferedDocument.consistency$,
patch: patches => patches.map(patch => ({patch: {...patch, id: documentId}})),
create: document => ({create: prepareDoc(document)}),
createIfNotExists: document => ({createIfNotExists: prepareDoc(document)}),
createOrReplace: document => ({createOrReplace: prepareDoc(document)}),
delete: () => DELETE,

mutate: (mutations: Mutation[]) => bufferedDocument.addMutations(mutations),
commit: () => bufferedDocument.commit()
}
}

0 comments on commit 87d9299

Please sign in to comment.