@squiz/formatted-text-editor 2.1.0 → 2.2.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 (39) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/demo/App.tsx +5 -0
  3. package/demo/AppContext.tsx +107 -70
  4. package/lib/EditorToolbar/FloatingToolbar.js +1 -1
  5. package/lib/EditorToolbar/Tools/ContentTools/ContentToolsDropdown.js +3 -3
  6. package/lib/EditorToolbar/Tools/Image/Form/ImageForm.d.ts +2 -1
  7. package/lib/EditorToolbar/Tools/Image/Form/ImageForm.js +53 -10
  8. package/lib/EditorToolbar/Tools/Image/ImageButton.js +8 -5
  9. package/lib/EditorToolbar/Tools/Image/ImageModal.js +3 -1
  10. package/lib/Extensions/Extensions.d.ts +1 -0
  11. package/lib/Extensions/Extensions.js +3 -0
  12. package/lib/Extensions/FetchUrlExtension/FetchUrlExtension.js +6 -0
  13. package/lib/Extensions/ImageExtension/DAMImageExtension.d.ts +17 -0
  14. package/lib/Extensions/ImageExtension/DAMImageExtension.js +97 -0
  15. package/lib/ui/Fields/ResourceBrowserSelector/ResourceBrowserSelector.d.ts +28 -0
  16. package/lib/ui/Fields/ResourceBrowserSelector/ResourceBrowserSelector.js +88 -0
  17. package/lib/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.js +9 -0
  18. package/lib/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.js +9 -0
  19. package/package.json +4 -2
  20. package/src/EditorToolbar/FloatingToolbar.spec.tsx +3 -1
  21. package/src/EditorToolbar/FloatingToolbar.tsx +1 -1
  22. package/src/EditorToolbar/Tools/ContentTools/ContentToolsDropdown.tsx +3 -3
  23. package/src/EditorToolbar/Tools/Image/Form/ImageForm.spec.tsx +27 -2
  24. package/src/EditorToolbar/Tools/Image/Form/ImageForm.tsx +61 -14
  25. package/src/EditorToolbar/Tools/Image/ImageButton.spec.tsx +70 -2
  26. package/src/EditorToolbar/Tools/Image/ImageButton.tsx +12 -6
  27. package/src/EditorToolbar/Tools/Image/ImageModal.tsx +4 -1
  28. package/src/Extensions/Extensions.ts +3 -0
  29. package/src/Extensions/FetchUrlExtension/FetchUrlExtension.ts +9 -0
  30. package/src/Extensions/ImageExtension/DAMImageExtension.spec.ts +87 -0
  31. package/src/Extensions/ImageExtension/DAMImageExtension.ts +119 -0
  32. package/src/ui/Fields/ResourceBrowserSelector/ResourceBrowserSelector.spec.tsx +219 -0
  33. package/src/ui/Fields/ResourceBrowserSelector/ResourceBrowserSelector.tsx +109 -0
  34. package/src/utils/converters/mocks/squizNodeJson.mock.ts +21 -0
  35. package/src/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.ts +10 -0
  36. package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.ts +8 -0
  37. package/src/utils/getNodeNamesByGroup.spec.ts +1 -0
  38. package/tests/index.ts +1 -0
  39. package/tests/mockResourceBrowser.tsx +46 -0
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.ResourceBrowserSelector = void 0;
27
+ const react_1 = __importStar(require("react"));
28
+ const resource_browser_1 = require("@squiz/resource-browser");
29
+ const InputContainer_1 = require("../InputContainer/InputContainer");
30
+ const Extensions_1 = require("../../../Extensions/Extensions");
31
+ const ResourceBrowserSelector = ({ modalTitle, allowedTypes, value, onChange, ...props }) => {
32
+ const convertFormDataToResourceBrowserValue = (0, react_1.useCallback)((value) => {
33
+ if (value?.matrixIdentifier && value?.matrixAssetId) {
34
+ return {
35
+ sourceId: value.matrixIdentifier,
36
+ resourceId: value.matrixAssetId,
37
+ };
38
+ }
39
+ else if (value?.damSystemIdentifier && value?.damObjectId) {
40
+ return {
41
+ sourceId: value.damSystemIdentifier,
42
+ resourceId: value.damObjectId,
43
+ };
44
+ }
45
+ return null;
46
+ }, []);
47
+ const handleResourceChange = (0, react_1.useCallback)((resource) => {
48
+ // Clear out any key properties for clear resource use case
49
+ let onChangeData = {
50
+ ...value,
51
+ matrixIdentifier: undefined,
52
+ matrixAssetId: undefined,
53
+ damSystemIdentifier: undefined,
54
+ damObjectId: undefined,
55
+ damSystemType: undefined,
56
+ url: undefined,
57
+ };
58
+ if (resource?.source?.type === 'matrix') {
59
+ onChangeData = {
60
+ ...value,
61
+ matrixIdentifier: resource?.source?.id,
62
+ matrixAssetId: resource?.id,
63
+ url: resource?.url,
64
+ nodeType: Extensions_1.NodeName.AssetImage,
65
+ };
66
+ }
67
+ else if (resource?.source?.type === 'dam') {
68
+ onChangeData = {
69
+ ...value,
70
+ damSystemIdentifier: resource?.source?.id,
71
+ damObjectId: resource?.id,
72
+ damSystemType: (resource?.source).configuration.externalType,
73
+ url: resource?.url,
74
+ nodeType: Extensions_1.NodeName.DAMImage,
75
+ };
76
+ }
77
+ onChange({
78
+ target: {
79
+ value: {
80
+ ...onChangeData,
81
+ },
82
+ },
83
+ });
84
+ }, []);
85
+ return (react_1.default.createElement(InputContainer_1.InputContainer, { ...props },
86
+ react_1.default.createElement(resource_browser_1.ResourceBrowser, { modalTitle: modalTitle, allowedTypes: allowedTypes, value: convertFormDataToResourceBrowserValue(value), onChange: handleResourceChange })));
87
+ };
88
+ exports.ResourceBrowserSelector = ResourceBrowserSelector;
@@ -117,6 +117,15 @@ const transformNode = (node) => {
117
117
  matrixDomain: node.attrs.matrixDomain,
118
118
  };
119
119
  }
120
+ if (node.type.name === Extensions_1.NodeName.DAMImage) {
121
+ transformedNode = {
122
+ type: 'dam-image',
123
+ damObjectId: node.attrs.damObjectId,
124
+ damSystemIdentifier: node.attrs.damSystemIdentifier,
125
+ damSystemType: node.attrs.damSystemType,
126
+ damAdditional: node.attrs.damAdditional ? JSON.parse(node.attrs.damAdditional) : undefined,
127
+ };
128
+ }
120
129
  node.marks.forEach((mark) => {
121
130
  transformedNode = transformMark(mark, transformedNode);
122
131
  });
@@ -7,6 +7,7 @@ const getNodeType = (node) => {
7
7
  const typeMap = {
8
8
  'link-to-matrix-asset': Extensions_1.NodeName.Text,
9
9
  'matrix-image': Extensions_1.NodeName.AssetImage,
10
+ 'dam-image': Extensions_1.NodeName.DAMImage,
10
11
  text: 'text',
11
12
  };
12
13
  const tagMap = {
@@ -87,6 +88,14 @@ const getNodeAttributes = (node) => {
87
88
  matrixIdentifier: node.matrixIdentifier,
88
89
  };
89
90
  }
91
+ else if (node.type === 'dam-image') {
92
+ return {
93
+ damObjectId: node.damObjectId,
94
+ damSystemIdentifier: node.damSystemIdentifier,
95
+ damSystemType: node.damSystemType,
96
+ damAdditional: node.damAdditional ? JSON.stringify(node.damAdditional) : undefined,
97
+ };
98
+ }
90
99
  else if (node.type === 'tag') {
91
100
  return {
92
101
  nodeIndent: null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@squiz/formatted-text-editor",
3
- "version": "2.1.0",
3
+ "version": "2.2.1",
4
4
  "main": "lib/index.js",
5
5
  "types": "lib/index.d.ts",
6
6
  "private": false,
@@ -24,11 +24,12 @@
24
24
  "@mui/icons-material": "5.15.18",
25
25
  "@remirror/extension-react-tables": "^2.2.19",
26
26
  "@remirror/react": "2.0.35",
27
- "@squiz/dx-json-schema-lib": "^1.65.1",
27
+ "@squiz/dx-json-schema-lib": "^1.72.0",
28
28
  "@squiz/dxp-ai-client-react": "^0.1.3-alpha",
29
29
  "@squiz/matrix-resource-browser-plugin": "^2.0.0",
30
30
  "@squiz/resource-browser": "^2.0.0",
31
31
  "@squiz/sds": "^1.0.0-alpha.50",
32
+ "@squiz/dam-resource-browser-plugin": "^0.9.0-rc.0",
32
33
  "clsx": "2.1.1",
33
34
  "react-hook-form": "7.51.4",
34
35
  "react-image-size": "2.0.0",
@@ -58,6 +59,7 @@
58
59
  "react": "18.2.0",
59
60
  "react-diff-viewer-continued": "3.2.6",
60
61
  "react-dom": "18.2.0",
62
+ "react-query": "^3.19.6",
61
63
  "rimraf": "5.0.7",
62
64
  "tailwindcss": "3.2.6",
63
65
  "ts-jest": "29.0.5",
@@ -31,13 +31,15 @@ describe('FloatingToolbar', () => {
31
31
  it.each([
32
32
  ['Image selected', 1, ['Image (Ctrl+L)']],
33
33
  ['Asset image selected', 2, ['Image (Ctrl+L)']],
34
+ ['DAM image selected', 3, ['Image (Ctrl+L)']],
34
35
  ])(
35
36
  'Renders formatting buttons when node is selected - %s',
36
37
  async (description: string, pos: number, expectedButtons: string[]) => {
37
38
  const { editor } = await renderWithEditor(null, {
38
39
  content:
39
40
  '<img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" />' +
40
- '<img data-matrix-asset-id="100" data-matrix-identifier="key" data-matrix-domain="my-matrix.squiz.net" />',
41
+ '<img data-matrix-asset-id="100" data-matrix-identifier="key" data-matrix-domain="my-matrix.squiz.net" />' +
42
+ '<img data-dam-object-id="5ce8e8dc-1adc-4254-87a8-d1f5a5c9045a" data-dam-system-identifier="byder001" data-dam-system-type="bynder" />',
41
43
  editable: true,
42
44
  });
43
45
 
@@ -30,7 +30,7 @@ export const FloatingToolbar = () => {
30
30
  extensionNames.underline && <UnderlineButton key="underline" />,
31
31
  ];
32
32
 
33
- if (active.image() || active.assetImage()) {
33
+ if (active.image() || active.assetImage() || active.DAMImage()) {
34
34
  buttons = [<ImageButton key="add-image" inPopover={true} />];
35
35
  } else if (marks?.[MarkName.Link].isExclusivelyActive || marks?.[MarkName.AssetLink].isExclusivelyActive) {
36
36
  // if all of the selected text is a link show the options to update/remove the link instead of the regular
@@ -7,7 +7,7 @@ import { VerticalDivider } from '@remirror/react';
7
7
  const ContentToolsDropdown = () => {
8
8
  const { contentTools } = useAiService();
9
9
 
10
- const dropdownItems = contentTools.map((item) => ({
10
+ const dropdownItems = contentTools?.map((item) => ({
11
11
  items: [
12
12
  {
13
13
  action: () => alert(JSON.stringify(item, null, 2)),
@@ -19,7 +19,7 @@ const ContentToolsDropdown = () => {
19
19
  }));
20
20
 
21
21
  // No content tools to show, don't show dropdown at all
22
- if (contentTools.length === 0) {
22
+ if (!contentTools || contentTools?.length === 0) {
23
23
  return null;
24
24
  }
25
25
 
@@ -37,7 +37,7 @@ const ContentToolsDropdown = () => {
37
37
  className="content-tools-dropdown"
38
38
  dropdownPosition={DROPDOWN_POSITION_RIGHT}
39
39
  heading={'Rewrite to...'}
40
- sections={dropdownItems}
40
+ sections={dropdownItems ?? []}
41
41
  />
42
42
  </>
43
43
  );
@@ -3,7 +3,13 @@ import { render, screen, act, fireEvent, waitFor } from '@testing-library/react'
3
3
  import React from 'react';
4
4
  import ImageForm from './ImageForm';
5
5
  import { NodeName } from '../../../../Extensions/Extensions';
6
- import { mockResourceBrowserContext } from '../../../../../tests';
6
+ import { mockResourceBrowser, mockResourceBrowserContext } from '../../../../../tests';
7
+
8
+ const { setShouldUseMockResourceBrowser, mockResourceBrowserImpl } = mockResourceBrowser();
9
+ jest.mock('@squiz/resource-browser', () => ({
10
+ ...jest.requireActual('@squiz/resource-browser'),
11
+ ResourceBrowser: (props: any) => mockResourceBrowserImpl(props),
12
+ }));
7
13
 
8
14
  describe('Image Form', () => {
9
15
  const handleSubmit = jest.fn();
@@ -48,7 +54,7 @@ describe('Image Form', () => {
48
54
  data={{
49
55
  ...data,
50
56
  imageType: NodeName.AssetImage,
51
- assetImage: { matrixAssetId: '100', matrixIdentifier: 'matrix-api-identifier' },
57
+ resourceImage: { matrixAssetId: '100', matrixIdentifier: 'matrix-api-identifier' },
52
58
  }}
53
59
  onSubmit={handleSubmit}
54
60
  />
@@ -61,6 +67,25 @@ describe('Image Form', () => {
61
67
  });
62
68
  });
63
69
 
70
+ it('Renders the form with the relevant fields for dam images', async () => {
71
+ setShouldUseMockResourceBrowser(true);
72
+ render(
73
+ <ImageForm
74
+ data={{
75
+ ...data,
76
+ imageType: NodeName.DAMImage,
77
+ resourceImage: { damObjectId: '100', damSystemIdentifier: 'dam-api-identifier' },
78
+ }}
79
+ onSubmit={handleSubmit}
80
+ />,
81
+ );
82
+
83
+ expect(document.querySelector('div[data-headlessui-state="selected"]')).toHaveTextContent('From source');
84
+ await waitFor(() => {
85
+ expect(screen.getByText('ResourceBrowser was rendered')).toBeInTheDocument();
86
+ });
87
+ });
88
+
64
89
  it('calculates the height when width changes and aspect ratio is locked', () => {
65
90
  render(<ImageForm data={data} onSubmit={handleSubmit} />);
66
91
  const widthInput = screen.getByLabelText('Width');
@@ -1,4 +1,4 @@
1
- import React, { ReactElement, useState } from 'react';
1
+ import React, { ReactElement, useState, useCallback, useEffect } from 'react';
2
2
  import { Controller, SubmitHandler, useForm } from 'react-hook-form';
3
3
  import { getImageSize } from 'react-image-size';
4
4
  import clsx from 'clsx';
@@ -9,20 +9,25 @@ import LinkOffIcon from '@mui/icons-material/LinkOff';
9
9
  import InsertLinkRoundedIcon from '@mui/icons-material/InsertLinkRounded';
10
10
  import { NodeName } from '../../../../Extensions/Extensions';
11
11
  import { AssetImageAttributes } from '../../../../Extensions/ImageExtension/AssetImageExtension';
12
+ import { DAMImageAttributes } from '../../../../Extensions/ImageExtension/DAMImageExtension';
12
13
  import { DeepPartial } from '../../../../types';
13
14
  import { noEmptySpacesValidation, regexDataURI, hasProperties } from '../../../../utils/validation';
14
15
  import { TabOptions, Tabs } from '../../../../ui/Tabs/Tabs';
15
- import { MatrixAsset } from '../../../../ui/Fields/MatrixAsset/MatrixAsset';
16
+ import { ResourceBrowserSelector } from '../../../../ui/Fields/ResourceBrowserSelector/ResourceBrowserSelector';
16
17
 
17
18
  export type ImageFormData = {
18
19
  imageType: NodeName;
19
20
  image: Pick<ImageAttributes, 'src' | 'alt' | 'width' | 'height'>;
20
- assetImage: AssetImageAttributes;
21
+ resourceImage: AssetImageAttributes & DAMImageAttributes;
21
22
  };
22
23
 
24
+ enum ViewTypes {
25
+ Resource = 'Resource',
26
+ URL = 'URL',
27
+ }
23
28
  const imageTypeOptions: TabOptions = {
24
- [NodeName.AssetImage]: { label: 'From source' },
25
- [NodeName.Image]: { label: 'From URL' },
29
+ [ViewTypes.Resource]: { label: 'From source' },
30
+ [ViewTypes.URL]: { label: 'From URL' },
26
31
  };
27
32
 
28
33
  export type FormProps = {
@@ -42,7 +47,8 @@ const ImageForm = ({ data, onSubmit }: FormProps): ReactElement => {
42
47
  } = useForm<ImageFormData>({
43
48
  defaultValues: data,
44
49
  });
45
- const imageType = watch('imageType') || NodeName.AssetImage;
50
+ const imageType = watch('imageType');
51
+ const [viewType, setViewType] = useState<ViewTypes>(ViewTypes.Resource);
46
52
  const [aspectRatioFromWidth, setAspectRatioFromWidth] = useState(9 / 16);
47
53
  const [aspectRatioFromHeight, setAspectRatioFromHeight] = useState(16 / 9);
48
54
  const [aspectRatioLocked, setAspectRatioLocked] = useState(true);
@@ -86,16 +92,41 @@ const ImageForm = ({ data, onSubmit }: FormProps): ReactElement => {
86
92
  setAspectRatioLocked(!aspectRatioLocked);
87
93
  };
88
94
 
95
+ useEffect(() => {
96
+ if (imageType === NodeName.Image) {
97
+ setViewType(ViewTypes.URL);
98
+ } else {
99
+ setViewType(ViewTypes.Resource);
100
+ }
101
+ }, [imageType, setViewType]);
102
+
103
+ const handleChangeViewType = useCallback(
104
+ (value: string) => {
105
+ setViewType(value as ViewTypes);
106
+ // If its the URL field type we know what the imageType should be
107
+ if (value === ViewTypes.URL) {
108
+ console.log(`handleChangeViewType: ${value} NodeName.Image`);
109
+ setValue('imageType', NodeName.Image);
110
+ } else {
111
+ // Need a value here and this is the assumed default elsewhere
112
+ // Will be set again later once Resource Browser returns a resource value
113
+ console.log(`handleChangeViewType: ${value} NodeName.AssetImage`);
114
+ setValue('imageType', NodeName.AssetImage);
115
+ }
116
+ },
117
+ [setViewType, setValue],
118
+ );
119
+
89
120
  return (
90
121
  <form className="squiz-fte-form" onSubmit={handleSubmit(onSubmit)}>
91
122
  <div className="squiz-fte-form-group mb-4">
92
123
  <Tabs
93
- value={imageType}
124
+ value={imageType === NodeName.Image ? ViewTypes.URL : ViewTypes.Resource}
94
125
  options={imageTypeOptions}
95
- onChange={(value) => setValue('imageType', value as NodeName)}
126
+ onChange={handleChangeViewType}
96
127
  />
97
128
  </div>
98
- {imageType === NodeName.Image && (
129
+ {viewType === ViewTypes.URL && (
99
130
  <>
100
131
  <div className="squiz-fte-form-group mb-2">
101
132
  <Input
@@ -174,20 +205,36 @@ const ImageForm = ({ data, onSubmit }: FormProps): ReactElement => {
174
205
  </div>
175
206
  </>
176
207
  )}
177
- {imageType === NodeName.AssetImage && (
208
+ {viewType === ViewTypes.Resource && (
178
209
  <div className="squiz-fte-form-group mb-2">
179
210
  <Controller
180
211
  control={control}
181
- name="assetImage"
212
+ name="resourceImage"
182
213
  rules={{
183
- validate: hasProperties('An image must be selected', ['matrixIdentifier', 'matrixAssetId']),
214
+ validate: (value) => {
215
+ const matrixValidation = hasProperties('An image must be selected', [
216
+ 'matrixIdentifier',
217
+ 'matrixAssetId',
218
+ ])(value);
219
+ const damValidation = hasProperties('An image must be selected', [
220
+ 'damObjectId',
221
+ 'damSystemIdentifier',
222
+ ])(value);
223
+
224
+ // One of the two needs to validate
225
+ return matrixValidation && damValidation;
226
+ },
184
227
  }}
185
228
  render={({ field: { onChange, value }, fieldState: { error } }) => (
186
- <MatrixAsset
229
+ <ResourceBrowserSelector
187
230
  modalTitle="Insert image"
188
231
  allowedTypes={['image']}
189
232
  value={value}
190
- onChange={onChange}
233
+ onChange={(value: { target: { value: any } }) => {
234
+ console.log(`onChange: ${value}`);
235
+ setValue('imageType', value.target.value.nodeType);
236
+ onChange(value);
237
+ }}
191
238
  error={error?.message}
192
239
  />
193
240
  )}
@@ -1,11 +1,18 @@
1
1
  import '@testing-library/jest-dom';
2
2
  import { screen, fireEvent, waitForElementToBeRemoved, act, waitFor } from '@testing-library/react';
3
+ import { ResourceBrowserResource } from '@squiz/resource-browser';
3
4
  import { NodeSelection } from 'prosemirror-state';
4
5
  import React from 'react';
5
- import { renderWithEditor, mockResourceBrowserContext } from '../../../../tests';
6
+ import { renderWithEditor, mockResourceBrowserContext, mockResourceBrowser } from '../../../../tests';
6
7
  import ImageButton from './ImageButton';
7
8
  import { getImageSize } from 'react-image-size';
8
9
 
10
+ const { setSelectedResource, setShouldUseMockResourceBrowser, mockResourceBrowserImpl } = mockResourceBrowser();
11
+ jest.mock('@squiz/resource-browser', () => ({
12
+ ...jest.requireActual('@squiz/resource-browser'),
13
+ ResourceBrowser: (props: any) => mockResourceBrowserImpl(props),
14
+ }));
15
+
9
16
  jest.mock('react-image-size');
10
17
 
11
18
  describe('ImageButton', () => {
@@ -295,7 +302,7 @@ describe('ImageButton', () => {
295
302
  });
296
303
  });
297
304
 
298
- it('Updates the attributes of an existing asset image', async () => {
305
+ it('assetImage: Updates the attributes of an existing asset image', async () => {
299
306
  const matrixIdentifier = 'matrix-api-identifier';
300
307
  const matrixDomain = 'https://my-matrix.squiz.net';
301
308
  const { MockResourceBrowserContext, selectResource } = mockResourceBrowserContext({
@@ -362,6 +369,67 @@ describe('ImageButton', () => {
362
369
  });
363
370
  });
364
371
 
372
+ it('DAMImage: Updates the attributes of an existing dam image', async () => {
373
+ setShouldUseMockResourceBrowser(true);
374
+ setSelectedResource({
375
+ id: 'my-resource-id',
376
+ name: 'My resource',
377
+ url: 'myResourceUrl',
378
+ source: {
379
+ id: 'my-source-id',
380
+ type: 'dam',
381
+ configuration: {
382
+ externalType: 'bynder',
383
+ },
384
+ },
385
+ } as unknown as ResourceBrowserResource);
386
+
387
+ const { editor, getJsonContent } = await renderWithEditor(<ImageButton />, {
388
+ content: 'Some <img src="https://httpcats.com/529.jpg" alt="hi" /> nonsense',
389
+ context: {
390
+ editor: {
391
+ matrix: {
392
+ matrixDomain: 'https://my-matrix.squiz.net',
393
+ },
394
+ },
395
+ },
396
+ });
397
+
398
+ await act(() => editor.selectText(new NodeSelection(editor.state.doc.resolve(6))));
399
+
400
+ await openModal();
401
+ fireEvent.click(screen.getByRole('button', { name: 'From source' }));
402
+ // Trigger the mock Resource Browser to invoke its onChange callback to fill the form with data
403
+ await waitFor(() => {
404
+ expect(screen.getByRole('button', { name: 'Select ResourceBrowser Resource' })).toBeInTheDocument();
405
+ });
406
+ fireEvent.click(await screen.findByRole('button', { name: 'Select ResourceBrowser Resource' }));
407
+ // Close the image selection modal
408
+ await act(() => fireEvent.click(screen.getByRole('button', { name: 'Apply' })));
409
+
410
+ expect(getJsonContent()).toEqual({
411
+ type: 'paragraph',
412
+ attrs: expect.any(Object),
413
+ content: [
414
+ {
415
+ text: 'Some ',
416
+ type: 'text',
417
+ },
418
+ {
419
+ type: 'DAMImage',
420
+ attrs: {
421
+ damObjectId: 'my-resource-id',
422
+ damSystemIdentifier: 'my-source-id',
423
+ damAdditional: undefined,
424
+ damSystemType: 'bynder',
425
+ src: 'myResourceUrl',
426
+ },
427
+ },
428
+ { type: 'text', text: ' nonsense' },
429
+ ],
430
+ });
431
+ });
432
+
365
433
  it('Shows an error if a resource is not selected', async () => {
366
434
  await renderWithEditor(<ImageButton />);
367
435
  await openModal();
@@ -6,7 +6,8 @@ import { ImageFormData } from './Form/ImageForm';
6
6
  import Button from '../../../ui/Button/Button';
7
7
  import { ImageExtension } from '../../../Extensions/ImageExtension/ImageExtension';
8
8
  import { NodeName } from '../../../Extensions/Extensions';
9
- import { AssetImageExtension } from '../../../Extensions/ImageExtension/AssetImageExtension';
9
+ import { AssetImageExtension, AssetImageAttributes } from '../../../Extensions/ImageExtension/AssetImageExtension';
10
+ import { DAMImageExtension, DAMImageAttributes } from '../../../Extensions/ImageExtension/DAMImageExtension';
10
11
  import { CodeBlockExtension } from 'remirror/dist-types/extensions';
11
12
  import { getShortcutSymbol } from '../../../utils/getShortcutSymbol';
12
13
 
@@ -16,11 +17,14 @@ type ImageButtonProps = {
16
17
 
17
18
  const ImageButton = ({ inPopover = false }: ImageButtonProps) => {
18
19
  const [showModal, setShowModal] = useState(false);
19
- const { insertImage, insertAssetImage } = useCommands<ImageExtension | AssetImageExtension>();
20
+ const { insertImage, insertAssetImage, insertDAMImage } = useCommands<
21
+ ImageExtension | AssetImageExtension | DAMImageExtension
22
+ >();
20
23
  const active = useActive<ImageExtension | AssetImageExtension | CodeBlockExtension>();
21
24
  const selection = useCurrentSelection();
22
25
  // if the active selection is not an image, disable the button as it means it will be text
23
- const disabled = (!selection.empty && !active.image() && !active.assetImage()) || active.codeBlock();
26
+ const disabled =
27
+ (!selection.empty && !active.image() && !active.assetImage() && !active.DAMImage()) || active.codeBlock();
24
28
 
25
29
  const handleClick = () => {
26
30
  if (!showModal) {
@@ -29,11 +33,13 @@ const ImageButton = ({ inPopover = false }: ImageButtonProps) => {
29
33
  };
30
34
 
31
35
  const insertImageFromData = (data: ImageFormData) => {
32
- const { imageType, image, assetImage } = data;
36
+ const { imageType, image, resourceImage } = data;
33
37
  if (imageType === NodeName.Image) {
34
38
  insertImage(image);
39
+ } else if (imageType === NodeName.DAMImage) {
40
+ insertDAMImage(resourceImage as DAMImageAttributes);
35
41
  } else {
36
- insertAssetImage(assetImage);
42
+ insertAssetImage(resourceImage as AssetImageAttributes);
37
43
  }
38
44
  };
39
45
 
@@ -59,7 +65,7 @@ const ImageButton = ({ inPopover = false }: ImageButtonProps) => {
59
65
  <>
60
66
  <Button
61
67
  handleOnClick={handleClick}
62
- isActive={active.image() || active.assetImage()}
68
+ isActive={active.image() || active.assetImage() || active.DAMImage()}
63
69
  icon={<ImageRoundedIcon />}
64
70
  label={`Image (${getShortcutSymbol()}+L)`}
65
71
  isDisabled={disabled}
@@ -19,7 +19,10 @@ const ImageModal = ({ onCancel, onSubmit }: ImageModalProps) => {
19
19
  const formData = {
20
20
  imageType: currentImage?.type.name === NodeName.Image ? NodeName.Image : NodeName.AssetImage,
21
21
  image: currentImage?.type?.name === NodeName.Image ? currentImageAttrs : {},
22
- assetImage: currentImage?.type?.name === NodeName.AssetImage ? currentImageAttrs : {},
22
+ resourceImage:
23
+ currentImage?.type?.name === NodeName.DAMImage || currentImage?.type?.name === NodeName.AssetImage
24
+ ? currentImageAttrs
25
+ : {},
23
26
  };
24
27
 
25
28
  return (
@@ -23,6 +23,7 @@ import { ImageExtension } from './ImageExtension/ImageExtension';
23
23
  import { CommandsExtension } from './CommandsExtension/CommandsExtension';
24
24
  import { EditorContextOptions } from '../Editor/EditorContext';
25
25
  import { AssetImageExtension } from './ImageExtension/AssetImageExtension';
26
+ import { DAMImageExtension } from './ImageExtension/DAMImageExtension';
26
27
  import { ExtendedCodeBlockExtension } from './CodeBlockExtension/CodeBlockExtension';
27
28
  import { ClearFormattingExtension } from './ClearFormattingExtension/ClearFormattingExtension';
28
29
  import { UnsupportedNodeExtension } from './UnsuportedExtension/UnsupportedNodeExtension';
@@ -34,6 +35,7 @@ export enum NodeName {
34
35
  Image = 'image',
35
36
  CodeBlock = 'codeBlock',
36
37
  AssetImage = 'assetImage',
38
+ DAMImage = 'DAMImage',
37
39
  Text = 'text',
38
40
  TableControllerCell = 'tableControllerCell',
39
41
  tableCell = 'tableCell',
@@ -66,6 +68,7 @@ export const createExtensions = (context: EditorContextOptions) => {
66
68
  new AssetImageExtension({
67
69
  matrixDomain: context.matrix.matrixDomain,
68
70
  }),
71
+ new DAMImageExtension(),
69
72
  new LinkExtension(),
70
73
  new AssetLinkExtension({
71
74
  matrixDomain: context.matrix.matrixDomain,
@@ -33,6 +33,15 @@ export class FetchUrlExtension extends PlainExtension<FetchUrlOptions> {
33
33
  );
34
34
  }
35
35
 
36
+ if (node.type.name === NodeName.DAMImage && node.attrs.src === '') {
37
+ promises.push(
38
+ this.fetchAndReplace(node.attrs, (url: string) => {
39
+ const newNode = state.schema.nodes[NodeName.DAMImage].create({ ...node.attrs, src: url });
40
+ tr.replaceWith(pos, pos + node.nodeSize, newNode);
41
+ }),
42
+ );
43
+ }
44
+
36
45
  const assetLinkMark = this.findAssetLinkMark(node.marks as Mark[]);
37
46
  if (node.type.name === 'text' && assetLinkMark) {
38
47
  promises.push(