@contentful/field-editor-rich-text 3.12.7 → 3.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/dist/cjs/Toolbar/components/EmbedEntityWidget.js +6 -0
  2. package/dist/cjs/Toolbar/index.js +2 -1
  3. package/dist/cjs/constants/Schema.js +14 -0
  4. package/dist/cjs/helpers/{newEntitySelectorConfigFromRichTextField.js → config.js} +19 -5
  5. package/dist/cjs/helpers/editor.js +1 -0
  6. package/dist/cjs/plugins/DragAndDrop/index.js +4 -2
  7. package/dist/cjs/plugins/EmbeddedEntityInline/index.js +21 -4
  8. package/dist/cjs/plugins/EmbeddedResourceInline/FetchingWrappedResourceInlineCard.js +102 -0
  9. package/dist/cjs/plugins/EmbeddedResourceInline/LinkedResourceInline.js +51 -0
  10. package/dist/cjs/plugins/EmbeddedResourceInline/index.js +56 -0
  11. package/dist/cjs/plugins/Heading/__tests__/createHeadingPlugin.test.js +2 -0
  12. package/dist/cjs/plugins/Hyperlink/HyperlinkModal.js +63 -8
  13. package/dist/cjs/plugins/Hyperlink/__tests__/createHyperlinkPlugin.test.js +3 -1
  14. package/dist/cjs/plugins/Hyperlink/components/ResourceHyperlink.js +93 -0
  15. package/dist/cjs/plugins/Hyperlink/createHyperlinkPlugin.js +29 -1
  16. package/dist/cjs/plugins/Hyperlink/useResourceEntityInfo.js +71 -0
  17. package/dist/cjs/plugins/Hyperlink/utils.js +5 -2
  18. package/dist/cjs/plugins/Paragraph/__tests__/createParagraphPlugin.test.js +2 -0
  19. package/dist/cjs/plugins/Quote/__test__/createQuotePlugin.test.js +2 -0
  20. package/dist/cjs/plugins/index.js +2 -0
  21. package/dist/cjs/plugins/shared/EmbeddedBlockToolbarIcon.js +2 -4
  22. package/dist/cjs/plugins/shared/EmbeddedBlockUtil.js +3 -4
  23. package/dist/cjs/plugins/shared/EmbeddedInlineToolbarIcon.js +11 -6
  24. package/dist/cjs/plugins/shared/EmbeddedInlineUtil.js +66 -27
  25. package/dist/cjs/plugins/shared/ResourceNewBadge.js +57 -0
  26. package/dist/cjs/test-utils/jsx.js +11 -0
  27. package/dist/esm/Toolbar/components/EmbedEntityWidget.js +6 -0
  28. package/dist/esm/Toolbar/index.js +2 -1
  29. package/dist/esm/constants/Schema.js +14 -0
  30. package/dist/esm/helpers/{newEntitySelectorConfigFromRichTextField.js → config.js} +8 -2
  31. package/dist/esm/helpers/editor.js +1 -0
  32. package/dist/esm/plugins/DragAndDrop/index.js +4 -2
  33. package/dist/esm/plugins/EmbeddedEntityInline/index.js +22 -5
  34. package/dist/esm/plugins/EmbeddedResourceInline/FetchingWrappedResourceInlineCard.js +53 -0
  35. package/dist/esm/plugins/EmbeddedResourceInline/LinkedResourceInline.js +36 -0
  36. package/dist/esm/plugins/EmbeddedResourceInline/index.js +46 -0
  37. package/dist/esm/plugins/Heading/__tests__/createHeadingPlugin.test.js +2 -0
  38. package/dist/esm/plugins/Hyperlink/HyperlinkModal.js +63 -8
  39. package/dist/esm/plugins/Hyperlink/__tests__/createHyperlinkPlugin.test.js +3 -1
  40. package/dist/esm/plugins/Hyperlink/components/ResourceHyperlink.js +44 -0
  41. package/dist/esm/plugins/Hyperlink/createHyperlinkPlugin.js +29 -1
  42. package/dist/esm/plugins/Hyperlink/useResourceEntityInfo.js +22 -0
  43. package/dist/esm/plugins/Hyperlink/utils.js +2 -2
  44. package/dist/esm/plugins/Paragraph/__tests__/createParagraphPlugin.test.js +2 -0
  45. package/dist/esm/plugins/Quote/__test__/createQuotePlugin.test.js +2 -0
  46. package/dist/esm/plugins/index.js +2 -0
  47. package/dist/esm/plugins/shared/EmbeddedBlockToolbarIcon.js +3 -5
  48. package/dist/esm/plugins/shared/EmbeddedBlockUtil.js +1 -2
  49. package/dist/esm/plugins/shared/EmbeddedInlineToolbarIcon.js +12 -7
  50. package/dist/esm/plugins/shared/EmbeddedInlineUtil.js +64 -25
  51. package/dist/esm/plugins/shared/ResourceNewBadge.js +8 -0
  52. package/dist/esm/test-utils/jsx.js +11 -0
  53. package/dist/types/constants/Schema.d.ts +10 -0
  54. package/dist/types/helpers/config.d.ts +33 -0
  55. package/dist/types/helpers/editor.d.ts +1 -1
  56. package/dist/types/plugins/EmbeddedResourceInline/FetchingWrappedResourceInlineCard.d.ts +13 -0
  57. package/dist/types/plugins/EmbeddedResourceInline/LinkedResourceInline.d.ts +13 -0
  58. package/dist/types/plugins/EmbeddedResourceInline/index.d.ts +3 -0
  59. package/dist/types/plugins/Hyperlink/HyperlinkModal.d.ts +2 -1
  60. package/dist/types/plugins/Hyperlink/components/ResourceHyperlink.d.ts +20 -0
  61. package/dist/types/plugins/Hyperlink/useResourceEntityInfo.d.ts +7 -0
  62. package/dist/types/plugins/Hyperlink/utils.d.ts +1 -0
  63. package/dist/types/plugins/shared/EmbeddedInlineToolbarIcon.d.ts +2 -1
  64. package/dist/types/plugins/shared/EmbeddedInlineUtil.d.ts +3 -17
  65. package/dist/types/plugins/shared/ResourceNewBadge.d.ts +2 -0
  66. package/package.json +2 -2
  67. package/dist/cjs/helpers/newResourceEntitySelectorConfigFromRichTextField.js +0 -21
  68. package/dist/esm/helpers/newResourceEntitySelectorConfigFromRichTextField.js +0 -6
  69. package/dist/types/helpers/newEntitySelectorConfigFromRichTextField.d.ts +0 -14
  70. package/dist/types/helpers/newResourceEntitySelectorConfigFromRichTextField.d.ts +0 -16
@@ -1,11 +1,12 @@
1
1
  import { INLINES } from '@contentful/rich-text-types';
2
- import { createInlineEntryNode, getWithEmbeddedEntryInlineEvents } from '../shared/EmbeddedInlineUtil';
2
+ import { getWithEmbeddedEntryInlineEvents } from '../shared/EmbeddedInlineUtil';
3
3
  import { LinkedEntityInline } from './LinkedEntityInline';
4
4
  export function createEmbeddedEntityInlinePlugin(sdk) {
5
5
  const htmlAttributeName = 'data-embedded-entity-inline-id';
6
+ const nodeType = INLINES.EMBEDDED_ENTRY;
6
7
  return {
7
- key: INLINES.EMBEDDED_ENTRY,
8
- type: INLINES.EMBEDDED_ENTRY,
8
+ key: nodeType,
9
+ type: nodeType,
9
10
  isElement: true,
10
11
  isInline: true,
11
12
  isVoid: true,
@@ -14,7 +15,7 @@ export function createEmbeddedEntityInlinePlugin(sdk) {
14
15
  hotkey: 'mod+shift+2'
15
16
  },
16
17
  handlers: {
17
- onKeyDown: getWithEmbeddedEntryInlineEvents(sdk)
18
+ onKeyDown: getWithEmbeddedEntryInlineEvents(nodeType, sdk)
18
19
  },
19
20
  deserializeHtml: {
20
21
  rules: [
@@ -23,7 +24,23 @@ export function createEmbeddedEntityInlinePlugin(sdk) {
23
24
  }
24
25
  ],
25
26
  withoutChildren: true,
26
- getNode: (el)=>createInlineEntryNode(el.getAttribute(htmlAttributeName))
27
+ getNode: (el)=>({
28
+ type: nodeType,
29
+ children: [
30
+ {
31
+ text: ''
32
+ }
33
+ ],
34
+ data: {
35
+ target: {
36
+ sys: {
37
+ id: el.getAttribute('data-entity-id'),
38
+ type: 'Link',
39
+ linkType: el.getAttribute('data-entity-type')
40
+ }
41
+ }
42
+ }
43
+ })
27
44
  }
28
45
  };
29
46
  }
@@ -0,0 +1,53 @@
1
+ import * as React from 'react';
2
+ import { InlineEntryCard, MenuItem, Text } from '@contentful/f36-components';
3
+ import { useResource } from '@contentful/field-editor-reference';
4
+ import { entityHelpers } from '@contentful/field-editor-shared';
5
+ import { INLINES } from '@contentful/rich-text-types';
6
+ const { getEntryTitle , getEntryStatus } = entityHelpers;
7
+ export function FetchingWrappedResourceInlineCard(props) {
8
+ const { link , onEntityFetchComplete } = props;
9
+ const { data , status: requestStatus } = useResource(link.linkType, link.urn);
10
+ React.useEffect(()=>{
11
+ if (requestStatus === 'success') {
12
+ onEntityFetchComplete?.();
13
+ }
14
+ }, [
15
+ onEntityFetchComplete,
16
+ requestStatus
17
+ ]);
18
+ if (requestStatus === 'error') {
19
+ return React.createElement(InlineEntryCard, {
20
+ title: "Entry missing or inaccessible",
21
+ testId: INLINES.EMBEDDED_RESOURCE,
22
+ isSelected: props.isSelected
23
+ });
24
+ }
25
+ if (requestStatus === 'loading' || data === undefined) {
26
+ return React.createElement(InlineEntryCard, {
27
+ isLoading: true
28
+ });
29
+ }
30
+ const { resource: entry , contentType , defaultLocaleCode , space } = data;
31
+ const title = getEntryTitle({
32
+ entry,
33
+ contentType,
34
+ defaultLocaleCode,
35
+ localeCode: defaultLocaleCode,
36
+ defaultTitle: 'Untitled'
37
+ });
38
+ const status = getEntryStatus(entry?.sys);
39
+ return React.createElement(InlineEntryCard, {
40
+ testId: INLINES.EMBEDDED_RESOURCE,
41
+ isSelected: props.isSelected,
42
+ title: `${data.contentType.name}: ${title} (Space: ${space.name})`,
43
+ status: status,
44
+ actions: [
45
+ React.createElement(MenuItem, {
46
+ key: "remove",
47
+ onClick: props.onRemove,
48
+ disabled: props.isDisabled,
49
+ testId: "delete"
50
+ }, "Remove")
51
+ ]
52
+ }, React.createElement(Text, null, title));
53
+ }
@@ -0,0 +1,36 @@
1
+ import React from 'react';
2
+ import { useSelected, useReadOnly } from 'slate-react';
3
+ import { useContentfulEditor } from '../../ContentfulEditorProvider';
4
+ import { findNodePath, removeNodes } from '../../internal';
5
+ import { useSdkContext } from '../../SdkProvider';
6
+ import { useLinkTracking } from '../links-tracking';
7
+ import { LinkedInlineWrapper } from '../shared/LinkedInlineWrapper';
8
+ import { FetchingWrappedResourceInlineCard } from './FetchingWrappedResourceInlineCard';
9
+ export function LinkedResourceInline(props) {
10
+ const { attributes , children , element } = props;
11
+ const { onEntityFetchComplete } = useLinkTracking();
12
+ const isSelected = useSelected();
13
+ const editor = useContentfulEditor();
14
+ const sdk = useSdkContext();
15
+ const isDisabled = useReadOnly();
16
+ const link = element.data.target.sys;
17
+ function handleRemoveClick() {
18
+ if (!editor) return;
19
+ const pathToElement = findNodePath(editor, element);
20
+ removeNodes(editor, {
21
+ at: pathToElement
22
+ });
23
+ }
24
+ return React.createElement(LinkedInlineWrapper, {
25
+ attributes: attributes,
26
+ link: element.data.target,
27
+ card: React.createElement(FetchingWrappedResourceInlineCard, {
28
+ sdk: sdk,
29
+ link: link,
30
+ isDisabled: isDisabled,
31
+ isSelected: isSelected,
32
+ onRemove: handleRemoveClick,
33
+ onEntityFetchComplete: onEntityFetchComplete
34
+ })
35
+ }, children);
36
+ }
@@ -0,0 +1,46 @@
1
+ import { INLINES } from '@contentful/rich-text-types';
2
+ import { getWithEmbeddedEntryInlineEvents } from '../shared/EmbeddedInlineUtil';
3
+ import { LinkedResourceInline } from './LinkedResourceInline';
4
+ export function createEmbeddedResourceInlinePlugin(sdk) {
5
+ const htmlAttributeName = 'data-embedded-resource-inline-id';
6
+ const nodeType = INLINES.EMBEDDED_RESOURCE;
7
+ return {
8
+ key: nodeType,
9
+ type: nodeType,
10
+ isElement: true,
11
+ isInline: true,
12
+ isVoid: true,
13
+ component: LinkedResourceInline,
14
+ options: {
15
+ hotkey: 'mod+shift+p'
16
+ },
17
+ handlers: {
18
+ onKeyDown: getWithEmbeddedEntryInlineEvents(nodeType, sdk)
19
+ },
20
+ deserializeHtml: {
21
+ rules: [
22
+ {
23
+ validAttribute: htmlAttributeName
24
+ }
25
+ ],
26
+ withoutChildren: true,
27
+ getNode: (el)=>({
28
+ type: nodeType,
29
+ children: [
30
+ {
31
+ text: ''
32
+ }
33
+ ],
34
+ data: {
35
+ target: {
36
+ sys: {
37
+ urn: el.getAttribute('data-entity-id'),
38
+ linkType: el.getAttribute('data-entity-type'),
39
+ type: 'ResourceLink'
40
+ }
41
+ }
42
+ }
43
+ })
44
+ }
45
+ };
46
+ }
@@ -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")));
@@ -6,6 +6,7 @@ import { createDragAndDropPlugin } from './DragAndDrop';
6
6
  import { createEmbeddedAssetBlockPlugin, createEmbeddedEntryBlockPlugin } from './EmbeddedEntityBlock';
7
7
  import { createEmbeddedEntityInlinePlugin } from './EmbeddedEntityInline';
8
8
  import { createEmbeddedResourceBlockPlugin } from './EmbeddedResourceBlock';
9
+ import { createEmbeddedResourceInlinePlugin } from './EmbeddedResourceInline';
9
10
  import { createHeadingPlugin } from './Heading';
10
11
  import { createHrPlugin } from './Hr';
11
12
  import { createHyperlinkPlugin } from './Hyperlink';
@@ -39,6 +40,7 @@ export const getPlugins = (sdk, onAction, restrictedMarks)=>[
39
40
  createEmbeddedResourceBlockPlugin(sdk),
40
41
  createHyperlinkPlugin(sdk),
41
42
  createEmbeddedEntityInlinePlugin(sdk),
43
+ createEmbeddedResourceInlinePlugin(sdk),
42
44
  createMarksPlugin(),
43
45
  createTrailingParagraphPlugin(),
44
46
  createTextPlugin(restrictedMarks),
@@ -1,11 +1,12 @@
1
1
  import * as React from 'react';
2
- import { Badge, Flex, Icon, Menu } from '@contentful/f36-components';
2
+ import { Flex, Icon, Menu } from '@contentful/f36-components';
3
3
  import { AssetIcon, EmbeddedEntryBlockIcon } from '@contentful/f36-icons';
4
4
  import { BLOCKS } from '@contentful/rich-text-types';
5
5
  import { css } from 'emotion';
6
6
  import { useContentfulEditor } from '../../ContentfulEditorProvider';
7
7
  import { useSdkContext } from '../../SdkProvider';
8
8
  import { selectEntityAndInsert, selectResourceEntityAndInsert } from '../shared/EmbeddedBlockUtil';
9
+ import { ResourceNewBadge } from './ResourceNewBadge';
9
10
  export const styles = {
10
11
  icon: css({
11
12
  marginRight: '10px'
@@ -40,10 +41,7 @@ export function EmbeddedBlockToolbarIcon({ isDisabled , nodeType , onClose }) {
40
41
  as: type === 'Asset' ? AssetIcon : EmbeddedEntryBlockIcon,
41
42
  className: `rich-text__embedded-entry-list-icon ${styles.icon}`,
42
43
  variant: "secondary"
43
- }), React.createElement("span", null, type, nodeType == BLOCKS.EMBEDDED_RESOURCE && React.createElement(React.Fragment, null, ' ', "(different space)", ' ', React.createElement(Badge, {
44
- variant: "primary-filled",
45
- size: "small"
46
- }, "new")))));
44
+ }), React.createElement("span", null, type, nodeType == BLOCKS.EMBEDDED_RESOURCE && React.createElement(ResourceNewBadge, null))));
47
45
  }
48
46
  function getEntityTypeFromNodeType(nodeType) {
49
47
  const words = nodeType.toLowerCase().split('-');
@@ -1,8 +1,7 @@
1
1
  import { BLOCKS, TEXT_CONTAINERS } from '@contentful/rich-text-types';
2
2
  import isHotkey from 'is-hotkey';
3
+ import { newEntitySelectorConfigFromRichTextField, newResourceEntitySelectorConfigFromRichTextField } from '../../helpers/config';
3
4
  import { focus, getNodeEntryFromSelection, insertEmptyParagraph, moveToTheNextChar } from '../../helpers/editor';
4
- import newEntitySelectorConfigFromRichTextField from '../../helpers/newEntitySelectorConfigFromRichTextField';
5
- import newResourceEntitySelectorConfigFromRichTextField from '../../helpers/newResourceEntitySelectorConfigFromRichTextField';
6
5
  import { watchCurrentSlide } from '../../helpers/sdkNavigatorSlideIn';
7
6
  import { getText, getAboveNode, getLastNodeByLevel, insertNodes, setNodes, select, removeNodes } from '../../internal';
8
7
  export function getWithEmbeddedBlockEvents(nodeType, sdk) {
@@ -7,7 +7,8 @@ import { css } from 'emotion';
7
7
  import { useContentfulEditor } from '../../ContentfulEditorProvider';
8
8
  import { moveToTheNextChar } from '../../helpers/editor';
9
9
  import { useSdkContext } from '../../SdkProvider';
10
- import { selectEntityAndInsert } from '../shared/EmbeddedInlineUtil';
10
+ import { selectEntityAndInsert, selectResourceEntityAndInsert } from '../shared/EmbeddedInlineUtil';
11
+ import { ResourceNewBadge } from './ResourceNewBadge';
11
12
  const styles = {
12
13
  icon: css({
13
14
  marginRight: '10px'
@@ -21,20 +22,24 @@ const styles = {
21
22
  }
22
23
  })
23
24
  };
24
- export function EmbeddedInlineToolbarIcon(props) {
25
+ export function EmbeddedInlineToolbarIcon({ onClose , nodeType , isDisabled }) {
25
26
  const editor = useContentfulEditor();
26
27
  const sdk = useSdkContext();
27
28
  async function handleClick(event) {
28
29
  event.preventDefault();
29
30
  if (!editor) return;
30
- props.onClose();
31
- await selectEntityAndInsert(editor, sdk, editor.tracking.onToolbarAction);
31
+ onClose();
32
+ if (nodeType === INLINES.EMBEDDED_RESOURCE) {
33
+ await selectResourceEntityAndInsert(editor, sdk, editor.tracking.onToolbarAction);
34
+ } else {
35
+ await selectEntityAndInsert(editor, sdk, editor.tracking.onToolbarAction);
36
+ }
32
37
  moveToTheNextChar(editor);
33
38
  }
34
39
  return React.createElement(Menu.Item, {
35
- disabled: props.isDisabled,
40
+ disabled: isDisabled,
36
41
  className: "rich-text__entry-link-block-button",
37
- testId: `toolbar-toggle-${INLINES.EMBEDDED_ENTRY}`,
42
+ testId: `toolbar-toggle-${nodeType}`,
38
43
  onClick: handleClick
39
44
  }, React.createElement(Flex, {
40
45
  alignItems: "center",
@@ -42,5 +47,5 @@ export function EmbeddedInlineToolbarIcon(props) {
42
47
  }, React.createElement(EmbeddedEntryInlineIcon, {
43
48
  variant: "secondary",
44
49
  className: `rich-text__embedded-entry-list-icon ${styles.icon}`
45
- }), React.createElement("span", null, "Inline entry")));
50
+ }), React.createElement("span", null, "Inline entry", nodeType == INLINES.EMBEDDED_RESOURCE && React.createElement(ResourceNewBadge, null))));
46
51
  }