@contentful/field-editor-rich-text 3.13.0 → 3.14.1

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.
Files changed (40) hide show
  1. package/dist/cjs/Toolbar/index.js +2 -1
  2. package/dist/cjs/constants/Schema.js +11 -0
  3. package/dist/cjs/helpers/editor.js +1 -0
  4. package/dist/cjs/plugins/EmbeddedResourceBlock/LinkedResourceBlock.js +2 -2
  5. package/dist/cjs/plugins/Heading/__tests__/createHeadingPlugin.test.js +2 -0
  6. package/dist/cjs/plugins/Hyperlink/HyperlinkModal.js +63 -8
  7. package/dist/cjs/plugins/Hyperlink/__tests__/createHyperlinkPlugin.test.js +3 -1
  8. package/dist/cjs/plugins/Hyperlink/components/ResourceHyperlink.js +93 -0
  9. package/dist/cjs/plugins/Hyperlink/createHyperlinkPlugin.js +29 -1
  10. package/dist/cjs/plugins/Hyperlink/useResourceEntityInfo.js +71 -0
  11. package/dist/cjs/plugins/Hyperlink/utils.js +5 -2
  12. package/dist/cjs/plugins/Paragraph/__tests__/createParagraphPlugin.test.js +2 -0
  13. package/dist/cjs/plugins/Quote/__test__/createQuotePlugin.test.js +2 -0
  14. package/dist/cjs/plugins/{EmbeddedResourceBlock/FetchingWrappedResourceBlockCard.js → shared/FetchingWrappedResourceCard.js} +3 -3
  15. package/dist/cjs/plugins/shared/__tests__/FetchingWrappedResourceCard.test.js +2 -2
  16. package/dist/cjs/test-utils/jsx.js +11 -0
  17. package/dist/esm/Toolbar/index.js +2 -1
  18. package/dist/esm/constants/Schema.js +11 -0
  19. package/dist/esm/helpers/editor.js +1 -0
  20. package/dist/esm/plugins/EmbeddedResourceBlock/LinkedResourceBlock.js +2 -2
  21. package/dist/esm/plugins/Heading/__tests__/createHeadingPlugin.test.js +2 -0
  22. package/dist/esm/plugins/Hyperlink/HyperlinkModal.js +63 -8
  23. package/dist/esm/plugins/Hyperlink/__tests__/createHyperlinkPlugin.test.js +3 -1
  24. package/dist/esm/plugins/Hyperlink/components/ResourceHyperlink.js +44 -0
  25. package/dist/esm/plugins/Hyperlink/createHyperlinkPlugin.js +29 -1
  26. package/dist/esm/plugins/Hyperlink/useResourceEntityInfo.js +22 -0
  27. package/dist/esm/plugins/Hyperlink/utils.js +2 -2
  28. package/dist/esm/plugins/Paragraph/__tests__/createParagraphPlugin.test.js +2 -0
  29. package/dist/esm/plugins/Quote/__test__/createQuotePlugin.test.js +2 -0
  30. package/dist/esm/plugins/{EmbeddedResourceBlock/FetchingWrappedResourceBlockCard.js → shared/FetchingWrappedResourceCard.js} +1 -1
  31. package/dist/esm/plugins/shared/__tests__/FetchingWrappedResourceCard.test.js +2 -2
  32. package/dist/esm/test-utils/jsx.js +11 -0
  33. package/dist/types/constants/Schema.d.ts +7 -0
  34. package/dist/types/helpers/editor.d.ts +1 -1
  35. package/dist/types/plugins/Hyperlink/HyperlinkModal.d.ts +2 -1
  36. package/dist/types/plugins/Hyperlink/components/ResourceHyperlink.d.ts +20 -0
  37. package/dist/types/plugins/Hyperlink/useResourceEntityInfo.d.ts +7 -0
  38. package/dist/types/plugins/Hyperlink/utils.d.ts +1 -0
  39. package/dist/types/plugins/{EmbeddedResourceBlock/FetchingWrappedResourceBlockCard.d.ts → shared/FetchingWrappedResourceCard.d.ts} +2 -2
  40. package/package.json +3 -3
@@ -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(_FetchingWrappedResourceBlockCard.FetchingWrappedResourceBlockCard, {
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
- return !!(linkText && linkEntity);
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
- setLinkTarget('');
145
- setLinkEntity(entityToLink(entry));
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
- setLinkTarget('');
153
- setLinkEntity(entityToLink(asset));
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.ASSET_HYPERLINK && _react.createElement(_FetchingWrappedAssetCard.FetchingWrappedAssetCard, {
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 truncate(str, length) {
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 = truncate(entityTitle, 60) || 'Untitled';
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, "FetchingWrappedResourceBlockCard", {
5
+ Object.defineProperty(exports, "FetchingWrappedResourceCard", {
6
6
  enumerable: true,
7
7
  get: function() {
8
- return FetchingWrappedResourceBlockCard;
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 FetchingWrappedResourceBlockCard = (props)=>{
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 _FetchingWrappedResourceBlockCard = require("../../EmbeddedResourceBlock/FetchingWrappedResourceBlockCard");
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(_FetchingWrappedResourceBlockCard.FetchingWrappedResourceBlockCard, {
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,
@@ -144,6 +144,17 @@ export default {
144
144
  }
145
145
  ]
146
146
  },
147
+ [INLINES.RESOURCE_HYPERLINK]: {
148
+ nodes: [
149
+ {
150
+ match: [
151
+ {
152
+ object: 'text'
153
+ }
154
+ ]
155
+ }
156
+ ]
157
+ },
147
158
  [INLINES.ASSET_HYPERLINK]: {
148
159
  nodes: [
149
160
  {
@@ -6,6 +6,7 @@ import { IS_SAFARI } from './environment';
6
6
  export const LINK_TYPES = [
7
7
  INLINES.HYPERLINK,
8
8
  INLINES.ENTRY_HYPERLINK,
9
+ INLINES.RESOURCE_HYPERLINK,
9
10
  INLINES.ASSET_HYPERLINK
10
11
  ];
11
12
  const LIST_TYPES = [
@@ -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(FetchingWrappedResourceBlockCard, {
30
+ card: React.createElement(FetchingWrappedResourceCard, {
31
31
  sdk: sdk,
32
32
  link: link,
33
33
  isDisabled: isDisabled,
@@ -8,6 +8,8 @@ describe('normalization', ()=>{
8
8
  uri: "https://contentful.com"
9
9
  }), jsx("hlink", {
10
10
  entry: "entry-id"
11
+ }), jsx("hlink", {
12
+ resource: "resource-urn"
11
13
  }), jsx("hlink", {
12
14
  asset: "asset-id"
13
15
  }), "some text after"), jsx("hp", null, jsx("htext", null)));
@@ -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
- return !!(linkText && linkEntity);
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
- setLinkTarget('');
83
- setLinkEntity(entityToLink(entry));
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
- setLinkTarget('');
91
- setLinkEntity(entityToLink(asset));
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.ASSET_HYPERLINK && React.createElement(FetchingWrappedAssetCard, {
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 truncate(str, length) {
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 = truncate(entityTitle, 60) || 'Untitled';
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
@@ -8,6 +8,8 @@ describe('normalization', ()=>{
8
8
  uri: "https://contentful.com"
9
9
  }), jsx("hlink", {
10
10
  entry: "entry-id"
11
+ }), jsx("hlink", {
12
+ resource: "resource-urn"
11
13
  }), jsx("hlink", {
12
14
  asset: "asset-id"
13
15
  }), "some text after"));
@@ -8,6 +8,8 @@ describe('normalization', ()=>{
8
8
  uri: "https://contentful.com"
9
9
  }), jsx("hlink", {
10
10
  entry: "entry-id"
11
+ }), jsx("hlink", {
12
+ resource: "resource-urn"
11
13
  }), jsx("hlink", {
12
14
  asset: "asset-id"
13
15
  }), "some text after")));
@@ -26,7 +26,7 @@ const InternalEntryCard = React.memo((props)=>{
26
26
  });
27
27
  }, areEqual);
28
28
  InternalEntryCard.displayName = 'ReferenceCard';
29
- export const FetchingWrappedResourceBlockCard = (props)=>{
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 { FetchingWrappedResourceBlockCard } from '../../EmbeddedResourceBlock/FetchingWrappedResourceBlockCard';
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(FetchingWrappedResourceBlockCard, {
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);
@@ -96,6 +96,13 @@ declare const _default: {
96
96
  }[];
97
97
  }[];
98
98
  };
99
+ "resource-hyperlink": {
100
+ nodes: {
101
+ match: {
102
+ object: string;
103
+ }[];
104
+ }[];
105
+ };
99
106
  "asset-hyperlink": {
100
107
  nodes: {
101
108
  match: {
@@ -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 FetchingWrappedResourceBlockCardProps {
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 FetchingWrappedResourceBlockCard: (props: FetchingWrappedResourceBlockCardProps) => React.JSX.Element;
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.13.0",
3
+ "version": "3.14.1",
4
4
  "source": "./src/index.tsx",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -44,7 +44,7 @@
44
44
  "@contentful/f36-icons": "^4.1.1",
45
45
  "@contentful/f36-tokens": "^4.0.0",
46
46
  "@contentful/f36-utils": "^4.19.0",
47
- "@contentful/field-editor-reference": "^5.17.0",
47
+ "@contentful/field-editor-reference": "^5.18.0",
48
48
  "@contentful/field-editor-shared": "^1.4.2",
49
49
  "@contentful/rich-text-plain-text-renderer": "^16.0.4",
50
50
  "@contentful/rich-text-types": "16.3.0",
@@ -81,5 +81,5 @@
81
81
  "prism-react-renderer": "2.0.5",
82
82
  "react": ">=16.14.0"
83
83
  },
84
- "gitHead": "8cbc98b0d9bd617de2199269659c6ce16807fbeb"
84
+ "gitHead": "ed5a4c379ae53144fc8b9f68d86d48ff4009a784"
85
85
  }