@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,28 @@
1
+ import React from 'react';
2
+ import { InputContainerProps } from '../InputContainer/InputContainer';
3
+ import { NodeName } from '../../../Extensions/Extensions';
4
+ type MatrixAsset = {
5
+ matrixIdentifier?: string;
6
+ matrixAssetId?: string;
7
+ };
8
+ type DAMAsset = {
9
+ damSystemIdentifier?: string;
10
+ damObjectId?: string;
11
+ damSystemType?: string;
12
+ };
13
+ type ResourceBrowserSelectorValue = MatrixAsset & DAMAsset & {
14
+ nodeType?: NodeName;
15
+ url?: string;
16
+ };
17
+ export type ResourceBrowserSelectorProps<T extends ResourceBrowserSelectorValue> = Omit<InputContainerProps, 'children'> & {
18
+ modalTitle: string;
19
+ allowedTypes?: string[];
20
+ value?: T | null;
21
+ onChange: (value: {
22
+ target: {
23
+ value: T;
24
+ };
25
+ }) => void;
26
+ };
27
+ export declare const ResourceBrowserSelector: <T extends ResourceBrowserSelectorValue>({ modalTitle, allowedTypes, value, onChange, ...props }: ResourceBrowserSelectorProps<T>) => React.JSX.Element;
28
+ export {};
@@ -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.0.1",
3
+ "version": "2.2.0",
4
4
  "main": "lib/index.js",
5
5
  "types": "lib/index.d.ts",
6
6
  "private": false,
@@ -24,9 +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
+ "@squiz/dxp-ai-client-react": "^0.1.3-alpha",
28
29
  "@squiz/matrix-resource-browser-plugin": "^2.0.0",
29
30
  "@squiz/resource-browser": "^2.0.0",
31
+ "@squiz/sds": "^1.0.0-alpha.50",
32
+ "@squiz/dam-resource-browser-plugin": "^0.9.0-rc.0",
30
33
  "clsx": "2.1.1",
31
34
  "react-hook-form": "7.51.4",
32
35
  "react-image-size": "2.0.0",
@@ -56,6 +59,7 @@
56
59
  "react": "18.2.0",
57
60
  "react-diff-viewer-continued": "3.2.6",
58
61
  "react-dom": "18.2.0",
62
+ "react-query": "^3.19.6",
59
63
  "rimraf": "5.0.7",
60
64
  "tailwindcss": "3.2.6",
61
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
@@ -15,6 +15,7 @@ import ClearFormattingButton from './Tools/ClearFormatting/ClearFormattingButton
15
15
  import ListButtons from './Tools/Lists/ListButtons';
16
16
  import HorizontalLineButton from './Tools/HorizontalLine/HorizontalLineButton';
17
17
  import TableButton from './Tools/Table/TableButton';
18
+ import ContentToolsDropdown from './Tools/ContentTools/ContentToolsDropdown';
18
19
  import { useExtensionNames } from '../hooks';
19
20
 
20
21
  type ToolbarProps = {
@@ -54,6 +55,7 @@ export const Toolbar = ({ isVisible, enableTableTool }: ToolbarProps) => {
54
55
  {extensionNames.image && <ImageButton />}
55
56
  {extensionNames.clearFormatting && <ClearFormattingButton />}
56
57
  {enableTableTool && extensionNames.table && <TableButton />}
58
+ <ContentToolsDropdown />
57
59
  </div>
58
60
  </RemirrorToolbar>
59
61
  );
@@ -0,0 +1,78 @@
1
+ import '@testing-library/jest-dom';
2
+ import { fireEvent } from '@testing-library/react';
3
+ import React from 'react';
4
+ import { useAiService } from '@squiz/dxp-ai-client-react';
5
+ import ContentToolsDropdown from './ContentToolsDropdown';
6
+ import { renderWithContext } from '../../../../tests';
7
+
8
+ // Mock the module
9
+ jest.mock('@squiz/dxp-ai-client-react', () => ({
10
+ useAiService: jest.fn(),
11
+ }));
12
+
13
+ // Cast useAiService to a Jest mock
14
+ const mockedUseAiService = useAiService as jest.MockedFunction<typeof useAiService>;
15
+
16
+ describe('Content tools dropdown', () => {
17
+ it('should not render the content tools dropdown component when there are no content tools', () => {
18
+ mockedUseAiService.mockReturnValue({
19
+ contentTools: [],
20
+ } as any);
21
+
22
+ const { baseElement } = renderWithContext(<ContentToolsDropdown />);
23
+
24
+ expect(baseElement).toBeTruthy();
25
+ expect(baseElement.querySelector('.sds-dropdown.active')).toBeFalsy();
26
+ });
27
+
28
+ it('should render the content tools dropdown when there are content tools', () => {
29
+ mockedUseAiService.mockReturnValue({
30
+ contentTools: [
31
+ {
32
+ id: 'unique-id',
33
+ name: 'Testing content tools',
34
+ },
35
+ ],
36
+ } as any);
37
+
38
+ const { baseElement } = renderWithContext(<ContentToolsDropdown />);
39
+
40
+ expect(baseElement).toBeTruthy();
41
+ expect(baseElement.querySelector(`.sds-dropdown.active`)).toBeFalsy();
42
+
43
+ const launchButton = baseElement.querySelector(`.sds-button`) as HTMLButtonElement;
44
+ expect(launchButton).toBeTruthy();
45
+
46
+ fireEvent.click(launchButton);
47
+ expect(baseElement.querySelector(`.sds-dropdown.active`)).toBeTruthy();
48
+ });
49
+
50
+ it('should handle when a content tool is clicked', () => {
51
+ mockedUseAiService.mockReturnValue({
52
+ contentTools: [
53
+ {
54
+ id: 'unique-id',
55
+ name: 'Testing content tools',
56
+ },
57
+ ],
58
+ } as any);
59
+
60
+ const { baseElement } = renderWithContext(<ContentToolsDropdown />);
61
+
62
+ expect(baseElement).toBeTruthy();
63
+ expect(baseElement.querySelector(`.sds-dropdown.active`)).toBeFalsy();
64
+
65
+ const launchButton = baseElement.querySelector(`.sds-button`) as HTMLButtonElement;
66
+ expect(launchButton).toBeTruthy();
67
+
68
+ fireEvent.click(launchButton);
69
+ expect(baseElement.querySelector(`.sds-dropdown.active`)).toBeTruthy();
70
+
71
+ const actionItems = baseElement.querySelectorAll(`.sds-dropdown-button`);
72
+ const firstItem = actionItems[0] as HTMLButtonElement;
73
+ expect(firstItem.textContent).toBe('Testing content tools');
74
+
75
+ fireEvent.click(firstItem);
76
+ expect(baseElement.querySelector(`.sds-dropdown.active`)).toBeFalsy();
77
+ });
78
+ });
@@ -0,0 +1,46 @@
1
+ import React from 'react';
2
+ import { ICON_AI } from '../../../Icons/AiIcon';
3
+ import { BUTTON_FORMAT_TRANSPARENT, BUTTON_THEME_DEFAULT, DROPDOWN_POSITION_RIGHT, Dropdown } from '@squiz/sds';
4
+ import { useAiService } from '@squiz/dxp-ai-client-react';
5
+ import { VerticalDivider } from '@remirror/react';
6
+
7
+ const ContentToolsDropdown = () => {
8
+ const { contentTools } = useAiService();
9
+
10
+ const dropdownItems = contentTools.map((item) => ({
11
+ items: [
12
+ {
13
+ action: () => alert(JSON.stringify(item, null, 2)),
14
+ key: item.id,
15
+ label: <span>{item.name}</span>,
16
+ },
17
+ ],
18
+ key: item.id,
19
+ }));
20
+
21
+ // No content tools to show, don't show dropdown at all
22
+ if (contentTools.length === 0) {
23
+ return null;
24
+ }
25
+
26
+ return (
27
+ <>
28
+ <VerticalDivider />
29
+ <Dropdown
30
+ title="Content tools"
31
+ aria-label="Content tools"
32
+ buttonProps={{
33
+ format: BUTTON_FORMAT_TRANSPARENT,
34
+ icon: ICON_AI,
35
+ theme: BUTTON_THEME_DEFAULT,
36
+ }}
37
+ className="content-tools-dropdown"
38
+ dropdownPosition={DROPDOWN_POSITION_RIGHT}
39
+ heading={'Rewrite to...'}
40
+ sections={dropdownItems}
41
+ />
42
+ </>
43
+ );
44
+ };
45
+
46
+ export default ContentToolsDropdown;
@@ -0,0 +1,45 @@
1
+ .content-tools-dropdown {
2
+ .sds-button-icon svg {
3
+ fill: none !important;
4
+ }
5
+
6
+ .sds-dropdown {
7
+ left: auto;
8
+ right: auto;
9
+ top: auto;
10
+
11
+ width: 188px;
12
+ margin-top: 0.45rem;
13
+ margin-left: -2rem;
14
+
15
+ .sds-dropdown-heading {
16
+ font-size: 0.8125rem;
17
+ font-weight: 600;
18
+ letter-spacing: 0;
19
+ line-height: 1rem;
20
+ background-color: #f5f5f5;
21
+ border-radius: 0.25rem;
22
+ padding: 0.25rem 0.5rem;
23
+ pointer-events: none;
24
+ text-transform: none;
25
+ }
26
+
27
+ .sds-dropdown-section {
28
+ border-top: 1px solid #ededed;
29
+ border-bottom: none;
30
+ padding-top: 0.25rem;
31
+ margin-bottom: 0;
32
+
33
+ .sds-dropdown-label {
34
+ font-size: 0.8125rem;
35
+ color: #4f4f4f;
36
+ line-height: 1rem;
37
+ }
38
+ }
39
+
40
+ &.active {
41
+ display: block;
42
+ padding: 0.25rem;
43
+ }
44
+ }
45
+ }
@@ -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
  )}