@squiz/formatted-text-editor 1.33.1-alpha.3 → 1.33.1-alpha.5

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 (57) hide show
  1. package/demo/App.tsx +4 -24
  2. package/demo/AppContext.tsx +28 -0
  3. package/demo/index.scss +0 -2
  4. package/demo/main.tsx +2 -0
  5. package/demo/resources.json +28 -0
  6. package/demo/sources.json +23 -0
  7. package/lib/Editor/EditorContext.d.ts +0 -7
  8. package/lib/Editor/EditorContext.js +0 -2
  9. package/lib/EditorToolbar/Tools/Image/Form/ImageForm.js +8 -19
  10. package/lib/EditorToolbar/Tools/Link/Form/LinkForm.js +6 -38
  11. package/lib/EditorToolbar/Tools/Link/RemoveLinkButton.js +1 -1
  12. package/lib/Extensions/Extensions.js +0 -2
  13. package/lib/Extensions/ImageExtension/AssetImageExtension.d.ts +0 -1
  14. package/lib/Extensions/ImageExtension/AssetImageExtension.js +1 -2
  15. package/lib/Extensions/LinkExtension/AssetLinkExtension.d.ts +0 -1
  16. package/lib/Extensions/LinkExtension/AssetLinkExtension.js +1 -2
  17. package/lib/index.css +7 -2
  18. package/lib/types.d.ts +3 -3
  19. package/lib/ui/Fields/Checkbox/Checkbox.js +1 -1
  20. package/lib/ui/Fields/Input/Input.d.ts +2 -4
  21. package/lib/ui/Fields/Input/Input.js +3 -9
  22. package/lib/ui/Fields/InputContainer/InputContainer.d.ts +9 -0
  23. package/lib/ui/Fields/InputContainer/InputContainer.js +16 -0
  24. package/lib/ui/Fields/MatrixAsset/MatrixAsset.d.ts +17 -0
  25. package/lib/ui/Fields/MatrixAsset/MatrixAsset.js +29 -0
  26. package/lib/utils/validation.d.ts +2 -1
  27. package/lib/utils/validation.js +8 -2
  28. package/package.json +4 -3
  29. package/src/Editor/Editor.spec.tsx +1 -1
  30. package/src/Editor/EditorContext.spec.tsx +11 -13
  31. package/src/Editor/EditorContext.ts +0 -11
  32. package/src/EditorToolbar/Tools/Image/Form/ImageForm.spec.tsx +27 -10
  33. package/src/EditorToolbar/Tools/Image/Form/ImageForm.tsx +25 -29
  34. package/src/EditorToolbar/Tools/Image/ImageButton.spec.tsx +69 -43
  35. package/src/EditorToolbar/Tools/Link/Form/LinkForm.spec.tsx +15 -5
  36. package/src/EditorToolbar/Tools/Link/Form/LinkForm.tsx +14 -20
  37. package/src/EditorToolbar/Tools/Link/LinkButton.spec.tsx +45 -29
  38. package/src/EditorToolbar/Tools/Link/RemoveLinkButton.spec.tsx +47 -4
  39. package/src/EditorToolbar/Tools/Link/RemoveLinkButton.tsx +3 -2
  40. package/src/Extensions/Extensions.ts +0 -2
  41. package/src/Extensions/ImageExtension/AssetImageExtension.ts +1 -3
  42. package/src/Extensions/LinkExtension/AssetLinkExtension.ts +1 -3
  43. package/src/types.ts +7 -5
  44. package/src/ui/Fields/Checkbox/Checkbox.tsx +1 -1
  45. package/src/ui/Fields/Input/Input.tsx +4 -18
  46. package/src/ui/Fields/InputContainer/InputContainer.spec.tsx +18 -0
  47. package/src/ui/Fields/InputContainer/InputContainer.tsx +29 -0
  48. package/src/ui/Fields/MatrixAsset/MatrixAsset.spec.tsx +103 -0
  49. package/src/ui/Fields/MatrixAsset/MatrixAsset.tsx +55 -0
  50. package/src/ui/_forms.scss +4 -2
  51. package/src/utils/validation.spec.ts +22 -0
  52. package/src/utils/validation.ts +9 -1
  53. package/tests/index.ts +2 -0
  54. package/tests/mockResourceBrowserContext.tsx +63 -0
  55. package/tests/renderWithContext.tsx +18 -0
  56. package/tests/renderWithEditor.tsx +18 -21
  57. package/vite.config.ts +8 -0
@@ -1,26 +1,24 @@
1
- import React, { useContext } from 'react';
2
- import { EditorContext } from './EditorContext';
1
+ import React from 'react';
2
+ import { EditorContext, EditorContextOptions } from './EditorContext';
3
3
  import { render } from '@testing-library/react';
4
4
 
5
5
  describe('EditorContext', () => {
6
- const defaultContextFn = jest.fn();
7
- const Component = () => {
8
- defaultContextFn(useContext(EditorContext));
9
- return null;
10
- };
11
-
12
6
  it('Has expected defaults', async () => {
13
- render(<Component />);
7
+ let defaultContext: EditorContextOptions | null = null;
14
8
 
15
- const defaultContext = defaultContextFn.mock.calls[0][0];
9
+ render(
10
+ <EditorContext.Consumer>
11
+ {(value) => {
12
+ defaultContext = value;
13
+ return null;
14
+ }}
15
+ </EditorContext.Consumer>,
16
+ );
16
17
 
17
18
  expect(defaultContext).toEqual({
18
19
  matrix: {
19
- resolveMatrixAsset: expect.any(Function),
20
20
  matrixDomain: '',
21
- matrixIdentifier: '',
22
21
  },
23
22
  });
24
- expect(await defaultContext.matrix.resolveMatrixAsset('fake-asset-id')).toBeNull();
25
23
  });
26
24
  });
@@ -1,25 +1,14 @@
1
1
  import React from 'react';
2
2
 
3
- export type MatrixAsset = {
4
- id: string;
5
- type: string | 'image';
6
- };
7
-
8
- export type MatrixAssetResolver = (assetId: string) => Promise<MatrixAsset | null>;
9
-
10
3
  export type EditorContextOptions = {
11
4
  matrix: {
12
- matrixIdentifier: string;
13
5
  matrixDomain: string;
14
- resolveMatrixAsset: MatrixAssetResolver;
15
6
  };
16
7
  };
17
8
 
18
9
  export const defaultEditorContext: EditorContextOptions = {
19
10
  matrix: {
20
- matrixIdentifier: '',
21
11
  matrixDomain: '',
22
- resolveMatrixAsset: () => Promise.resolve(null),
23
12
  },
24
13
  };
25
14
 
@@ -3,6 +3,7 @@ import { render, screen, act, fireEvent } 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
7
 
7
8
  describe('Image Form', () => {
8
9
  const handleSubmit = jest.fn();
@@ -26,20 +27,36 @@ describe('Image Form', () => {
26
27
  expect(screen.getByLabelText('Height')).toHaveValue(1400);
27
28
  });
28
29
 
29
- it('Renders the form with the relevant fields for asset images', () => {
30
+ it('Renders the form with the relevant fields for asset images', async () => {
31
+ const { MockResourceBrowserContext } = mockResourceBrowserContext({
32
+ sources: [{ id: 'my-source-id' }],
33
+ resources: [
34
+ {
35
+ id: '100',
36
+ name: 'My selected image',
37
+ type: {
38
+ code: 'image',
39
+ name: 'Image',
40
+ },
41
+ },
42
+ ],
43
+ });
44
+
30
45
  render(
31
- <ImageForm
32
- data={{
33
- ...data,
34
- imageType: NodeName.AssetImage,
35
- assetImage: { matrixAssetId: '100' },
36
- }}
37
- onSubmit={handleSubmit}
38
- />,
46
+ <MockResourceBrowserContext>
47
+ <ImageForm
48
+ data={{
49
+ ...data,
50
+ imageType: NodeName.AssetImage,
51
+ assetImage: { matrixAssetId: '100', matrixIdentifier: 'matrix-identifier' },
52
+ }}
53
+ onSubmit={handleSubmit}
54
+ />
55
+ </MockResourceBrowserContext>,
39
56
  );
40
57
 
41
58
  expect(document.querySelector('div[data-headlessui-state="selected"]')).toHaveTextContent('From source');
42
- expect(screen.getByLabelText('Asset ID')).toHaveValue('100');
59
+ expect(screen.getByText('My selected image')).toBeInTheDocument();
43
60
  });
44
61
 
45
62
  it('calculates the height when width changes and aspect ratio is locked', () => {
@@ -1,18 +1,18 @@
1
- import React, { ReactElement, useContext, useState } from 'react';
2
- import { Input } from '../../../../ui/Fields/Input/Input';
3
- import { SubmitHandler, useForm } from 'react-hook-form';
1
+ import React, { ReactElement, useState } from 'react';
2
+ import { Controller, SubmitHandler, useForm } from 'react-hook-form';
4
3
  import { getImageSize } from 'react-image-size';
4
+ import clsx from 'clsx';
5
+ import { Input } from '../../../../ui/Fields/Input/Input';
5
6
  import { ImageAttributes } from '@remirror/extension-image/dist-types/image-extension';
6
7
  import Button from '../../../../ui/Button/Button';
7
8
  import LinkOffIcon from '@mui/icons-material/LinkOff';
8
9
  import InsertLinkRoundedIcon from '@mui/icons-material/InsertLinkRounded';
9
- import clsx from 'clsx';
10
10
  import { NodeName } from '../../../../Extensions/Extensions';
11
11
  import { AssetImageAttributes } from '../../../../Extensions/ImageExtension/AssetImageExtension';
12
12
  import { DeepPartial } from '../../../../types';
13
- import { EditorContext } from '../../../../Editor/EditorContext';
14
- import { noEmptySpacesValidation, regexDataURI } from '../../../../utils/validation';
13
+ import { noEmptySpacesValidation, regexDataURI, hasProperties } from '../../../../utils/validation';
15
14
  import { TabOptions, Tabs } from '../../../../ui/Tabs/Tabs';
15
+ import { MatrixAsset } from '../../../../ui/Fields/MatrixAsset/MatrixAsset';
16
16
 
17
17
  export type ImageFormData = {
18
18
  imageType: NodeName;
@@ -33,6 +33,7 @@ export type Dimensions = 'image.width' | 'image.height';
33
33
 
34
34
  const ImageForm = ({ data, onSubmit }: FormProps): ReactElement => {
35
35
  const {
36
+ control,
36
37
  register,
37
38
  handleSubmit,
38
39
  setValue,
@@ -42,7 +43,6 @@ const ImageForm = ({ data, onSubmit }: FormProps): ReactElement => {
42
43
  defaultValues: data,
43
44
  });
44
45
  const imageType = watch('imageType') || NodeName.AssetImage;
45
- const context = useContext(EditorContext);
46
46
  const [aspectRatioFromWidth, setAspectRatioFromWidth] = useState(9 / 16);
47
47
  const [aspectRatioFromHeight, setAspectRatioFromHeight] = useState(16 / 9);
48
48
  const [aspectRatioLocked, setAspectRatioLocked] = useState(true);
@@ -175,28 +175,24 @@ const ImageForm = ({ data, onSubmit }: FormProps): ReactElement => {
175
175
  </>
176
176
  )}
177
177
  {imageType === NodeName.AssetImage && (
178
- <>
179
- <div className="squiz-fte-form-group mb-2">
180
- <Input
181
- label="Asset ID"
182
- required
183
- error={errors?.assetImage?.matrixAssetId?.message}
184
- {...register('assetImage.matrixAssetId', {
185
- required: 'Asset ID is required',
186
- validate: {
187
- isImage: async (assetId) => {
188
- const asset = await context.matrix.resolveMatrixAsset(assetId);
189
-
190
- if (asset?.type !== 'image') {
191
- return 'Asset ID is invalid or not an image';
192
- }
193
- },
194
- noEmptySpaces: noEmptySpacesValidation,
195
- },
196
- })}
197
- />
198
- </div>
199
- </>
178
+ <div className="squiz-fte-form-group mb-2">
179
+ <Controller
180
+ control={control}
181
+ name="assetImage"
182
+ rules={{
183
+ validate: hasProperties('An image must be selected', ['matrixIdentifier', 'matrixAssetId']),
184
+ }}
185
+ render={({ field: { onChange, value }, fieldState: { error } }) => (
186
+ <MatrixAsset
187
+ modalTitle="Insert image"
188
+ allowedTypes={['image']}
189
+ value={value}
190
+ onChange={onChange}
191
+ error={error?.message}
192
+ />
193
+ )}
194
+ />
195
+ </div>
200
196
  )}
201
197
  </form>
202
198
  );
@@ -2,7 +2,7 @@ import '@testing-library/jest-dom';
2
2
  import { screen, fireEvent, waitForElementToBeRemoved, act, waitFor } from '@testing-library/react';
3
3
  import { NodeSelection } from 'prosemirror-state';
4
4
  import React from 'react';
5
- import { renderWithEditor } from '../../../../tests';
5
+ import { renderWithEditor, mockResourceBrowserContext } from '../../../../tests';
6
6
  import ImageButton from './ImageButton';
7
7
  import { getImageSize } from 'react-image-size';
8
8
 
@@ -29,14 +29,13 @@ describe('ImageButton', () => {
29
29
  });
30
30
 
31
31
  it('Opens the modal when clicking the keyboard shortcut', async () => {
32
- const { editor, elements } = await renderWithEditor(<ImageButton />);
32
+ const { elements } = await renderWithEditor(<ImageButton />);
33
33
 
34
34
  // press the keyboard shortcut.
35
35
  fireEvent.keyDown(elements.editor, { key: 'l', ctrlKey: true });
36
36
 
37
37
  // verify the modal opens
38
- await act(() => editor.selectText(2));
39
- expect(await screen.findByLabelText('Asset ID')).toHaveValue('');
38
+ expect(await screen.findByRole('button', { name: 'Choose image' })).toBeInTheDocument();
40
39
  });
41
40
 
42
41
  it('Adds a new image', async () => {
@@ -237,21 +236,40 @@ describe('ImageButton', () => {
237
236
  it('Adds a new asset image', async () => {
238
237
  const matrixIdentifier = 'matrix-api-identifier';
239
238
  const matrixDomain = 'https://my-matrix.squiz.net';
240
- const { getJsonContent } = await renderWithEditor(<ImageButton />, {
241
- content: 'Some nonsense content here',
242
- context: {
243
- matrix: {
244
- matrixIdentifier,
245
- matrixDomain,
246
- resolveMatrixAsset: () => Promise.resolve({ id: '100', type: 'image' }),
239
+ const { MockResourceBrowserContext, selectResource } = mockResourceBrowserContext({
240
+ sources: [{ id: matrixIdentifier }],
241
+ resources: [
242
+ {
243
+ id: 'image-resource-id',
244
+ name: 'My image resource',
245
+ type: {
246
+ code: 'image',
247
+ name: 'Image',
248
+ },
247
249
  },
248
- },
250
+ ],
249
251
  });
250
252
 
253
+ const { getJsonContent } = await renderWithEditor(
254
+ <MockResourceBrowserContext>
255
+ <ImageButton />
256
+ </MockResourceBrowserContext>,
257
+ {
258
+ content: 'Some nonsense content here',
259
+ context: {
260
+ editor: {
261
+ matrix: {
262
+ matrixDomain,
263
+ },
264
+ },
265
+ },
266
+ },
267
+ );
268
+
251
269
  // open the modal and add an image.
252
270
  await openModal();
253
271
  fireEvent.click(screen.getByRole('button', { name: 'From source' }));
254
- fireEvent.change(screen.getByLabelText('Asset ID'), { target: { value: '100' } });
272
+ await selectResource(screen.getByRole('button', { name: 'Choose image' }), 'My image resource');
255
273
  fireEvent.click(screen.getByRole('button', { name: 'Apply' }));
256
274
 
257
275
  await waitForElementToBeRemoved(() => screen.getByRole('button', { name: 'Apply' }));
@@ -262,7 +280,7 @@ describe('ImageButton', () => {
262
280
  content: [
263
281
  {
264
282
  type: 'assetImage',
265
- attrs: { matrixAssetId: '100', matrixIdentifier, matrixDomain },
283
+ attrs: { matrixAssetId: 'image-resource-id', matrixIdentifier, matrixDomain },
266
284
  },
267
285
  { type: 'text', text: 'Some nonsense content here' },
268
286
  ],
@@ -272,22 +290,41 @@ describe('ImageButton', () => {
272
290
  it('Updates the attributes of an existing asset image', async () => {
273
291
  const matrixIdentifier = 'matrix-api-identifier';
274
292
  const matrixDomain = 'https://my-matrix.squiz.net';
275
- const { editor, getJsonContent } = await renderWithEditor(<ImageButton />, {
276
- content: 'Some <img src="https://httpcats.com/529.jpg" alt="hi" /> nonsense',
277
- context: {
278
- matrix: {
279
- matrixIdentifier,
280
- matrixDomain,
281
- resolveMatrixAsset: () => Promise.resolve({ id: '100', type: 'image' }),
293
+ const { MockResourceBrowserContext, selectResource } = mockResourceBrowserContext({
294
+ sources: [{ id: matrixIdentifier }],
295
+ resources: [
296
+ {
297
+ id: 'image-resource-id',
298
+ name: 'My image resource',
299
+ type: {
300
+ code: 'image',
301
+ name: 'Image',
302
+ },
282
303
  },
283
- },
304
+ ],
284
305
  });
285
306
 
307
+ const { editor, getJsonContent } = await renderWithEditor(
308
+ <MockResourceBrowserContext>
309
+ <ImageButton />
310
+ </MockResourceBrowserContext>,
311
+ {
312
+ content: 'Some <img src="https://httpcats.com/529.jpg" alt="hi" /> nonsense',
313
+ context: {
314
+ editor: {
315
+ matrix: {
316
+ matrixDomain,
317
+ },
318
+ },
319
+ },
320
+ },
321
+ );
322
+
286
323
  await act(() => editor.selectText(new NodeSelection(editor.state.doc.resolve(6))));
287
324
 
288
325
  await openModal();
289
326
  fireEvent.click(screen.getByRole('button', { name: 'From source' }));
290
- fireEvent.change(screen.getByLabelText('Asset ID'), { target: { value: '100' } });
327
+ await selectResource(screen.getByRole('button', { name: 'Choose image' }), 'My image resource');
291
328
  fireEvent.click(screen.getByRole('button', { name: 'Apply' }));
292
329
 
293
330
  await waitForElementToBeRemoved(() => screen.getByRole('button', { name: 'Apply' }));
@@ -302,32 +339,21 @@ describe('ImageButton', () => {
302
339
  },
303
340
  {
304
341
  type: 'assetImage',
305
- attrs: { matrixAssetId: '100', matrixIdentifier, matrixDomain },
342
+ attrs: { matrixAssetId: 'image-resource-id', matrixIdentifier, matrixDomain },
306
343
  },
307
344
  { type: 'text', text: ' nonsense' },
308
345
  ],
309
346
  });
310
347
  });
311
348
 
312
- it.each([
313
- ['Asset ID not provided', '', null, 'Asset ID is required'],
314
- ['Asset does not exist', '100', null, 'Asset ID is invalid or not an image'],
315
- ['Asset is not an image', '100', { id: 100, type: 'physical_file' }, 'Asset ID is invalid or not an image'],
316
- ])(
317
- 'Shows an error if an invalid asset ID is provided - %s',
318
- async (description: string, assetId: string, asset: any, expectedError: string) => {
319
- const resolveMatrixAsset = jest.fn(() => Promise.resolve(asset));
320
-
321
- await renderWithEditor(<ImageButton />, { context: { matrix: { resolveMatrixAsset } } });
322
-
323
- await openModal();
324
- fireEvent.click(screen.getByRole('button', { name: 'From source' }));
325
- fireEvent.change(screen.getByLabelText('Asset ID'), { target: { value: assetId } });
326
- await act(() => fireEvent.click(screen.getByRole('button', { name: 'Apply' })));
327
-
328
- expect(screen.getByText(expectedError)).toBeInTheDocument();
329
- },
330
- );
349
+ it('Shows an error if a resource is not selected', async () => {
350
+ await renderWithEditor(<ImageButton />);
351
+ await openModal();
352
+ fireEvent.click(screen.getByRole('button', { name: 'From source' }));
353
+ await act(() => fireEvent.click(screen.getByRole('button', { name: 'Apply' })));
354
+
355
+ expect(screen.getByText('An image must be selected')).toBeInTheDocument();
356
+ });
331
357
 
332
358
  it('Shows an error if the field value is just an empty space', async () => {
333
359
  await renderWithEditor(<ImageButton />);
@@ -4,6 +4,7 @@ import React from 'react';
4
4
  import { LinkForm } from './LinkForm';
5
5
  import { LinkTarget } from '../../../../Extensions/LinkExtension/common';
6
6
  import { MarkName } from '../../../../Extensions/Extensions';
7
+ import { mockResourceBrowserContext } from '../../../../../tests';
7
8
 
8
9
  describe('Link Form', () => {
9
10
  const handleSubmit = jest.fn();
@@ -28,10 +29,10 @@ describe('Link Form', () => {
28
29
  render(<LinkForm onSubmit={handleSubmit} />);
29
30
 
30
31
  expect(document.querySelector('div[data-headlessui-state="selected"]')).toHaveTextContent('From source');
31
- expect(screen.getByLabelText('Asset ID')).toHaveValue('');
32
+ expect(screen.queryByRole('button', { name: 'Choose asset' })).toBeInTheDocument();
32
33
  expect(screen.getByLabelText('Text')).toHaveValue('');
33
34
  expect(document.querySelector('div.squiz-fte-checkbox')).toHaveTextContent('Open link in new window');
34
- expect(document.querySelectorAll('label')).toHaveLength(2);
35
+ expect(document.querySelectorAll('label')).toHaveLength(1);
35
36
  });
36
37
 
37
38
  it('Renders the form with the expected fields for arbitrary links', () => {
@@ -46,12 +47,21 @@ describe('Link Form', () => {
46
47
  });
47
48
 
48
49
  it('Renders the form with the expected fields for asset links', () => {
49
- render(<LinkForm data={{ ...data, linkType: MarkName.AssetLink }} onSubmit={handleSubmit} />);
50
+ const { MockResourceBrowserContext } = mockResourceBrowserContext({
51
+ sources: [{ id: 'my-source-id' }],
52
+ resources: [{ id: '100', name: 'My selected resource' }],
53
+ });
54
+
55
+ render(
56
+ <MockResourceBrowserContext>
57
+ <LinkForm data={{ ...data, linkType: MarkName.AssetLink }} onSubmit={handleSubmit} />
58
+ </MockResourceBrowserContext>,
59
+ );
50
60
 
51
61
  expect(document.querySelector('div[data-headlessui-state="selected"]')).toHaveTextContent('From source');
52
- expect(screen.getByLabelText('Asset ID')).toHaveValue('100');
62
+ expect(screen.getByText('My selected resource')).toBeInTheDocument();
53
63
  expect(screen.getByLabelText('Text')).toHaveValue('Link text');
54
64
  expect(document.querySelector('div.squiz-fte-checkbox')).toHaveTextContent('Open link in new window');
55
- expect(document.querySelectorAll('label')).toHaveLength(2);
65
+ expect(document.querySelectorAll('label')).toHaveLength(1);
56
66
  });
57
67
  });
@@ -1,17 +1,17 @@
1
- import React, { ReactElement, useContext } from 'react';
1
+ import React, { ReactElement } from 'react';
2
2
  import clsx from 'clsx';
3
- import { SubmitHandler, useForm } from 'react-hook-form';
3
+ import { Controller, SubmitHandler, useForm } from 'react-hook-form';
4
4
  import { FromToProps } from 'remirror';
5
5
  import { Input } from '../../../../ui/Fields/Input/Input';
6
6
  import { Checkbox } from '../../../../ui/Fields/Checkbox/Checkbox';
7
7
  import { UpdateLinkProps } from '../../../../Extensions/LinkExtension/LinkExtension';
8
8
  import { UpdateAssetLinkProps } from '../../../../Extensions/LinkExtension/AssetLinkExtension';
9
9
  import { LinkTarget } from '../../../../Extensions/LinkExtension/common';
10
- import { EditorContext } from '../../../../Editor/EditorContext';
11
10
  import { MarkName } from '../../../../Extensions/Extensions';
12
11
  import { DeepPartial } from '../../../../types';
13
- import { noEmptySpacesValidation } from '../../../../utils/validation';
12
+ import { hasProperties, noEmptySpacesValidation } from '../../../../utils/validation';
14
13
  import { TabOptions, Tabs } from '../../../../ui/Tabs/Tabs';
14
+ import { MatrixAsset } from '../../../../ui/Fields/MatrixAsset/MatrixAsset';
15
15
 
16
16
  export type LinkFormData = {
17
17
  linkType: MarkName;
@@ -32,8 +32,8 @@ const linkTypeOptions: TabOptions = {
32
32
  };
33
33
 
34
34
  export const LinkForm = ({ data, onSubmit }: FormProps): ReactElement => {
35
- const context = useContext(EditorContext);
36
35
  const {
36
+ control,
37
37
  register,
38
38
  handleSubmit,
39
39
  setValue,
@@ -101,21 +101,15 @@ export const LinkForm = ({ data, onSubmit }: FormProps): ReactElement => {
101
101
  {linkType === MarkName.AssetLink && (
102
102
  <>
103
103
  <div className={clsx('squiz-fte-form-group mb-2')}>
104
- <Input
105
- label="Asset ID"
106
- required
107
- error={errors?.assetLink?.matrixAssetId?.message}
108
- {...register('assetLink.matrixAssetId', {
109
- required: 'Asset ID is required',
110
- validate: {
111
- isValidAsset: async (assetId: string | undefined) => {
112
- if (assetId && !(await context.matrix.resolveMatrixAsset(assetId))) {
113
- return 'Invalid asset ID';
114
- }
115
- },
116
- noEmptySpaces: noEmptySpacesValidation,
117
- },
118
- })}
104
+ <Controller
105
+ control={control}
106
+ name="assetLink"
107
+ rules={{
108
+ validate: hasProperties('An asset must be selected', ['matrixIdentifier', 'matrixAssetId']),
109
+ }}
110
+ render={({ field: { onChange, value }, fieldState: { error } }) => (
111
+ <MatrixAsset modalTitle="Insert link" value={value} onChange={onChange} error={error?.message} />
112
+ )}
119
113
  />
120
114
  </div>
121
115
  <div className={clsx('squiz-fte-form-group mb-2')}>
@@ -1,7 +1,7 @@
1
1
  import '@testing-library/jest-dom';
2
2
  import { act, screen, fireEvent, waitForElementToBeRemoved } from '@testing-library/react';
3
3
  import React from 'react';
4
- import { renderWithEditor } from '../../../../tests';
4
+ import { renderWithEditor, mockResourceBrowserContext } from '../../../../tests';
5
5
  import LinkButton from './LinkButton';
6
6
 
7
7
  describe('LinkButton', () => {
@@ -236,19 +236,29 @@ describe('LinkButton', () => {
236
236
  it('Add a new asset link', async () => {
237
237
  const matrixIdentifier = 'matrix-api-identifier';
238
238
  const matrixDomain = 'https://my-matrix.squiz.net';
239
- const { getJsonContent } = await renderWithEditor(<LinkButton />, {
240
- context: {
241
- matrix: {
242
- matrixIdentifier,
243
- matrixDomain,
244
- resolveMatrixAsset: () => Promise.resolve({ id: '100', type: 'physical_file' }),
239
+ const { MockResourceBrowserContext, selectResource } = mockResourceBrowserContext({
240
+ sources: [{ id: matrixIdentifier }],
241
+ resources: [{ id: 'my-resource-id', name: 'My resource' }],
242
+ });
243
+
244
+ const { getJsonContent } = await renderWithEditor(
245
+ <MockResourceBrowserContext>
246
+ <LinkButton />
247
+ </MockResourceBrowserContext>,
248
+ {
249
+ context: {
250
+ editor: {
251
+ matrix: {
252
+ matrixDomain,
253
+ },
254
+ },
245
255
  },
246
256
  },
247
- });
257
+ );
248
258
 
249
259
  await openModal();
250
260
  fireEvent.click(screen.getByRole('button', { name: 'From source' }));
251
- fireEvent.change(screen.getByLabelText('Asset ID'), { target: { value: '123' } });
261
+ await selectResource(screen.getByRole('button', { name: 'Choose asset' }), 'My resource');
252
262
  fireEvent.change(screen.getByLabelText('Text'), { target: { value: 'Link text' } });
253
263
  fireEvent.click(screen.getByRole('button', { name: 'Apply' }));
254
264
 
@@ -264,7 +274,7 @@ describe('LinkButton', () => {
264
274
  marks: [
265
275
  {
266
276
  type: 'assetLink',
267
- attrs: { matrixAssetId: '123', target: '_self', matrixDomain, matrixIdentifier },
277
+ attrs: { matrixAssetId: 'my-resource-id', target: '_self', matrixDomain, matrixIdentifier },
268
278
  },
269
279
  ],
270
280
  },
@@ -275,24 +285,34 @@ describe('LinkButton', () => {
275
285
  it('Updates an existing link to be an asset link', async () => {
276
286
  const matrixIdentifier = 'matrix-api-identifier';
277
287
  const matrixDomain = 'https://my-matrix.squiz.net';
278
- const { editor, getJsonContent } = await renderWithEditor(<LinkButton />, {
279
- content:
280
- '<a href="https://www.example.org/my-link">Sample link</a> with ' +
281
- '<a href="https://www.example.org/another-link">another link</a>',
282
- context: {
283
- matrix: {
284
- matrixIdentifier,
285
- matrixDomain,
286
- resolveMatrixAsset: () => Promise.resolve({ id: '100', type: 'physical_file' }),
288
+ const { MockResourceBrowserContext, selectResource } = mockResourceBrowserContext({
289
+ sources: [{ id: matrixIdentifier }],
290
+ resources: [{ id: 'my-resource-id', name: 'My resource' }],
291
+ });
292
+
293
+ const { editor, getJsonContent } = await renderWithEditor(
294
+ <MockResourceBrowserContext>
295
+ <LinkButton />
296
+ </MockResourceBrowserContext>,
297
+ {
298
+ content:
299
+ '<a href="https://www.example.org/my-link">Sample link</a> with ' +
300
+ '<a href="https://www.example.org/another-link">another link</a>',
301
+ context: {
302
+ editor: {
303
+ matrix: {
304
+ matrixDomain,
305
+ },
306
+ },
287
307
  },
288
308
  },
289
- });
309
+ );
290
310
 
291
311
  await act(() => editor.selectText(5));
292
312
 
293
313
  await openModal();
294
314
  fireEvent.click(screen.getByRole('button', { name: 'From source' }));
295
- fireEvent.change(screen.getByLabelText('Asset ID'), { target: { value: '123' } });
315
+ await selectResource(screen.getByRole('button', { name: 'Choose asset' }), 'My resource');
296
316
  fireEvent.click(document.querySelector('div.squiz-fte-checkbox button') as HTMLButtonElement);
297
317
  fireEvent.click(screen.getByRole('button', { name: 'Apply' }));
298
318
 
@@ -308,7 +328,7 @@ describe('LinkButton', () => {
308
328
  marks: [
309
329
  {
310
330
  type: 'assetLink',
311
- attrs: { matrixAssetId: '123', target: '_blank', matrixDomain, matrixIdentifier },
331
+ attrs: { matrixAssetId: 'my-resource-id', target: '_blank', matrixDomain, matrixIdentifier },
312
332
  },
313
333
  ],
314
334
  },
@@ -327,18 +347,14 @@ describe('LinkButton', () => {
327
347
  });
328
348
  });
329
349
 
330
- it('Shows an error if an invalid asset ID is provided', async () => {
331
- const resolveMatrixAsset = jest.fn(() => Promise.resolve(null));
332
-
333
- await renderWithEditor(<LinkButton />, { context: { matrix: { resolveMatrixAsset } } });
350
+ it('Shows an error if no asset is selected', async () => {
351
+ await renderWithEditor(<LinkButton />);
334
352
 
335
353
  await openModal();
336
354
  fireEvent.click(screen.getByRole('button', { name: 'From source' }));
337
- fireEvent.change(screen.getByLabelText('Asset ID'), { target: { value: 'invalid-asset-id' } });
338
355
  await act(() => fireEvent.click(screen.getByRole('button', { name: 'Apply' })));
339
356
 
340
- expect(screen.getByText('Invalid asset ID')).toBeInTheDocument();
341
- expect(resolveMatrixAsset).toHaveBeenCalledWith('invalid-asset-id');
357
+ expect(screen.getByText('An asset must be selected')).toBeInTheDocument();
342
358
  });
343
359
 
344
360
  it('Shows an error if a required field is not provided', async () => {