@contentful/field-editor-rich-text 3.13.0 → 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/index.js +2 -1
- package/dist/cjs/constants/Schema.js +11 -0
- package/dist/cjs/helpers/editor.js +1 -0
- package/dist/cjs/plugins/EmbeddedResourceBlock/LinkedResourceBlock.js +2 -2
- 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/{EmbeddedResourceBlock/FetchingWrappedResourceBlockCard.js → shared/FetchingWrappedResourceCard.js} +3 -3
- package/dist/cjs/plugins/shared/__tests__/FetchingWrappedResourceCard.test.js +2 -2
- package/dist/cjs/test-utils/jsx.js +11 -0
- package/dist/esm/Toolbar/index.js +2 -1
- package/dist/esm/constants/Schema.js +11 -0
- package/dist/esm/helpers/editor.js +1 -0
- package/dist/esm/plugins/EmbeddedResourceBlock/LinkedResourceBlock.js +2 -2
- 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/{EmbeddedResourceBlock/FetchingWrappedResourceBlockCard.js → shared/FetchingWrappedResourceCard.js} +1 -1
- package/dist/esm/plugins/shared/__tests__/FetchingWrappedResourceCard.test.js +2 -2
- package/dist/esm/test-utils/jsx.js +11 -0
- package/dist/types/constants/Schema.d.ts +7 -0
- package/dist/types/helpers/editor.d.ts +1 -1
- 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/{EmbeddedResourceBlock/FetchingWrappedResourceBlockCard.d.ts → shared/FetchingWrappedResourceCard.d.ts} +2 -2
- package/package.json +2 -2
|
@@ -205,7 +205,8 @@ function getValidationInfo(field) {
|
|
|
205
205
|
const isAnyHyperlinkEnabled = someWithValidation([
|
|
206
206
|
_richtexttypes.INLINES.HYPERLINK,
|
|
207
207
|
_richtexttypes.INLINES.ASSET_HYPERLINK,
|
|
208
|
-
_richtexttypes.INLINES.ENTRY_HYPERLINK
|
|
208
|
+
_richtexttypes.INLINES.ENTRY_HYPERLINK,
|
|
209
|
+
_richtexttypes.INLINES.RESOURCE_HYPERLINK
|
|
209
210
|
], _validations.isNodeTypeEnabled);
|
|
210
211
|
const isAnyBlockFormattingEnabled = someWithValidation([
|
|
211
212
|
_richtexttypes.BLOCKS.UL_LIST,
|
|
@@ -154,6 +154,17 @@ const _default = {
|
|
|
154
154
|
}
|
|
155
155
|
]
|
|
156
156
|
},
|
|
157
|
+
[_richtexttypes.INLINES.RESOURCE_HYPERLINK]: {
|
|
158
|
+
nodes: [
|
|
159
|
+
{
|
|
160
|
+
match: [
|
|
161
|
+
{
|
|
162
|
+
object: 'text'
|
|
163
|
+
}
|
|
164
|
+
]
|
|
165
|
+
}
|
|
166
|
+
]
|
|
167
|
+
},
|
|
157
168
|
[_richtexttypes.INLINES.ASSET_HYPERLINK]: {
|
|
158
169
|
nodes: [
|
|
159
170
|
{
|
|
@@ -84,6 +84,7 @@ const _environment = require("./environment");
|
|
|
84
84
|
const LINK_TYPES = [
|
|
85
85
|
_richtexttypes.INLINES.HYPERLINK,
|
|
86
86
|
_richtexttypes.INLINES.ENTRY_HYPERLINK,
|
|
87
|
+
_richtexttypes.INLINES.RESOURCE_HYPERLINK,
|
|
87
88
|
_richtexttypes.INLINES.ASSET_HYPERLINK
|
|
88
89
|
];
|
|
89
90
|
const LIST_TYPES = [
|
|
@@ -14,8 +14,8 @@ const _ContentfulEditorProvider = require("../../ContentfulEditorProvider");
|
|
|
14
14
|
const _internal = require("../../internal");
|
|
15
15
|
const _SdkProvider = require("../../SdkProvider");
|
|
16
16
|
const _linkstracking = require("../links-tracking");
|
|
17
|
+
const _FetchingWrappedResourceCard = require("../shared/FetchingWrappedResourceCard");
|
|
17
18
|
const _LinkedBlockWrapper = require("../shared/LinkedBlockWrapper");
|
|
18
|
-
const _FetchingWrappedResourceBlockCard = require("./FetchingWrappedResourceBlockCard");
|
|
19
19
|
function _interop_require_default(obj) {
|
|
20
20
|
return obj && obj.__esModule ? obj : {
|
|
21
21
|
default: obj
|
|
@@ -42,7 +42,7 @@ function LinkedResourceBlock(props) {
|
|
|
42
42
|
return _react.default.createElement(_LinkedBlockWrapper.LinkedBlockWrapper, {
|
|
43
43
|
attributes: attributes,
|
|
44
44
|
link: element.data.target,
|
|
45
|
-
card: _react.default.createElement(
|
|
45
|
+
card: _react.default.createElement(_FetchingWrappedResourceCard.FetchingWrappedResourceCard, {
|
|
46
46
|
sdk: sdk,
|
|
47
47
|
link: link,
|
|
48
48
|
isDisabled: isDisabled,
|
|
@@ -12,6 +12,8 @@ describe('normalization', ()=>{
|
|
|
12
12
|
uri: "https://contentful.com"
|
|
13
13
|
}), (0, _testutils.jsx)("hlink", {
|
|
14
14
|
entry: "entry-id"
|
|
15
|
+
}), (0, _testutils.jsx)("hlink", {
|
|
16
|
+
resource: "resource-urn"
|
|
15
17
|
}), (0, _testutils.jsx)("hlink", {
|
|
16
18
|
asset: "asset-id"
|
|
17
19
|
}), "some text after"), (0, _testutils.jsx)("hp", null, (0, _testutils.jsx)("htext", null)));
|
|
@@ -24,6 +24,7 @@ const _fieldeditorshared = require("@contentful/field-editor-shared");
|
|
|
24
24
|
const _richtexttypes = require("@contentful/rich-text-types");
|
|
25
25
|
const _emotion = require("emotion");
|
|
26
26
|
const _editor = require("../../helpers/editor");
|
|
27
|
+
const _getAllowedResourcesForNodeType = _interop_require_default(require("../../helpers/getAllowedResourcesForNodeType"));
|
|
27
28
|
const _getLinkedContentTypeIdsForNodeType = _interop_require_default(require("../../helpers/getLinkedContentTypeIdsForNodeType"));
|
|
28
29
|
const _validations = require("../../helpers/validations");
|
|
29
30
|
const _internal = require("../../internal");
|
|
@@ -31,6 +32,7 @@ const _queries = require("../../internal/queries");
|
|
|
31
32
|
const _transforms = require("../../internal/transforms");
|
|
32
33
|
const _FetchingWrappedAssetCard = require("../shared/FetchingWrappedAssetCard");
|
|
33
34
|
const _FetchingWrappedEntryCard = require("../shared/FetchingWrappedEntryCard");
|
|
35
|
+
const _FetchingWrappedResourceCard = require("../shared/FetchingWrappedResourceCard");
|
|
34
36
|
function _interop_require_default(obj) {
|
|
35
37
|
return obj && obj.__esModule ? obj : {
|
|
36
38
|
default: obj
|
|
@@ -78,15 +80,18 @@ function _interop_require_wildcard(obj, nodeInterop) {
|
|
|
78
80
|
const styles = {
|
|
79
81
|
removeSelectionLabel: (0, _emotion.css)`
|
|
80
82
|
margin-left: ${_f36tokens.default.spacingS};
|
|
83
|
+
margin-bottom: ${_f36tokens.default.spacingXs}; // to match FormLabel margin
|
|
81
84
|
`
|
|
82
85
|
};
|
|
83
86
|
const SYS_LINK_TYPES = {
|
|
84
87
|
[_richtexttypes.INLINES.ENTRY_HYPERLINK]: 'Entry',
|
|
85
|
-
[_richtexttypes.INLINES.ASSET_HYPERLINK]: 'Asset'
|
|
88
|
+
[_richtexttypes.INLINES.ASSET_HYPERLINK]: 'Asset',
|
|
89
|
+
[_richtexttypes.INLINES.RESOURCE_HYPERLINK]: 'Contentful:Entry'
|
|
86
90
|
};
|
|
87
91
|
const LINK_TYPE_SELECTION_VALUES = {
|
|
88
92
|
[_richtexttypes.INLINES.HYPERLINK]: 'URL',
|
|
89
93
|
[_richtexttypes.INLINES.ENTRY_HYPERLINK]: 'Entry',
|
|
94
|
+
[_richtexttypes.INLINES.RESOURCE_HYPERLINK]: 'Entry (different space)',
|
|
90
95
|
[_richtexttypes.INLINES.ASSET_HYPERLINK]: 'Asset'
|
|
91
96
|
};
|
|
92
97
|
function HyperlinkModal(props) {
|
|
@@ -112,7 +117,16 @@ function HyperlinkModal(props) {
|
|
|
112
117
|
const entityLinks = Object.keys(SYS_LINK_TYPES);
|
|
113
118
|
const isEntityLink = entityLinks.includes(linkType);
|
|
114
119
|
if (isEntityLink) {
|
|
115
|
-
|
|
120
|
+
if (linkType === _richtexttypes.INLINES.ENTRY_HYPERLINK) {
|
|
121
|
+
return !!(linkText && isEntryLink(linkEntity));
|
|
122
|
+
}
|
|
123
|
+
if (linkType === _richtexttypes.INLINES.ASSET_HYPERLINK) {
|
|
124
|
+
return !!(linkText && isAssetLink(linkEntity));
|
|
125
|
+
}
|
|
126
|
+
if (linkType === _richtexttypes.INLINES.RESOURCE_HYPERLINK) {
|
|
127
|
+
return !!(linkText && isResourceLink(linkEntity));
|
|
128
|
+
}
|
|
129
|
+
return false;
|
|
116
130
|
}
|
|
117
131
|
return false;
|
|
118
132
|
}
|
|
@@ -135,22 +149,55 @@ function HyperlinkModal(props) {
|
|
|
135
149
|
}
|
|
136
150
|
};
|
|
137
151
|
}
|
|
152
|
+
function entityToResourceLink(entity) {
|
|
153
|
+
const { urn } = entity.sys;
|
|
154
|
+
return {
|
|
155
|
+
sys: {
|
|
156
|
+
urn,
|
|
157
|
+
type: 'ResourceLink',
|
|
158
|
+
linkType: 'Contentful:Entry'
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function isResourceLink(link) {
|
|
163
|
+
return !!link && !!link.sys.urn;
|
|
164
|
+
}
|
|
165
|
+
function isEntryLink(link) {
|
|
166
|
+
return !!link && link.sys.type === 'Link' && link.sys.linkType === 'Entry';
|
|
167
|
+
}
|
|
168
|
+
function isAssetLink(link) {
|
|
169
|
+
return !!link && link.sys.type === 'Link' && link.sys.linkType === 'Asset';
|
|
170
|
+
}
|
|
138
171
|
async function selectEntry() {
|
|
139
172
|
const options = {
|
|
140
173
|
locale: props.sdk.field.locale,
|
|
141
174
|
contentTypes: (0, _getLinkedContentTypeIdsForNodeType.default)(props.sdk.field, _richtexttypes.INLINES.ENTRY_HYPERLINK)
|
|
142
175
|
};
|
|
143
176
|
const entry = await props.sdk.dialogs.selectSingleEntry(options);
|
|
144
|
-
|
|
145
|
-
|
|
177
|
+
if (entry) {
|
|
178
|
+
setLinkTarget('');
|
|
179
|
+
setLinkEntity(entityToLink(entry));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
async function selectResourceEntry() {
|
|
183
|
+
const options = {
|
|
184
|
+
allowedResources: (0, _getAllowedResourcesForNodeType.default)(props.sdk.field, _richtexttypes.INLINES.RESOURCE_HYPERLINK)
|
|
185
|
+
};
|
|
186
|
+
const entry = await props.sdk.dialogs.selectSingleResourceEntry(options);
|
|
187
|
+
if (entry) {
|
|
188
|
+
setLinkTarget('');
|
|
189
|
+
setLinkEntity(entityToResourceLink(entry));
|
|
190
|
+
}
|
|
146
191
|
}
|
|
147
192
|
async function selectAsset() {
|
|
148
193
|
const options = {
|
|
149
194
|
locale: props.sdk.field.locale
|
|
150
195
|
};
|
|
151
196
|
const asset = await props.sdk.dialogs.selectSingleAsset(options);
|
|
152
|
-
|
|
153
|
-
|
|
197
|
+
if (asset) {
|
|
198
|
+
setLinkTarget('');
|
|
199
|
+
setLinkEntity(entityToLink(asset));
|
|
200
|
+
}
|
|
154
201
|
}
|
|
155
202
|
function resetLinkEntity(event) {
|
|
156
203
|
event.preventDefault();
|
|
@@ -196,13 +243,18 @@ function HyperlinkModal(props) {
|
|
|
196
243
|
testId: "entity-selection-link",
|
|
197
244
|
onClick: resetLinkEntity,
|
|
198
245
|
className: styles.removeSelectionLabel
|
|
199
|
-
}, "Remove selection"), _react.createElement("div", null, linkType === _richtexttypes.INLINES.ENTRY_HYPERLINK && _react.createElement(_FetchingWrappedEntryCard.FetchingWrappedEntryCard, {
|
|
246
|
+
}, "Remove selection"), _react.createElement("div", null, linkType === _richtexttypes.INLINES.ENTRY_HYPERLINK && isEntryLink(linkEntity) && _react.createElement(_FetchingWrappedEntryCard.FetchingWrappedEntryCard, {
|
|
200
247
|
sdk: props.sdk,
|
|
201
248
|
locale: props.sdk.field.locale,
|
|
202
249
|
entryId: linkEntity.sys.id,
|
|
203
250
|
isDisabled: true,
|
|
204
251
|
isSelected: false
|
|
205
|
-
}), linkType === _richtexttypes.INLINES.
|
|
252
|
+
}), linkType === _richtexttypes.INLINES.RESOURCE_HYPERLINK && isResourceLink(linkEntity) && _react.createElement(_FetchingWrappedResourceCard.FetchingWrappedResourceCard, {
|
|
253
|
+
sdk: props.sdk,
|
|
254
|
+
link: linkEntity.sys,
|
|
255
|
+
isDisabled: true,
|
|
256
|
+
isSelected: false
|
|
257
|
+
}), linkType === _richtexttypes.INLINES.ASSET_HYPERLINK && isAssetLink(linkEntity) && _react.createElement(_FetchingWrappedAssetCard.FetchingWrappedAssetCard, {
|
|
206
258
|
sdk: props.sdk,
|
|
207
259
|
locale: props.sdk.field.locale,
|
|
208
260
|
assetId: linkEntity.sys.id,
|
|
@@ -211,6 +263,9 @@ function HyperlinkModal(props) {
|
|
|
211
263
|
}))) : _react.createElement("div", null, linkType === _richtexttypes.INLINES.ENTRY_HYPERLINK && _react.createElement(_f36components.TextLink, {
|
|
212
264
|
testId: "entity-selection-link",
|
|
213
265
|
onClick: selectEntry
|
|
266
|
+
}, "Select entry"), linkType === _richtexttypes.INLINES.RESOURCE_HYPERLINK && _react.createElement(_f36components.TextLink, {
|
|
267
|
+
testId: "entity-selection-link",
|
|
268
|
+
onClick: selectResourceEntry
|
|
214
269
|
}, "Select entry"), linkType === _richtexttypes.INLINES.ASSET_HYPERLINK && _react.createElement(_f36components.TextLink, {
|
|
215
270
|
testId: "entity-selection-link",
|
|
216
271
|
onClick: selectAsset
|
|
@@ -11,12 +11,14 @@ describe('normalization', ()=>{
|
|
|
11
11
|
asset: "asset-id"
|
|
12
12
|
})), (0, _testutils.jsx)("hp", null, (0, _testutils.jsx)("htext", null, "entry"), (0, _testutils.jsx)("hlink", {
|
|
13
13
|
entry: "entry-id"
|
|
14
|
+
})), (0, _testutils.jsx)("hp", null, (0, _testutils.jsx)("htext", null, "resource"), (0, _testutils.jsx)("hlink", {
|
|
15
|
+
resource: "resource-urn"
|
|
14
16
|
})), (0, _testutils.jsx)("hp", null, (0, _testutils.jsx)("htext", null, "explicit empty link"), (0, _testutils.jsx)("hlink", {
|
|
15
17
|
uri: "https://link.com"
|
|
16
18
|
}, '')), (0, _testutils.jsx)("hp", null, (0, _testutils.jsx)("htext", null, "link with empty space"), (0, _testutils.jsx)("hlink", {
|
|
17
19
|
uri: "https://link.com"
|
|
18
20
|
}, " ")));
|
|
19
|
-
const expected = (0, _testutils.jsx)("editor", null, (0, _testutils.jsx)("hp", null, (0, _testutils.jsx)("htext", null, "link")), (0, _testutils.jsx)("hp", null, (0, _testutils.jsx)("htext", null, "asset")), (0, _testutils.jsx)("hp", null, (0, _testutils.jsx)("htext", null, "entry")), (0, _testutils.jsx)("hp", null, (0, _testutils.jsx)("htext", null, "explicit empty link")), (0, _testutils.jsx)("hp", null, (0, _testutils.jsx)("htext", null, "link with empty space")));
|
|
21
|
+
const expected = (0, _testutils.jsx)("editor", null, (0, _testutils.jsx)("hp", null, (0, _testutils.jsx)("htext", null, "link")), (0, _testutils.jsx)("hp", null, (0, _testutils.jsx)("htext", null, "asset")), (0, _testutils.jsx)("hp", null, (0, _testutils.jsx)("htext", null, "entry")), (0, _testutils.jsx)("hp", null, (0, _testutils.jsx)("htext", null, "resource")), (0, _testutils.jsx)("hp", null, (0, _testutils.jsx)("htext", null, "explicit empty link")), (0, _testutils.jsx)("hp", null, (0, _testutils.jsx)("htext", null, "link with empty space")));
|
|
20
22
|
(0, _testutils.assertOutput)({
|
|
21
23
|
input,
|
|
22
24
|
expected
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
Object.defineProperty(exports, "ResourceHyperlink", {
|
|
6
|
+
enumerable: true,
|
|
7
|
+
get: function() {
|
|
8
|
+
return ResourceHyperlink;
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
const _react = _interop_require_wildcard(require("react"));
|
|
12
|
+
const _f36components = require("@contentful/f36-components");
|
|
13
|
+
const _ContentfulEditorProvider = require("../../../ContentfulEditorProvider");
|
|
14
|
+
const _internal = require("../../../internal");
|
|
15
|
+
const _linkstracking = require("../../../plugins/links-tracking");
|
|
16
|
+
const _SdkProvider = require("../../../SdkProvider");
|
|
17
|
+
const _HyperlinkModal = require("../HyperlinkModal");
|
|
18
|
+
const _useResourceEntityInfo = require("../useResourceEntityInfo");
|
|
19
|
+
const _styles = require("./styles");
|
|
20
|
+
function _getRequireWildcardCache(nodeInterop) {
|
|
21
|
+
if (typeof WeakMap !== "function") return null;
|
|
22
|
+
var cacheBabelInterop = new WeakMap();
|
|
23
|
+
var cacheNodeInterop = new WeakMap();
|
|
24
|
+
return (_getRequireWildcardCache = function(nodeInterop) {
|
|
25
|
+
return nodeInterop ? cacheNodeInterop : cacheBabelInterop;
|
|
26
|
+
})(nodeInterop);
|
|
27
|
+
}
|
|
28
|
+
function _interop_require_wildcard(obj, nodeInterop) {
|
|
29
|
+
if (!nodeInterop && obj && obj.__esModule) {
|
|
30
|
+
return obj;
|
|
31
|
+
}
|
|
32
|
+
if (obj === null || typeof obj !== "object" && typeof obj !== "function") {
|
|
33
|
+
return {
|
|
34
|
+
default: obj
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
var cache = _getRequireWildcardCache(nodeInterop);
|
|
38
|
+
if (cache && cache.has(obj)) {
|
|
39
|
+
return cache.get(obj);
|
|
40
|
+
}
|
|
41
|
+
var newObj = {};
|
|
42
|
+
var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor;
|
|
43
|
+
for(var key in obj){
|
|
44
|
+
if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
45
|
+
var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null;
|
|
46
|
+
if (desc && (desc.get || desc.set)) {
|
|
47
|
+
Object.defineProperty(newObj, key, desc);
|
|
48
|
+
} else {
|
|
49
|
+
newObj[key] = obj[key];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
newObj.default = obj;
|
|
54
|
+
if (cache) {
|
|
55
|
+
cache.set(obj, newObj);
|
|
56
|
+
}
|
|
57
|
+
return newObj;
|
|
58
|
+
}
|
|
59
|
+
function ResourceHyperlink(props) {
|
|
60
|
+
const editor = (0, _ContentfulEditorProvider.useContentfulEditor)();
|
|
61
|
+
const sdk = (0, _SdkProvider.useSdkContext)();
|
|
62
|
+
const { target } = props.element.data;
|
|
63
|
+
const { onEntityFetchComplete } = (0, _linkstracking.useLinkTracking)();
|
|
64
|
+
const tooltipContent = (0, _useResourceEntityInfo.useResourceEntityInfo)({
|
|
65
|
+
target,
|
|
66
|
+
onEntityFetchComplete
|
|
67
|
+
});
|
|
68
|
+
if (!target) return null;
|
|
69
|
+
function handleClick(event) {
|
|
70
|
+
event.preventDefault();
|
|
71
|
+
event.stopPropagation();
|
|
72
|
+
if (!editor) return;
|
|
73
|
+
const p = (0, _internal.fromDOMPoint)(editor, [
|
|
74
|
+
event.target,
|
|
75
|
+
0
|
|
76
|
+
]);
|
|
77
|
+
if (p) {
|
|
78
|
+
(0, _HyperlinkModal.addOrEditLink)(editor, sdk, editor.tracking.onViewportAction, p.path);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return _react.createElement(_f36components.Tooltip, {
|
|
82
|
+
content: tooltipContent,
|
|
83
|
+
targetWrapperClassName: _styles.styles.hyperlinkWrapper,
|
|
84
|
+
placement: "bottom",
|
|
85
|
+
maxWidth: "auto"
|
|
86
|
+
}, _react.createElement(_f36components.TextLink, {
|
|
87
|
+
as: "a",
|
|
88
|
+
onClick: handleClick,
|
|
89
|
+
className: _styles.styles.hyperlink,
|
|
90
|
+
"data-resource-link-type": target.sys.linkType,
|
|
91
|
+
"data-resource-link-urn": target.sys.urn
|
|
92
|
+
}, props.children));
|
|
93
|
+
}
|
|
@@ -14,6 +14,7 @@ const _ishotkey = _interop_require_default(require("is-hotkey"));
|
|
|
14
14
|
const _editor = require("../../helpers/editor");
|
|
15
15
|
const _transformers = require("../../helpers/transformers");
|
|
16
16
|
const _EntityHyperlink = require("./components/EntityHyperlink");
|
|
17
|
+
const _ResourceHyperlink = require("./components/ResourceHyperlink");
|
|
17
18
|
const _UrlHyperlink = require("./components/UrlHyperlink");
|
|
18
19
|
const _HyperlinkModal = require("./HyperlinkModal");
|
|
19
20
|
const _utils = require("./utils");
|
|
@@ -64,6 +65,7 @@ function _interop_require_wildcard(obj, nodeInterop) {
|
|
|
64
65
|
const isAnchor = (element)=>element.nodeName === 'A' && !!element.getAttribute('href') && element.getAttribute('href') !== '#';
|
|
65
66
|
const isEntryAnchor = (element)=>element.nodeName === 'A' && element.getAttribute('data-link-type') === 'Entry';
|
|
66
67
|
const isAssetAnchor = (element)=>element.nodeName === 'A' && element.getAttribute('data-link-type') === 'Asset';
|
|
68
|
+
const isResourceAnchor = (element)=>element.nodeName === 'A' && element.getAttribute('data-resource-link-type') === 'Contentful:Entry';
|
|
67
69
|
const buildHyperlinkEventHandler = (sdk)=>(editor, { options: { hotkey } })=>{
|
|
68
70
|
return (event)=>{
|
|
69
71
|
if (!editor.selection) {
|
|
@@ -85,6 +87,14 @@ const getNodeOfType = (type)=>(el, node)=>({
|
|
|
85
87
|
children: node.children,
|
|
86
88
|
data: type === _richtexttypes.INLINES.HYPERLINK ? {
|
|
87
89
|
uri: el.getAttribute('href')
|
|
90
|
+
} : type === _richtexttypes.INLINES.RESOURCE_HYPERLINK ? {
|
|
91
|
+
target: {
|
|
92
|
+
sys: {
|
|
93
|
+
urn: el.getAttribute('data-resource-link-urn'),
|
|
94
|
+
linkType: el.getAttribute('data-resource-link-type'),
|
|
95
|
+
type: 'ResourceLink'
|
|
96
|
+
}
|
|
97
|
+
}
|
|
88
98
|
} : {
|
|
89
99
|
target: {
|
|
90
100
|
sys: {
|
|
@@ -143,6 +153,23 @@ const createHyperlinkPlugin = (sdk)=>{
|
|
|
143
153
|
getNode: getNodeOfType(_richtexttypes.INLINES.ENTRY_HYPERLINK)
|
|
144
154
|
}
|
|
145
155
|
},
|
|
156
|
+
{
|
|
157
|
+
...common,
|
|
158
|
+
key: _richtexttypes.INLINES.RESOURCE_HYPERLINK,
|
|
159
|
+
type: _richtexttypes.INLINES.RESOURCE_HYPERLINK,
|
|
160
|
+
component: _ResourceHyperlink.ResourceHyperlink,
|
|
161
|
+
deserializeHtml: {
|
|
162
|
+
rules: [
|
|
163
|
+
{
|
|
164
|
+
validNodeName: [
|
|
165
|
+
'A'
|
|
166
|
+
]
|
|
167
|
+
}
|
|
168
|
+
],
|
|
169
|
+
query: (el)=>isResourceAnchor(el),
|
|
170
|
+
getNode: getNodeOfType(_richtexttypes.INLINES.RESOURCE_HYPERLINK)
|
|
171
|
+
}
|
|
172
|
+
},
|
|
146
173
|
{
|
|
147
174
|
...common,
|
|
148
175
|
key: _richtexttypes.INLINES.ASSET_HYPERLINK,
|
|
@@ -167,7 +194,8 @@ const createHyperlinkPlugin = (sdk)=>{
|
|
|
167
194
|
type: [
|
|
168
195
|
_richtexttypes.INLINES.HYPERLINK,
|
|
169
196
|
_richtexttypes.INLINES.ASSET_HYPERLINK,
|
|
170
|
-
_richtexttypes.INLINES.ENTRY_HYPERLINK
|
|
197
|
+
_richtexttypes.INLINES.ENTRY_HYPERLINK,
|
|
198
|
+
_richtexttypes.INLINES.RESOURCE_HYPERLINK
|
|
171
199
|
]
|
|
172
200
|
},
|
|
173
201
|
validNode: _utils.hasText,
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
Object.defineProperty(exports, "useResourceEntityInfo", {
|
|
6
|
+
enumerable: true,
|
|
7
|
+
get: function() {
|
|
8
|
+
return useResourceEntityInfo;
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
const _react = _interop_require_wildcard(require("react"));
|
|
12
|
+
const _fieldeditorreference = require("@contentful/field-editor-reference");
|
|
13
|
+
const _utils = require("./utils");
|
|
14
|
+
function _getRequireWildcardCache(nodeInterop) {
|
|
15
|
+
if (typeof WeakMap !== "function") return null;
|
|
16
|
+
var cacheBabelInterop = new WeakMap();
|
|
17
|
+
var cacheNodeInterop = new WeakMap();
|
|
18
|
+
return (_getRequireWildcardCache = function(nodeInterop) {
|
|
19
|
+
return nodeInterop ? cacheNodeInterop : cacheBabelInterop;
|
|
20
|
+
})(nodeInterop);
|
|
21
|
+
}
|
|
22
|
+
function _interop_require_wildcard(obj, nodeInterop) {
|
|
23
|
+
if (!nodeInterop && obj && obj.__esModule) {
|
|
24
|
+
return obj;
|
|
25
|
+
}
|
|
26
|
+
if (obj === null || typeof obj !== "object" && typeof obj !== "function") {
|
|
27
|
+
return {
|
|
28
|
+
default: obj
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
var cache = _getRequireWildcardCache(nodeInterop);
|
|
32
|
+
if (cache && cache.has(obj)) {
|
|
33
|
+
return cache.get(obj);
|
|
34
|
+
}
|
|
35
|
+
var newObj = {};
|
|
36
|
+
var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor;
|
|
37
|
+
for(var key in obj){
|
|
38
|
+
if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
39
|
+
var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null;
|
|
40
|
+
if (desc && (desc.get || desc.set)) {
|
|
41
|
+
Object.defineProperty(newObj, key, desc);
|
|
42
|
+
} else {
|
|
43
|
+
newObj[key] = obj[key];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
newObj.default = obj;
|
|
48
|
+
if (cache) {
|
|
49
|
+
cache.set(obj, newObj);
|
|
50
|
+
}
|
|
51
|
+
return newObj;
|
|
52
|
+
}
|
|
53
|
+
function useResourceEntityInfo({ onEntityFetchComplete , target }) {
|
|
54
|
+
const { data , error , status } = (0, _fieldeditorreference.useResource)(target.sys.linkType, target.sys.urn);
|
|
55
|
+
_react.useEffect(()=>{
|
|
56
|
+
if (status === 'success') {
|
|
57
|
+
onEntityFetchComplete?.();
|
|
58
|
+
}
|
|
59
|
+
}, [
|
|
60
|
+
status,
|
|
61
|
+
onEntityFetchComplete
|
|
62
|
+
]);
|
|
63
|
+
if (status === 'loading') {
|
|
64
|
+
return `Loading entry...`;
|
|
65
|
+
}
|
|
66
|
+
if (!data || error) {
|
|
67
|
+
return `Entry missing or inaccessible`;
|
|
68
|
+
}
|
|
69
|
+
const title = (0, _utils.truncateTitle)(data.resource.fields[data.contentType.displayField]?.[data.defaultLocaleCode], 40) || 'Untitled';
|
|
70
|
+
return `${data.contentType.name}: ${title} (Space: ${data.space.name} – Env.: ${data.resource.sys.environment.sys.id})`;
|
|
71
|
+
}
|
|
@@ -12,6 +12,9 @@ _export(exports, {
|
|
|
12
12
|
hasText: function() {
|
|
13
13
|
return hasText;
|
|
14
14
|
},
|
|
15
|
+
truncateTitle: function() {
|
|
16
|
+
return truncateTitle;
|
|
17
|
+
},
|
|
15
18
|
getEntityInfo: function() {
|
|
16
19
|
return getEntityInfo;
|
|
17
20
|
}
|
|
@@ -23,7 +26,7 @@ const hasText = (editor, entry)=>{
|
|
|
23
26
|
const [node, path] = entry;
|
|
24
27
|
return !(0, _platecommon.isAncestorEmpty)(editor, node) && (0, _queries.getText)(editor, path).trim() !== '';
|
|
25
28
|
};
|
|
26
|
-
function
|
|
29
|
+
function truncateTitle(str, length) {
|
|
27
30
|
if (typeof str === 'string' && str.length > length) {
|
|
28
31
|
return str && str.substr(0, length + 1).replace(/(\s+\S(?=\S)|\s*)\.?.$/, '…');
|
|
29
32
|
}
|
|
@@ -34,7 +37,7 @@ function getEntityInfo(data) {
|
|
|
34
37
|
return '';
|
|
35
38
|
}
|
|
36
39
|
const { entityTitle , contentTypeName , entityStatus , jobs } = data;
|
|
37
|
-
const title =
|
|
40
|
+
const title = truncateTitle(entityTitle, 60) || 'Untitled';
|
|
38
41
|
const scheduledActions = jobs.length > 0 ? (0, _fieldeditorreference.getScheduleTooltipContent)({
|
|
39
42
|
job: jobs[0],
|
|
40
43
|
jobsCount: jobs.length
|
|
@@ -12,6 +12,8 @@ describe('normalization', ()=>{
|
|
|
12
12
|
uri: "https://contentful.com"
|
|
13
13
|
}), (0, _testutils.jsx)("hlink", {
|
|
14
14
|
entry: "entry-id"
|
|
15
|
+
}), (0, _testutils.jsx)("hlink", {
|
|
16
|
+
resource: "resource-urn"
|
|
15
17
|
}), (0, _testutils.jsx)("hlink", {
|
|
16
18
|
asset: "asset-id"
|
|
17
19
|
}), "some text after"));
|
|
@@ -12,6 +12,8 @@ describe('normalization', ()=>{
|
|
|
12
12
|
uri: "https://contentful.com"
|
|
13
13
|
}), (0, _testutils.jsx)("hlink", {
|
|
14
14
|
entry: "entry-id"
|
|
15
|
+
}), (0, _testutils.jsx)("hlink", {
|
|
16
|
+
resource: "resource-urn"
|
|
15
17
|
}), (0, _testutils.jsx)("hlink", {
|
|
16
18
|
asset: "asset-id"
|
|
17
19
|
}), "some text after")));
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", {
|
|
3
3
|
value: true
|
|
4
4
|
});
|
|
5
|
-
Object.defineProperty(exports, "
|
|
5
|
+
Object.defineProperty(exports, "FetchingWrappedResourceCard", {
|
|
6
6
|
enumerable: true,
|
|
7
7
|
get: function() {
|
|
8
|
-
return
|
|
8
|
+
return FetchingWrappedResourceCard;
|
|
9
9
|
}
|
|
10
10
|
});
|
|
11
11
|
const _react = _interop_require_wildcard(require("react"));
|
|
@@ -80,7 +80,7 @@ const InternalEntryCard = _react.memo((props)=>{
|
|
|
80
80
|
});
|
|
81
81
|
}, _fastdeepequal.default);
|
|
82
82
|
InternalEntryCard.displayName = 'ReferenceCard';
|
|
83
|
-
const
|
|
83
|
+
const FetchingWrappedResourceCard = (props)=>{
|
|
84
84
|
const { link , onEntityFetchComplete } = props;
|
|
85
85
|
const { data , status , error } = (0, _fieldeditorreference.useResource)(link.linkType, link.urn);
|
|
86
86
|
_react.useEffect(()=>{
|
|
@@ -10,7 +10,7 @@ const _react1 = require("@testing-library/react");
|
|
|
10
10
|
const _published_content_typejson = _interop_require_default(require("../__fixtures__/published_content_type.json"));
|
|
11
11
|
const _published_entryjson = _interop_require_default(require("../__fixtures__/published_entry.json"));
|
|
12
12
|
const _spacejson = _interop_require_default(require("../__fixtures__/space.json"));
|
|
13
|
-
const
|
|
13
|
+
const _FetchingWrappedResourceCard = require("../FetchingWrappedResourceCard");
|
|
14
14
|
function _interop_require_default(obj) {
|
|
15
15
|
return obj && obj.__esModule ? obj : {
|
|
16
16
|
default: obj
|
|
@@ -111,7 +111,7 @@ beforeEach(()=>{
|
|
|
111
111
|
function renderResourceCard({ linkType ='Contentful:Entry' , entryUrn =resolvableEntryUrn } = {}) {
|
|
112
112
|
return (0, _react1.render)(_react.createElement(_fieldeditorreference.EntityProvider, {
|
|
113
113
|
sdk: sdk
|
|
114
|
-
}, _react.createElement(
|
|
114
|
+
}, _react.createElement(_FetchingWrappedResourceCard.FetchingWrappedResourceCard, {
|
|
115
115
|
isDisabled: false,
|
|
116
116
|
isSelected: false,
|
|
117
117
|
sdk: sdk,
|
|
@@ -29,6 +29,13 @@ const createSysLink = (linkType, id)=>({
|
|
|
29
29
|
linkType
|
|
30
30
|
}
|
|
31
31
|
});
|
|
32
|
+
const createSysResourceLink = (urn)=>({
|
|
33
|
+
sys: {
|
|
34
|
+
urn,
|
|
35
|
+
type: 'ResourceLink',
|
|
36
|
+
linkType: 'Contentful:Entry'
|
|
37
|
+
}
|
|
38
|
+
});
|
|
32
39
|
const createHyperlink = (_, attrs, children)=>{
|
|
33
40
|
const data = {};
|
|
34
41
|
let type = _richtexttypes.INLINES.HYPERLINK;
|
|
@@ -44,6 +51,10 @@ const createHyperlink = (_, attrs, children)=>{
|
|
|
44
51
|
type = _richtexttypes.INLINES.ENTRY_HYPERLINK;
|
|
45
52
|
data.target = createSysLink('Entry', attrs.entry);
|
|
46
53
|
}
|
|
54
|
+
if (attrs.resource) {
|
|
55
|
+
type = _richtexttypes.INLINES.RESOURCE_HYPERLINK;
|
|
56
|
+
data.target = createSysResourceLink(attrs.resource);
|
|
57
|
+
}
|
|
47
58
|
children = children.map((child)=>typeof child === 'string' ? {
|
|
48
59
|
text: child
|
|
49
60
|
} : child);
|
|
@@ -151,7 +151,8 @@ function getValidationInfo(field) {
|
|
|
151
151
|
const isAnyHyperlinkEnabled = someWithValidation([
|
|
152
152
|
INLINES.HYPERLINK,
|
|
153
153
|
INLINES.ASSET_HYPERLINK,
|
|
154
|
-
INLINES.ENTRY_HYPERLINK
|
|
154
|
+
INLINES.ENTRY_HYPERLINK,
|
|
155
|
+
INLINES.RESOURCE_HYPERLINK
|
|
155
156
|
], isNodeTypeEnabled);
|
|
156
157
|
const isAnyBlockFormattingEnabled = someWithValidation([
|
|
157
158
|
BLOCKS.UL_LIST,
|
|
@@ -4,8 +4,8 @@ import { useContentfulEditor } from '../../ContentfulEditorProvider';
|
|
|
4
4
|
import { findNodePath, removeNodes } from '../../internal';
|
|
5
5
|
import { useSdkContext } from '../../SdkProvider';
|
|
6
6
|
import { useLinkTracking } from '../links-tracking';
|
|
7
|
+
import { FetchingWrappedResourceCard } from '../shared/FetchingWrappedResourceCard';
|
|
7
8
|
import { LinkedBlockWrapper } from '../shared/LinkedBlockWrapper';
|
|
8
|
-
import { FetchingWrappedResourceBlockCard } from './FetchingWrappedResourceBlockCard';
|
|
9
9
|
export function LinkedResourceBlock(props) {
|
|
10
10
|
const { attributes , children , element } = props;
|
|
11
11
|
const { onEntityFetchComplete } = useLinkTracking();
|
|
@@ -27,7 +27,7 @@ export function LinkedResourceBlock(props) {
|
|
|
27
27
|
return React.createElement(LinkedBlockWrapper, {
|
|
28
28
|
attributes: attributes,
|
|
29
29
|
link: element.data.target,
|
|
30
|
-
card: React.createElement(
|
|
30
|
+
card: React.createElement(FetchingWrappedResourceCard, {
|
|
31
31
|
sdk: sdk,
|
|
32
32
|
link: link,
|
|
33
33
|
isDisabled: isDisabled,
|
|
@@ -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
|
|
@@ -26,7 +26,7 @@ const InternalEntryCard = React.memo((props)=>{
|
|
|
26
26
|
});
|
|
27
27
|
}, areEqual);
|
|
28
28
|
InternalEntryCard.displayName = 'ReferenceCard';
|
|
29
|
-
export const
|
|
29
|
+
export const FetchingWrappedResourceCard = (props)=>{
|
|
30
30
|
const { link , onEntityFetchComplete } = props;
|
|
31
31
|
const { data , status , error } = useResource(link.linkType, link.urn);
|
|
32
32
|
React.useEffect(()=>{
|
|
@@ -6,7 +6,7 @@ import { configure, render, waitFor } from '@testing-library/react';
|
|
|
6
6
|
import publishedCT from '../__fixtures__/published_content_type.json';
|
|
7
7
|
import publishedEntry from '../__fixtures__/published_entry.json';
|
|
8
8
|
import space from '../__fixtures__/space.json';
|
|
9
|
-
import {
|
|
9
|
+
import { FetchingWrappedResourceCard } from '../FetchingWrappedResourceCard';
|
|
10
10
|
configure({
|
|
11
11
|
testIdAttribute: 'data-test-id'
|
|
12
12
|
});
|
|
@@ -63,7 +63,7 @@ beforeEach(()=>{
|
|
|
63
63
|
function renderResourceCard({ linkType ='Contentful:Entry' , entryUrn =resolvableEntryUrn } = {}) {
|
|
64
64
|
return render(React.createElement(EntityProvider, {
|
|
65
65
|
sdk: sdk
|
|
66
|
-
}, React.createElement(
|
|
66
|
+
}, React.createElement(FetchingWrappedResourceCard, {
|
|
67
67
|
isDisabled: false,
|
|
68
68
|
isSelected: false,
|
|
69
69
|
sdk: sdk,
|
|
@@ -19,6 +19,13 @@ const createSysLink = (linkType, id)=>({
|
|
|
19
19
|
linkType
|
|
20
20
|
}
|
|
21
21
|
});
|
|
22
|
+
const createSysResourceLink = (urn)=>({
|
|
23
|
+
sys: {
|
|
24
|
+
urn,
|
|
25
|
+
type: 'ResourceLink',
|
|
26
|
+
linkType: 'Contentful:Entry'
|
|
27
|
+
}
|
|
28
|
+
});
|
|
22
29
|
const createHyperlink = (_, attrs, children)=>{
|
|
23
30
|
const data = {};
|
|
24
31
|
let type = INLINES.HYPERLINK;
|
|
@@ -34,6 +41,10 @@ const createHyperlink = (_, attrs, children)=>{
|
|
|
34
41
|
type = INLINES.ENTRY_HYPERLINK;
|
|
35
42
|
data.target = createSysLink('Entry', attrs.entry);
|
|
36
43
|
}
|
|
44
|
+
if (attrs.resource) {
|
|
45
|
+
type = INLINES.RESOURCE_HYPERLINK;
|
|
46
|
+
data.target = createSysResourceLink(attrs.resource);
|
|
47
|
+
}
|
|
37
48
|
children = children.map((child)=>typeof child === 'string' ? {
|
|
38
49
|
text: child
|
|
39
50
|
} : child);
|
|
@@ -17,7 +17,7 @@ export declare function isList(editor?: PlateEditor): boolean;
|
|
|
17
17
|
export declare function getTableSize(table: Element): Record<'numRows' | 'numColumns', number> | null;
|
|
18
18
|
interface InsertLinkOptions {
|
|
19
19
|
text: string;
|
|
20
|
-
type: INLINES.HYPERLINK | INLINES.ENTRY_HYPERLINK | INLINES.ASSET_HYPERLINK;
|
|
20
|
+
type: INLINES.HYPERLINK | INLINES.ENTRY_HYPERLINK | INLINES.RESOURCE_HYPERLINK | INLINES.ASSET_HYPERLINK;
|
|
21
21
|
url?: string;
|
|
22
22
|
target?: Link;
|
|
23
23
|
path?: Path;
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { Link } from '@contentful/field-editor-reference';
|
|
3
3
|
import { FieldAppSDK } from '@contentful/field-editor-shared';
|
|
4
|
+
import { ResourceLink } from '@contentful/rich-text-types';
|
|
4
5
|
import { PlateEditor, Path } from '../../internal/types';
|
|
5
6
|
import { TrackingPluginActions } from '../../plugins/Tracking';
|
|
6
7
|
interface HyperlinkModalProps {
|
|
7
8
|
linkText?: string;
|
|
8
9
|
linkType?: string;
|
|
9
10
|
linkTarget?: string;
|
|
10
|
-
linkEntity?: Link;
|
|
11
|
+
linkEntity?: Link | ResourceLink;
|
|
11
12
|
onClose: (value: unknown) => void;
|
|
12
13
|
sdk: FieldAppSDK;
|
|
13
14
|
readonly: boolean;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Link } from '@contentful/app-sdk';
|
|
3
|
+
import { Element, RenderElementProps } from '../../../internal/types';
|
|
4
|
+
export type ResourceHyperlinkProps = {
|
|
5
|
+
element: Element & {
|
|
6
|
+
data: {
|
|
7
|
+
target: {
|
|
8
|
+
sys: {
|
|
9
|
+
urn: string;
|
|
10
|
+
linkType: 'Contentful:Entry';
|
|
11
|
+
type: 'ResourceLink';
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
target: Link;
|
|
17
|
+
attributes: Pick<RenderElementProps, 'attributes'>;
|
|
18
|
+
children: Pick<RenderElementProps, 'children'>;
|
|
19
|
+
};
|
|
20
|
+
export declare function ResourceHyperlink(props: ResourceHyperlinkProps): React.JSX.Element | null;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { ResourceLink } from '@contentful/rich-text-types';
|
|
2
|
+
type ResourceEntityInfoProps = {
|
|
3
|
+
target: ResourceLink;
|
|
4
|
+
onEntityFetchComplete?: VoidFunction;
|
|
5
|
+
};
|
|
6
|
+
export declare function useResourceEntityInfo({ onEntityFetchComplete, target }: ResourceEntityInfoProps): string;
|
|
7
|
+
export {};
|
|
@@ -2,4 +2,5 @@ import { NodeEntry } from '../../internal/types';
|
|
|
2
2
|
import { PlateEditor } from '../../internal/types';
|
|
3
3
|
import { FetchedEntityData } from './useEntityInfo';
|
|
4
4
|
export declare const hasText: (editor: PlateEditor, entry: NodeEntry) => boolean;
|
|
5
|
+
export declare function truncateTitle(str: string, length: number): string;
|
|
5
6
|
export declare function getEntityInfo(data?: FetchedEntityData): string;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { FieldAppSDK } from '@contentful/app-sdk';
|
|
3
3
|
import { ResourceLink } from '@contentful/rich-text-types';
|
|
4
|
-
interface
|
|
4
|
+
interface FetchingWrappedResourceCardProps {
|
|
5
5
|
link: ResourceLink['sys'];
|
|
6
6
|
isDisabled: boolean;
|
|
7
7
|
isSelected: boolean;
|
|
@@ -10,5 +10,5 @@ interface FetchingWrappedResourceBlockCardProps {
|
|
|
10
10
|
onEdit?: VoidFunction;
|
|
11
11
|
onRemove?: VoidFunction;
|
|
12
12
|
}
|
|
13
|
-
export declare const
|
|
13
|
+
export declare const FetchingWrappedResourceCard: (props: FetchingWrappedResourceCardProps) => React.JSX.Element;
|
|
14
14
|
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contentful/field-editor-rich-text",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.14.0",
|
|
4
4
|
"source": "./src/index.tsx",
|
|
5
5
|
"main": "dist/cjs/index.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
@@ -81,5 +81,5 @@
|
|
|
81
81
|
"prism-react-renderer": "2.0.5",
|
|
82
82
|
"react": ">=16.14.0"
|
|
83
83
|
},
|
|
84
|
-
"gitHead": "
|
|
84
|
+
"gitHead": "5df6fd753ac999a99c4aaaac9fac54336f0c4974"
|
|
85
85
|
}
|