@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.
- package/demo/App.tsx +4 -24
- package/demo/AppContext.tsx +28 -0
- package/demo/index.scss +0 -2
- package/demo/main.tsx +2 -0
- package/demo/resources.json +28 -0
- package/demo/sources.json +23 -0
- package/lib/Editor/EditorContext.d.ts +0 -7
- package/lib/Editor/EditorContext.js +0 -2
- package/lib/EditorToolbar/Tools/Image/Form/ImageForm.js +8 -19
- package/lib/EditorToolbar/Tools/Link/Form/LinkForm.js +6 -38
- package/lib/EditorToolbar/Tools/Link/RemoveLinkButton.js +1 -1
- package/lib/Extensions/Extensions.js +0 -2
- package/lib/Extensions/ImageExtension/AssetImageExtension.d.ts +0 -1
- package/lib/Extensions/ImageExtension/AssetImageExtension.js +1 -2
- package/lib/Extensions/LinkExtension/AssetLinkExtension.d.ts +0 -1
- package/lib/Extensions/LinkExtension/AssetLinkExtension.js +1 -2
- package/lib/index.css +7 -2
- package/lib/types.d.ts +3 -3
- package/lib/ui/Fields/Checkbox/Checkbox.js +1 -1
- package/lib/ui/Fields/Input/Input.d.ts +2 -4
- package/lib/ui/Fields/Input/Input.js +3 -9
- package/lib/ui/Fields/InputContainer/InputContainer.d.ts +9 -0
- package/lib/ui/Fields/InputContainer/InputContainer.js +16 -0
- package/lib/ui/Fields/MatrixAsset/MatrixAsset.d.ts +17 -0
- package/lib/ui/Fields/MatrixAsset/MatrixAsset.js +29 -0
- package/lib/utils/validation.d.ts +2 -1
- package/lib/utils/validation.js +8 -2
- package/package.json +4 -3
- package/src/Editor/Editor.spec.tsx +1 -1
- package/src/Editor/EditorContext.spec.tsx +11 -13
- package/src/Editor/EditorContext.ts +0 -11
- package/src/EditorToolbar/Tools/Image/Form/ImageForm.spec.tsx +27 -10
- package/src/EditorToolbar/Tools/Image/Form/ImageForm.tsx +25 -29
- package/src/EditorToolbar/Tools/Image/ImageButton.spec.tsx +69 -43
- package/src/EditorToolbar/Tools/Link/Form/LinkForm.spec.tsx +15 -5
- package/src/EditorToolbar/Tools/Link/Form/LinkForm.tsx +14 -20
- package/src/EditorToolbar/Tools/Link/LinkButton.spec.tsx +45 -29
- package/src/EditorToolbar/Tools/Link/RemoveLinkButton.spec.tsx +47 -4
- package/src/EditorToolbar/Tools/Link/RemoveLinkButton.tsx +3 -2
- package/src/Extensions/Extensions.ts +0 -2
- package/src/Extensions/ImageExtension/AssetImageExtension.ts +1 -3
- package/src/Extensions/LinkExtension/AssetLinkExtension.ts +1 -3
- package/src/types.ts +7 -5
- package/src/ui/Fields/Checkbox/Checkbox.tsx +1 -1
- package/src/ui/Fields/Input/Input.tsx +4 -18
- package/src/ui/Fields/InputContainer/InputContainer.spec.tsx +18 -0
- package/src/ui/Fields/InputContainer/InputContainer.tsx +29 -0
- package/src/ui/Fields/MatrixAsset/MatrixAsset.spec.tsx +103 -0
- package/src/ui/Fields/MatrixAsset/MatrixAsset.tsx +55 -0
- package/src/ui/_forms.scss +4 -2
- package/src/utils/validation.spec.ts +22 -0
- package/src/utils/validation.ts +9 -1
- package/tests/index.ts +2 -0
- package/tests/mockResourceBrowserContext.tsx +63 -0
- package/tests/renderWithContext.tsx +18 -0
- package/tests/renderWithEditor.tsx +18 -21
- package/vite.config.ts +8 -0
@@ -1,26 +1,24 @@
|
|
1
|
-
import 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
|
-
|
7
|
+
let defaultContext: EditorContextOptions | null = null;
|
14
8
|
|
15
|
-
|
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
|
-
<
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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.
|
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,
|
2
|
-
import {
|
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 {
|
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
|
-
<
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
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 {
|
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
|
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 {
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
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
|
-
|
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: '
|
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 {
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
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
|
-
|
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: '
|
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
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
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.
|
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(
|
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
|
-
|
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.
|
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(
|
65
|
+
expect(document.querySelectorAll('label')).toHaveLength(1);
|
56
66
|
});
|
57
67
|
});
|
@@ -1,17 +1,17 @@
|
|
1
|
-
import React, { ReactElement
|
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
|
-
<
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
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 {
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
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
|
-
|
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: '
|
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 {
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
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
|
-
|
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: '
|
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
|
331
|
-
|
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('
|
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 () => {
|