@squiz/formatted-text-editor 2.0.1 → 2.2.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 (53) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/demo/App.tsx +5 -0
  3. package/demo/AppContext.tsx +111 -51
  4. package/jest.config.ts +1 -1
  5. package/jest.setup.ts +16 -0
  6. package/lib/EditorToolbar/FloatingToolbar.js +1 -1
  7. package/lib/EditorToolbar/Toolbar.js +3 -1
  8. package/lib/EditorToolbar/Tools/ContentTools/ContentToolsDropdown.d.ts +3 -0
  9. package/lib/EditorToolbar/Tools/ContentTools/ContentToolsDropdown.js +35 -0
  10. package/lib/EditorToolbar/Tools/Image/Form/ImageForm.d.ts +2 -1
  11. package/lib/EditorToolbar/Tools/Image/Form/ImageForm.js +53 -10
  12. package/lib/EditorToolbar/Tools/Image/ImageButton.js +8 -5
  13. package/lib/EditorToolbar/Tools/Image/ImageModal.js +3 -1
  14. package/lib/EditorToolbar/Tools/Table/TableButton.js +1 -3
  15. package/lib/Extensions/Extensions.d.ts +1 -0
  16. package/lib/Extensions/Extensions.js +3 -0
  17. package/lib/Extensions/FetchUrlExtension/FetchUrlExtension.js +6 -0
  18. package/lib/Extensions/ImageExtension/DAMImageExtension.d.ts +17 -0
  19. package/lib/Extensions/ImageExtension/DAMImageExtension.js +97 -0
  20. package/lib/Icons/AiIcon.d.ts +2 -0
  21. package/lib/Icons/AiIcon.js +60 -0
  22. package/lib/index.css +4224 -0
  23. package/lib/ui/Fields/ResourceBrowserSelector/ResourceBrowserSelector.d.ts +28 -0
  24. package/lib/ui/Fields/ResourceBrowserSelector/ResourceBrowserSelector.js +88 -0
  25. package/lib/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.js +9 -0
  26. package/lib/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.js +9 -0
  27. package/package.json +6 -2
  28. package/src/EditorToolbar/FloatingToolbar.spec.tsx +3 -1
  29. package/src/EditorToolbar/FloatingToolbar.tsx +1 -1
  30. package/src/EditorToolbar/Toolbar.tsx +2 -0
  31. package/src/EditorToolbar/Tools/ContentTools/ContentToolsDropdown.spec.tsx +78 -0
  32. package/src/EditorToolbar/Tools/ContentTools/ContentToolsDropdown.tsx +46 -0
  33. package/src/EditorToolbar/Tools/ContentTools/_content-tools.scss +45 -0
  34. package/src/EditorToolbar/Tools/Image/Form/ImageForm.spec.tsx +27 -2
  35. package/src/EditorToolbar/Tools/Image/Form/ImageForm.tsx +61 -14
  36. package/src/EditorToolbar/Tools/Image/ImageButton.spec.tsx +70 -2
  37. package/src/EditorToolbar/Tools/Image/ImageButton.tsx +12 -6
  38. package/src/EditorToolbar/Tools/Image/ImageModal.tsx +4 -1
  39. package/src/EditorToolbar/Tools/Table/TableButton.tsx +0 -2
  40. package/src/Extensions/Extensions.ts +3 -0
  41. package/src/Extensions/FetchUrlExtension/FetchUrlExtension.ts +9 -0
  42. package/src/Extensions/ImageExtension/DAMImageExtension.spec.ts +87 -0
  43. package/src/Extensions/ImageExtension/DAMImageExtension.ts +119 -0
  44. package/src/Icons/AiIcon.tsx +140 -0
  45. package/src/index.scss +4 -0
  46. package/src/ui/Fields/ResourceBrowserSelector/ResourceBrowserSelector.spec.tsx +219 -0
  47. package/src/ui/Fields/ResourceBrowserSelector/ResourceBrowserSelector.tsx +109 -0
  48. package/src/utils/converters/mocks/squizNodeJson.mock.ts +21 -0
  49. package/src/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.ts +10 -0
  50. package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.ts +8 -0
  51. package/src/utils/getNodeNamesByGroup.spec.ts +1 -0
  52. package/tests/index.ts +1 -0
  53. package/tests/mockResourceBrowser.tsx +46 -0
@@ -0,0 +1,219 @@
1
+ import '@testing-library/jest-dom';
2
+ import React from 'react';
3
+ import { fireEvent, render, screen, waitFor } from '@testing-library/react';
4
+ import { ResourceBrowserResource } from '@squiz/resource-browser';
5
+ import { ResourceBrowserSelector } from './ResourceBrowserSelector';
6
+ import { mockResourceBrowser } from '../../../../tests';
7
+
8
+ const resourceBrowserSpy = jest.fn();
9
+ const { setSelectedResource, setShouldUseMockResourceBrowser, mockResourceBrowserImpl } = mockResourceBrowser();
10
+ jest.mock('@squiz/resource-browser', () => ({
11
+ ...jest.requireActual('@squiz/resource-browser'),
12
+ ResourceBrowser: (props: any) => {
13
+ resourceBrowserSpy(props);
14
+ return mockResourceBrowserImpl(props);
15
+ },
16
+ }));
17
+ setShouldUseMockResourceBrowser(true);
18
+
19
+ describe('ResourceBrowserSelector', () => {
20
+ it('Renders empty state when no value is provided', async () => {
21
+ render(<ResourceBrowserSelector modalTitle="Insert asset" onChange={jest.fn()} />);
22
+
23
+ expect(resourceBrowserSpy).toHaveBeenCalledWith(
24
+ expect.objectContaining({
25
+ value: null,
26
+ }),
27
+ );
28
+ });
29
+
30
+ it('AssetImage: Renders a selected state when a value is provided', async () => {
31
+ render(
32
+ <ResourceBrowserSelector
33
+ modalTitle="Insert asset"
34
+ value={{
35
+ matrixIdentifier: 'matrix-api-identifier',
36
+ matrixAssetId: 'my-resource-id',
37
+ addional: 'addditional data',
38
+ }}
39
+ onChange={jest.fn()}
40
+ />,
41
+ );
42
+
43
+ expect(resourceBrowserSpy).toHaveBeenCalledWith(
44
+ expect.objectContaining({
45
+ value: {
46
+ resourceId: 'my-resource-id',
47
+ sourceId: 'matrix-api-identifier',
48
+ },
49
+ }),
50
+ );
51
+ });
52
+
53
+ it('DAMImage: Renders a selected state when a value is provided', async () => {
54
+ render(
55
+ <ResourceBrowserSelector
56
+ modalTitle="Insert asset"
57
+ value={{
58
+ damObjectId: '123-456-789',
59
+ damSystemIdentifier: 'zzz-xxx-ccc',
60
+ addional: 'addditional data',
61
+ }}
62
+ onChange={jest.fn()}
63
+ />,
64
+ );
65
+
66
+ expect(resourceBrowserSpy).toHaveBeenCalledWith(
67
+ expect.objectContaining({
68
+ value: {
69
+ resourceId: '123-456-789',
70
+ sourceId: 'zzz-xxx-ccc',
71
+ },
72
+ }),
73
+ );
74
+ });
75
+
76
+ it('AssetImage: Calls onChange with expected value when resources is selected', async () => {
77
+ const handleChange = jest.fn();
78
+ setSelectedResource({
79
+ id: 'my-resource-id',
80
+ name: 'My resource',
81
+ url: 'myResourceUrl',
82
+ source: {
83
+ id: 'my-source-id',
84
+ type: 'matrix',
85
+ },
86
+ } as unknown as ResourceBrowserResource);
87
+
88
+ render(
89
+ <ResourceBrowserSelector
90
+ modalTitle="Insert asset"
91
+ value={{ matrixIdentifier: undefined, matrixAssetId: undefined, additional: 'additional data' }}
92
+ onChange={handleChange}
93
+ />,
94
+ );
95
+
96
+ // Trigger the onChange handler to be called
97
+ await waitFor(() => {
98
+ expect(screen.getByRole('button', { name: 'Select ResourceBrowser Resource' })).toBeInTheDocument();
99
+ });
100
+ fireEvent.click(await screen.findByRole('button', { name: 'Select ResourceBrowser Resource' }));
101
+
102
+ expect(handleChange).toHaveBeenCalledWith({
103
+ target: {
104
+ value: {
105
+ additional: 'additional data',
106
+ matrixAssetId: 'my-resource-id',
107
+ matrixIdentifier: 'my-source-id',
108
+ url: 'myResourceUrl',
109
+ nodeType: 'assetImage',
110
+ },
111
+ },
112
+ });
113
+ });
114
+
115
+ it('DAMImage: Calls onChange with expected value when resources is selected', async () => {
116
+ const handleChange = jest.fn();
117
+ setSelectedResource({
118
+ id: 'my-resource-id',
119
+ name: 'My resource',
120
+ url: 'myResourceUrl',
121
+ source: {
122
+ id: 'my-source-id',
123
+ type: 'dam',
124
+ configuration: {
125
+ externalType: 'bynder',
126
+ },
127
+ },
128
+ } as unknown as ResourceBrowserResource);
129
+
130
+ render(
131
+ <ResourceBrowserSelector
132
+ modalTitle="Insert asset"
133
+ value={{ matrixIdentifier: undefined, matrixAssetId: undefined, additional: 'additional data' }}
134
+ onChange={handleChange}
135
+ />,
136
+ );
137
+
138
+ // Trigger the onChange handler to be called
139
+ await waitFor(() => {
140
+ expect(screen.getByRole('button', { name: 'Select ResourceBrowser Resource' })).toBeInTheDocument();
141
+ });
142
+ fireEvent.click(await screen.findByRole('button', { name: 'Select ResourceBrowser Resource' }));
143
+
144
+ expect(handleChange).toHaveBeenCalledWith({
145
+ target: {
146
+ value: {
147
+ additional: 'additional data',
148
+ damObjectId: 'my-resource-id',
149
+ damSystemIdentifier: 'my-source-id',
150
+ damSystemType: 'bynder',
151
+ url: 'myResourceUrl',
152
+ nodeType: 'DAMImage',
153
+ },
154
+ },
155
+ });
156
+ });
157
+
158
+ it('ImageAsset: Calls onChange with expected value when resources is cleared', async () => {
159
+ const handleChange = jest.fn();
160
+ setSelectedResource(null);
161
+
162
+ render(
163
+ <ResourceBrowserSelector
164
+ modalTitle="Insert asset"
165
+ value={{
166
+ matrixIdentifier: 'my-source-id',
167
+ matrixAssetId: 'my-resource-id',
168
+ additional: 'additional data',
169
+ }}
170
+ onChange={handleChange}
171
+ />,
172
+ );
173
+
174
+ // Trigger the onChange handler to be called
175
+ await waitFor(() => {
176
+ expect(screen.getByRole('button', { name: 'Select ResourceBrowser Resource' })).toBeInTheDocument();
177
+ });
178
+ fireEvent.click(await screen.findByRole('button', { name: 'Select ResourceBrowser Resource' }));
179
+
180
+ expect(handleChange).toHaveBeenCalledWith({
181
+ target: {
182
+ value: {
183
+ additional: 'additional data',
184
+ },
185
+ },
186
+ });
187
+ });
188
+
189
+ it('DAMImage: Calls onChange with expected value when resources is cleared', async () => {
190
+ const handleChange = jest.fn();
191
+ setSelectedResource(null);
192
+
193
+ render(
194
+ <ResourceBrowserSelector
195
+ modalTitle="Insert asset"
196
+ value={{
197
+ damObjectId: 'my-resource-id',
198
+ damSystemIdentifier: 'my-source-id',
199
+ additional: 'additional data',
200
+ }}
201
+ onChange={handleChange}
202
+ />,
203
+ );
204
+
205
+ // Trigger the onChange handler to be called
206
+ await waitFor(() => {
207
+ expect(screen.getByRole('button', { name: 'Select ResourceBrowser Resource' })).toBeInTheDocument();
208
+ });
209
+ fireEvent.click(await screen.findByRole('button', { name: 'Select ResourceBrowser Resource' }));
210
+
211
+ expect(handleChange).toHaveBeenCalledWith({
212
+ target: {
213
+ value: {
214
+ additional: 'additional data',
215
+ },
216
+ },
217
+ });
218
+ });
219
+ });
@@ -0,0 +1,109 @@
1
+ import React, { useCallback } from 'react';
2
+ import { ResourceBrowserResource, ResourceBrowser } from '@squiz/resource-browser';
3
+ import { DamResourceBrowserSource } from '@squiz/dam-resource-browser-plugin';
4
+ import { InputContainer, InputContainerProps } from '../InputContainer/InputContainer';
5
+ import { NodeName } from '../../../Extensions/Extensions';
6
+
7
+ type MatrixAsset = {
8
+ matrixIdentifier?: string;
9
+ matrixAssetId?: string;
10
+ };
11
+ type DAMAsset = {
12
+ damSystemIdentifier?: string;
13
+ damObjectId?: string;
14
+ damSystemType?: string;
15
+ };
16
+ type ResourceBrowserSelectorValue = MatrixAsset &
17
+ DAMAsset & {
18
+ nodeType?: NodeName;
19
+ url?: string;
20
+ };
21
+
22
+ export type ResourceBrowserSelectorProps<T extends ResourceBrowserSelectorValue> = Omit<
23
+ InputContainerProps,
24
+ 'children'
25
+ > & {
26
+ modalTitle: string;
27
+ allowedTypes?: string[];
28
+ value?: T | null;
29
+ // LinkForm contains a "target" property.
30
+ // react-hook-form treats the presence of this property as an "Event" object being passed through, see:
31
+ // https://github.com/react-hook-form/react-hook-form/blob/master/src/logic/getEventValue.ts
32
+ // Nest the value under a "target" object to work around the behaviour.
33
+ onChange: (value: { target: { value: T } }) => void;
34
+ };
35
+
36
+ export const ResourceBrowserSelector = <T extends ResourceBrowserSelectorValue>({
37
+ modalTitle,
38
+ allowedTypes,
39
+ value,
40
+ onChange,
41
+ ...props
42
+ }: ResourceBrowserSelectorProps<T>) => {
43
+ const convertFormDataToResourceBrowserValue = useCallback((value: T | null) => {
44
+ if (value?.matrixIdentifier && value?.matrixAssetId) {
45
+ return {
46
+ sourceId: value.matrixIdentifier,
47
+ resourceId: value.matrixAssetId,
48
+ };
49
+ } else if (value?.damSystemIdentifier && value?.damObjectId) {
50
+ return {
51
+ sourceId: value.damSystemIdentifier,
52
+ resourceId: value.damObjectId,
53
+ };
54
+ }
55
+
56
+ return null;
57
+ }, []);
58
+
59
+ const handleResourceChange = useCallback((resource: ResourceBrowserResource | null) => {
60
+ // Clear out any key properties for clear resource use case
61
+ let onChangeData: ResourceBrowserSelectorValue = {
62
+ ...value,
63
+ matrixIdentifier: undefined,
64
+ matrixAssetId: undefined,
65
+ damSystemIdentifier: undefined,
66
+ damObjectId: undefined,
67
+ damSystemType: undefined,
68
+ url: undefined,
69
+ };
70
+
71
+ if (resource?.source?.type === 'matrix') {
72
+ onChangeData = {
73
+ ...value,
74
+ matrixIdentifier: resource?.source?.id,
75
+ matrixAssetId: resource?.id,
76
+ url: resource?.url,
77
+ nodeType: NodeName.AssetImage,
78
+ };
79
+ } else if (resource?.source?.type === 'dam') {
80
+ onChangeData = {
81
+ ...value,
82
+ damSystemIdentifier: resource?.source?.id,
83
+ damObjectId: resource?.id,
84
+ damSystemType: (resource?.source as DamResourceBrowserSource).configuration.externalType,
85
+ url: resource?.url,
86
+ nodeType: NodeName.DAMImage,
87
+ };
88
+ }
89
+
90
+ onChange({
91
+ target: {
92
+ value: {
93
+ ...onChangeData,
94
+ } as T,
95
+ },
96
+ });
97
+ }, []);
98
+
99
+ return (
100
+ <InputContainer {...props}>
101
+ <ResourceBrowser
102
+ modalTitle={modalTitle}
103
+ allowedTypes={allowedTypes}
104
+ value={convertFormDataToResourceBrowserValue(value as T)}
105
+ onChange={handleResourceChange}
106
+ />
107
+ </InputContainer>
108
+ );
109
+ };
@@ -164,6 +164,27 @@ export const sharedNodeExamples: NodeExample[] = [
164
164
  },
165
165
  ],
166
166
  },
167
+ {
168
+ description: 'DAM image',
169
+ remirrorNode: {
170
+ type: 'DAMImage',
171
+ attrs: {
172
+ damObjectId: '123-456-789',
173
+ damSystemIdentifier: 'zzz-xxx-ccc',
174
+ damSystemType: 'bynder',
175
+ damAdditional: JSON.stringify({ variant: 'xyz' }),
176
+ },
177
+ },
178
+ squizNode: [
179
+ {
180
+ type: 'dam-image',
181
+ damObjectId: '123-456-789',
182
+ damSystemIdentifier: 'zzz-xxx-ccc',
183
+ damSystemType: 'bynder',
184
+ damAdditional: { variant: 'xyz' },
185
+ },
186
+ ],
187
+ },
167
188
  ];
168
189
 
169
190
  export const squizOnlyNodeExamples: NodeExample[] = [
@@ -157,6 +157,16 @@ const transformNode = (node: ProsemirrorNode): FormattedNode => {
157
157
  };
158
158
  }
159
159
 
160
+ if (node.type.name === NodeName.DAMImage) {
161
+ transformedNode = {
162
+ type: 'dam-image',
163
+ damObjectId: node.attrs.damObjectId,
164
+ damSystemIdentifier: node.attrs.damSystemIdentifier,
165
+ damSystemType: node.attrs.damSystemType,
166
+ damAdditional: node.attrs.damAdditional ? JSON.parse(node.attrs.damAdditional) : undefined,
167
+ };
168
+ }
169
+
160
170
  node.marks.forEach((mark) => {
161
171
  transformedNode = transformMark(mark, transformedNode);
162
172
  });
@@ -11,6 +11,7 @@ const getNodeType = (node: FormattedNodes): string => {
11
11
  const typeMap: Record<string, string> = {
12
12
  'link-to-matrix-asset': NodeName.Text,
13
13
  'matrix-image': NodeName.AssetImage,
14
+ 'dam-image': NodeName.DAMImage,
14
15
  text: 'text',
15
16
  };
16
17
 
@@ -95,6 +96,13 @@ const getNodeAttributes = (node: FormattedNodes): Attrs => {
95
96
  matrixDomain: node.matrixDomain,
96
97
  matrixIdentifier: node.matrixIdentifier,
97
98
  };
99
+ } else if (node.type === 'dam-image') {
100
+ return {
101
+ damObjectId: node.damObjectId,
102
+ damSystemIdentifier: node.damSystemIdentifier,
103
+ damSystemType: node.damSystemType,
104
+ damAdditional: node.damAdditional ? JSON.stringify(node.damAdditional) : undefined,
105
+ };
98
106
  } else if (node.type === 'tag') {
99
107
  return {
100
108
  nodeIndent: null,
@@ -17,6 +17,7 @@ describe('getNodeNamesByGroup', () => {
17
17
  expect(formattingNodeNames).toEqual(['paragraph', 'heading', 'preformatted']);
18
18
  expect(otherNodeNames).toEqual([
19
19
  'assetImage',
20
+ 'DAMImage',
20
21
  'doc',
21
22
  'text',
22
23
  'tableControllerCell',
package/tests/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './renderWithEditor';
2
2
  export * from './renderWithContext';
3
3
  export * from './mockResourceBrowserContext';
4
+ export * from './mockResourceBrowser';
4
5
  export * from './select';
@@ -0,0 +1,46 @@
1
+ import React from 'react';
2
+ import { ResourceBrowserResource, ResourceBrowserProps } from '@squiz/resource-browser';
3
+
4
+ /*
5
+ When testing using the DAM plugin this needs to be mocked as its data fetch
6
+ logic is internal, but most of the tests in this repo were written for the Matrix plugin
7
+ which can be controlled externally.
8
+
9
+ So this is mocked in this way so the original tests can run against the actual Resource Browser
10
+ and the DAMImage tests can be run against a completely mocked one.
11
+ */
12
+ export const mockResourceBrowser = () => {
13
+ let shouldUseMockResourceBrowser = false;
14
+ let selectedResource: ResourceBrowserResource | null = null;
15
+ const actual = jest.requireActual('@squiz/resource-browser');
16
+
17
+ return {
18
+ setSelectedResource: (value: ResourceBrowserResource | null) => {
19
+ selectedResource = value;
20
+ },
21
+ setShouldUseMockResourceBrowser: (value: boolean) => {
22
+ shouldUseMockResourceBrowser = value;
23
+ },
24
+ mockResourceBrowserImpl: (props: ResourceBrowserProps) => {
25
+ if (!shouldUseMockResourceBrowser) {
26
+ return actual.ResourceBrowser(props);
27
+ }
28
+
29
+ // This would be easier if this repo allowed 'var' statements given jest mocks don't function easily without them
30
+ return (
31
+ <div>
32
+ ResourceBrowser was rendered
33
+ <button
34
+ id="selectResource"
35
+ type="button"
36
+ onClick={() => {
37
+ props.onChange(selectedResource);
38
+ }}
39
+ >
40
+ Select ResourceBrowser Resource
41
+ </button>
42
+ </div>
43
+ );
44
+ },
45
+ };
46
+ };