@contentful/field-editor-rich-text 3.12.7 → 3.14.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/Toolbar/components/EmbedEntityWidget.js +6 -0
- package/dist/cjs/Toolbar/index.js +2 -1
- package/dist/cjs/constants/Schema.js +14 -0
- package/dist/cjs/helpers/{newEntitySelectorConfigFromRichTextField.js → config.js} +19 -5
- package/dist/cjs/helpers/editor.js +1 -0
- package/dist/cjs/plugins/DragAndDrop/index.js +4 -2
- package/dist/cjs/plugins/EmbeddedEntityInline/index.js +21 -4
- package/dist/cjs/plugins/EmbeddedResourceInline/FetchingWrappedResourceInlineCard.js +102 -0
- package/dist/cjs/plugins/EmbeddedResourceInline/LinkedResourceInline.js +51 -0
- package/dist/cjs/plugins/EmbeddedResourceInline/index.js +56 -0
- package/dist/cjs/plugins/Heading/__tests__/createHeadingPlugin.test.js +2 -0
- package/dist/cjs/plugins/Hyperlink/HyperlinkModal.js +63 -8
- package/dist/cjs/plugins/Hyperlink/__tests__/createHyperlinkPlugin.test.js +3 -1
- package/dist/cjs/plugins/Hyperlink/components/ResourceHyperlink.js +93 -0
- package/dist/cjs/plugins/Hyperlink/createHyperlinkPlugin.js +29 -1
- package/dist/cjs/plugins/Hyperlink/useResourceEntityInfo.js +71 -0
- package/dist/cjs/plugins/Hyperlink/utils.js +5 -2
- package/dist/cjs/plugins/Paragraph/__tests__/createParagraphPlugin.test.js +2 -0
- package/dist/cjs/plugins/Quote/__test__/createQuotePlugin.test.js +2 -0
- package/dist/cjs/plugins/index.js +2 -0
- package/dist/cjs/plugins/shared/EmbeddedBlockToolbarIcon.js +2 -4
- package/dist/cjs/plugins/shared/EmbeddedBlockUtil.js +3 -4
- package/dist/cjs/plugins/shared/EmbeddedInlineToolbarIcon.js +11 -6
- package/dist/cjs/plugins/shared/EmbeddedInlineUtil.js +66 -27
- package/dist/cjs/plugins/shared/ResourceNewBadge.js +57 -0
- package/dist/cjs/test-utils/jsx.js +11 -0
- package/dist/esm/Toolbar/components/EmbedEntityWidget.js +6 -0
- package/dist/esm/Toolbar/index.js +2 -1
- package/dist/esm/constants/Schema.js +14 -0
- package/dist/esm/helpers/{newEntitySelectorConfigFromRichTextField.js → config.js} +8 -2
- package/dist/esm/helpers/editor.js +1 -0
- package/dist/esm/plugins/DragAndDrop/index.js +4 -2
- package/dist/esm/plugins/EmbeddedEntityInline/index.js +22 -5
- package/dist/esm/plugins/EmbeddedResourceInline/FetchingWrappedResourceInlineCard.js +53 -0
- package/dist/esm/plugins/EmbeddedResourceInline/LinkedResourceInline.js +36 -0
- package/dist/esm/plugins/EmbeddedResourceInline/index.js +46 -0
- package/dist/esm/plugins/Heading/__tests__/createHeadingPlugin.test.js +2 -0
- package/dist/esm/plugins/Hyperlink/HyperlinkModal.js +63 -8
- package/dist/esm/plugins/Hyperlink/__tests__/createHyperlinkPlugin.test.js +3 -1
- package/dist/esm/plugins/Hyperlink/components/ResourceHyperlink.js +44 -0
- package/dist/esm/plugins/Hyperlink/createHyperlinkPlugin.js +29 -1
- package/dist/esm/plugins/Hyperlink/useResourceEntityInfo.js +22 -0
- package/dist/esm/plugins/Hyperlink/utils.js +2 -2
- package/dist/esm/plugins/Paragraph/__tests__/createParagraphPlugin.test.js +2 -0
- package/dist/esm/plugins/Quote/__test__/createQuotePlugin.test.js +2 -0
- package/dist/esm/plugins/index.js +2 -0
- package/dist/esm/plugins/shared/EmbeddedBlockToolbarIcon.js +3 -5
- package/dist/esm/plugins/shared/EmbeddedBlockUtil.js +1 -2
- package/dist/esm/plugins/shared/EmbeddedInlineToolbarIcon.js +12 -7
- package/dist/esm/plugins/shared/EmbeddedInlineUtil.js +64 -25
- package/dist/esm/plugins/shared/ResourceNewBadge.js +8 -0
- package/dist/esm/test-utils/jsx.js +11 -0
- package/dist/types/constants/Schema.d.ts +10 -0
- package/dist/types/helpers/config.d.ts +33 -0
- package/dist/types/helpers/editor.d.ts +1 -1
- package/dist/types/plugins/EmbeddedResourceInline/FetchingWrappedResourceInlineCard.d.ts +13 -0
- package/dist/types/plugins/EmbeddedResourceInline/LinkedResourceInline.d.ts +13 -0
- package/dist/types/plugins/EmbeddedResourceInline/index.d.ts +3 -0
- package/dist/types/plugins/Hyperlink/HyperlinkModal.d.ts +2 -1
- package/dist/types/plugins/Hyperlink/components/ResourceHyperlink.d.ts +20 -0
- package/dist/types/plugins/Hyperlink/useResourceEntityInfo.d.ts +7 -0
- package/dist/types/plugins/Hyperlink/utils.d.ts +1 -0
- package/dist/types/plugins/shared/EmbeddedInlineToolbarIcon.d.ts +2 -1
- package/dist/types/plugins/shared/EmbeddedInlineUtil.d.ts +3 -17
- package/dist/types/plugins/shared/ResourceNewBadge.d.ts +2 -0
- package/package.json +2 -2
- package/dist/cjs/helpers/newResourceEntitySelectorConfigFromRichTextField.js +0 -21
- package/dist/esm/helpers/newResourceEntitySelectorConfigFromRichTextField.js +0 -6
- package/dist/types/helpers/newEntitySelectorConfigFromRichTextField.d.ts +0 -14
- package/dist/types/helpers/newResourceEntitySelectorConfigFromRichTextField.d.ts +0 -16
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { INLINES } from '@contentful/rich-text-types';
|
|
2
|
-
import {
|
|
2
|
+
import { getWithEmbeddedEntryInlineEvents } from '../shared/EmbeddedInlineUtil';
|
|
3
3
|
import { LinkedEntityInline } from './LinkedEntityInline';
|
|
4
4
|
export function createEmbeddedEntityInlinePlugin(sdk) {
|
|
5
5
|
const htmlAttributeName = 'data-embedded-entity-inline-id';
|
|
6
|
+
const nodeType = INLINES.EMBEDDED_ENTRY;
|
|
6
7
|
return {
|
|
7
|
-
key:
|
|
8
|
-
type:
|
|
8
|
+
key: nodeType,
|
|
9
|
+
type: nodeType,
|
|
9
10
|
isElement: true,
|
|
10
11
|
isInline: true,
|
|
11
12
|
isVoid: true,
|
|
@@ -14,7 +15,7 @@ export function createEmbeddedEntityInlinePlugin(sdk) {
|
|
|
14
15
|
hotkey: 'mod+shift+2'
|
|
15
16
|
},
|
|
16
17
|
handlers: {
|
|
17
|
-
onKeyDown: getWithEmbeddedEntryInlineEvents(sdk)
|
|
18
|
+
onKeyDown: getWithEmbeddedEntryInlineEvents(nodeType, sdk)
|
|
18
19
|
},
|
|
19
20
|
deserializeHtml: {
|
|
20
21
|
rules: [
|
|
@@ -23,7 +24,23 @@ export function createEmbeddedEntityInlinePlugin(sdk) {
|
|
|
23
24
|
}
|
|
24
25
|
],
|
|
25
26
|
withoutChildren: true,
|
|
26
|
-
getNode: (el)=>
|
|
27
|
+
getNode: (el)=>({
|
|
28
|
+
type: nodeType,
|
|
29
|
+
children: [
|
|
30
|
+
{
|
|
31
|
+
text: ''
|
|
32
|
+
}
|
|
33
|
+
],
|
|
34
|
+
data: {
|
|
35
|
+
target: {
|
|
36
|
+
sys: {
|
|
37
|
+
id: el.getAttribute('data-entity-id'),
|
|
38
|
+
type: 'Link',
|
|
39
|
+
linkType: el.getAttribute('data-entity-type')
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
})
|
|
27
44
|
}
|
|
28
45
|
};
|
|
29
46
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { InlineEntryCard, MenuItem, Text } from '@contentful/f36-components';
|
|
3
|
+
import { useResource } from '@contentful/field-editor-reference';
|
|
4
|
+
import { entityHelpers } from '@contentful/field-editor-shared';
|
|
5
|
+
import { INLINES } from '@contentful/rich-text-types';
|
|
6
|
+
const { getEntryTitle , getEntryStatus } = entityHelpers;
|
|
7
|
+
export function FetchingWrappedResourceInlineCard(props) {
|
|
8
|
+
const { link , onEntityFetchComplete } = props;
|
|
9
|
+
const { data , status: requestStatus } = useResource(link.linkType, link.urn);
|
|
10
|
+
React.useEffect(()=>{
|
|
11
|
+
if (requestStatus === 'success') {
|
|
12
|
+
onEntityFetchComplete?.();
|
|
13
|
+
}
|
|
14
|
+
}, [
|
|
15
|
+
onEntityFetchComplete,
|
|
16
|
+
requestStatus
|
|
17
|
+
]);
|
|
18
|
+
if (requestStatus === 'error') {
|
|
19
|
+
return React.createElement(InlineEntryCard, {
|
|
20
|
+
title: "Entry missing or inaccessible",
|
|
21
|
+
testId: INLINES.EMBEDDED_RESOURCE,
|
|
22
|
+
isSelected: props.isSelected
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
if (requestStatus === 'loading' || data === undefined) {
|
|
26
|
+
return React.createElement(InlineEntryCard, {
|
|
27
|
+
isLoading: true
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
const { resource: entry , contentType , defaultLocaleCode , space } = data;
|
|
31
|
+
const title = getEntryTitle({
|
|
32
|
+
entry,
|
|
33
|
+
contentType,
|
|
34
|
+
defaultLocaleCode,
|
|
35
|
+
localeCode: defaultLocaleCode,
|
|
36
|
+
defaultTitle: 'Untitled'
|
|
37
|
+
});
|
|
38
|
+
const status = getEntryStatus(entry?.sys);
|
|
39
|
+
return React.createElement(InlineEntryCard, {
|
|
40
|
+
testId: INLINES.EMBEDDED_RESOURCE,
|
|
41
|
+
isSelected: props.isSelected,
|
|
42
|
+
title: `${data.contentType.name}: ${title} (Space: ${space.name})`,
|
|
43
|
+
status: status,
|
|
44
|
+
actions: [
|
|
45
|
+
React.createElement(MenuItem, {
|
|
46
|
+
key: "remove",
|
|
47
|
+
onClick: props.onRemove,
|
|
48
|
+
disabled: props.isDisabled,
|
|
49
|
+
testId: "delete"
|
|
50
|
+
}, "Remove")
|
|
51
|
+
]
|
|
52
|
+
}, React.createElement(Text, null, title));
|
|
53
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useSelected, useReadOnly } from 'slate-react';
|
|
3
|
+
import { useContentfulEditor } from '../../ContentfulEditorProvider';
|
|
4
|
+
import { findNodePath, removeNodes } from '../../internal';
|
|
5
|
+
import { useSdkContext } from '../../SdkProvider';
|
|
6
|
+
import { useLinkTracking } from '../links-tracking';
|
|
7
|
+
import { LinkedInlineWrapper } from '../shared/LinkedInlineWrapper';
|
|
8
|
+
import { FetchingWrappedResourceInlineCard } from './FetchingWrappedResourceInlineCard';
|
|
9
|
+
export function LinkedResourceInline(props) {
|
|
10
|
+
const { attributes , children , element } = props;
|
|
11
|
+
const { onEntityFetchComplete } = useLinkTracking();
|
|
12
|
+
const isSelected = useSelected();
|
|
13
|
+
const editor = useContentfulEditor();
|
|
14
|
+
const sdk = useSdkContext();
|
|
15
|
+
const isDisabled = useReadOnly();
|
|
16
|
+
const link = element.data.target.sys;
|
|
17
|
+
function handleRemoveClick() {
|
|
18
|
+
if (!editor) return;
|
|
19
|
+
const pathToElement = findNodePath(editor, element);
|
|
20
|
+
removeNodes(editor, {
|
|
21
|
+
at: pathToElement
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
return React.createElement(LinkedInlineWrapper, {
|
|
25
|
+
attributes: attributes,
|
|
26
|
+
link: element.data.target,
|
|
27
|
+
card: React.createElement(FetchingWrappedResourceInlineCard, {
|
|
28
|
+
sdk: sdk,
|
|
29
|
+
link: link,
|
|
30
|
+
isDisabled: isDisabled,
|
|
31
|
+
isSelected: isSelected,
|
|
32
|
+
onRemove: handleRemoveClick,
|
|
33
|
+
onEntityFetchComplete: onEntityFetchComplete
|
|
34
|
+
})
|
|
35
|
+
}, children);
|
|
36
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { INLINES } from '@contentful/rich-text-types';
|
|
2
|
+
import { getWithEmbeddedEntryInlineEvents } from '../shared/EmbeddedInlineUtil';
|
|
3
|
+
import { LinkedResourceInline } from './LinkedResourceInline';
|
|
4
|
+
export function createEmbeddedResourceInlinePlugin(sdk) {
|
|
5
|
+
const htmlAttributeName = 'data-embedded-resource-inline-id';
|
|
6
|
+
const nodeType = INLINES.EMBEDDED_RESOURCE;
|
|
7
|
+
return {
|
|
8
|
+
key: nodeType,
|
|
9
|
+
type: nodeType,
|
|
10
|
+
isElement: true,
|
|
11
|
+
isInline: true,
|
|
12
|
+
isVoid: true,
|
|
13
|
+
component: LinkedResourceInline,
|
|
14
|
+
options: {
|
|
15
|
+
hotkey: 'mod+shift+p'
|
|
16
|
+
},
|
|
17
|
+
handlers: {
|
|
18
|
+
onKeyDown: getWithEmbeddedEntryInlineEvents(nodeType, sdk)
|
|
19
|
+
},
|
|
20
|
+
deserializeHtml: {
|
|
21
|
+
rules: [
|
|
22
|
+
{
|
|
23
|
+
validAttribute: htmlAttributeName
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
withoutChildren: true,
|
|
27
|
+
getNode: (el)=>({
|
|
28
|
+
type: nodeType,
|
|
29
|
+
children: [
|
|
30
|
+
{
|
|
31
|
+
text: ''
|
|
32
|
+
}
|
|
33
|
+
],
|
|
34
|
+
data: {
|
|
35
|
+
target: {
|
|
36
|
+
sys: {
|
|
37
|
+
urn: el.getAttribute('data-entity-id'),
|
|
38
|
+
linkType: el.getAttribute('data-entity-type'),
|
|
39
|
+
type: 'ResourceLink'
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -6,6 +6,7 @@ import { ModalDialogLauncher } from '@contentful/field-editor-shared';
|
|
|
6
6
|
import { INLINES } from '@contentful/rich-text-types';
|
|
7
7
|
import { css } from 'emotion';
|
|
8
8
|
import { getNodeEntryFromSelection, insertLink, LINK_TYPES, focus } from '../../helpers/editor';
|
|
9
|
+
import getAllowedResourcesForNodeType from '../../helpers/getAllowedResourcesForNodeType';
|
|
9
10
|
import getLinkedContentTypeIdsForNodeType from '../../helpers/getLinkedContentTypeIdsForNodeType';
|
|
10
11
|
import { isNodeTypeEnabled } from '../../helpers/validations';
|
|
11
12
|
import { withoutNormalizing } from '../../internal';
|
|
@@ -13,18 +14,22 @@ import { getText, isEditorReadOnly } from '../../internal/queries';
|
|
|
13
14
|
import { select } from '../../internal/transforms';
|
|
14
15
|
import { FetchingWrappedAssetCard } from '../shared/FetchingWrappedAssetCard';
|
|
15
16
|
import { FetchingWrappedEntryCard } from '../shared/FetchingWrappedEntryCard';
|
|
17
|
+
import { FetchingWrappedResourceCard } from '../shared/FetchingWrappedResourceCard';
|
|
16
18
|
const styles = {
|
|
17
19
|
removeSelectionLabel: css`
|
|
18
20
|
margin-left: ${tokens.spacingS};
|
|
21
|
+
margin-bottom: ${tokens.spacingXs}; // to match FormLabel margin
|
|
19
22
|
`
|
|
20
23
|
};
|
|
21
24
|
const SYS_LINK_TYPES = {
|
|
22
25
|
[INLINES.ENTRY_HYPERLINK]: 'Entry',
|
|
23
|
-
[INLINES.ASSET_HYPERLINK]: 'Asset'
|
|
26
|
+
[INLINES.ASSET_HYPERLINK]: 'Asset',
|
|
27
|
+
[INLINES.RESOURCE_HYPERLINK]: 'Contentful:Entry'
|
|
24
28
|
};
|
|
25
29
|
const LINK_TYPE_SELECTION_VALUES = {
|
|
26
30
|
[INLINES.HYPERLINK]: 'URL',
|
|
27
31
|
[INLINES.ENTRY_HYPERLINK]: 'Entry',
|
|
32
|
+
[INLINES.RESOURCE_HYPERLINK]: 'Entry (different space)',
|
|
28
33
|
[INLINES.ASSET_HYPERLINK]: 'Asset'
|
|
29
34
|
};
|
|
30
35
|
export function HyperlinkModal(props) {
|
|
@@ -50,7 +55,16 @@ export function HyperlinkModal(props) {
|
|
|
50
55
|
const entityLinks = Object.keys(SYS_LINK_TYPES);
|
|
51
56
|
const isEntityLink = entityLinks.includes(linkType);
|
|
52
57
|
if (isEntityLink) {
|
|
53
|
-
|
|
58
|
+
if (linkType === INLINES.ENTRY_HYPERLINK) {
|
|
59
|
+
return !!(linkText && isEntryLink(linkEntity));
|
|
60
|
+
}
|
|
61
|
+
if (linkType === INLINES.ASSET_HYPERLINK) {
|
|
62
|
+
return !!(linkText && isAssetLink(linkEntity));
|
|
63
|
+
}
|
|
64
|
+
if (linkType === INLINES.RESOURCE_HYPERLINK) {
|
|
65
|
+
return !!(linkText && isResourceLink(linkEntity));
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
54
68
|
}
|
|
55
69
|
return false;
|
|
56
70
|
}
|
|
@@ -73,22 +87,55 @@ export function HyperlinkModal(props) {
|
|
|
73
87
|
}
|
|
74
88
|
};
|
|
75
89
|
}
|
|
90
|
+
function entityToResourceLink(entity) {
|
|
91
|
+
const { urn } = entity.sys;
|
|
92
|
+
return {
|
|
93
|
+
sys: {
|
|
94
|
+
urn,
|
|
95
|
+
type: 'ResourceLink',
|
|
96
|
+
linkType: 'Contentful:Entry'
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function isResourceLink(link) {
|
|
101
|
+
return !!link && !!link.sys.urn;
|
|
102
|
+
}
|
|
103
|
+
function isEntryLink(link) {
|
|
104
|
+
return !!link && link.sys.type === 'Link' && link.sys.linkType === 'Entry';
|
|
105
|
+
}
|
|
106
|
+
function isAssetLink(link) {
|
|
107
|
+
return !!link && link.sys.type === 'Link' && link.sys.linkType === 'Asset';
|
|
108
|
+
}
|
|
76
109
|
async function selectEntry() {
|
|
77
110
|
const options = {
|
|
78
111
|
locale: props.sdk.field.locale,
|
|
79
112
|
contentTypes: getLinkedContentTypeIdsForNodeType(props.sdk.field, INLINES.ENTRY_HYPERLINK)
|
|
80
113
|
};
|
|
81
114
|
const entry = await props.sdk.dialogs.selectSingleEntry(options);
|
|
82
|
-
|
|
83
|
-
|
|
115
|
+
if (entry) {
|
|
116
|
+
setLinkTarget('');
|
|
117
|
+
setLinkEntity(entityToLink(entry));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async function selectResourceEntry() {
|
|
121
|
+
const options = {
|
|
122
|
+
allowedResources: getAllowedResourcesForNodeType(props.sdk.field, INLINES.RESOURCE_HYPERLINK)
|
|
123
|
+
};
|
|
124
|
+
const entry = await props.sdk.dialogs.selectSingleResourceEntry(options);
|
|
125
|
+
if (entry) {
|
|
126
|
+
setLinkTarget('');
|
|
127
|
+
setLinkEntity(entityToResourceLink(entry));
|
|
128
|
+
}
|
|
84
129
|
}
|
|
85
130
|
async function selectAsset() {
|
|
86
131
|
const options = {
|
|
87
132
|
locale: props.sdk.field.locale
|
|
88
133
|
};
|
|
89
134
|
const asset = await props.sdk.dialogs.selectSingleAsset(options);
|
|
90
|
-
|
|
91
|
-
|
|
135
|
+
if (asset) {
|
|
136
|
+
setLinkTarget('');
|
|
137
|
+
setLinkEntity(entityToLink(asset));
|
|
138
|
+
}
|
|
92
139
|
}
|
|
93
140
|
function resetLinkEntity(event) {
|
|
94
141
|
event.preventDefault();
|
|
@@ -134,13 +181,18 @@ export function HyperlinkModal(props) {
|
|
|
134
181
|
testId: "entity-selection-link",
|
|
135
182
|
onClick: resetLinkEntity,
|
|
136
183
|
className: styles.removeSelectionLabel
|
|
137
|
-
}, "Remove selection"), React.createElement("div", null, linkType === INLINES.ENTRY_HYPERLINK && React.createElement(FetchingWrappedEntryCard, {
|
|
184
|
+
}, "Remove selection"), React.createElement("div", null, linkType === INLINES.ENTRY_HYPERLINK && isEntryLink(linkEntity) && React.createElement(FetchingWrappedEntryCard, {
|
|
138
185
|
sdk: props.sdk,
|
|
139
186
|
locale: props.sdk.field.locale,
|
|
140
187
|
entryId: linkEntity.sys.id,
|
|
141
188
|
isDisabled: true,
|
|
142
189
|
isSelected: false
|
|
143
|
-
}), linkType === INLINES.
|
|
190
|
+
}), linkType === INLINES.RESOURCE_HYPERLINK && isResourceLink(linkEntity) && React.createElement(FetchingWrappedResourceCard, {
|
|
191
|
+
sdk: props.sdk,
|
|
192
|
+
link: linkEntity.sys,
|
|
193
|
+
isDisabled: true,
|
|
194
|
+
isSelected: false
|
|
195
|
+
}), linkType === INLINES.ASSET_HYPERLINK && isAssetLink(linkEntity) && React.createElement(FetchingWrappedAssetCard, {
|
|
144
196
|
sdk: props.sdk,
|
|
145
197
|
locale: props.sdk.field.locale,
|
|
146
198
|
assetId: linkEntity.sys.id,
|
|
@@ -149,6 +201,9 @@ export function HyperlinkModal(props) {
|
|
|
149
201
|
}))) : React.createElement("div", null, linkType === INLINES.ENTRY_HYPERLINK && React.createElement(TextLink, {
|
|
150
202
|
testId: "entity-selection-link",
|
|
151
203
|
onClick: selectEntry
|
|
204
|
+
}, "Select entry"), linkType === INLINES.RESOURCE_HYPERLINK && React.createElement(TextLink, {
|
|
205
|
+
testId: "entity-selection-link",
|
|
206
|
+
onClick: selectResourceEntry
|
|
152
207
|
}, "Select entry"), linkType === INLINES.ASSET_HYPERLINK && React.createElement(TextLink, {
|
|
153
208
|
testId: "entity-selection-link",
|
|
154
209
|
onClick: selectAsset
|
|
@@ -7,12 +7,14 @@ describe('normalization', ()=>{
|
|
|
7
7
|
asset: "asset-id"
|
|
8
8
|
})), jsx("hp", null, jsx("htext", null, "entry"), jsx("hlink", {
|
|
9
9
|
entry: "entry-id"
|
|
10
|
+
})), jsx("hp", null, jsx("htext", null, "resource"), jsx("hlink", {
|
|
11
|
+
resource: "resource-urn"
|
|
10
12
|
})), jsx("hp", null, jsx("htext", null, "explicit empty link"), jsx("hlink", {
|
|
11
13
|
uri: "https://link.com"
|
|
12
14
|
}, '')), jsx("hp", null, jsx("htext", null, "link with empty space"), jsx("hlink", {
|
|
13
15
|
uri: "https://link.com"
|
|
14
16
|
}, " ")));
|
|
15
|
-
const expected = jsx("editor", null, jsx("hp", null, jsx("htext", null, "link")), jsx("hp", null, jsx("htext", null, "asset")), jsx("hp", null, jsx("htext", null, "entry")), jsx("hp", null, jsx("htext", null, "explicit empty link")), jsx("hp", null, jsx("htext", null, "link with empty space")));
|
|
17
|
+
const expected = jsx("editor", null, jsx("hp", null, jsx("htext", null, "link")), jsx("hp", null, jsx("htext", null, "asset")), jsx("hp", null, jsx("htext", null, "entry")), jsx("hp", null, jsx("htext", null, "resource")), jsx("hp", null, jsx("htext", null, "explicit empty link")), jsx("hp", null, jsx("htext", null, "link with empty space")));
|
|
16
18
|
assertOutput({
|
|
17
19
|
input,
|
|
18
20
|
expected
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Tooltip, TextLink } from '@contentful/f36-components';
|
|
3
|
+
import { useContentfulEditor } from '../../../ContentfulEditorProvider';
|
|
4
|
+
import { fromDOMPoint } from '../../../internal';
|
|
5
|
+
import { useLinkTracking } from '../../../plugins/links-tracking';
|
|
6
|
+
import { useSdkContext } from '../../../SdkProvider';
|
|
7
|
+
import { addOrEditLink } from '../HyperlinkModal';
|
|
8
|
+
import { useResourceEntityInfo } from '../useResourceEntityInfo';
|
|
9
|
+
import { styles } from './styles';
|
|
10
|
+
export function ResourceHyperlink(props) {
|
|
11
|
+
const editor = useContentfulEditor();
|
|
12
|
+
const sdk = useSdkContext();
|
|
13
|
+
const { target } = props.element.data;
|
|
14
|
+
const { onEntityFetchComplete } = useLinkTracking();
|
|
15
|
+
const tooltipContent = useResourceEntityInfo({
|
|
16
|
+
target,
|
|
17
|
+
onEntityFetchComplete
|
|
18
|
+
});
|
|
19
|
+
if (!target) return null;
|
|
20
|
+
function handleClick(event) {
|
|
21
|
+
event.preventDefault();
|
|
22
|
+
event.stopPropagation();
|
|
23
|
+
if (!editor) return;
|
|
24
|
+
const p = fromDOMPoint(editor, [
|
|
25
|
+
event.target,
|
|
26
|
+
0
|
|
27
|
+
]);
|
|
28
|
+
if (p) {
|
|
29
|
+
addOrEditLink(editor, sdk, editor.tracking.onViewportAction, p.path);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return React.createElement(Tooltip, {
|
|
33
|
+
content: tooltipContent,
|
|
34
|
+
targetWrapperClassName: styles.hyperlinkWrapper,
|
|
35
|
+
placement: "bottom",
|
|
36
|
+
maxWidth: "auto"
|
|
37
|
+
}, React.createElement(TextLink, {
|
|
38
|
+
as: "a",
|
|
39
|
+
onClick: handleClick,
|
|
40
|
+
className: styles.hyperlink,
|
|
41
|
+
"data-resource-link-type": target.sys.linkType,
|
|
42
|
+
"data-resource-link-urn": target.sys.urn
|
|
43
|
+
}, props.children));
|
|
44
|
+
}
|
|
@@ -4,12 +4,14 @@ import isHotkey from 'is-hotkey';
|
|
|
4
4
|
import { isLinkActive, unwrapLink } from '../../helpers/editor';
|
|
5
5
|
import { transformRemove } from '../../helpers/transformers';
|
|
6
6
|
import { EntityHyperlink } from './components/EntityHyperlink';
|
|
7
|
+
import { ResourceHyperlink } from './components/ResourceHyperlink';
|
|
7
8
|
import { UrlHyperlink } from './components/UrlHyperlink';
|
|
8
9
|
import { addOrEditLink } from './HyperlinkModal';
|
|
9
10
|
import { hasText } from './utils';
|
|
10
11
|
const isAnchor = (element)=>element.nodeName === 'A' && !!element.getAttribute('href') && element.getAttribute('href') !== '#';
|
|
11
12
|
const isEntryAnchor = (element)=>element.nodeName === 'A' && element.getAttribute('data-link-type') === 'Entry';
|
|
12
13
|
const isAssetAnchor = (element)=>element.nodeName === 'A' && element.getAttribute('data-link-type') === 'Asset';
|
|
14
|
+
const isResourceAnchor = (element)=>element.nodeName === 'A' && element.getAttribute('data-resource-link-type') === 'Contentful:Entry';
|
|
13
15
|
const buildHyperlinkEventHandler = (sdk)=>(editor, { options: { hotkey } })=>{
|
|
14
16
|
return (event)=>{
|
|
15
17
|
if (!editor.selection) {
|
|
@@ -31,6 +33,14 @@ const getNodeOfType = (type)=>(el, node)=>({
|
|
|
31
33
|
children: node.children,
|
|
32
34
|
data: type === INLINES.HYPERLINK ? {
|
|
33
35
|
uri: el.getAttribute('href')
|
|
36
|
+
} : type === INLINES.RESOURCE_HYPERLINK ? {
|
|
37
|
+
target: {
|
|
38
|
+
sys: {
|
|
39
|
+
urn: el.getAttribute('data-resource-link-urn'),
|
|
40
|
+
linkType: el.getAttribute('data-resource-link-type'),
|
|
41
|
+
type: 'ResourceLink'
|
|
42
|
+
}
|
|
43
|
+
}
|
|
34
44
|
} : {
|
|
35
45
|
target: {
|
|
36
46
|
sys: {
|
|
@@ -89,6 +99,23 @@ export const createHyperlinkPlugin = (sdk)=>{
|
|
|
89
99
|
getNode: getNodeOfType(INLINES.ENTRY_HYPERLINK)
|
|
90
100
|
}
|
|
91
101
|
},
|
|
102
|
+
{
|
|
103
|
+
...common,
|
|
104
|
+
key: INLINES.RESOURCE_HYPERLINK,
|
|
105
|
+
type: INLINES.RESOURCE_HYPERLINK,
|
|
106
|
+
component: ResourceHyperlink,
|
|
107
|
+
deserializeHtml: {
|
|
108
|
+
rules: [
|
|
109
|
+
{
|
|
110
|
+
validNodeName: [
|
|
111
|
+
'A'
|
|
112
|
+
]
|
|
113
|
+
}
|
|
114
|
+
],
|
|
115
|
+
query: (el)=>isResourceAnchor(el),
|
|
116
|
+
getNode: getNodeOfType(INLINES.RESOURCE_HYPERLINK)
|
|
117
|
+
}
|
|
118
|
+
},
|
|
92
119
|
{
|
|
93
120
|
...common,
|
|
94
121
|
key: INLINES.ASSET_HYPERLINK,
|
|
@@ -113,7 +140,8 @@ export const createHyperlinkPlugin = (sdk)=>{
|
|
|
113
140
|
type: [
|
|
114
141
|
INLINES.HYPERLINK,
|
|
115
142
|
INLINES.ASSET_HYPERLINK,
|
|
116
|
-
INLINES.ENTRY_HYPERLINK
|
|
143
|
+
INLINES.ENTRY_HYPERLINK,
|
|
144
|
+
INLINES.RESOURCE_HYPERLINK
|
|
117
145
|
]
|
|
118
146
|
},
|
|
119
147
|
validNode: hasText,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { useResource } from '@contentful/field-editor-reference';
|
|
3
|
+
import { truncateTitle } from './utils';
|
|
4
|
+
export function useResourceEntityInfo({ onEntityFetchComplete , target }) {
|
|
5
|
+
const { data , error , status } = useResource(target.sys.linkType, target.sys.urn);
|
|
6
|
+
React.useEffect(()=>{
|
|
7
|
+
if (status === 'success') {
|
|
8
|
+
onEntityFetchComplete?.();
|
|
9
|
+
}
|
|
10
|
+
}, [
|
|
11
|
+
status,
|
|
12
|
+
onEntityFetchComplete
|
|
13
|
+
]);
|
|
14
|
+
if (status === 'loading') {
|
|
15
|
+
return `Loading entry...`;
|
|
16
|
+
}
|
|
17
|
+
if (!data || error) {
|
|
18
|
+
return `Entry missing or inaccessible`;
|
|
19
|
+
}
|
|
20
|
+
const title = truncateTitle(data.resource.fields[data.contentType.displayField]?.[data.defaultLocaleCode], 40) || 'Untitled';
|
|
21
|
+
return `${data.contentType.name}: ${title} (Space: ${data.space.name} – Env.: ${data.resource.sys.environment.sys.id})`;
|
|
22
|
+
}
|
|
@@ -5,7 +5,7 @@ export const hasText = (editor, entry)=>{
|
|
|
5
5
|
const [node, path] = entry;
|
|
6
6
|
return !isAncestorEmpty(editor, node) && getText(editor, path).trim() !== '';
|
|
7
7
|
};
|
|
8
|
-
function
|
|
8
|
+
export function truncateTitle(str, length) {
|
|
9
9
|
if (typeof str === 'string' && str.length > length) {
|
|
10
10
|
return str && str.substr(0, length + 1).replace(/(\s+\S(?=\S)|\s*)\.?.$/, '…');
|
|
11
11
|
}
|
|
@@ -16,7 +16,7 @@ export function getEntityInfo(data) {
|
|
|
16
16
|
return '';
|
|
17
17
|
}
|
|
18
18
|
const { entityTitle , contentTypeName , entityStatus , jobs } = data;
|
|
19
|
-
const title =
|
|
19
|
+
const title = truncateTitle(entityTitle, 60) || 'Untitled';
|
|
20
20
|
const scheduledActions = jobs.length > 0 ? getScheduleTooltipContent({
|
|
21
21
|
job: jobs[0],
|
|
22
22
|
jobsCount: jobs.length
|
|
@@ -6,6 +6,7 @@ import { createDragAndDropPlugin } from './DragAndDrop';
|
|
|
6
6
|
import { createEmbeddedAssetBlockPlugin, createEmbeddedEntryBlockPlugin } from './EmbeddedEntityBlock';
|
|
7
7
|
import { createEmbeddedEntityInlinePlugin } from './EmbeddedEntityInline';
|
|
8
8
|
import { createEmbeddedResourceBlockPlugin } from './EmbeddedResourceBlock';
|
|
9
|
+
import { createEmbeddedResourceInlinePlugin } from './EmbeddedResourceInline';
|
|
9
10
|
import { createHeadingPlugin } from './Heading';
|
|
10
11
|
import { createHrPlugin } from './Hr';
|
|
11
12
|
import { createHyperlinkPlugin } from './Hyperlink';
|
|
@@ -39,6 +40,7 @@ export const getPlugins = (sdk, onAction, restrictedMarks)=>[
|
|
|
39
40
|
createEmbeddedResourceBlockPlugin(sdk),
|
|
40
41
|
createHyperlinkPlugin(sdk),
|
|
41
42
|
createEmbeddedEntityInlinePlugin(sdk),
|
|
43
|
+
createEmbeddedResourceInlinePlugin(sdk),
|
|
42
44
|
createMarksPlugin(),
|
|
43
45
|
createTrailingParagraphPlugin(),
|
|
44
46
|
createTextPlugin(restrictedMarks),
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { Flex, Icon, Menu } from '@contentful/f36-components';
|
|
3
3
|
import { AssetIcon, EmbeddedEntryBlockIcon } from '@contentful/f36-icons';
|
|
4
4
|
import { BLOCKS } from '@contentful/rich-text-types';
|
|
5
5
|
import { css } from 'emotion';
|
|
6
6
|
import { useContentfulEditor } from '../../ContentfulEditorProvider';
|
|
7
7
|
import { useSdkContext } from '../../SdkProvider';
|
|
8
8
|
import { selectEntityAndInsert, selectResourceEntityAndInsert } from '../shared/EmbeddedBlockUtil';
|
|
9
|
+
import { ResourceNewBadge } from './ResourceNewBadge';
|
|
9
10
|
export const styles = {
|
|
10
11
|
icon: css({
|
|
11
12
|
marginRight: '10px'
|
|
@@ -40,10 +41,7 @@ export function EmbeddedBlockToolbarIcon({ isDisabled , nodeType , onClose }) {
|
|
|
40
41
|
as: type === 'Asset' ? AssetIcon : EmbeddedEntryBlockIcon,
|
|
41
42
|
className: `rich-text__embedded-entry-list-icon ${styles.icon}`,
|
|
42
43
|
variant: "secondary"
|
|
43
|
-
}), React.createElement("span", null, type, nodeType == BLOCKS.EMBEDDED_RESOURCE && React.createElement(
|
|
44
|
-
variant: "primary-filled",
|
|
45
|
-
size: "small"
|
|
46
|
-
}, "new")))));
|
|
44
|
+
}), React.createElement("span", null, type, nodeType == BLOCKS.EMBEDDED_RESOURCE && React.createElement(ResourceNewBadge, null))));
|
|
47
45
|
}
|
|
48
46
|
function getEntityTypeFromNodeType(nodeType) {
|
|
49
47
|
const words = nodeType.toLowerCase().split('-');
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { BLOCKS, TEXT_CONTAINERS } from '@contentful/rich-text-types';
|
|
2
2
|
import isHotkey from 'is-hotkey';
|
|
3
|
+
import { newEntitySelectorConfigFromRichTextField, newResourceEntitySelectorConfigFromRichTextField } from '../../helpers/config';
|
|
3
4
|
import { focus, getNodeEntryFromSelection, insertEmptyParagraph, moveToTheNextChar } from '../../helpers/editor';
|
|
4
|
-
import newEntitySelectorConfigFromRichTextField from '../../helpers/newEntitySelectorConfigFromRichTextField';
|
|
5
|
-
import newResourceEntitySelectorConfigFromRichTextField from '../../helpers/newResourceEntitySelectorConfigFromRichTextField';
|
|
6
5
|
import { watchCurrentSlide } from '../../helpers/sdkNavigatorSlideIn';
|
|
7
6
|
import { getText, getAboveNode, getLastNodeByLevel, insertNodes, setNodes, select, removeNodes } from '../../internal';
|
|
8
7
|
export function getWithEmbeddedBlockEvents(nodeType, sdk) {
|
|
@@ -7,7 +7,8 @@ import { css } from 'emotion';
|
|
|
7
7
|
import { useContentfulEditor } from '../../ContentfulEditorProvider';
|
|
8
8
|
import { moveToTheNextChar } from '../../helpers/editor';
|
|
9
9
|
import { useSdkContext } from '../../SdkProvider';
|
|
10
|
-
import { selectEntityAndInsert } from '../shared/EmbeddedInlineUtil';
|
|
10
|
+
import { selectEntityAndInsert, selectResourceEntityAndInsert } from '../shared/EmbeddedInlineUtil';
|
|
11
|
+
import { ResourceNewBadge } from './ResourceNewBadge';
|
|
11
12
|
const styles = {
|
|
12
13
|
icon: css({
|
|
13
14
|
marginRight: '10px'
|
|
@@ -21,20 +22,24 @@ const styles = {
|
|
|
21
22
|
}
|
|
22
23
|
})
|
|
23
24
|
};
|
|
24
|
-
export function EmbeddedInlineToolbarIcon(
|
|
25
|
+
export function EmbeddedInlineToolbarIcon({ onClose , nodeType , isDisabled }) {
|
|
25
26
|
const editor = useContentfulEditor();
|
|
26
27
|
const sdk = useSdkContext();
|
|
27
28
|
async function handleClick(event) {
|
|
28
29
|
event.preventDefault();
|
|
29
30
|
if (!editor) return;
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
onClose();
|
|
32
|
+
if (nodeType === INLINES.EMBEDDED_RESOURCE) {
|
|
33
|
+
await selectResourceEntityAndInsert(editor, sdk, editor.tracking.onToolbarAction);
|
|
34
|
+
} else {
|
|
35
|
+
await selectEntityAndInsert(editor, sdk, editor.tracking.onToolbarAction);
|
|
36
|
+
}
|
|
32
37
|
moveToTheNextChar(editor);
|
|
33
38
|
}
|
|
34
39
|
return React.createElement(Menu.Item, {
|
|
35
|
-
disabled:
|
|
40
|
+
disabled: isDisabled,
|
|
36
41
|
className: "rich-text__entry-link-block-button",
|
|
37
|
-
testId: `toolbar-toggle-${
|
|
42
|
+
testId: `toolbar-toggle-${nodeType}`,
|
|
38
43
|
onClick: handleClick
|
|
39
44
|
}, React.createElement(Flex, {
|
|
40
45
|
alignItems: "center",
|
|
@@ -42,5 +47,5 @@ export function EmbeddedInlineToolbarIcon(props) {
|
|
|
42
47
|
}, React.createElement(EmbeddedEntryInlineIcon, {
|
|
43
48
|
variant: "secondary",
|
|
44
49
|
className: `rich-text__embedded-entry-list-icon ${styles.icon}`
|
|
45
|
-
}), React.createElement("span", null, "Inline entry")));
|
|
50
|
+
}), React.createElement("span", null, "Inline entry", nodeType == INLINES.EMBEDDED_RESOURCE && React.createElement(ResourceNewBadge, null))));
|
|
46
51
|
}
|