@squiz/formatted-text-editor 1.33.1-alpha.3 → 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.
- 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
@@ -7,7 +7,7 @@ import RemoveLinkButton from './RemoveLinkButton';
|
|
7
7
|
describe('RemoveLinkButton', () => {
|
8
8
|
it('Removes a link', async () => {
|
9
9
|
const { editor, getJsonContent } = await renderWithEditor(<RemoveLinkButton />, {
|
10
|
-
context: { matrix: { matrixDomain: 'my-matrix.squiz.net' } },
|
10
|
+
context: { editor: { matrix: { matrixDomain: 'my-matrix.squiz.net' } } },
|
11
11
|
content: {
|
12
12
|
type: 'doc',
|
13
13
|
content: [
|
@@ -17,7 +17,12 @@ describe('RemoveLinkButton', () => {
|
|
17
17
|
{
|
18
18
|
type: 'text',
|
19
19
|
text: 'Sample link',
|
20
|
-
marks: [
|
20
|
+
marks: [
|
21
|
+
{
|
22
|
+
type: 'assetLink',
|
23
|
+
attrs: { matrixAssetId: '123', matrixIdentifier: 'matrix-identifier', target: '_blank' },
|
24
|
+
},
|
25
|
+
],
|
21
26
|
},
|
22
27
|
{ type: 'text', text: ' with ' },
|
23
28
|
{
|
@@ -47,7 +52,7 @@ describe('RemoveLinkButton', () => {
|
|
47
52
|
|
48
53
|
it('Removes the link when clicking the keyboard shortcut', async () => {
|
49
54
|
const { elements, editor, getJsonContent } = await renderWithEditor(<RemoveLinkButton />, {
|
50
|
-
context: { matrix: { matrixDomain: 'my-matrix.squiz.net' } },
|
55
|
+
context: { editor: { matrix: { matrixDomain: 'my-matrix.squiz.net' } } },
|
51
56
|
content: {
|
52
57
|
type: 'doc',
|
53
58
|
content: [
|
@@ -57,7 +62,12 @@ describe('RemoveLinkButton', () => {
|
|
57
62
|
{
|
58
63
|
type: 'text',
|
59
64
|
text: 'Sample link',
|
60
|
-
marks: [
|
65
|
+
marks: [
|
66
|
+
{
|
67
|
+
type: 'assetLink',
|
68
|
+
attrs: { matrixAssetId: '123', matrixIdentifier: 'matrix-identifier', target: '_blank' },
|
69
|
+
},
|
70
|
+
],
|
61
71
|
},
|
62
72
|
{ type: 'text', text: ' with ' },
|
63
73
|
{
|
@@ -97,4 +107,37 @@ describe('RemoveLinkButton', () => {
|
|
97
107
|
// expect remove button to be enabled
|
98
108
|
expect(screen.getByRole('button', { name: 'Remove link (shift+cmd+K)' })).not.toBeDisabled();
|
99
109
|
});
|
110
|
+
|
111
|
+
it('Enables the Remove link button when asset link text is selected', async () => {
|
112
|
+
const { editor } = await renderWithEditor(<RemoveLinkButton />, {
|
113
|
+
context: { editor: { matrix: { matrixDomain: 'my-matrix.squiz.net' } } },
|
114
|
+
content: {
|
115
|
+
type: 'doc',
|
116
|
+
content: [
|
117
|
+
{
|
118
|
+
type: 'paragraph',
|
119
|
+
content: [
|
120
|
+
{
|
121
|
+
type: 'text',
|
122
|
+
text: 'Sample link',
|
123
|
+
marks: [
|
124
|
+
{
|
125
|
+
type: 'assetLink',
|
126
|
+
attrs: { matrixAssetId: '123', matrixIdentifier: 'matrix-identifier', target: '_blank' },
|
127
|
+
},
|
128
|
+
],
|
129
|
+
},
|
130
|
+
],
|
131
|
+
},
|
132
|
+
],
|
133
|
+
},
|
134
|
+
});
|
135
|
+
|
136
|
+
// expect remove button to be disabled
|
137
|
+
expect(screen.getByRole('button', { name: 'Remove link (shift+cmd+K)' })).toBeDisabled();
|
138
|
+
// jump to the middle of the link.
|
139
|
+
await act(() => editor.selectText(3));
|
140
|
+
// expect remove button to be enabled
|
141
|
+
expect(screen.getByRole('button', { name: 'Remove link (shift+cmd+K)' })).not.toBeDisabled();
|
142
|
+
});
|
100
143
|
});
|
@@ -2,13 +2,14 @@ import React, { useCallback } from 'react';
|
|
2
2
|
import { useChainedCommands, useActive, useKeymap } from '@remirror/react';
|
3
3
|
import Button from '../../../ui/Button/Button';
|
4
4
|
import LinkOffIcon from '@mui/icons-material/LinkOff';
|
5
|
+
import { AssetLinkExtension } from '../../../Extensions/LinkExtension/AssetLinkExtension';
|
5
6
|
import { LinkExtension } from '../../../Extensions/LinkExtension/LinkExtension';
|
6
7
|
import { LinkButtonProps } from './LinkButton';
|
7
8
|
|
8
9
|
const RemoveLinkButton = ({ inPopover = false }: LinkButtonProps) => {
|
9
10
|
const chain = useChainedCommands();
|
10
|
-
const active = useActive<LinkExtension>();
|
11
|
-
const disabled = !active.link();
|
11
|
+
const active = useActive<LinkExtension | AssetLinkExtension>();
|
12
|
+
const disabled = !active.link() && !active.assetLink();
|
12
13
|
|
13
14
|
const handleClick = () => {
|
14
15
|
chain.removeLink().removeAssetLink().focus().run();
|
@@ -42,12 +42,10 @@ export const createExtensions = (context: EditorContextOptions) => {
|
|
42
42
|
new ImageExtension(),
|
43
43
|
new ImageExtension({ preferPastedTextContent: false }),
|
44
44
|
new AssetImageExtension({
|
45
|
-
matrixIdentifier: context.matrix.matrixIdentifier,
|
46
45
|
matrixDomain: context.matrix.matrixDomain,
|
47
46
|
}),
|
48
47
|
new LinkExtension(),
|
49
48
|
new AssetLinkExtension({
|
50
|
-
matrixIdentifier: context.matrix.matrixIdentifier,
|
51
49
|
matrixDomain: context.matrix.matrixDomain,
|
52
50
|
}),
|
53
51
|
];
|
@@ -16,7 +16,6 @@ import { resolveMatrixAssetUrl } from '../../utils/resolveMatrixAssetUrl';
|
|
16
16
|
import { NodeName } from '../Extensions';
|
17
17
|
|
18
18
|
export type AssetImageOptions = {
|
19
|
-
matrixIdentifier?: string;
|
20
19
|
matrixDomain?: string;
|
21
20
|
};
|
22
21
|
|
@@ -28,7 +27,6 @@ export type AssetImageAttributes = {
|
|
28
27
|
|
29
28
|
@extension<AssetImageOptions>({
|
30
29
|
defaultOptions: {
|
31
|
-
matrixIdentifier: '',
|
32
30
|
matrixDomain: '',
|
33
31
|
},
|
34
32
|
defaultPriority: ExtensionPriority.High,
|
@@ -51,7 +49,7 @@ export class AssetImageExtension extends NodeExtension<AssetImageOptions> {
|
|
51
49
|
attrs: {
|
52
50
|
...extra.defaults(),
|
53
51
|
matrixAssetId: {},
|
54
|
-
matrixIdentifier: {
|
52
|
+
matrixIdentifier: {},
|
55
53
|
matrixDomain: { default: this.options.matrixDomain },
|
56
54
|
},
|
57
55
|
parseDOM: [
|
@@ -20,7 +20,6 @@ export type AssetLinkAttributes = {
|
|
20
20
|
};
|
21
21
|
|
22
22
|
export type AssetLinkOptions = {
|
23
|
-
matrixIdentifier?: string;
|
24
23
|
matrixDomain?: string;
|
25
24
|
defaultTarget?: LinkTarget;
|
26
25
|
supportedTargets?: LinkTarget[];
|
@@ -34,7 +33,6 @@ export type UpdateAssetLinkProps = {
|
|
34
33
|
|
35
34
|
@extension<AssetLinkOptions>({
|
36
35
|
defaultOptions: {
|
37
|
-
matrixIdentifier: '',
|
38
36
|
matrixDomain: '',
|
39
37
|
defaultTarget: LinkTarget.Self,
|
40
38
|
supportedTargets: [LinkTarget.Self, LinkTarget.Blank],
|
@@ -54,7 +52,7 @@ export class AssetLinkExtension extends MarkExtension<AssetLinkOptions> {
|
|
54
52
|
attrs: {
|
55
53
|
...extra.defaults(),
|
56
54
|
matrixAssetId: {},
|
57
|
-
matrixIdentifier: {
|
55
|
+
matrixIdentifier: {},
|
58
56
|
matrixDomain: { default: this.options.matrixDomain },
|
59
57
|
target: { default: this.options.defaultTarget },
|
60
58
|
},
|
package/src/types.ts
CHANGED
@@ -1,5 +1,7 @@
|
|
1
|
-
export type DeepPartial<T> =
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
1
|
+
export type DeepPartial<T> = {
|
2
|
+
[P in keyof T]?: T[P] extends Array<infer U>
|
3
|
+
? Array<DeepPartial<U>>
|
4
|
+
: T[P] extends ReadonlyArray<infer U>
|
5
|
+
? ReadonlyArray<DeepPartial<U>>
|
6
|
+
: DeepPartial<T[P]>;
|
7
|
+
};
|
@@ -41,7 +41,7 @@ export const Checkbox = <TChecked, TUnchecked>({
|
|
41
41
|
{toggled && <CheckRoundedIcon />}
|
42
42
|
</button>
|
43
43
|
{/* Checkbox label as a button, acts as a secondary way to toggle */}
|
44
|
-
<button type="button" className="label" onClick={toggleCheckbox}>
|
44
|
+
<button type="button" className="label" onClick={toggleCheckbox} tabIndex={-1}>
|
45
45
|
{label}
|
46
46
|
</button>
|
47
47
|
</div>
|
@@ -1,27 +1,14 @@
|
|
1
1
|
import React, { ForwardedRef, forwardRef, InputHTMLAttributes } from 'react';
|
2
|
-
import
|
2
|
+
import { InputContainer, InputContainerProps } from '../InputContainer/InputContainer';
|
3
3
|
|
4
|
-
type InputProps = InputHTMLAttributes<HTMLInputElement> &
|
5
|
-
label?: string;
|
6
|
-
error?: string;
|
7
|
-
};
|
4
|
+
type InputProps = InputHTMLAttributes<HTMLInputElement> & Omit<InputContainerProps, 'children'>;
|
8
5
|
|
9
6
|
const InputInternal = (
|
10
7
|
{ name, label, type = 'text', error, required, ...rest }: InputProps,
|
11
8
|
ref: ForwardedRef<HTMLInputElement>,
|
12
9
|
) => {
|
13
10
|
return (
|
14
|
-
<
|
15
|
-
{label && (
|
16
|
-
<label htmlFor={name} className="squiz-fte-form-label">
|
17
|
-
{label}
|
18
|
-
</label>
|
19
|
-
)}
|
20
|
-
{required && (
|
21
|
-
<span className="text-gray-600" aria-label="Required field">
|
22
|
-
*
|
23
|
-
</span>
|
24
|
-
)}
|
11
|
+
<InputContainer name={name} label={label} error={error} required={required}>
|
25
12
|
<input
|
26
13
|
ref={ref}
|
27
14
|
id={name}
|
@@ -31,8 +18,7 @@ const InputInternal = (
|
|
31
18
|
className="squiz-fte-form-control"
|
32
19
|
{...rest}
|
33
20
|
/>
|
34
|
-
|
35
|
-
</div>
|
21
|
+
</InputContainer>
|
36
22
|
);
|
37
23
|
};
|
38
24
|
|
@@ -0,0 +1,18 @@
|
|
1
|
+
import '@testing-library/jest-dom';
|
2
|
+
import React from 'react';
|
3
|
+
import { render, screen } from '@testing-library/react';
|
4
|
+
import { InputContainer } from './InputContainer';
|
5
|
+
|
6
|
+
describe('InputContainer', () => {
|
7
|
+
it('Renders with expected content', () => {
|
8
|
+
render(
|
9
|
+
<InputContainer name="my-input" label="My input" error="Input is invalid" required={true}>
|
10
|
+
input element
|
11
|
+
</InputContainer>,
|
12
|
+
);
|
13
|
+
|
14
|
+
expect(screen.getByText('My input')).toHaveClass('squiz-fte-form-label');
|
15
|
+
expect(screen.getByText('input element')).toBeInTheDocument();
|
16
|
+
expect(screen.getByText('Input is invalid')).toHaveClass('squiz-fte-form-error');
|
17
|
+
});
|
18
|
+
});
|
@@ -0,0 +1,29 @@
|
|
1
|
+
import React, { ReactNode } from 'react';
|
2
|
+
import clsx from 'clsx';
|
3
|
+
|
4
|
+
export type InputContainerProps = {
|
5
|
+
name?: string;
|
6
|
+
label?: string;
|
7
|
+
error?: string;
|
8
|
+
required?: boolean;
|
9
|
+
children: ReactNode;
|
10
|
+
};
|
11
|
+
|
12
|
+
export const InputContainer = ({ name, label, error, required, children }: InputContainerProps) => {
|
13
|
+
return (
|
14
|
+
<div className={clsx(error && 'squiz-fte-invalid-form-field')}>
|
15
|
+
{label && (
|
16
|
+
<label htmlFor={name} className="squiz-fte-form-label">
|
17
|
+
{label}
|
18
|
+
</label>
|
19
|
+
)}
|
20
|
+
{label && required && (
|
21
|
+
<span className="text-gray-600" aria-label="Required field">
|
22
|
+
*
|
23
|
+
</span>
|
24
|
+
)}
|
25
|
+
{children}
|
26
|
+
{error && <div className="squiz-fte-form-error">{error}</div>}
|
27
|
+
</div>
|
28
|
+
);
|
29
|
+
};
|
@@ -0,0 +1,103 @@
|
|
1
|
+
import '@testing-library/jest-dom';
|
2
|
+
import React from 'react';
|
3
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
4
|
+
import { MatrixAsset } from './MatrixAsset';
|
5
|
+
import { mockResourceBrowserContext } from '../../../../tests';
|
6
|
+
|
7
|
+
describe('MatrixAsset', () => {
|
8
|
+
it('Renders empty state when no value is provided', () => {
|
9
|
+
render(<MatrixAsset modalTitle="Insert asset" onChange={jest.fn()} />);
|
10
|
+
|
11
|
+
expect(screen.getByRole('button', { name: 'Choose asset' })).toBeInTheDocument();
|
12
|
+
});
|
13
|
+
|
14
|
+
it('Renders a selected state when a value is provided', () => {
|
15
|
+
const { MockResourceBrowserContext } = mockResourceBrowserContext({
|
16
|
+
sources: [{ id: 'my-source-id' }],
|
17
|
+
resources: [{ id: 'my-resource-id', name: 'My resource' }],
|
18
|
+
});
|
19
|
+
|
20
|
+
render(
|
21
|
+
<MockResourceBrowserContext>
|
22
|
+
<MatrixAsset
|
23
|
+
modalTitle="Insert asset"
|
24
|
+
value={{
|
25
|
+
matrixIdentifier: 'my-source-id',
|
26
|
+
matrixAssetId: 'my-resource-id',
|
27
|
+
addional: 'addditional data',
|
28
|
+
}}
|
29
|
+
onChange={jest.fn()}
|
30
|
+
/>
|
31
|
+
</MockResourceBrowserContext>,
|
32
|
+
);
|
33
|
+
|
34
|
+
expect(screen.getByText('My resource')).toBeInTheDocument();
|
35
|
+
});
|
36
|
+
|
37
|
+
it('Calls onChange with expected value when resources is selected', async () => {
|
38
|
+
const handleChange = jest.fn();
|
39
|
+
const { MockResourceBrowserContext, selectResource } = mockResourceBrowserContext({
|
40
|
+
sources: [{ id: 'my-source-id' }],
|
41
|
+
resources: [{ id: 'my-resource-id', name: 'My resource' }],
|
42
|
+
});
|
43
|
+
|
44
|
+
render(
|
45
|
+
<MockResourceBrowserContext>
|
46
|
+
<MatrixAsset
|
47
|
+
modalTitle="Insert asset"
|
48
|
+
value={{ matrixIdentifier: undefined, matrixAssetId: undefined, additional: 'additional data' }}
|
49
|
+
onChange={handleChange}
|
50
|
+
/>
|
51
|
+
</MockResourceBrowserContext>,
|
52
|
+
);
|
53
|
+
|
54
|
+
await selectResource(screen.getByRole('button', { name: 'Choose asset' }), 'My resource');
|
55
|
+
|
56
|
+
expect(handleChange).toHaveBeenCalledWith({
|
57
|
+
target: {
|
58
|
+
value: {
|
59
|
+
additional: 'additional data',
|
60
|
+
matrixAssetId: 'my-resource-id',
|
61
|
+
matrixIdentifier: 'my-source-id',
|
62
|
+
},
|
63
|
+
},
|
64
|
+
});
|
65
|
+
});
|
66
|
+
|
67
|
+
it('Calls onChange with expected value when resources is cleared', async () => {
|
68
|
+
const handleChange = jest.fn();
|
69
|
+
const { MockResourceBrowserContext } = mockResourceBrowserContext({
|
70
|
+
sources: [{ id: 'my-source-id' }],
|
71
|
+
resources: [
|
72
|
+
{ id: 'my-resource-id', name: 'My resource' },
|
73
|
+
{ id: 'updated-resource-id', name: 'Updated resource' },
|
74
|
+
],
|
75
|
+
});
|
76
|
+
|
77
|
+
render(
|
78
|
+
<MockResourceBrowserContext>
|
79
|
+
<MatrixAsset
|
80
|
+
modalTitle="Insert asset"
|
81
|
+
value={{
|
82
|
+
matrixIdentifier: 'my-source-id',
|
83
|
+
matrixAssetId: 'my-resource-id',
|
84
|
+
additional: 'additional data',
|
85
|
+
}}
|
86
|
+
onChange={handleChange}
|
87
|
+
/>
|
88
|
+
</MockResourceBrowserContext>,
|
89
|
+
);
|
90
|
+
|
91
|
+
fireEvent.click(screen.getByRole('button', { name: 'Remove selection' }));
|
92
|
+
|
93
|
+
expect(handleChange).toHaveBeenCalledWith({
|
94
|
+
target: {
|
95
|
+
value: {
|
96
|
+
additional: 'additional data',
|
97
|
+
matrixAssetId: undefined,
|
98
|
+
matrixIdentifier: undefined,
|
99
|
+
},
|
100
|
+
},
|
101
|
+
});
|
102
|
+
});
|
103
|
+
});
|
@@ -0,0 +1,55 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { HydratedResourceReference, ResourceBrowserInput } from '@squiz/resource-browser';
|
3
|
+
import { InputContainer, InputContainerProps } from '../InputContainer/InputContainer';
|
4
|
+
|
5
|
+
type MatrixAssetValue = {
|
6
|
+
matrixIdentifier?: string;
|
7
|
+
matrixAssetId?: string;
|
8
|
+
};
|
9
|
+
|
10
|
+
export type MatrixAssetProps<T extends MatrixAssetValue> = Omit<InputContainerProps, 'children'> & {
|
11
|
+
modalTitle: string;
|
12
|
+
allowedTypes?: string[];
|
13
|
+
value?: T | null;
|
14
|
+
// LinkForm contains a "target" property.
|
15
|
+
// react-hook-form treats the presence of this property as an "Event" object being passed through, see:
|
16
|
+
// https://github.com/react-hook-form/react-hook-form/blob/master/src/logic/getEventValue.ts
|
17
|
+
// Nest the value under a "target" object to work around the behaviour.
|
18
|
+
onChange: (value: { target: { value: T } }) => void;
|
19
|
+
};
|
20
|
+
|
21
|
+
export const MatrixAsset = <T extends MatrixAssetValue>({
|
22
|
+
modalTitle,
|
23
|
+
allowedTypes,
|
24
|
+
value,
|
25
|
+
onChange,
|
26
|
+
...props
|
27
|
+
}: MatrixAssetProps<T>) => {
|
28
|
+
return (
|
29
|
+
<InputContainer {...props}>
|
30
|
+
<ResourceBrowserInput
|
31
|
+
modalTitle={modalTitle}
|
32
|
+
allowedTypes={allowedTypes}
|
33
|
+
value={
|
34
|
+
value && value.matrixIdentifier && value.matrixAssetId
|
35
|
+
? {
|
36
|
+
source: value.matrixIdentifier,
|
37
|
+
resource: value.matrixAssetId,
|
38
|
+
}
|
39
|
+
: null
|
40
|
+
}
|
41
|
+
onChange={(reference: HydratedResourceReference | null) => {
|
42
|
+
onChange({
|
43
|
+
target: {
|
44
|
+
value: {
|
45
|
+
...value,
|
46
|
+
matrixIdentifier: reference?.source?.id,
|
47
|
+
matrixAssetId: reference?.resource?.id,
|
48
|
+
} as T,
|
49
|
+
},
|
50
|
+
});
|
51
|
+
}}
|
52
|
+
/>
|
53
|
+
</InputContainer>
|
54
|
+
);
|
55
|
+
};
|
package/src/ui/_forms.scss
CHANGED
@@ -6,6 +6,7 @@
|
|
6
6
|
@apply mb-1 text-md font-semibold text-gray-600;
|
7
7
|
}
|
8
8
|
&-form-control {
|
9
|
+
height: 36px;
|
9
10
|
padding: 6px 12px;
|
10
11
|
@apply placeholder-slate-300 text-gray-800 relative bg-white rounded text-md border-2 border-gray-300 outline-0 focus:outline-0 focus:border-blue-300 w-full;
|
11
12
|
&:focus,
|
@@ -14,10 +15,11 @@
|
|
14
15
|
}
|
15
16
|
}
|
16
17
|
&-invalid-form-field {
|
17
|
-
.squiz-fte-form-control
|
18
|
+
.squiz-fte-form-control,
|
19
|
+
.resource-picker {
|
18
20
|
@apply border-red-300 bg-no-repeat pr-8;
|
19
21
|
background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjQiIHdpZHRoPSIyNCI+PHJlY3Qgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgZmlsbD0ibm9uZSIvPjxnIGNsYXNzPSJjdXJyZW50TGF5ZXIiPjxwYXRoIGQ9Ik00LjQ3IDIxaDE1LjA2YzEuNTQgMCAyLjUtMS42NyAxLjczLTNMMTMuNzMgNC45OWMtLjc3LTEuMzMtMi42OS0xLjMzLTMuNDYgMEwyLjc0IDE4Yy0uNzcgMS4zMy4xOSAzIDEuNzMgM3pNMTIgMTRjLS41NSAwLTEtLjQ1LTEtMXYtMmMwLS41NS40NS0xIDEtMXMxIC40NSAxIDF2MmMwIC41NS0uNDUgMS0xIDF6bTEgNGgtMnYtMmgydjJ6IiBjbGFzcz0ic2VsZWN0ZWQiIGZpbGw9IiNkNzIzMjEiLz48L2c+PC9zdmc+');
|
20
|
-
background-position:
|
22
|
+
background-position: center right 0.25rem;
|
21
23
|
background-size: 1.5rem;
|
22
24
|
}
|
23
25
|
}
|
@@ -0,0 +1,22 @@
|
|
1
|
+
import { hasProperties, noEmptySpacesValidation } from './validation';
|
2
|
+
|
3
|
+
describe('validation', () => {
|
4
|
+
it.each([
|
5
|
+
['String with only spaces', ' ', 'Empty space is not allowed'],
|
6
|
+
['Empty string', '', undefined],
|
7
|
+
['Non-empty string', 'test value', undefined],
|
8
|
+
['String with trailing/leading spaces', ' test value ', undefined],
|
9
|
+
])('noEmptySpacesValidation - %s', (description: string, value: string, expected: string | undefined) => {
|
10
|
+
expect(noEmptySpacesValidation(value)).toBe(expected);
|
11
|
+
});
|
12
|
+
|
13
|
+
it.each([
|
14
|
+
['Value is null', null, ['prop1'], 'Field is invalid'],
|
15
|
+
['Value is undefined', undefined, ['prop1'], 'Field is invalid'],
|
16
|
+
['Value has none of the required props', { prop2: 'Not what we want' }, ['prop1'], 'Field is invalid'],
|
17
|
+
['Value has all of the required props', { prop1: 'Valid' }, ['prop1'], undefined],
|
18
|
+
['Value has all of the required props + extra', { prop1: 'Valid', prop2: 'Another prop' }, ['prop1'], undefined],
|
19
|
+
])('hasProperties - %s', (description: string, value: any, properties: string[], expected: string | undefined) => {
|
20
|
+
expect(hasProperties('Field is invalid', properties)(value)).toBe(expected);
|
21
|
+
});
|
22
|
+
});
|
package/src/utils/validation.ts
CHANGED
@@ -1,8 +1,16 @@
|
|
1
|
-
export const noEmptySpacesValidation =
|
1
|
+
export const noEmptySpacesValidation = (value: string | undefined) => {
|
2
2
|
if (value && !(value.trim().length > 0)) {
|
3
3
|
return 'Empty space is not allowed';
|
4
4
|
}
|
5
5
|
};
|
6
6
|
|
7
|
+
export const hasProperties =
|
8
|
+
<T>(message: string, properties: (keyof T)[]) =>
|
9
|
+
(value: T) => {
|
10
|
+
if (!value || properties.filter((property) => value[property]).length !== properties.length) {
|
11
|
+
return message;
|
12
|
+
}
|
13
|
+
};
|
14
|
+
|
7
15
|
export const regexDataURI =
|
8
16
|
/^data:([a-z]+\/[a-z0-9-+.]+(;[a-z-]+=[a-z0-9-]+)?)?(;base64)?,([a-z0-9!$&',()*+;=\-._~:@/?%\s]*)$/i;
|
package/tests/index.ts
CHANGED
@@ -0,0 +1,63 @@
|
|
1
|
+
import React, { ReactNode } from 'react';
|
2
|
+
import { ResourceBrowserContext, Source, Resource, ResourceReference } from '@squiz/resource-browser';
|
3
|
+
import { fireEvent, screen } from '@testing-library/react';
|
4
|
+
import { DeepPartial } from '../src/types';
|
5
|
+
|
6
|
+
export type MockResourceBrowserContextOptions = DeepPartial<{
|
7
|
+
sources: Source[];
|
8
|
+
resources: Resource[];
|
9
|
+
}>;
|
10
|
+
|
11
|
+
const mockResource = (resource: DeepPartial<Resource> = {}): Resource =>
|
12
|
+
({
|
13
|
+
id: 'default-resource',
|
14
|
+
name: 'Default resource',
|
15
|
+
url: 'https://default-resource/',
|
16
|
+
urls: [],
|
17
|
+
childCount: 0,
|
18
|
+
type: {
|
19
|
+
code: 'unspecified',
|
20
|
+
name: 'Unspecified',
|
21
|
+
},
|
22
|
+
status: {
|
23
|
+
code: 'live',
|
24
|
+
name: 'Live',
|
25
|
+
},
|
26
|
+
...resource,
|
27
|
+
} as Resource);
|
28
|
+
|
29
|
+
const mockSource = (source: DeepPartial<Source>): Source => ({
|
30
|
+
id: 'default-source',
|
31
|
+
name: 'Default source',
|
32
|
+
...source,
|
33
|
+
nodes: (source.nodes || [mockResource()]).map((resource) => mockResource(resource)),
|
34
|
+
});
|
35
|
+
|
36
|
+
export const mockResourceBrowserContext = ({ sources, resources }: MockResourceBrowserContextOptions) => {
|
37
|
+
sources = (sources || []).map((source) => mockSource(source));
|
38
|
+
resources = (resources || []).map((resource) => mockResource(resource));
|
39
|
+
|
40
|
+
const onRequestSources = jest.fn().mockResolvedValue(sources);
|
41
|
+
const onRequestChildren = jest.fn().mockResolvedValue(resources);
|
42
|
+
const onRequestResource = jest
|
43
|
+
.fn()
|
44
|
+
.mockImplementation(
|
45
|
+
(reference: ResourceReference) => resources?.find((resource) => resource.id === reference.resource) || null,
|
46
|
+
);
|
47
|
+
|
48
|
+
return {
|
49
|
+
MockResourceBrowserContext: ({ children }: { children: ReactNode }) => (
|
50
|
+
<ResourceBrowserContext.Provider value={{ onRequestSources, onRequestChildren, onRequestResource }}>
|
51
|
+
{children}
|
52
|
+
</ResourceBrowserContext.Provider>
|
53
|
+
),
|
54
|
+
selectResource: async (opener: HTMLElement, resourceName: string) => {
|
55
|
+
const sourceLabel = `Drill down to ${sources?.[0]?.nodes?.[0]?.name} children`;
|
56
|
+
|
57
|
+
fireEvent.click(opener);
|
58
|
+
fireEvent.click(await screen.findByRole('button', { name: sourceLabel }));
|
59
|
+
fireEvent.click(await screen.findByTitle(resourceName));
|
60
|
+
fireEvent.click(await screen.findByRole('button', { name: 'Select' }));
|
61
|
+
},
|
62
|
+
};
|
63
|
+
};
|
@@ -0,0 +1,18 @@
|
|
1
|
+
import React, { ReactElement } from 'react';
|
2
|
+
import { render, RenderOptions, RenderResult } from '@testing-library/react';
|
3
|
+
import merge from 'deepmerge';
|
4
|
+
import { EditorContext } from '../src';
|
5
|
+
import { defaultEditorContext, EditorContextOptions } from '../src/Editor/EditorContext';
|
6
|
+
import { DeepPartial } from '../src/types';
|
7
|
+
|
8
|
+
export type ContextRenderOptions = RenderOptions & {
|
9
|
+
context?: DeepPartial<{
|
10
|
+
editor: EditorContextOptions;
|
11
|
+
}>;
|
12
|
+
};
|
13
|
+
|
14
|
+
export const renderWithContext = (ui: ReactElement | null, options?: ContextRenderOptions): RenderResult => {
|
15
|
+
const editorContext = merge(defaultEditorContext, options?.context?.editor || {}) as EditorContextOptions;
|
16
|
+
|
17
|
+
return render(<EditorContext.Provider value={editorContext}>{ui}</EditorContext.Provider>);
|
18
|
+
};
|
@@ -1,22 +1,18 @@
|
|
1
1
|
import React, { ReactElement, useContext, useEffect } from 'react';
|
2
|
-
import { render, RenderOptions } from '@testing-library/react';
|
3
2
|
import { Extension, RemirrorContentType, RemirrorManager } from '@remirror/core';
|
4
3
|
import { CorePreset } from '@remirror/preset-core';
|
5
4
|
import { BuiltinPreset } from 'remirror';
|
6
5
|
import { EditorComponent, Remirror, useRemirror } from '@remirror/react';
|
7
6
|
import { RemirrorTestChain } from 'jest-remirror';
|
8
|
-
import merge from 'deepmerge';
|
9
7
|
import { createExtensions } from '../src/Extensions/Extensions';
|
10
|
-
import { EditorContext } from '../src
|
11
|
-
import { FloatingToolbar } from '../src/EditorToolbar
|
12
|
-
import {
|
13
|
-
import { DeepPartial } from '../src/types';
|
8
|
+
import { EditorContext } from '../src';
|
9
|
+
import { FloatingToolbar } from '../src/EditorToolbar';
|
10
|
+
import { renderWithContext, ContextRenderOptions } from './renderWithContext';
|
14
11
|
|
15
|
-
export type EditorRenderOptions =
|
12
|
+
export type EditorRenderOptions = ContextRenderOptions & {
|
16
13
|
content?: RemirrorContentType;
|
17
14
|
editable?: boolean;
|
18
15
|
extensions?: Extension[];
|
19
|
-
context?: DeepPartial<EditorContextOptions>;
|
20
16
|
};
|
21
17
|
|
22
18
|
type TestEditorProps = EditorRenderOptions & {
|
@@ -41,6 +37,9 @@ const TestEditor = ({ children, extensions, content, onReady, editable }: TestEd
|
|
41
37
|
content: content,
|
42
38
|
selection: 'start',
|
43
39
|
stringHandler: 'html',
|
40
|
+
onError: ({ error }) => {
|
41
|
+
throw error;
|
42
|
+
},
|
44
43
|
});
|
45
44
|
|
46
45
|
useEffect(() => {
|
@@ -89,7 +88,6 @@ export const renderWithEditor = async (
|
|
89
88
|
ui: ReactElement | null,
|
90
89
|
options?: EditorRenderOptions,
|
91
90
|
): Promise<EditorRenderResult> => {
|
92
|
-
const context = merge(defaultEditorContext, options?.context || {}) as EditorContextOptions;
|
93
91
|
const result: Partial<EditorRenderResult> = {
|
94
92
|
getHtmlContent: () => document.querySelector('.remirror-editor')?.innerHTML,
|
95
93
|
getJsonContent: () => result.editor?.state.doc.content.child(0).toJSON(),
|
@@ -97,18 +95,17 @@ export const renderWithEditor = async (
|
|
97
95
|
};
|
98
96
|
let isReady = false;
|
99
97
|
|
100
|
-
const { container } =
|
101
|
-
<
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
</EditorContext.Provider>,
|
98
|
+
const { container } = renderWithContext(
|
99
|
+
<TestEditor
|
100
|
+
onReady={(manager) => {
|
101
|
+
result.editor = RemirrorTestChain.create(manager);
|
102
|
+
isReady = true;
|
103
|
+
}}
|
104
|
+
{...options}
|
105
|
+
>
|
106
|
+
{ui}
|
107
|
+
</TestEditor>,
|
108
|
+
options,
|
112
109
|
);
|
113
110
|
|
114
111
|
if (!isReady) {
|