Skip to content

Commit

Permalink
[field] Add portable text diff component
Browse files Browse the repository at this point in the history
  • Loading branch information
skogsmaskin authored and rexxars committed Oct 6, 2020
1 parent 52915f3 commit 7ab9ce9
Show file tree
Hide file tree
Showing 15 changed files with 351 additions and 0 deletions.
12 changes: 12 additions & 0 deletions packages/@sanity/field/src/diff/components/portableText/PTDiff.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
@import 'part:@sanity/base/theme/variables-style';

.diffedSpan {
}

.summary {
font-size: var(--font-size-xsmall);
color: var(--text-muted);
}

.summary li {
}
94 changes: 94 additions & 0 deletions packages/@sanity/field/src/diff/components/portableText/PTDiff.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import React from 'react'
import PortableText from '@sanity/block-content-to-react'
import {DiffComponent, ObjectDiff, ObjectSchemaType} from '@sanity/field/diff'

import Blockquote from './previews/Blockquote'
import Decorator from './previews/Decorator'
import Header from './previews/Header'
import Paragraph from './previews/Paragraph'

import {createChildMap, isHeader} from './helpers'

import styles from './PTDiff.css'
import {ChildMap, PortableTextBlock, PortableTextChild} from './types'

export const PTDiff: DiffComponent<ObjectDiff> = function PTDiff({
diff,
schemaType
}: {
diff: ObjectDiff
schemaType: ObjectSchemaType
}) {
const block = (diff.toValue ? diff.toValue : diff.fromValue) as PortableTextBlock
const blocks = [block] as PortableTextBlock[]
const childMap = createChildMap(block, diff)
const serializers = createSerializers(schemaType, childMap)
return (
<div className={styles.root}>
<PortableText blocks={blocks} serializers={serializers} />
<ul className={styles.summary}>
{block.children.map(child => {
return childMap[child._key].summary.map((line, i) => (
<li key={`summary-${child._key.concat(i.toString())}`}>{line}</li>
))
})}
</ul>
</div>
)
}

function createSerializers(schemaType: ObjectSchemaType, childMap: ChildMap) {
const renderDecorator = ({mark, children}: {mark: string; children: React.ReactNode}) => {
return <Decorator mark={mark}>{children}</Decorator>
}
const renderBlock = ({node, children}: {node: PortableTextBlock; children: React.ReactNode}) => {
let returned: React.ReactNode = children
if (node.style === 'blockquote') {
returned = <Blockquote>{returned}</Blockquote>
} else if (node.style && isHeader(node)) {
returned = <Header style={node.style}>{returned}</Header>
} else {
returned = <Paragraph>{returned}</Paragraph>
}
return returned
}
const renderText = (text: {children: string}) => {
// With '@sanity/block-content-to-react', spans without marks doesn't run through the renderSpan function.
// They are sent directly to the 'text' serializer. This is a hack to render those with annotations from childMap
// The _key for the child is not known at this point.

// Find child that has no marks, and a text similar to what is in the childMap.
const fromMap = Object.keys(childMap)
.map(key => childMap[key])
.filter(entry => (entry.child.marks || []).length === 0)
.find(entry => entry.child.text === text.children)
if (fromMap && fromMap.annotation) {
return <span className={styles.diffedSpan}>{fromMap.annotation}</span>
}
return text.children
}
const renderSpan = (props: {node: PortableTextChild}): React.ReactNode => {
const fromMap = childMap[props.node._key]
if (fromMap && fromMap.annotation) {
const annotatedProps = {
...props,
node: {...props.node, children: fromMap.annotation}
}
return (
<span className={styles.diffedSpan}>
{PortableText.defaultSerializers.span(annotatedProps)}
</span>
)
}
return PortableText.defaultSerializers.span(props)
}
// TODO: create serializers according to schemaType (marks etc)
return {
marks: {strong: renderDecorator, italic: renderDecorator},
span: renderSpan,
text: renderText,
types: {
block: renderBlock
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React from 'react'
import {
AnnotatedStringDiff,
ArrayDiff,
ObjectDiff,
ObjectSchemaType,
StringDiff
} from '@sanity/field/diff'
import {startCase} from 'lodash'
import {ChildMap, PortableTextBlock, PortableTextChild} from './types'

export function isPTSchemaType(schemaType: ObjectSchemaType) {
return schemaType.jsonType === 'object' && schemaType.name === 'block'
}
export function isHeader(node: PortableTextBlock) {
return !!node.style && ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(node.style)
}

export function createChildMap(block: PortableTextBlock, blockDiff: ObjectDiff) {
// Create a map from span to diff
const childMap: ChildMap = {}
block.children.forEach(child => {
const childDiffs = findChildDiffs(blockDiff, child)
let annotation
const summary: React.ReactNode[] = []

// Summarize all diffs to this child
// eslint-disable-next-line complexity
childDiffs.forEach(cDiff => {
const textDiff = cDiff.fields.text as StringDiff
if (textDiff && textDiff.isChanged) {
if (textDiff.action === 'changed') {
summary.push(`Changed '${textDiff.fromValue}' to '${textDiff.toValue}'`)
} else {
const text = textDiff.toValue || textDiff.fromValue
summary.push(`${startCase(textDiff.action)}${text ? '' : ' (empty) '} text '${text}'`)
}
annotation = <AnnotatedStringDiff diff={textDiff} />
}
if (
cDiff.fields.marks &&
cDiff.fields.marks.isChanged &&
cDiff.fields.marks.action === 'added' &&
Array.isArray(cDiff.fields.marks.toValue) &&
cDiff.fields.marks.toValue.length > 0
) {
const marks = cDiff.fields.marks.toValue
summary.push(`Added mark ${(Array.isArray(marks) ? marks : []).join(', ')}`)
}
})

if (childDiffs.length !== 0 && summary.length === 0) {
summary.push(<pre>{`Unknown diff ${JSON.stringify(childDiffs, null, 2)}`}</pre>)
}

childMap[child._key] = {
annotation,
diffs: childDiffs,
child,
summary
}
})
return childMap
}

function findChildDiffs(diff: ObjectDiff, child: PortableTextChild): ObjectDiff[] {
const childrenDiff = diff.fields.children as ArrayDiff
return childrenDiff.items
.filter(item => item.diff.isChanged && item.diff.toValue === child)
.map(item => item.diff)
.map(childDiff => childDiff as ObjectDiff)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './helpers'
export * from './PTDiff'
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
@import 'part:@sanity/base/theme/variables-style';

.root {
text-decoration: none;
display: inline;
position: relative;
background: red;
border-bottom: 2px dotted color(var(--text-color) a(100%));
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React from 'react'
import styles from './Annotation.css'

type Props = {
children: React.ReactNode
}

export default function Annotation(props: Props) {
return <span className={styles.root}>{props.children}</span>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.quote {
composes: blockquote from 'part:@sanity/base/theme/typography/text-blocks-style';
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from 'react'
import styles from './Blockquote.css'

type Props = {
children: React.ReactNode
}
export default function Blockquote(props: Props) {
return (
<div className={styles.root}>
<blockquote className={styles.quote}>{props.children}</blockquote>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
@import 'part:@sanity/base/theme/variables-style';

.root {
display: inline;
}

.strong {
font-weight: bold;
}

.em {
font-style: italic;
}

.underline {
text-decoration: underline;
}

.overline {
text-decoration: overline;
}

.strike-through {
text-decoration: line-through;
}

.code {
font-family: var(--font-family-monospace);
background: color(var(--text-color) alpha(5%));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React from 'react'
import styles from './Decorator.css'

type Props = {
mark: string
children: React.ReactNode
}
export default function Decorator(props: Props) {
return <span className={`${styles.root} ${styles[props.mark]}`}>{props.children}</span>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
@import 'part:@sanity/base/theme/variables-style';

.root {
text-transform: none;
font-family: var(--block-editor-header-font-family);
}

.h1 {
composes: root;
composes: heading1 from 'part:@sanity/base/theme/typography/headings-style';
}

.h2 {
composes: root;
composes: heading2 from 'part:@sanity/base/theme/typography/headings-style';
}

.h3 {
composes: root;
composes: heading3 from 'part:@sanity/base/theme/typography/headings-style';
}

.h4 {
composes: root;
composes: heading4 from 'part:@sanity/base/theme/typography/headings-style';
}

.h5 {
composes: root;
composes: heading5 from 'part:@sanity/base/theme/typography/headings-style';
}

.h6 {
composes: root;
composes: heading6 from 'part:@sanity/base/theme/typography/headings-style';
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React from 'react'
import styles from './Header.css'

type Props = {
style: string
children: React.ReactNode
}
export default function Header(props: Props) {
return <div className={styles[props.style]}>{props.children}</div>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
@import 'part:@sanity/base/theme/variables-style';

.root {
composes: paragraph from 'part:@sanity/base/theme/typography/text-blocks-style';
text-transform: none;
white-space: wrap;
overflow-wrap: anywhere;
/* color: red; */

@nest div[class~='pt-list-item-inner'] > & {
margin: 0;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React from 'react'
import styles from './Paragraph.css'

interface Props {
children: React.ReactNode
}

export default function Paragraph(props: Props) {
return <div className={styles.root}>{props.children}</div>
}
26 changes: 26 additions & 0 deletions packages/@sanity/field/src/diff/components/portableText/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import {ArrayDiff, ObjectDiff} from '@sanity/field/diff'
import {ReactElement} from 'react'

export type PortableTextBlock = {
_key: string
_type: string
children: PortableTextChild[]
style?: string
}

export type PortableTextChild = {
_key: string
_type: string
marks?: string[]
text?: string
}

export type ChildMap = Record<
string,
{
annotation: React.ReactNode | undefined
child: PortableTextChild
diffs: ObjectDiff[] | ArrayDiff[]
summary: React.ReactNode[]
}
>

0 comments on commit 7ab9ce9

Please sign in to comment.