@contentful/field-editor-rich-text 3.6.0 → 3.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/RichTextEditor.js +42 -53
- package/dist/cjs/SyncEditorValue.js +107 -0
- package/dist/cjs/helpers/callbacks.js +35 -0
- package/dist/cjs/helpers/toSlateValue.js +51 -0
- package/dist/cjs/internal/hooks.js +12 -2
- package/dist/cjs/internal/misc.js +0 -6
- package/dist/cjs/plugins/Table/onKeyDownTable.js +14 -0
- package/dist/cjs/plugins/Text/__tests__/createTextPlugin.test.js +5 -15
- package/dist/cjs/plugins/index.js +0 -5
- package/dist/cjs/plugins/shared/FetchingWrappedResourceCard.js +11 -9
- package/dist/cjs/plugins/shared/__tests__/FetchingWrappedResourceCard.test.js +146 -0
- package/dist/esm/RichTextEditor.js +37 -48
- package/dist/esm/SyncEditorValue.js +53 -0
- package/dist/esm/helpers/callbacks.js +20 -0
- package/dist/{cjs/helpers/sanitizeIncomingSlateDoc.js → esm/helpers/toSlateValue.js} +13 -10
- package/dist/esm/internal/hooks.js +9 -2
- package/dist/esm/internal/misc.js +0 -3
- package/dist/esm/plugins/Table/onKeyDownTable.js +15 -1
- package/dist/esm/plugins/Text/__tests__/createTextPlugin.test.js +5 -15
- package/dist/esm/plugins/index.js +0 -5
- package/dist/esm/plugins/shared/FetchingWrappedResourceCard.js +12 -10
- package/dist/esm/plugins/shared/__tests__/FetchingWrappedResourceCard.test.js +98 -0
- package/dist/types/ContentfulEditorProvider.d.ts +2 -3
- package/dist/types/RichTextEditor.d.ts +2 -2
- package/dist/types/SyncEditorValue.d.ts +13 -0
- package/dist/types/helpers/callbacks.d.ts +3 -0
- package/dist/types/helpers/toSlateValue.d.ts +7 -0
- package/dist/types/internal/hooks.d.ts +4 -2
- package/dist/types/internal/misc.d.ts +2 -2
- package/dist/types/plugins/shared/__tests__/FetchingWrappedResourceCard.test.d.ts +1 -0
- package/dist/types/test-utils/createEditor.d.ts +2 -0
- package/package.json +14 -14
- package/dist/cjs/prepareDocument.js +0 -86
- package/dist/cjs/useOnValueChanged.js +0 -58
- package/dist/esm/helpers/sanitizeIncomingSlateDoc.js +0 -23
- package/dist/esm/prepareDocument.js +0 -57
- package/dist/esm/useOnValueChanged.js +0 -43
- package/dist/types/helpers/sanitizeIncomingSlateDoc.d.ts +0 -6
- package/dist/types/prepareDocument.d.ts +0 -19
- package/dist/types/useOnValueChanged.d.ts +0 -8
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as React from 'react';
|
|
2
2
|
import { EntityProvider } from '@contentful/field-editor-reference';
|
|
3
3
|
import { FieldConnector } from '@contentful/field-editor-shared';
|
|
4
4
|
import * as Contentful from '@contentful/rich-text-types';
|
|
5
|
-
import { Plate,
|
|
5
|
+
import { Plate, PlateProvider } from '@udecode/plate-core';
|
|
6
6
|
import { css, cx } from 'emotion';
|
|
7
7
|
import deepEquals from 'fast-deep-equal';
|
|
8
8
|
import noop from 'lodash/noop';
|
|
9
9
|
import { ContentfulEditorIdProvider, getContentfulEditorId } from './ContentfulEditorProvider';
|
|
10
|
-
import {
|
|
10
|
+
import { createOnChangeCallback } from './helpers/callbacks';
|
|
11
|
+
import { toSlateValue } from './helpers/toSlateValue';
|
|
11
12
|
import { getPlugins, disableCorePlugins } from './plugins';
|
|
12
|
-
import { documentToEditorValue, normalizeEditorValue, setEditorContent } from './prepareDocument';
|
|
13
13
|
import { styles } from './RichTextEditor.styles';
|
|
14
14
|
import { SdkProvider } from './SdkProvider';
|
|
15
|
+
import { SyncEditorValue } from './SyncEditorValue';
|
|
15
16
|
import Toolbar from './Toolbar';
|
|
16
17
|
import StickyToolbarWrapper from './Toolbar/components/StickyToolbarWrapper';
|
|
17
|
-
import { useOnValueChanged } from './useOnValueChanged';
|
|
18
18
|
export const ConnectedRichTextEditor = (props)=>{
|
|
19
19
|
const id = getContentfulEditorId(props.sdk);
|
|
20
20
|
const plugins = React.useMemo(()=>getPlugins(props.sdk, props.onAction ?? noop, props.restrictedMarks), [
|
|
@@ -22,43 +22,24 @@ export const ConnectedRichTextEditor = (props)=>{
|
|
|
22
22
|
props.onAction,
|
|
23
23
|
props.restrictedMarks
|
|
24
24
|
]);
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
setPendingExternalUpdate(true);
|
|
40
|
-
setEditorContent(editor, documentToEditorValue(props.value));
|
|
25
|
+
const handleChange = props.onChange;
|
|
26
|
+
const isFirstRender = React.useRef(true);
|
|
27
|
+
const value = toSlateValue(props.value);
|
|
28
|
+
const onChange = React.useMemo(()=>createOnChangeCallback((document)=>{
|
|
29
|
+
if (!isFirstRender.current && handleChange) {
|
|
30
|
+
handleChange(document);
|
|
31
|
+
}
|
|
32
|
+
}), [
|
|
33
|
+
handleChange
|
|
34
|
+
]);
|
|
35
|
+
const firstInteractionHandler = React.useCallback(()=>{
|
|
36
|
+
isFirstRender.current = false;
|
|
41
37
|
}, [
|
|
42
|
-
|
|
43
|
-
id
|
|
38
|
+
isFirstRender
|
|
44
39
|
]);
|
|
45
40
|
const classNames = cx(styles.editor, props.minHeight !== undefined ? css({
|
|
46
41
|
minHeight: props.minHeight
|
|
47
42
|
}) : undefined, props.isDisabled ? styles.disabled : styles.enabled, props.isToolbarHidden && styles.hiddenToolbar);
|
|
48
|
-
useEffect(()=>{
|
|
49
|
-
if (!isFirstRender) {
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
getPlateActions(id).value(normalizeEditorValue(documentToEditorValue(props.value), {
|
|
53
|
-
plugins,
|
|
54
|
-
disableCorePlugins
|
|
55
|
-
}));
|
|
56
|
-
}, [
|
|
57
|
-
isFirstRender,
|
|
58
|
-
plugins,
|
|
59
|
-
id,
|
|
60
|
-
props.value
|
|
61
|
-
]);
|
|
62
43
|
return React.createElement(SdkProvider, {
|
|
63
44
|
sdk: props.sdk
|
|
64
45
|
}, React.createElement(ContentfulEditorIdProvider, {
|
|
@@ -66,25 +47,33 @@ export const ConnectedRichTextEditor = (props)=>{
|
|
|
66
47
|
}, React.createElement("div", {
|
|
67
48
|
className: styles.root,
|
|
68
49
|
"data-test-id": "rich-text-editor"
|
|
69
|
-
}, React.createElement(
|
|
50
|
+
}, React.createElement(PlateProvider, {
|
|
70
51
|
id: id,
|
|
52
|
+
initialValue: value,
|
|
53
|
+
normalizeInitialValue: true,
|
|
71
54
|
plugins: plugins,
|
|
72
55
|
disableCorePlugins: disableCorePlugins,
|
|
56
|
+
onChange: onChange
|
|
57
|
+
}, !props.isToolbarHidden && React.createElement(StickyToolbarWrapper, {
|
|
58
|
+
isDisabled: props.isDisabled
|
|
59
|
+
}, React.createElement(Toolbar, {
|
|
60
|
+
isDisabled: props.isDisabled
|
|
61
|
+
})), React.createElement(SyncEditorValue, {
|
|
62
|
+
incomingValue: value
|
|
63
|
+
}), React.createElement(Plate, {
|
|
64
|
+
id: id,
|
|
73
65
|
editableProps: {
|
|
74
66
|
className: classNames,
|
|
75
|
-
readOnly: props.isDisabled
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
isDisabled: props.isDisabled
|
|
82
|
-
}))
|
|
83
|
-
}))));
|
|
67
|
+
readOnly: props.isDisabled,
|
|
68
|
+
onKeyDown: firstInteractionHandler,
|
|
69
|
+
onChange: firstInteractionHandler,
|
|
70
|
+
onClick: firstInteractionHandler
|
|
71
|
+
}
|
|
72
|
+
})))));
|
|
84
73
|
};
|
|
85
74
|
const RichTextEditor = (props)=>{
|
|
86
75
|
const { sdk , isInitiallyDisabled , onAction , restrictedMarks , ...otherProps } = props;
|
|
87
|
-
const isEmptyValue = useCallback((value)=>!value || deepEquals(value, Contentful.EMPTY_DOCUMENT), []);
|
|
76
|
+
const isEmptyValue = React.useCallback((value)=>!value || deepEquals(value, Contentful.EMPTY_DOCUMENT), []);
|
|
88
77
|
const id = getContentfulEditorId(props.sdk);
|
|
89
78
|
return React.createElement(EntityProvider, {
|
|
90
79
|
sdk: sdk
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import equal from 'fast-deep-equal';
|
|
3
|
+
import { usePlateSelectors } from './internal/hooks';
|
|
4
|
+
import { withoutNormalizing } from './internal/misc';
|
|
5
|
+
import { isNode, getEndPoint } from './internal/queries';
|
|
6
|
+
import { select } from './internal/transforms';
|
|
7
|
+
const setEditorContent = (editor, nodes)=>{
|
|
8
|
+
withoutNormalizing(editor, ()=>{
|
|
9
|
+
const children = [
|
|
10
|
+
...editor.children
|
|
11
|
+
];
|
|
12
|
+
children.forEach((node)=>editor.apply({
|
|
13
|
+
type: 'remove_node',
|
|
14
|
+
path: [
|
|
15
|
+
0
|
|
16
|
+
],
|
|
17
|
+
node
|
|
18
|
+
}));
|
|
19
|
+
if (nodes) {
|
|
20
|
+
const nodesArray = isNode(nodes) ? [
|
|
21
|
+
nodes
|
|
22
|
+
] : nodes;
|
|
23
|
+
nodesArray.forEach((node, i)=>{
|
|
24
|
+
editor.apply({
|
|
25
|
+
type: 'insert_node',
|
|
26
|
+
path: [
|
|
27
|
+
i
|
|
28
|
+
],
|
|
29
|
+
node
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
const point = getEndPoint(editor, []);
|
|
34
|
+
if (point) {
|
|
35
|
+
select(editor, point);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
export const SyncEditorValue = ({ incomingValue })=>{
|
|
40
|
+
const editor = usePlateSelectors().editor();
|
|
41
|
+
const lastIncomingValue = React.useRef(incomingValue);
|
|
42
|
+
React.useEffect(()=>{
|
|
43
|
+
if (equal(lastIncomingValue.current, incomingValue)) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
lastIncomingValue.current = incomingValue;
|
|
47
|
+
setEditorContent(editor, incomingValue);
|
|
48
|
+
}, [
|
|
49
|
+
editor,
|
|
50
|
+
incomingValue
|
|
51
|
+
]);
|
|
52
|
+
return null;
|
|
53
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { toContentfulDocument } from '@contentful/contentful-slatejs-adapter';
|
|
2
|
+
import equal from 'fast-deep-equal';
|
|
3
|
+
import debounce from 'lodash/debounce';
|
|
4
|
+
import schema from '../constants/Schema';
|
|
5
|
+
import { removeInternalMarks } from './removeInternalMarks';
|
|
6
|
+
export const createOnChangeCallback = (handler)=>{
|
|
7
|
+
let cache = null;
|
|
8
|
+
return debounce((document)=>{
|
|
9
|
+
if (equal(document, cache)) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
cache = document;
|
|
13
|
+
const doc = removeInternalMarks(toContentfulDocument({
|
|
14
|
+
document: document,
|
|
15
|
+
schema: schema
|
|
16
|
+
}));
|
|
17
|
+
const cleanedDocument = removeInternalMarks(doc);
|
|
18
|
+
handler?.(cleanedDocument);
|
|
19
|
+
}, 500);
|
|
20
|
+
};
|
|
@@ -1,13 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
});
|
|
5
|
-
Object.defineProperty(exports, "sanitizeIncomingSlateDoc", {
|
|
6
|
-
enumerable: true,
|
|
7
|
-
get: function() {
|
|
8
|
-
return sanitizeIncomingSlateDoc;
|
|
9
|
-
}
|
|
10
|
-
});
|
|
1
|
+
import { toSlatejsDocument } from '@contentful/contentful-slatejs-adapter';
|
|
2
|
+
import { EMPTY_DOCUMENT } from '@contentful/rich-text-types';
|
|
3
|
+
import schema from '../constants/Schema';
|
|
11
4
|
const isTextElement = (node)=>'text' in node;
|
|
12
5
|
function sanitizeIncomingSlateDoc(nodes = []) {
|
|
13
6
|
return nodes.map((node)=>{
|
|
@@ -31,3 +24,13 @@ function sanitizeIncomingSlateDoc(nodes = []) {
|
|
|
31
24
|
};
|
|
32
25
|
});
|
|
33
26
|
}
|
|
27
|
+
export const toSlateValue = (doc)=>{
|
|
28
|
+
const hasContent = (doc)=>{
|
|
29
|
+
return (doc?.content || []).length > 0;
|
|
30
|
+
};
|
|
31
|
+
const slateDoc = toSlatejsDocument({
|
|
32
|
+
document: doc && hasContent(doc) ? doc : EMPTY_DOCUMENT,
|
|
33
|
+
schema
|
|
34
|
+
});
|
|
35
|
+
return sanitizeIncomingSlateDoc(slateDoc);
|
|
36
|
+
};
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import * as p from '@udecode/plate-core';
|
|
2
2
|
import * as sr from 'slate-react';
|
|
3
3
|
export const useReadOnly = sr.useReadOnly;
|
|
4
|
-
export const usePlateEditorRef =
|
|
5
|
-
|
|
4
|
+
export const usePlateEditorRef = (id)=>{
|
|
5
|
+
return p.usePlateEditorRef(id);
|
|
6
|
+
};
|
|
7
|
+
export const usePlateEditorState = (id)=>{
|
|
8
|
+
return p.usePlateEditorState(id);
|
|
9
|
+
};
|
|
10
|
+
export const usePlateSelectors = (id)=>{
|
|
11
|
+
return p.usePlateSelectors(id);
|
|
12
|
+
};
|
|
@@ -2,7 +2,7 @@ import { BLOCKS } from '@contentful/rich-text-types';
|
|
|
2
2
|
import { getTableEntries, onKeyDownTable as defaultKeyDownTable } from '@udecode/plate-table';
|
|
3
3
|
import { insertEmptyParagraph } from '../../helpers/editor';
|
|
4
4
|
import { blurEditor } from '../../internal/misc';
|
|
5
|
-
import { getAboveNode, isLastChildPath } from '../../internal/queries';
|
|
5
|
+
import { getAboveNode, isLastChildPath, getText, isFirstChild } from '../../internal/queries';
|
|
6
6
|
import { addRowBelow } from './actions';
|
|
7
7
|
export const onKeyDownTable = (editor, plugin)=>{
|
|
8
8
|
const defaultHandler = defaultKeyDownTable(editor, plugin);
|
|
@@ -30,6 +30,20 @@ export const onKeyDownTable = (editor, plugin)=>{
|
|
|
30
30
|
return;
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
|
+
if (event.key === 'Backspace') {
|
|
34
|
+
const entry = getTableEntries(editor, {});
|
|
35
|
+
if (entry) {
|
|
36
|
+
const { table , row , cell } = entry;
|
|
37
|
+
const cellText = getText(editor, cell[1]);
|
|
38
|
+
const isFirstCell = isFirstChild(row[1]);
|
|
39
|
+
const isFirstRow = isFirstChild(table[1]);
|
|
40
|
+
if (isFirstCell && isFirstRow && !cellText) {
|
|
41
|
+
event.preventDefault();
|
|
42
|
+
event.stopPropagation();
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
33
47
|
if (event.key === 'Tab' && !event.shiftKey) {
|
|
34
48
|
event.preventDefault();
|
|
35
49
|
const entry = getTableEntries(editor, {});
|
|
@@ -7,19 +7,9 @@ describe('delete backward', ()=>{
|
|
|
7
7
|
expected: jsx("hul", null, jsx("hli", null, jsx("hp", null, "p", jsx("cursor", null))))
|
|
8
8
|
},
|
|
9
9
|
{
|
|
10
|
-
title: '
|
|
10
|
+
title: 'does not delete the very first paragraph',
|
|
11
11
|
input: jsx("fragment", null, jsx("hp", null, jsx("cursor", null)), jsx("hp", null, "text")),
|
|
12
|
-
expected: jsx("hp", null, jsx("cursor", null), "text")
|
|
13
|
-
},
|
|
14
|
-
{
|
|
15
|
-
title: 'deletes the empty paragraph at the beginning of the RTE followed by li',
|
|
16
|
-
input: jsx("fragment", null, jsx("hp", null, jsx("cursor", null)), jsx("hul", null, jsx("hli", null, jsx("hp", null, "p1")))),
|
|
17
|
-
expected: jsx("hul", null, jsx("hli", null, jsx("hp", null, jsx("cursor", null), "p1")))
|
|
18
|
-
},
|
|
19
|
-
{
|
|
20
|
-
title: 'deletes the empty paragraph at the beginning of the RTE followed by a blockquote',
|
|
21
|
-
input: jsx("fragment", null, jsx("hp", null, jsx("cursor", null)), jsx("hquote", null, jsx("hp", null, "p1"))),
|
|
22
|
-
expected: jsx("hquote", null, jsx("hp", null, jsx("cursor", null), "p1"))
|
|
12
|
+
expected: jsx("fragment", null, jsx("hp", null, jsx("cursor", null)), jsx("hp", null, "text"))
|
|
23
13
|
}
|
|
24
14
|
];
|
|
25
15
|
const render = (children)=>jsx("editor", null, children, jsx("hp", null, jsx("htext", null)));
|
|
@@ -44,17 +34,17 @@ describe('delete forward', ()=>{
|
|
|
44
34
|
expected: jsx("hul", null, jsx("hli", null, jsx("hp", null, jsx("cursor", null), "1")))
|
|
45
35
|
},
|
|
46
36
|
{
|
|
47
|
-
title: 'deletes the
|
|
37
|
+
title: 'deletes the first paragraph when followed by another paragraph',
|
|
48
38
|
input: jsx("fragment", null, jsx("hp", null, jsx("cursor", null)), jsx("hp", null, "text")),
|
|
49
39
|
expected: jsx("hp", null, jsx("cursor", null), "text")
|
|
50
40
|
},
|
|
51
41
|
{
|
|
52
|
-
title: 'deletes the
|
|
42
|
+
title: 'deletes the first paragraph when followed by li',
|
|
53
43
|
input: jsx("fragment", null, jsx("hp", null, jsx("cursor", null)), jsx("hul", null, jsx("hli", null, jsx("hp", null, "p1")))),
|
|
54
44
|
expected: jsx("hul", null, jsx("hli", null, jsx("hp", null, jsx("cursor", null), "p1")))
|
|
55
45
|
},
|
|
56
46
|
{
|
|
57
|
-
title: 'deletes the
|
|
47
|
+
title: 'deletes the first paragraph when followed by a blockquote',
|
|
58
48
|
input: jsx("fragment", null, jsx("hp", null, jsx("cursor", null)), jsx("hquote", null, jsx("hp", null, "p1"))),
|
|
59
49
|
expected: jsx("hquote", null, jsx("hp", null, jsx("cursor", null), "p1"))
|
|
60
50
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { createDeserializeAstPlugin, createDeserializeHtmlPlugin } from '@udecode/plate-core';
|
|
2
1
|
import { createDeserializeDocxPlugin } from '@udecode/plate-serializer-docx';
|
|
3
2
|
import { createSoftBreakPlugin, createExitBreakPlugin, createResetNodePlugin } from './Break';
|
|
4
3
|
import { createCommandPalettePlugin } from './CommandPalette';
|
|
@@ -23,8 +22,6 @@ import { createTrackingPlugin } from './Tracking';
|
|
|
23
22
|
import { createTrailingParagraphPlugin } from './TrailingParagraph';
|
|
24
23
|
import { createVoidsPlugin } from './Voids';
|
|
25
24
|
export const getPlugins = (sdk, onAction, restrictedMarks)=>[
|
|
26
|
-
createDeserializeHtmlPlugin(),
|
|
27
|
-
createDeserializeAstPlugin(),
|
|
28
25
|
createDeserializeDocxPlugin(),
|
|
29
26
|
createTrackingPlugin(onAction),
|
|
30
27
|
createDragAndDropPlugin(),
|
|
@@ -54,7 +51,5 @@ export const getPlugins = (sdk, onAction, restrictedMarks)=>[
|
|
|
54
51
|
createNormalizerPlugin()
|
|
55
52
|
];
|
|
56
53
|
export const disableCorePlugins = {
|
|
57
|
-
deserializeAst: true,
|
|
58
|
-
deserializeHtml: true,
|
|
59
54
|
eventEditor: true
|
|
60
55
|
};
|
|
@@ -1,20 +1,13 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { EntryCard } from '@contentful/f36-components';
|
|
3
|
-
import {
|
|
3
|
+
import { ResourceEntityErrorCard, WrappedEntryCard, useResource } from '@contentful/field-editor-reference';
|
|
4
4
|
import areEqual from 'fast-deep-equal';
|
|
5
5
|
const InternalEntryCard = React.memo((props)=>{
|
|
6
|
-
if (props.status === 'loading') {
|
|
6
|
+
if (props.data === undefined || props.status === 'loading') {
|
|
7
7
|
return React.createElement(EntryCard, {
|
|
8
8
|
isLoading: true
|
|
9
9
|
});
|
|
10
10
|
}
|
|
11
|
-
if (!props.data || props.status === 'error') {
|
|
12
|
-
return React.createElement(MissingEntityCard, {
|
|
13
|
-
entityType: "Entry",
|
|
14
|
-
isDisabled: props.isDisabled,
|
|
15
|
-
onRemove: props.onRemove
|
|
16
|
-
});
|
|
17
|
-
}
|
|
18
11
|
const { contentType , resource: entry , space } = props.data;
|
|
19
12
|
return React.createElement(WrappedEntryCard, {
|
|
20
13
|
size: "default",
|
|
@@ -35,7 +28,7 @@ const InternalEntryCard = React.memo((props)=>{
|
|
|
35
28
|
InternalEntryCard.displayName = 'ReferenceCard';
|
|
36
29
|
export const FetchingWrappedResourceCard = (props)=>{
|
|
37
30
|
const { link , onEntityFetchComplete } = props;
|
|
38
|
-
const { data , status } = useResource(link.linkType, link.urn);
|
|
31
|
+
const { data , status , error } = useResource(link.linkType, link.urn);
|
|
39
32
|
React.useEffect(()=>{
|
|
40
33
|
if (status === 'success') {
|
|
41
34
|
onEntityFetchComplete?.();
|
|
@@ -44,6 +37,15 @@ export const FetchingWrappedResourceCard = (props)=>{
|
|
|
44
37
|
onEntityFetchComplete,
|
|
45
38
|
status
|
|
46
39
|
]);
|
|
40
|
+
if (status === 'error') {
|
|
41
|
+
return React.createElement(ResourceEntityErrorCard, {
|
|
42
|
+
error: error,
|
|
43
|
+
linkType: link.linkType,
|
|
44
|
+
isSelected: props.isSelected,
|
|
45
|
+
isDisabled: props.isDisabled,
|
|
46
|
+
onRemove: props.onRemove
|
|
47
|
+
});
|
|
48
|
+
}
|
|
47
49
|
return React.createElement(InternalEntryCard, {
|
|
48
50
|
data: data,
|
|
49
51
|
status: status,
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import '@testing-library/jest-dom/extend-expect';
|
|
3
|
+
import { EntityProvider } from '@contentful/field-editor-reference';
|
|
4
|
+
import { createFakeCMAAdapter } from '@contentful/field-editor-test-utils';
|
|
5
|
+
import { configure, render, waitFor } from '@testing-library/react';
|
|
6
|
+
import publishedCT from '../__fixtures__/published_content_type.json';
|
|
7
|
+
import publishedEntry from '../__fixtures__/published_entry.json';
|
|
8
|
+
import space from '../__fixtures__/space.json';
|
|
9
|
+
import { FetchingWrappedResourceCard } from '../FetchingWrappedResourceCard';
|
|
10
|
+
configure({
|
|
11
|
+
testIdAttribute: 'data-test-id'
|
|
12
|
+
});
|
|
13
|
+
let sdk;
|
|
14
|
+
const resolvableEntryUrn = 'crn:contentful:::content:spaces/space-id/entries/linked-entry-urn';
|
|
15
|
+
const unknownEntryUrn = 'crn:contentful:::content:spaces/space-id/entries/unknown-entry-urn';
|
|
16
|
+
beforeEach(()=>{
|
|
17
|
+
sdk = {
|
|
18
|
+
locales: {
|
|
19
|
+
default: 'en-US'
|
|
20
|
+
},
|
|
21
|
+
cmaAdapter: createFakeCMAAdapter({
|
|
22
|
+
ContentType: {
|
|
23
|
+
get: jest.fn().mockReturnValue(publishedCT)
|
|
24
|
+
},
|
|
25
|
+
Entry: {
|
|
26
|
+
get: jest.fn().mockImplementation(({ entryId })=>{
|
|
27
|
+
if (entryId === 'linked-entry-urn') {
|
|
28
|
+
return Promise.resolve(publishedEntry);
|
|
29
|
+
}
|
|
30
|
+
return Promise.reject(new Error());
|
|
31
|
+
})
|
|
32
|
+
},
|
|
33
|
+
Locale: {
|
|
34
|
+
getMany: jest.fn().mockResolvedValue({
|
|
35
|
+
items: [
|
|
36
|
+
{
|
|
37
|
+
default: true,
|
|
38
|
+
code: 'en'
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
})
|
|
42
|
+
},
|
|
43
|
+
ScheduledAction: {
|
|
44
|
+
getMany: jest.fn().mockResolvedValue({
|
|
45
|
+
items: [],
|
|
46
|
+
total: 0
|
|
47
|
+
})
|
|
48
|
+
},
|
|
49
|
+
Space: {
|
|
50
|
+
get: jest.fn().mockResolvedValue(space)
|
|
51
|
+
}
|
|
52
|
+
}),
|
|
53
|
+
space: {
|
|
54
|
+
onEntityChanged: jest.fn()
|
|
55
|
+
},
|
|
56
|
+
navigator: {},
|
|
57
|
+
ids: {
|
|
58
|
+
space: 'space-id',
|
|
59
|
+
environment: 'environment-id'
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
function renderResourceCard({ linkType ='Contentful:Entry' , entryUrn =resolvableEntryUrn } = {}) {
|
|
64
|
+
return render(React.createElement(EntityProvider, {
|
|
65
|
+
sdk: sdk
|
|
66
|
+
}, React.createElement(FetchingWrappedResourceCard, {
|
|
67
|
+
isDisabled: false,
|
|
68
|
+
isSelected: false,
|
|
69
|
+
sdk: sdk,
|
|
70
|
+
link: {
|
|
71
|
+
type: 'ResourceLink',
|
|
72
|
+
linkType: linkType,
|
|
73
|
+
urn: entryUrn
|
|
74
|
+
}
|
|
75
|
+
})));
|
|
76
|
+
}
|
|
77
|
+
test('renders entry card', async ()=>{
|
|
78
|
+
const { getByTestId , getByText } = renderResourceCard();
|
|
79
|
+
await waitFor(()=>expect(getByTestId('cf-ui-entry-card')).toBeDefined());
|
|
80
|
+
expect(getByText(publishedEntry.fields.exField.en)).toBeDefined();
|
|
81
|
+
expect(getByText(space.name)).toBeDefined();
|
|
82
|
+
});
|
|
83
|
+
test('renders skeleton when no data is provided', ()=>{
|
|
84
|
+
const { getByTestId } = renderResourceCard();
|
|
85
|
+
expect(getByTestId('cf-ui-skeleton-form')).toBeDefined();
|
|
86
|
+
});
|
|
87
|
+
test('renders unsupported entity card when unsupported link is passed', async ()=>{
|
|
88
|
+
const { getByText } = renderResourceCard({
|
|
89
|
+
linkType: 'Contentful:UnsupportedLink'
|
|
90
|
+
});
|
|
91
|
+
await waitFor(()=>expect(getByText('Resource type Contentful:UnsupportedLink is currently not supported')).toBeDefined());
|
|
92
|
+
});
|
|
93
|
+
test('renders missing entity card when unknown error is returned', async ()=>{
|
|
94
|
+
const { getByTestId } = renderResourceCard({
|
|
95
|
+
entryUrn: unknownEntryUrn
|
|
96
|
+
});
|
|
97
|
+
await waitFor(()=>expect(getByTestId('cf-ui-missing-entry-card')).toBeDefined());
|
|
98
|
+
});
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/// <reference types="react" />
|
|
2
2
|
import { FieldExtensionSDK } from '@contentful/app-sdk';
|
|
3
|
-
import { PlateEditor } from './internal/types';
|
|
4
3
|
export declare function getContentfulEditorId(sdk: FieldExtensionSDK): string;
|
|
5
4
|
export declare const editorContext: import("react").Context<string>;
|
|
6
5
|
export declare const ContentfulEditorIdProvider: import("react").Provider<string>;
|
|
7
6
|
export declare function useContentfulEditorId(id?: string): string;
|
|
8
|
-
export declare function useContentfulEditor(id?: string): PlateEditor
|
|
9
|
-
export declare function useContentfulEditorRef(id?: string): PlateEditor
|
|
7
|
+
export declare function useContentfulEditor(id?: string): import("./internal").PlateEditor;
|
|
8
|
+
export declare function useContentfulEditorRef(id?: string): import("./internal").PlateEditor;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import * as React from 'react';
|
|
2
2
|
import { FieldExtensionSDK } from '@contentful/app-sdk';
|
|
3
3
|
import * as Contentful from '@contentful/rich-text-types';
|
|
4
4
|
import { RichTextTrackingActionHandler } from './plugins/Tracking';
|
|
@@ -6,7 +6,7 @@ type ConnectedProps = {
|
|
|
6
6
|
sdk: FieldExtensionSDK;
|
|
7
7
|
onAction?: RichTextTrackingActionHandler;
|
|
8
8
|
minHeight?: string | number;
|
|
9
|
-
value?:
|
|
9
|
+
value?: Contentful.Document;
|
|
10
10
|
isDisabled?: boolean;
|
|
11
11
|
onChange?: (doc: Contentful.Document) => unknown;
|
|
12
12
|
isToolbarHidden?: boolean;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Value } from './internal/types';
|
|
2
|
+
export type SyncEditorStateProps = {
|
|
3
|
+
incomingValue?: Value;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* A void component that's responsible for keeping the editor
|
|
7
|
+
* state in sync with incoming changes (aka. external updates)
|
|
8
|
+
*
|
|
9
|
+
* This component is a hack to workaround the limitation of Plate v17+
|
|
10
|
+
* where we can no longer access the editor instance outside the Plate
|
|
11
|
+
* provider.
|
|
12
|
+
*/
|
|
13
|
+
export declare const SyncEditorValue: ({ incomingValue }: SyncEditorStateProps) => null;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Document } from '@contentful/rich-text-types';
|
|
2
|
+
import { Element } from '../internal/types';
|
|
3
|
+
/**
|
|
4
|
+
* Converts a Contentful rich text document to the corresponding slate editor
|
|
5
|
+
* value
|
|
6
|
+
*/
|
|
7
|
+
export declare const toSlateValue: (doc?: Document) => Element[];
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import * as p from '@udecode/plate-core';
|
|
2
|
+
import { PlateEditor, Value } from './types';
|
|
2
3
|
export declare const useReadOnly: () => boolean;
|
|
3
|
-
export declare const usePlateEditorRef:
|
|
4
|
-
export declare const usePlateEditorState:
|
|
4
|
+
export declare const usePlateEditorRef: (id?: string) => PlateEditor;
|
|
5
|
+
export declare const usePlateEditorState: (id?: string) => PlateEditor;
|
|
6
|
+
export declare const usePlateSelectors: (id?: string) => p.GetRecord<p.PlateStoreState<Value, PlateEditor>>;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/// <reference types="react" />
|
|
2
2
|
import * as p from '@udecode/plate-core';
|
|
3
|
-
import { StoreApiGet } from '@udecode/zustood';
|
|
4
3
|
import * as s from 'slate';
|
|
5
4
|
import type { Value, PlateEditor, Location, PlatePlugin } from './types';
|
|
6
5
|
export type CreatePlateEditorOptions = Omit<p.CreatePlateEditorOptions<Value, PlateEditor>, 'plugins'> & {
|
|
@@ -23,6 +22,8 @@ export declare const createPlateEditor: (options?: CreatePlateEditorOptions) =>
|
|
|
23
22
|
plugins: p.WithPlatePlugin<{}, Value, p.PlateEditor<Value>>[];
|
|
24
23
|
pluginsByKey: Record<string, p.WithPlatePlugin<{}, Value, p.PlateEditor<Value>>>;
|
|
25
24
|
prevSelection: s.BaseRange | null;
|
|
25
|
+
blockFactory: (node?: Partial<p.TElement> | undefined, path?: s.Path | undefined) => p.TElement;
|
|
26
|
+
childrenFactory: () => Value;
|
|
26
27
|
currentKeyboardEvent: import("react").KeyboardEvent<Element> | null;
|
|
27
28
|
};
|
|
28
29
|
export declare const withoutNormalizing: (editor: PlateEditor, fn: () => boolean | void) => boolean;
|
|
@@ -34,4 +35,3 @@ export declare const fromDOMPoint: (editor: PlateEditor, domPoint: [Node, number
|
|
|
34
35
|
suppressThrow: boolean;
|
|
35
36
|
}) => s.BasePoint | null | undefined;
|
|
36
37
|
export declare const mockPlugin: (plugin?: Partial<PlatePlugin> | undefined) => p.WithPlatePlugin<p.AnyObject, p.Value, p.PlateEditor<p.Value>>;
|
|
37
|
-
export declare const getPlateSelectors: (id?: string | undefined) => StoreApiGet<p.PlateStoreState<Value, PlateEditor>, {}>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import '@testing-library/jest-dom/extend-expect';
|
|
@@ -25,6 +25,8 @@ export declare const createTestEditor: (options: {
|
|
|
25
25
|
plugins: import("@udecode/plate-core").WithPlatePlugin<{}, Value, import("@udecode/plate-core").PlateEditor<Value>>[];
|
|
26
26
|
pluginsByKey: Record<string, import("@udecode/plate-core").WithPlatePlugin<{}, Value, import("@udecode/plate-core").PlateEditor<Value>>>;
|
|
27
27
|
prevSelection: import("slate").BaseRange | null;
|
|
28
|
+
blockFactory: (node?: Partial<import("@udecode/plate-core").TElement> | undefined, path?: import("slate").Path | undefined) => import("@udecode/plate-core").TElement;
|
|
29
|
+
childrenFactory: () => Value;
|
|
28
30
|
currentKeyboardEvent: import("react").KeyboardEvent<Element> | null;
|
|
29
31
|
};
|
|
30
32
|
normalize: () => void;
|