Skip to content

Commit

Permalink
feat(form-builder): add support for initial values in portable text e…
Browse files Browse the repository at this point in the history
…ditor
  • Loading branch information
bjoerge committed Apr 28, 2021
1 parent 959bdb4 commit 1bd6332
Show file tree
Hide file tree
Showing 3 changed files with 256 additions and 26 deletions.
157 changes: 157 additions & 0 deletions examples/test-studio/schemas/initialValuesTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,76 @@ export const initialValuesTest = {
title: 'Async string (initially set to "async string")',
initialValue: () => Promise.resolve('async string'),
},
{
name: 'portableText',
title: 'Portable text',
type: 'array',
of: [
{
type: 'block',
of: [
{
type: 'object',
name: 'person',
title: 'Inline object with initial value',
fields: [
{name: 'firstName', type: 'string'},
{name: 'lastName', type: 'string'},
],
initialValue: {firstName: 'Ada', lastName: 'Lovelace'},
},
{
type: 'object',
name: 'species',
title: 'Inline object with slow initial',
fields: [
{name: 'genus', type: 'string'},
{name: 'family', type: 'string'},
{name: 'commonName', type: 'string'},
],
initialValue: () =>
sleep(2000).then(() => ({
genus: 'Bradypus',
family: 'Bradypodidae',
commonName: 'Maned sloth',
})),
},
{
type: 'object',
name: 'errorTest',
title: 'Inline object with initial value resolution error',
fields: [{name: 'something', type: 'string'}],
initialValue: () =>
sleep(2000).then(() => Promise.reject(new Error('This took a wrong turn'))),
},
],
marks: {
annotations: [
{
type: 'object',
name: 'link',
fields: [{type: 'string', name: 'url', initialValue: 'https://sanity.io'}],
},
{
type: 'object',
name: 'test',
title: 'Test annotation with initial value',
fields: [{type: 'string', name: 'mystring'}],
initialValue: {mystring: 'initial!'},
},
],
},
},
{
type: 'object',
name: 'testObject',
title: 'Test object with initial value',
fields: [{name: 'first', type: 'string'}],
initialValue: {first: 'hello'},
},
],
initialValue: INITIAL_PORTABLE_TEXT_VALUE,
},
{
name: 'asyncArray',
type: 'array',
Expand Down Expand Up @@ -128,3 +198,90 @@ export const initialValuesTest = {
},
],
}

const INITIAL_PORTABLE_TEXT_VALUE = [
{
_type: 'block',
children: [
{
_type: 'span',
marks: [],
text: 'this ',
},
{
_type: 'span',
marks: ['strong'],
text: 'is',
},
{
_type: 'span',
marks: [],
text: ' the ',
},
{
_type: 'span',
marks: ['em'],
text: 'initial',
},
{
_type: 'span',
marks: [],
text: ' portable ',
},
{
_type: 'span',
marks: ['underline'],
text: 'text',
},
{
_type: 'span',
marks: [],
text: ' value',
},
],
markDefs: [],
style: 'normal',
},
{
_type: 'block',
children: [
{
_type: 'span',
marks: [],
text: 'foo',
},
],
level: 1,
listItem: 'bullet',
markDefs: [],
style: 'normal',
},
{
_type: 'block',
children: [
{
_type: 'span',
marks: ['code'],
text: 'bar',
},
],
level: 1,
listItem: 'bullet',
markDefs: [],
style: 'normal',
},
{
_type: 'block',
children: [
{
_type: 'span',
marks: [],
text: 'baz',
},
],
level: 1,
listItem: 'bullet',
markDefs: [],
style: 'normal',
},
]
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,24 @@ import {
RenderBlockFunction,
usePortableTextEditor,
usePortableTextEditorSelection,
Type,
PortableTextEditor,
} from '@sanity/portable-text-editor'
import classNames from 'classnames'
import React from 'react'
import {Path} from '@sanity/types'
import React, {useCallback} from 'react'
import {Path, SchemaType} from '@sanity/types'
import {FOCUS_TERMINATOR} from '@sanity/util/paths'
import {resolveInitialValueForType} from '@sanity/initial-value-templates'
import {useToast} from '@sanity/ui'
import ActionMenu from './ActionMenu'
import BlockStyleSelect from './BlockStyleSelect'
import InsertMenu from './InsertMenu'
import {getBlockStyleSelectProps, getInsertMenuItems, getPTEToolbarActionGroups} from './helpers'

import styles from './Toolbar.css'

const SLOW_INITIAL_VALUE_LIMIT = 300

interface Props {
hotkeys: HotkeyOptions
isFullscreen: boolean
Expand All @@ -32,18 +38,95 @@ function PTEToolbar(props: Props) {
const editor = usePortableTextEditor()
const selection = usePortableTextEditorSelection()
const disabled = !selection

const toast = useToast()

const resolveInitialValue = useCallback(
(type: Type) => {
let isSlow = false
const slowTimer = setTimeout(() => {
isSlow = true
toast.push({
id: 'resolving-initial-value',
status: 'info',
title: 'Resolving initial value…',
})
}, SLOW_INITIAL_VALUE_LIMIT)
return resolveInitialValueForType((type as any) as SchemaType)
.then((value) => {
if (isSlow) {
// I found no way to close an existing toast, so this will replace the message in the
// "Resolving initial value…"-toast and then make sure it gets closed.
toast.push({
id: 'resolving-initial-value',
status: 'info',
duration: 500,
title: 'Initial value resolved',
})
}
return value
})
.catch((error) => {
toast.push({
title: `Could not resolve initial value`,
id: 'resolving-initial-value',
description: `Unable to resolve initial value for type: ${type.name}: ${error.message}.`,
status: 'error',
})
return undefined
})
.finally(() => clearTimeout(slowTimer))
},
[toast]
)

const handleInsertBlock = useCallback(
async (type: Type) => {
const initialValue = await resolveInitialValue(type)
const path = PortableTextEditor.insertBlock(editor, type, initialValue)

setTimeout(() => onFocus(path.concat(FOCUS_TERMINATOR)), 0)
},
[editor, onFocus, resolveInitialValue]
)

const handleInsertInline = useCallback(
async (type: Type) => {
const initialValue = await resolveInitialValue(type)
const path = PortableTextEditor.insertChild(editor, type, initialValue)

setTimeout(() => onFocus(path.concat(FOCUS_TERMINATOR)), 0)
},
[editor, onFocus, resolveInitialValue]
)

const handleInsertAnnotation = useCallback(
async (type: Type) => {
const initialValue = await resolveInitialValue(type)

const paths = PortableTextEditor.addAnnotation(editor, type, initialValue)
if (paths && paths.markDefPath) {
onFocus(paths.markDefPath.concat(FOCUS_TERMINATOR))
}
},
[editor, onFocus, resolveInitialValue]
)

const actionGroups = React.useMemo(
() => (editor ? getPTEToolbarActionGroups(editor, selection, onFocus, hotkeys) : []),
[editor, selection, onFocus, hotkeys]
() =>
editor ? getPTEToolbarActionGroups(editor, selection, handleInsertAnnotation, hotkeys) : [],
[editor, selection, handleInsertAnnotation, hotkeys]
)
const actionsLen = actionGroups.reduce((acc, x) => acc + x.actions.length, 0)
const blockStyleSelectProps = React.useMemo(
() => (editor ? getBlockStyleSelectProps(editor) : null),
[selection]
[editor]
)

const insertMenuItems = React.useMemo(
() => (editor ? getInsertMenuItems(editor, selection, onFocus) : []),
[selection]
() =>
editor ? getInsertMenuItems(editor, selection, handleInsertBlock, handleInsertInline) : [],
[editor, handleInsertBlock, handleInsertInline, selection]
)

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
PortableTextFeature,
Type,
} from '@sanity/portable-text-editor'
import {FOCUS_TERMINATOR} from '@sanity/util/paths'
import {get} from 'lodash'
import LinkIcon from 'part:@sanity/base/link-icon'
import FormatBoldIcon from 'part:@sanity/base/format-bold-icon'
Expand All @@ -21,7 +20,6 @@ import FormatListNumberedIcon from 'part:@sanity/base/format-list-numbered-icon'
import BlockObjectIcon from 'part:@sanity/base/block-object-icon'
import InlineObjectIcon from 'part:@sanity/base/inline-object-icon'
import React from 'react'
import {Path} from '@sanity/types'
import CustomIcon from './CustomIcon'
import {BlockItem, BlockStyleItem, PTEToolbarAction, PTEToolbarActionGroup} from './types'

Expand Down Expand Up @@ -155,7 +153,7 @@ function getAnnotationIcon(item: PortableTextFeature, active: boolean): React.Co

function getPTEAnnotationActions(
editor: PortableTextEditor,
onFocus: (path: Path) => void
onInsert: (type: Type) => void
): PTEToolbarAction[] {
const features = PortableTextEditor.getPortableTextFeatures(editor)
const activeAnnotations = PortableTextEditor.activeAnnotations(editor)
Expand All @@ -174,11 +172,8 @@ function getPTEAnnotationActions(
if (active) {
PortableTextEditor.removeAnnotation(editor, item.type)
PortableTextEditor.focus(editor)
return
}
const paths = PortableTextEditor.addAnnotation(editor, item.type)
if (paths && paths.markDefPath) {
onFocus(paths.markDefPath.concat(FOCUS_TERMINATOR))
} else {
onInsert(item.type)
}
},
title: item.title,
Expand All @@ -189,13 +184,13 @@ function getPTEAnnotationActions(
export function getPTEToolbarActionGroups(
editor: PortableTextEditor,
selection: EditorSelection,
onFocus: (path: Path) => void,
onInsertAnnotation: (type: Type) => void,
hotkeyOpts: HotkeyOptions
): PTEToolbarActionGroup[] {
return [
{name: 'format', actions: getPTEFormatActions(editor, selection, hotkeyOpts)},
{name: 'list', actions: getPTEListActions(editor, selection)},
{name: 'annotation', actions: getPTEAnnotationActions(editor, onFocus)},
{name: 'annotation', actions: getPTEAnnotationActions(editor, onInsertAnnotation)},
]
}

Expand Down Expand Up @@ -244,18 +239,16 @@ function getInsertMenuIcon(
export function getInsertMenuItems(
editor: PortableTextEditor,
selection: EditorSelection,
onFocus: (path: Path) => void
onInsertBlock: (type: Type) => void,
onInsertInline: (type: Type) => void
): BlockItem[] {
const focusBlock = PortableTextEditor.focusBlock(editor)
const features = PortableTextEditor.getPortableTextFeatures(editor)

const blockItems = features.types.blockObjects.map(
(type, index): BlockItem => ({
disabled: !selection,
handle(): void {
const path = PortableTextEditor.insertBlock(editor, type)
onFocus(path.concat(FOCUS_TERMINATOR))
},
handle: () => onInsertBlock(type),
icon: getInsertMenuIcon(type, BlockObjectIcon),
inline: false,
key: `block-${index}`,
Expand All @@ -266,10 +259,7 @@ export function getInsertMenuItems(
const inlineItems = features.types.inlineObjects.map(
(type, index): BlockItem => ({
disabled: !selection || (focusBlock ? focusBlock._type !== features.types.block.name : true),
handle(): void {
const path = PortableTextEditor.insertChild(editor, type)
onFocus(path.concat(FOCUS_TERMINATOR))
},
handle: () => onInsertInline(type),
icon: getInsertMenuIcon(type, InlineObjectIcon),
inline: true,
key: `inline-${index}`,
Expand Down

0 comments on commit 1bd6332

Please sign in to comment.