@squiz/formatted-text-editor 1.33.1-alpha.2 → 1.33.1-alpha.4

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 (79) 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 +16 -32
  10. package/lib/EditorToolbar/Tools/Image/ImageModal.js +3 -2
  11. package/lib/EditorToolbar/Tools/Link/Form/LinkForm.js +18 -58
  12. package/lib/EditorToolbar/Tools/Link/LinkModal.js +3 -2
  13. package/lib/EditorToolbar/Tools/Link/RemoveLinkButton.js +1 -1
  14. package/lib/Extensions/Extensions.js +0 -2
  15. package/lib/Extensions/ImageExtension/AssetImageExtension.d.ts +0 -1
  16. package/lib/Extensions/ImageExtension/AssetImageExtension.js +1 -2
  17. package/lib/Extensions/LinkExtension/AssetLinkExtension.d.ts +0 -1
  18. package/lib/Extensions/LinkExtension/AssetLinkExtension.js +2 -3
  19. package/lib/Extensions/LinkExtension/LinkExtension.js +1 -1
  20. package/lib/index.css +84 -4
  21. package/lib/types.d.ts +3 -3
  22. package/lib/ui/Fields/Checkbox/Checkbox.d.ts +8 -0
  23. package/lib/ui/Fields/Checkbox/Checkbox.js +47 -0
  24. package/lib/ui/Fields/Input/Input.d.ts +2 -4
  25. package/lib/ui/Fields/Input/Input.js +3 -9
  26. package/lib/ui/Fields/InputContainer/InputContainer.d.ts +9 -0
  27. package/lib/ui/Fields/InputContainer/InputContainer.js +16 -0
  28. package/lib/ui/Fields/MatrixAsset/MatrixAsset.d.ts +17 -0
  29. package/lib/ui/Fields/MatrixAsset/MatrixAsset.js +29 -0
  30. package/lib/ui/Modal/Modal.d.ts +1 -0
  31. package/lib/ui/Modal/Modal.js +3 -2
  32. package/lib/ui/Tabs/Tabs.d.ts +10 -0
  33. package/lib/ui/Tabs/Tabs.js +46 -0
  34. package/lib/utils/validation.d.ts +2 -1
  35. package/lib/utils/validation.js +8 -2
  36. package/package.json +4 -3
  37. package/src/Editor/Editor.spec.tsx +1 -1
  38. package/src/Editor/EditorContext.spec.tsx +11 -13
  39. package/src/Editor/EditorContext.ts +0 -11
  40. package/src/EditorToolbar/Tools/Image/Form/ImageForm.spec.tsx +29 -12
  41. package/src/EditorToolbar/Tools/Image/Form/ImageForm.tsx +37 -53
  42. package/src/EditorToolbar/Tools/Image/ImageButton.spec.tsx +76 -49
  43. package/src/EditorToolbar/Tools/Image/ImageModal.spec.tsx +1 -0
  44. package/src/EditorToolbar/Tools/Image/ImageModal.tsx +3 -2
  45. package/src/EditorToolbar/Tools/Link/Form/LinkForm.spec.tsx +22 -13
  46. package/src/EditorToolbar/Tools/Link/Form/LinkForm.tsx +35 -57
  47. package/src/EditorToolbar/Tools/Link/LinkButton.spec.tsx +52 -36
  48. package/src/EditorToolbar/Tools/Link/LinkModal.tsx +3 -2
  49. package/src/EditorToolbar/Tools/Link/RemoveLinkButton.spec.tsx +47 -4
  50. package/src/EditorToolbar/Tools/Link/RemoveLinkButton.tsx +3 -2
  51. package/src/Extensions/Extensions.ts +0 -2
  52. package/src/Extensions/ImageExtension/AssetImageExtension.ts +1 -3
  53. package/src/Extensions/LinkExtension/AssetLinkExtension.ts +2 -4
  54. package/src/Extensions/LinkExtension/LinkExtension.ts +1 -1
  55. package/src/index.scss +1 -0
  56. package/src/types.ts +7 -5
  57. package/src/ui/Fields/Checkbox/Checkbox.spec.tsx +50 -0
  58. package/src/ui/Fields/Checkbox/Checkbox.tsx +49 -0
  59. package/src/ui/Fields/Checkbox/_checkbox.scss +26 -0
  60. package/src/ui/Fields/Input/Input.tsx +4 -18
  61. package/src/ui/Fields/InputContainer/InputContainer.spec.tsx +18 -0
  62. package/src/ui/Fields/InputContainer/InputContainer.tsx +29 -0
  63. package/src/ui/Fields/MatrixAsset/MatrixAsset.spec.tsx +103 -0
  64. package/src/ui/Fields/MatrixAsset/MatrixAsset.tsx +55 -0
  65. package/src/ui/Modal/FormModal.spec.tsx +2 -1
  66. package/src/ui/Modal/Modal.spec.tsx +15 -7
  67. package/src/ui/Modal/Modal.tsx +4 -2
  68. package/src/ui/Tabs/Tabs.spec.tsx +44 -0
  69. package/src/ui/Tabs/Tabs.tsx +41 -0
  70. package/src/ui/_forms.scss +4 -2
  71. package/src/utils/validation.spec.ts +22 -0
  72. package/src/utils/validation.ts +9 -1
  73. package/tests/index.ts +2 -0
  74. package/tests/mockResourceBrowserContext.tsx +63 -0
  75. package/tests/renderWithContext.tsx +18 -0
  76. package/tests/renderWithEditor.tsx +18 -21
  77. package/vite.config.ts +8 -0
  78. package/lib/ui/Fields/Select/Select.d.ts +0 -12
  79. package/lib/ui/Fields/Select/Select.js +0 -53
@@ -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 { Select, SelectOptions } from '../../../../ui/Fields/Select/Select';
14
- import { EditorContext } from '../../../../Editor/EditorContext';
15
- import { noEmptySpacesValidation, regexDataURI } from '../../../../utils/validation';
13
+ import { noEmptySpacesValidation, regexDataURI, hasProperties } from '../../../../utils/validation';
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;
@@ -20,9 +20,9 @@ export type ImageFormData = {
20
20
  assetImage: AssetImageAttributes;
21
21
  };
22
22
 
23
- const imageTypeOptions: SelectOptions = {
24
- [NodeName.Image]: { label: 'External image' },
25
- [NodeName.AssetImage]: { label: 'Asset image' },
23
+ const imageTypeOptions: TabOptions = {
24
+ [NodeName.AssetImage]: { label: 'From source' },
25
+ [NodeName.Image]: { label: 'From URL' },
26
26
  };
27
27
 
28
28
  export type FormProps = {
@@ -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,
@@ -41,8 +42,7 @@ const ImageForm = ({ data, onSubmit }: FormProps): ReactElement => {
41
42
  } = useForm<ImageFormData>({
42
43
  defaultValues: data,
43
44
  });
44
- const imageType = watch('imageType') || NodeName.Image;
45
- const context = useContext(EditorContext);
45
+ const imageType = watch('imageType') || NodeName.AssetImage;
46
46
  const [aspectRatioFromWidth, setAspectRatioFromWidth] = useState(9 / 16);
47
47
  const [aspectRatioFromHeight, setAspectRatioFromHeight] = useState(16 / 9);
48
48
  const [aspectRatioLocked, setAspectRatioLocked] = useState(true);
@@ -88,10 +88,8 @@ const ImageForm = ({ data, onSubmit }: FormProps): ReactElement => {
88
88
 
89
89
  return (
90
90
  <form className="squiz-fte-form" onSubmit={handleSubmit(onSubmit)}>
91
- <div className="squiz-fte-form-group mb-2">
92
- <Select
93
- name="imageType"
94
- label="Type"
91
+ <div className="squiz-fte-form-group mb-4">
92
+ <Tabs
95
93
  value={imageType}
96
94
  options={imageTypeOptions}
97
95
  onChange={(value) => setValue('imageType', value as NodeName)}
@@ -121,19 +119,6 @@ const ImageForm = ({ data, onSubmit }: FormProps): ReactElement => {
121
119
  })}
122
120
  />
123
121
  </div>
124
- <div className="squiz-fte-form-group mb-2">
125
- <Input
126
- label="Alternative description"
127
- required
128
- error={errors?.image?.alt?.message}
129
- {...register('image.alt', {
130
- required: 'Alternative description is required',
131
- validate: {
132
- noEmptySpaces: noEmptySpacesValidation,
133
- },
134
- })}
135
- />
136
- </div>
137
122
  <div className="flex flex-row">
138
123
  <div className="squiz-fte-form-group mb-2">
139
124
  <Input
@@ -146,7 +131,7 @@ const ImageForm = ({ data, onSubmit }: FormProps): ReactElement => {
146
131
  required: 'Width is required',
147
132
  validate: {
148
133
  isValidWidth: (value) => {
149
- if (value && !(value > 0)) {
134
+ if (value && !((value as number) > 0)) {
150
135
  return 'Must be higher than 0';
151
136
  }
152
137
  },
@@ -175,7 +160,7 @@ const ImageForm = ({ data, onSubmit }: FormProps): ReactElement => {
175
160
  required: 'Height is required',
176
161
  validate: {
177
162
  isValidHeight: (value) => {
178
- if (value && !(value > 0)) {
163
+ if (value && !((value as number) > 0)) {
179
164
  return 'Must be higher than 0';
180
165
  }
181
166
  },
@@ -184,32 +169,31 @@ const ImageForm = ({ data, onSubmit }: FormProps): ReactElement => {
184
169
  />
185
170
  </div>
186
171
  </div>
187
- </>
188
- )}
189
- {imageType === NodeName.AssetImage && (
190
- <>
191
172
  <div className="squiz-fte-form-group mb-2">
192
- <Input
193
- label="Asset ID"
194
- required
195
- error={errors?.assetImage?.matrixAssetId?.message}
196
- {...register('assetImage.matrixAssetId', {
197
- required: 'Asset ID is required',
198
- validate: {
199
- isImage: async (assetId) => {
200
- const asset = await context.matrix.resolveMatrixAsset(assetId);
201
-
202
- if (asset?.type !== 'image') {
203
- return 'Asset ID is invalid or not an image';
204
- }
205
- },
206
- noEmptySpaces: noEmptySpacesValidation,
207
- },
208
- })}
209
- />
173
+ <Input label="Alternative description" error={errors?.image?.alt?.message} {...register('image.alt')} />
210
174
  </div>
211
175
  </>
212
176
  )}
177
+ {imageType === NodeName.AssetImage && (
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>
196
+ )}
213
197
  </form>
214
198
  );
215
199
  };
@@ -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, select } from '../../../../tests';
5
+ import { renderWithEditor, mockResourceBrowserContext } from '../../../../tests';
6
6
  import ImageButton from './ImageButton';
7
7
  import { getImageSize } from 'react-image-size';
8
8
 
@@ -12,6 +12,7 @@ describe('ImageButton', () => {
12
12
  const openModal = async () => {
13
13
  fireEvent.click(screen.getByRole('button', { name: 'Image (cmd+L)' }));
14
14
  await screen.findByRole('button', { name: 'Apply' });
15
+ fireEvent.click(screen.getByRole('button', { name: 'From URL' }));
15
16
  };
16
17
 
17
18
  beforeEach(() => {
@@ -28,14 +29,13 @@ describe('ImageButton', () => {
28
29
  });
29
30
 
30
31
  it('Opens the modal when clicking the keyboard shortcut', async () => {
31
- const { editor, elements } = await renderWithEditor(<ImageButton />);
32
+ const { elements } = await renderWithEditor(<ImageButton />);
32
33
 
33
34
  // press the keyboard shortcut.
34
35
  fireEvent.keyDown(elements.editor, { key: 'l', ctrlKey: true });
35
36
 
36
37
  // verify the modal opens
37
- await act(() => editor.selectText(2));
38
- expect(await screen.findByLabelText('Source')).toHaveValue('');
38
+ expect(await screen.findByRole('button', { name: 'Choose image' })).toBeInTheDocument();
39
39
  });
40
40
 
41
41
  it('Adds a new image', async () => {
@@ -215,7 +215,10 @@ describe('ImageButton', () => {
215
215
  fireEvent.change(screen.getByLabelText('Source'), { target: { value: 'https://httpcats.com/529.jpg' } });
216
216
  fireEvent.change(screen.getByLabelText('Alternative description'), { target: { value: '' } });
217
217
  fireEvent.click(screen.getByRole('button', { name: 'Apply' }));
218
- expect(await screen.findByText('Alternative description is required')).toBeInTheDocument();
218
+
219
+ await waitFor(() => {
220
+ expect(document.querySelector('img[src="https://httpcats.com/529.jpg"]')).toBeInTheDocument();
221
+ });
219
222
  });
220
223
 
221
224
  it('Adds a new image with no width or height text', async () => {
@@ -233,21 +236,40 @@ describe('ImageButton', () => {
233
236
  it('Adds a new asset image', async () => {
234
237
  const matrixIdentifier = 'matrix-api-identifier';
235
238
  const matrixDomain = 'https://my-matrix.squiz.net';
236
- const { getJsonContent } = await renderWithEditor(<ImageButton />, {
237
- content: 'Some nonsense content here',
238
- context: {
239
- matrix: {
240
- matrixIdentifier,
241
- matrixDomain,
242
- 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
+ },
243
249
  },
244
- },
250
+ ],
245
251
  });
246
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
+
247
269
  // open the modal and add an image.
248
270
  await openModal();
249
- select(screen.getByLabelText('Type'), 'Asset image');
250
- fireEvent.change(screen.getByLabelText('Asset ID'), { target: { value: '100' } });
271
+ fireEvent.click(screen.getByRole('button', { name: 'From source' }));
272
+ await selectResource(screen.getByRole('button', { name: 'Choose image' }), 'My image resource');
251
273
  fireEvent.click(screen.getByRole('button', { name: 'Apply' }));
252
274
 
253
275
  await waitForElementToBeRemoved(() => screen.getByRole('button', { name: 'Apply' }));
@@ -258,7 +280,7 @@ describe('ImageButton', () => {
258
280
  content: [
259
281
  {
260
282
  type: 'assetImage',
261
- attrs: { matrixAssetId: '100', matrixIdentifier, matrixDomain },
283
+ attrs: { matrixAssetId: 'image-resource-id', matrixIdentifier, matrixDomain },
262
284
  },
263
285
  { type: 'text', text: 'Some nonsense content here' },
264
286
  ],
@@ -268,22 +290,41 @@ describe('ImageButton', () => {
268
290
  it('Updates the attributes of an existing asset image', async () => {
269
291
  const matrixIdentifier = 'matrix-api-identifier';
270
292
  const matrixDomain = 'https://my-matrix.squiz.net';
271
- const { editor, getJsonContent } = await renderWithEditor(<ImageButton />, {
272
- content: 'Some <img src="https://httpcats.com/529.jpg" alt="hi" /> nonsense',
273
- context: {
274
- matrix: {
275
- matrixIdentifier,
276
- matrixDomain,
277
- 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
+ },
278
303
  },
279
- },
304
+ ],
280
305
  });
281
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
+
282
323
  await act(() => editor.selectText(new NodeSelection(editor.state.doc.resolve(6))));
283
324
 
284
325
  await openModal();
285
- select(screen.getByLabelText('Type'), 'Asset image');
286
- fireEvent.change(screen.getByLabelText('Asset ID'), { target: { value: '100' } });
326
+ fireEvent.click(screen.getByRole('button', { name: 'From source' }));
327
+ await selectResource(screen.getByRole('button', { name: 'Choose image' }), 'My image resource');
287
328
  fireEvent.click(screen.getByRole('button', { name: 'Apply' }));
288
329
 
289
330
  await waitForElementToBeRemoved(() => screen.getByRole('button', { name: 'Apply' }));
@@ -298,32 +339,21 @@ describe('ImageButton', () => {
298
339
  },
299
340
  {
300
341
  type: 'assetImage',
301
- attrs: { matrixAssetId: '100', matrixIdentifier, matrixDomain },
342
+ attrs: { matrixAssetId: 'image-resource-id', matrixIdentifier, matrixDomain },
302
343
  },
303
344
  { type: 'text', text: ' nonsense' },
304
345
  ],
305
346
  });
306
347
  });
307
348
 
308
- it.each([
309
- ['Asset ID not provided', '', null, 'Asset ID is required'],
310
- ['Asset does not exist', '100', null, 'Asset ID is invalid or not an image'],
311
- ['Asset is not an image', '100', { id: 100, type: 'physical_file' }, 'Asset ID is invalid or not an image'],
312
- ])(
313
- 'Shows an error if an invalid asset ID is provided - %s',
314
- async (description: string, assetId: string, asset: any, expectedError: string) => {
315
- const resolveMatrixAsset = jest.fn(() => Promise.resolve(asset));
316
-
317
- await renderWithEditor(<ImageButton />, { context: { matrix: { resolveMatrixAsset } } });
318
-
319
- await openModal();
320
- select(screen.getByLabelText('Type'), 'Asset image');
321
- fireEvent.change(screen.getByLabelText('Asset ID'), { target: { value: assetId } });
322
- await act(() => fireEvent.click(screen.getByRole('button', { name: 'Apply' })));
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' })));
323
354
 
324
- expect(screen.getByText(expectedError)).toBeInTheDocument();
325
- },
326
- );
355
+ expect(screen.getByText('An image must be selected')).toBeInTheDocument();
356
+ });
327
357
 
328
358
  it('Shows an error if the field value is just an empty space', async () => {
329
359
  await renderWithEditor(<ImageButton />);
@@ -332,11 +362,8 @@ describe('ImageButton', () => {
332
362
  await act(async () => {
333
363
  fireEvent.change(screen.getByLabelText('Source'), { target: { value: ' ' } });
334
364
  });
335
- await act(async () => {
336
- fireEvent.change(screen.getByLabelText('Alternative description'), { target: { value: ' ' } });
337
- });
338
365
  await act(() => fireEvent.click(screen.getByRole('button', { name: 'Apply' })));
339
366
 
340
- expect(screen.getAllByText('Empty space is not allowed')).toHaveLength(2);
367
+ expect(screen.getAllByText('Empty space is not allowed')).toHaveLength(1);
341
368
  });
342
369
  });
@@ -13,6 +13,7 @@ const mockSubmitFunction = jest.fn();
13
13
  const mockCancelFunction = jest.fn();
14
14
  const setup = async () => {
15
15
  const utils = await renderWithEditor(<ImageModal onCancel={mockCancelFunction} onSubmit={mockSubmitFunction} />);
16
+ fireEvent.click(screen.getByRole('button', { name: 'From URL' }));
16
17
  const sourceInput = screen.getByRole('textbox', { name: /source/i }) as HTMLInputElement;
17
18
  const altInput = screen.getByRole('textbox', { name: /alt/i }) as HTMLInputElement;
18
19
  const widthInput = screen.getByRole('spinbutton', { name: /Width/i }) as HTMLInputElement;
@@ -1,6 +1,7 @@
1
1
  import ImageForm, { ImageFormData } from './Form/ImageForm';
2
2
  import React from 'react';
3
3
  import { useCurrentSelection } from '@remirror/react';
4
+ import ImageRoundedIcon from '@mui/icons-material/ImageRounded';
4
5
  import FormModal from '../../../ui/Modal/FormModal';
5
6
  import { SubmitHandler } from 'react-hook-form';
6
7
  import { NodeSelection } from 'prosemirror-state';
@@ -16,13 +17,13 @@ const ImageModal = ({ onCancel, onSubmit }: ImageModalProps) => {
16
17
  const currentImage = selection?.node;
17
18
  const currentImageAttrs = { ...currentImage?.attrs };
18
19
  const formData = {
19
- imageType: currentImage?.type.name === NodeName.AssetImage ? NodeName.AssetImage : NodeName.Image,
20
+ imageType: currentImage?.type.name === NodeName.Image ? NodeName.Image : NodeName.AssetImage,
20
21
  image: currentImage?.type?.name === NodeName.Image ? currentImageAttrs : {},
21
22
  assetImage: currentImage?.type?.name === NodeName.AssetImage ? currentImageAttrs : {},
22
23
  };
23
24
 
24
25
  return (
25
- <FormModal title="Image" onCancel={onCancel}>
26
+ <FormModal title="Image" icon={<ImageRoundedIcon />} onCancel={onCancel}>
26
27
  <ImageForm data={formData} onSubmit={onSubmit} />
27
28
  </FormModal>
28
29
  );
@@ -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();
@@ -27,32 +28,40 @@ describe('Link Form', () => {
27
28
  it('Renders the form with expected default values when no data is provided', () => {
28
29
  render(<LinkForm onSubmit={handleSubmit} />);
29
30
 
30
- expect(screen.getByLabelText('Type')).toHaveTextContent('Link to URL');
31
- expect(screen.getByLabelText('URL')).toHaveValue('');
31
+ expect(document.querySelector('div[data-headlessui-state="selected"]')).toHaveTextContent('From source');
32
+ expect(screen.queryByRole('button', { name: 'Choose asset' })).toBeInTheDocument();
32
33
  expect(screen.getByLabelText('Text')).toHaveValue('');
33
- expect(screen.getByLabelText('Title')).toHaveValue('');
34
- expect(screen.getByLabelText('Target')).toHaveTextContent('Current window');
35
- expect(document.querySelectorAll('label')).toHaveLength(5);
34
+ expect(document.querySelector('div.squiz-fte-checkbox')).toHaveTextContent('Open link in new window');
35
+ expect(document.querySelectorAll('label')).toHaveLength(1);
36
36
  });
37
37
 
38
38
  it('Renders the form with the expected fields for arbitrary links', () => {
39
39
  render(<LinkForm data={data} onSubmit={handleSubmit} />);
40
40
 
41
- expect(screen.getByLabelText('Type')).toHaveTextContent('Link to URL');
41
+ expect(document.querySelector('div[data-headlessui-state="selected"]')).toHaveTextContent('From URL');
42
42
  expect(screen.getByLabelText('URL')).toHaveValue('https://www.squiz.net/link-form');
43
43
  expect(screen.getByLabelText('Text')).toHaveValue('Link text');
44
44
  expect(screen.getByLabelText('Title')).toHaveValue('Link title');
45
- expect(screen.getByLabelText('Target')).toHaveTextContent('New window');
46
- expect(document.querySelectorAll('label')).toHaveLength(5);
45
+ expect(document.querySelector('div.squiz-fte-checkbox')).toHaveTextContent('Open link in new window');
46
+ expect(document.querySelectorAll('label')).toHaveLength(3);
47
47
  });
48
48
 
49
49
  it('Renders the form with the expected fields for asset links', () => {
50
- 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
+ });
51
54
 
52
- expect(screen.getByLabelText('Type')).toHaveTextContent('Link to asset');
53
- expect(screen.getByLabelText('Asset ID')).toHaveValue('100');
55
+ render(
56
+ <MockResourceBrowserContext>
57
+ <LinkForm data={{ ...data, linkType: MarkName.AssetLink }} onSubmit={handleSubmit} />
58
+ </MockResourceBrowserContext>,
59
+ );
60
+
61
+ expect(document.querySelector('div[data-headlessui-state="selected"]')).toHaveTextContent('From source');
62
+ expect(screen.getByText('My selected resource')).toBeInTheDocument();
54
63
  expect(screen.getByLabelText('Text')).toHaveValue('Link text');
55
- expect(screen.getByLabelText('Target')).toHaveTextContent('New window');
56
- expect(document.querySelectorAll('label')).toHaveLength(4);
64
+ expect(document.querySelector('div.squiz-fte-checkbox')).toHaveTextContent('Open link in new window');
65
+ expect(document.querySelectorAll('label')).toHaveLength(1);
57
66
  });
58
67
  });
@@ -1,16 +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
- import { Select, SelectOptions } from '../../../../ui/Fields/Select/Select';
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';
13
+ import { TabOptions, Tabs } from '../../../../ui/Tabs/Tabs';
14
+ import { MatrixAsset } from '../../../../ui/Fields/MatrixAsset/MatrixAsset';
14
15
 
15
16
  export type LinkFormData = {
16
17
  linkType: MarkName;
@@ -25,19 +26,14 @@ export type FormProps = {
25
26
  onSubmit: SubmitHandler<LinkFormData>;
26
27
  };
27
28
 
28
- const linkTypeOptions: SelectOptions = {
29
- [MarkName.Link]: { label: 'Link to URL' },
30
- [MarkName.AssetLink]: { label: 'Link to asset' },
31
- };
32
-
33
- const targetOptions: SelectOptions = {
34
- [LinkTarget.Self]: { label: 'Current window' },
35
- [LinkTarget.Blank]: { label: 'New window' },
29
+ const linkTypeOptions: TabOptions = {
30
+ [MarkName.AssetLink]: { label: 'From source' },
31
+ [MarkName.Link]: { label: 'From URL' },
36
32
  };
37
33
 
38
34
  export const LinkForm = ({ data, onSubmit }: FormProps): ReactElement => {
39
- const context = useContext(EditorContext);
40
35
  const {
36
+ control,
41
37
  register,
42
38
  handleSubmit,
43
39
  setValue,
@@ -46,14 +42,12 @@ export const LinkForm = ({ data, onSubmit }: FormProps): ReactElement => {
46
42
  } = useForm<LinkFormData>({
47
43
  defaultValues: data,
48
44
  });
49
- const linkType = watch('linkType') || MarkName.Link;
45
+ const linkType = watch('linkType') || MarkName.AssetLink;
50
46
 
51
47
  return (
52
48
  <form className="squiz-fte-form" onSubmit={handleSubmit(onSubmit)}>
53
- <div className="squiz-fte-form-group mb-2">
54
- <Select
55
- name="linkType"
56
- label="Type"
49
+ <div className="squiz-fte-form-group mb-4">
50
+ <Tabs
57
51
  value={linkType}
58
52
  options={linkTypeOptions}
59
53
  onChange={(value) => setValue('linkType', value as MarkName)}
@@ -90,25 +84,15 @@ export const LinkForm = ({ data, onSubmit }: FormProps): ReactElement => {
90
84
  />
91
85
  </div>
92
86
  <div className={clsx('squiz-fte-form-group mb-2')}>
93
- <Input
94
- label="Title"
95
- required
96
- error={errors?.link?.title?.message}
97
- {...register('link.title', {
98
- required: 'Title is required',
99
- validate: {
100
- noEmptySpaces: noEmptySpacesValidation,
101
- },
102
- })}
103
- />
87
+ <Input label="Title" error={errors?.link?.title?.message} {...register('link.title')} />
104
88
  </div>
105
- <div className={clsx('squiz-fte-form-group mb-0')}>
106
- <Select
107
- name="link.target"
108
- label="Target"
109
- value={data?.link?.target || '_self'}
110
- options={targetOptions}
89
+ <div className={clsx('squiz-fte-form-group mb-2')}>
90
+ <Checkbox
91
+ label="Open link in new window"
111
92
  onChange={(value) => setValue('link.target', value as LinkTarget)}
93
+ defaultChecked={data?.link?.target === LinkTarget.Blank}
94
+ unchecked={LinkTarget.Self}
95
+ checked={LinkTarget.Blank}
112
96
  />
113
97
  </div>
114
98
  </>
@@ -117,21 +101,15 @@ export const LinkForm = ({ data, onSubmit }: FormProps): ReactElement => {
117
101
  {linkType === MarkName.AssetLink && (
118
102
  <>
119
103
  <div className={clsx('squiz-fte-form-group mb-2')}>
120
- <Input
121
- label="Asset ID"
122
- required
123
- error={errors?.assetLink?.matrixAssetId?.message}
124
- {...register('assetLink.matrixAssetId', {
125
- required: 'Asset ID is required',
126
- validate: {
127
- isValidAsset: async (assetId: string | undefined) => {
128
- if (assetId && !(await context.matrix.resolveMatrixAsset(assetId))) {
129
- return 'Invalid asset ID';
130
- }
131
- },
132
- noEmptySpaces: noEmptySpacesValidation,
133
- },
134
- })}
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
+ )}
135
113
  />
136
114
  </div>
137
115
  <div className={clsx('squiz-fte-form-group mb-2')}>
@@ -147,13 +125,13 @@ export const LinkForm = ({ data, onSubmit }: FormProps): ReactElement => {
147
125
  })}
148
126
  />
149
127
  </div>
150
- <div className={clsx('squiz-fte-form-group mb-0')}>
151
- <Select
152
- name="assetLink.target"
153
- label="Target"
154
- value={data?.assetLink?.target || '_self'}
155
- options={targetOptions}
128
+ <div className={clsx('squiz-fte-form-group mb-2')}>
129
+ <Checkbox
130
+ label="Open link in new window"
156
131
  onChange={(value) => setValue('assetLink.target', value as LinkTarget)}
132
+ defaultChecked={data?.assetLink?.target === '_blank'}
133
+ unchecked={'_self' as LinkTarget}
134
+ checked={'_blank' as LinkTarget}
157
135
  />
158
136
  </div>
159
137
  </>