-
Notifications
You must be signed in to change notification settings - Fork 390
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[field] Add portable text diff component
- Loading branch information
1 parent
52915f3
commit 7ab9ce9
Showing
15 changed files
with
351 additions
and
0 deletions.
There are no files selected for viewing
12 changes: 12 additions & 0 deletions
12
packages/@sanity/field/src/diff/components/portableText/PTDiff.css
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
94
packages/@sanity/field/src/diff/components/portableText/PTDiff.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} | ||
} |
72 changes: 72 additions & 0 deletions
72
packages/@sanity/field/src/diff/components/portableText/helpers.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
2 changes: 2 additions & 0 deletions
2
packages/@sanity/field/src/diff/components/portableText/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from './helpers' | ||
export * from './PTDiff' |
10 changes: 10 additions & 0 deletions
10
packages/@sanity/field/src/diff/components/portableText/previews/Annotation.css
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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%)); | ||
} | ||
|
10 changes: 10 additions & 0 deletions
10
packages/@sanity/field/src/diff/components/portableText/previews/Annotation.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
} |
3 changes: 3 additions & 0 deletions
3
packages/@sanity/field/src/diff/components/portableText/previews/Blockquote.css
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; | ||
} |
13 changes: 13 additions & 0 deletions
13
packages/@sanity/field/src/diff/components/portableText/previews/Blockquote.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
) | ||
} |
30 changes: 30 additions & 0 deletions
30
packages/@sanity/field/src/diff/components/portableText/previews/Decorator.css
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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%)); | ||
} |
10 changes: 10 additions & 0 deletions
10
packages/@sanity/field/src/diff/components/portableText/previews/Decorator.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
} |
36 changes: 36 additions & 0 deletions
36
packages/@sanity/field/src/diff/components/portableText/previews/Header.css
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; | ||
} |
10 changes: 10 additions & 0 deletions
10
packages/@sanity/field/src/diff/components/portableText/previews/Header.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
} |
13 changes: 13 additions & 0 deletions
13
packages/@sanity/field/src/diff/components/portableText/previews/Paragraph.css
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
10 changes: 10 additions & 0 deletions
10
packages/@sanity/field/src/diff/components/portableText/previews/Paragraph.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
26
packages/@sanity/field/src/diff/components/portableText/types.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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[] | ||
} | ||
> |