@centreon/ui 23.10.51 → 23.10.53
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/package.json +3 -1
- package/src/Dashboard/Layout.tsx +4 -2
- package/src/Graph/BasicComponents/Axes/index.tsx +1 -1
- package/src/Graph/BasicComponents/Lines/RegularLines/index.tsx +6 -3
- package/src/Graph/BasicComponents/Lines/StackedLines/index.tsx +6 -3
- package/src/Graph/BasicComponents/Lines/Threshold/BasicThreshold.tsx +5 -3
- package/src/Graph/BasicComponents/Lines/Threshold/ThresholdWithPatternLines.tsx +5 -1
- package/src/Graph/BasicComponents/Lines/Threshold/ThresholdWithVariation.tsx +5 -1
- package/src/Graph/BasicComponents/Lines/Threshold/index.tsx +5 -1
- package/src/Graph/BasicComponents/Lines/index.tsx +6 -1
- package/src/Graph/BasicComponents/Lines/models.ts +3 -0
- package/src/Graph/Graph.tsx +40 -20
- package/src/Graph/Header/index.tsx +7 -7
- package/src/Graph/InteractiveComponents/AnchorPoint/TooltipAnchorPoint.tsx +48 -22
- package/src/Graph/InteractiveComponents/AnchorPoint/index.tsx +14 -12
- package/src/Graph/InteractiveComponents/TimeShiftZones/TimeShiftZone.tsx +1 -1
- package/src/Graph/index.stories.tsx +10 -0
- package/src/Graph/index.tsx +35 -23
- package/src/Graph/models.ts +1 -1
- package/src/InputField/Select/Autocomplete/Connected/index.tsx +9 -1
- package/src/InputField/Select/Autocomplete/Multi/index.tsx +7 -2
- package/src/RichTextEditor/ContentEditable.tsx +12 -9
- package/src/RichTextEditor/RichTextEditor.cypress.spec.tsx +109 -1
- package/src/RichTextEditor/RichTextEditor.tsx +19 -5
- package/src/RichTextEditor/plugins/AutoLinkPlugin/index.tsx +14 -5
- package/src/RichTextEditor/plugins/FloatingLinkEditorPlugin.tsx +14 -7
- package/src/RichTextEditor/plugins/ToolbarPlugin/BlockButtons.tsx +197 -0
- package/src/RichTextEditor/plugins/ToolbarPlugin/FormatButtons.tsx +3 -10
- package/src/RichTextEditor/plugins/ToolbarPlugin/MacrosButton.tsx +6 -1
- package/src/RichTextEditor/plugins/ToolbarPlugin/ToolbarPlugin.styles.ts +7 -0
- package/src/RichTextEditor/plugins/ToolbarPlugin/UndoRedoButtons.tsx +7 -3
- package/src/RichTextEditor/plugins/ToolbarPlugin/index.tsx +9 -7
- package/src/api/useGraphQuery/index.ts +92 -0
- package/src/api/useGraphQuery/models.ts +12 -0
- package/src/components/ItemComposition/Item.tsx +37 -0
- package/src/components/ItemComposition/ItemComposition.stories.tsx +78 -0
- package/src/components/ItemComposition/ItemComposition.styles.ts +26 -0
- package/src/components/ItemComposition/ItemComposition.tsx +41 -0
- package/src/components/ItemComposition/index.ts +6 -0
- package/src/components/Modal/Modal.styles.ts +1 -1
- package/src/components/index.ts +1 -0
- package/src/index.ts +3 -0
- package/src/Graph/InteractiveComponents/AnchorPoint/AnchorPoint.styles.ts +0 -58
package/src/Graph/index.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { MutableRefObject, useRef } from 'react';
|
|
2
2
|
|
|
3
|
-
import { Responsive } from '@visx/visx';
|
|
3
|
+
import { Curve, Responsive } from '@visx/visx';
|
|
4
4
|
import dayjs from 'dayjs';
|
|
5
5
|
import 'dayjs/locale/en';
|
|
6
6
|
import 'dayjs/locale/es';
|
|
@@ -14,16 +14,19 @@ import Graph from './Graph';
|
|
|
14
14
|
import LoadingSkeleton from './LoadingSkeleton';
|
|
15
15
|
import { GlobalAreaLines, GraphData, GraphProps, LegendModel } from './models';
|
|
16
16
|
import useGraphData from './useGraphData';
|
|
17
|
+
import { CurveType } from './BasicComponents/Lines/models';
|
|
17
18
|
|
|
18
19
|
dayjs.extend(localizedFormat);
|
|
19
20
|
dayjs.extend(utcPlugin);
|
|
20
21
|
dayjs.extend(timezonePlugin);
|
|
21
22
|
|
|
22
23
|
interface Props extends Partial<GraphProps> {
|
|
24
|
+
curve?: CurveType;
|
|
23
25
|
data?: GraphData;
|
|
24
26
|
end: string;
|
|
25
27
|
legend: LegendModel;
|
|
26
28
|
loading: boolean;
|
|
29
|
+
marginBottom?: number;
|
|
27
30
|
shapeLines?: GlobalAreaLines;
|
|
28
31
|
start: string;
|
|
29
32
|
}
|
|
@@ -43,7 +46,9 @@ const WrapperGraph = ({
|
|
|
43
46
|
tooltip,
|
|
44
47
|
annotationEvent,
|
|
45
48
|
legend,
|
|
46
|
-
header
|
|
49
|
+
header,
|
|
50
|
+
curve = Curve.curveLinear,
|
|
51
|
+
marginBottom = 0
|
|
47
52
|
}: Props): JSX.Element | null => {
|
|
48
53
|
const { adjustedData } = useGraphData({ data, end, start });
|
|
49
54
|
const graphRef = useRef<HTMLDivElement | null>(null);
|
|
@@ -52,36 +57,43 @@ const WrapperGraph = ({
|
|
|
52
57
|
return (
|
|
53
58
|
<LoadingSkeleton
|
|
54
59
|
displayTitleSkeleton={header?.displayTitle ?? true}
|
|
55
|
-
graphHeight={height}
|
|
60
|
+
graphHeight={height || 200}
|
|
56
61
|
/>
|
|
57
62
|
);
|
|
58
63
|
}
|
|
59
64
|
|
|
60
65
|
return (
|
|
61
|
-
<div
|
|
66
|
+
<div
|
|
67
|
+
ref={graphRef as MutableRefObject<HTMLDivElement>}
|
|
68
|
+
style={{ height: '100%', width: '100%' }}
|
|
69
|
+
>
|
|
62
70
|
<Responsive.ParentSize>
|
|
63
71
|
{({
|
|
64
72
|
height: responsiveHeight,
|
|
65
73
|
width: responsiveWidth
|
|
66
|
-
}): JSX.Element =>
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
74
|
+
}): JSX.Element => {
|
|
75
|
+
return (
|
|
76
|
+
<Graph
|
|
77
|
+
annotationEvent={annotationEvent}
|
|
78
|
+
axis={axis}
|
|
79
|
+
curve={curve}
|
|
80
|
+
displayAnchor={displayAnchor}
|
|
81
|
+
graphData={adjustedData}
|
|
82
|
+
graphInterval={{ end, start }}
|
|
83
|
+
graphRef={graphRef}
|
|
84
|
+
header={header}
|
|
85
|
+
height={height || responsiveHeight}
|
|
86
|
+
legend={legend}
|
|
87
|
+
loading={loading}
|
|
88
|
+
marginBottom={marginBottom}
|
|
89
|
+
shapeLines={shapeLines}
|
|
90
|
+
timeShiftZones={timeShiftZones}
|
|
91
|
+
tooltip={tooltip}
|
|
92
|
+
width={width ?? responsiveWidth}
|
|
93
|
+
zoomPreview={zoomPreview}
|
|
94
|
+
/>
|
|
95
|
+
);
|
|
96
|
+
}}
|
|
85
97
|
</Responsive.ParentSize>
|
|
86
98
|
</div>
|
|
87
99
|
);
|
package/src/Graph/models.ts
CHANGED
|
@@ -37,6 +37,7 @@ export interface ConnectedAutoCompleteFieldProps<TData> {
|
|
|
37
37
|
getRequestHeaders?: HeadersInit;
|
|
38
38
|
initialPage: number;
|
|
39
39
|
labelKey?: string;
|
|
40
|
+
queryKey?: string;
|
|
40
41
|
searchConditions?: Array<ConditionsSearchParameter>;
|
|
41
42
|
}
|
|
42
43
|
|
|
@@ -55,6 +56,7 @@ const ConnectedAutocompleteField = (
|
|
|
55
56
|
getRenderedOptionText = (option): string => option.name?.toString(),
|
|
56
57
|
getRequestHeaders,
|
|
57
58
|
displayOptionThumbnail,
|
|
59
|
+
queryKey,
|
|
58
60
|
...props
|
|
59
61
|
}: ConnectedAutoCompleteFieldProps<TData> &
|
|
60
62
|
Omit<AutocompleteFieldProps, 'options'>): JSX.Element => {
|
|
@@ -89,10 +91,16 @@ const ConnectedAutocompleteField = (
|
|
|
89
91
|
search: searchParameter
|
|
90
92
|
});
|
|
91
93
|
},
|
|
92
|
-
getQueryKey: () => [
|
|
94
|
+
getQueryKey: () => [
|
|
95
|
+
`autocomplete-${queryKey || props.label}`,
|
|
96
|
+
page,
|
|
97
|
+
searchParameter
|
|
98
|
+
],
|
|
93
99
|
isPaginated: true,
|
|
94
100
|
queryOptions: {
|
|
101
|
+
cacheTime: 0,
|
|
95
102
|
enabled: false,
|
|
103
|
+
staleTime: 0,
|
|
96
104
|
suspense: false
|
|
97
105
|
}
|
|
98
106
|
});
|
|
@@ -34,12 +34,17 @@ export interface Props
|
|
|
34
34
|
'multiple'
|
|
35
35
|
> {
|
|
36
36
|
disableSortedOptions?: boolean;
|
|
37
|
+
getTagLabel?: (option) => string;
|
|
38
|
+
optionProperty?: string;
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
const MultiAutocompleteField = ({
|
|
40
42
|
value,
|
|
41
43
|
options,
|
|
42
44
|
disableSortedOptions = false,
|
|
45
|
+
optionProperty = 'name',
|
|
46
|
+
getOptionLabel = (option): string => option.name,
|
|
47
|
+
getTagLabel = (option): string => option[optionProperty],
|
|
43
48
|
...props
|
|
44
49
|
}: Props): JSX.Element => {
|
|
45
50
|
const { classes } = useStyles();
|
|
@@ -52,7 +57,7 @@ const MultiAutocompleteField = ({
|
|
|
52
57
|
root: classes.tag
|
|
53
58
|
}}
|
|
54
59
|
key={option.id}
|
|
55
|
-
label={option
|
|
60
|
+
label={getTagLabel(option)}
|
|
56
61
|
size="medium"
|
|
57
62
|
{...getTagProps({ index })}
|
|
58
63
|
/>
|
|
@@ -84,7 +89,7 @@ const MultiAutocompleteField = ({
|
|
|
84
89
|
key={option.id}
|
|
85
90
|
{...(renderProps as React.HTMLAttributes<HTMLLIElement>)}
|
|
86
91
|
>
|
|
87
|
-
<Option checkboxSelected={selected}>{option
|
|
92
|
+
<Option checkboxSelected={selected}>{getOptionLabel(option)}</Option>
|
|
88
93
|
</li>
|
|
89
94
|
)}
|
|
90
95
|
renderTags={renderTags}
|
|
@@ -47,6 +47,7 @@ const useStyles = makeStyles<StyleProps>()(
|
|
|
47
47
|
|
|
48
48
|
interface Props {
|
|
49
49
|
className?: string;
|
|
50
|
+
disabled: boolean;
|
|
50
51
|
editable: boolean;
|
|
51
52
|
editorState?: string;
|
|
52
53
|
error?: string;
|
|
@@ -72,13 +73,13 @@ const ContentEditable = ({
|
|
|
72
73
|
initialEditorState,
|
|
73
74
|
error,
|
|
74
75
|
onBlur,
|
|
75
|
-
className
|
|
76
|
+
className,
|
|
77
|
+
disabled
|
|
76
78
|
}: Props): JSX.Element => {
|
|
77
79
|
const { classes, cx } = useStyles({ editable, error, minInputHeight });
|
|
78
80
|
const { t } = useTranslation();
|
|
79
81
|
|
|
80
82
|
const [editor] = useLexicalComposerContext();
|
|
81
|
-
const [isEditable, setEditable] = useState(false);
|
|
82
83
|
const [isFocused, setFocused] = useState(false);
|
|
83
84
|
const [root, setRoot] = useState('');
|
|
84
85
|
|
|
@@ -90,17 +91,11 @@ const ContentEditable = ({
|
|
|
90
91
|
);
|
|
91
92
|
|
|
92
93
|
useLayoutEffect(() => {
|
|
93
|
-
setEditable(editor.isEditable());
|
|
94
|
-
|
|
95
94
|
if (editorState && !editable) {
|
|
96
95
|
const newEditorState = editor.parseEditorState(editorState);
|
|
97
96
|
|
|
98
97
|
editor.setEditorState(newEditorState);
|
|
99
98
|
}
|
|
100
|
-
|
|
101
|
-
return editor.registerEditableListener((currentIsEditable) => {
|
|
102
|
-
setEditable(currentIsEditable);
|
|
103
|
-
});
|
|
104
99
|
}, [editor, editorState]);
|
|
105
100
|
|
|
106
101
|
useEffect(() => {
|
|
@@ -128,13 +123,21 @@ const ContentEditable = ({
|
|
|
128
123
|
editor.setEditorState(newEditorState);
|
|
129
124
|
}, [editorState]);
|
|
130
125
|
|
|
131
|
-
const isTextEmpty =
|
|
126
|
+
const isTextEmpty =
|
|
127
|
+
isEmpty(root) &&
|
|
128
|
+
!editor.getEditorState().toJSON().root.children?.[0]?.children?.length;
|
|
132
129
|
|
|
133
130
|
const handleBlur = (event: React.FocusEvent<HTMLInputElement>): void => {
|
|
134
131
|
setFocused(false);
|
|
135
132
|
onBlur?.(event);
|
|
136
133
|
};
|
|
137
134
|
|
|
135
|
+
const isEditable = editor.isEditable();
|
|
136
|
+
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
editor.setEditable(!disabled);
|
|
139
|
+
}, [disabled]);
|
|
140
|
+
|
|
138
141
|
return (
|
|
139
142
|
<div
|
|
140
143
|
className={cx(
|
|
@@ -144,9 +144,37 @@ describe('Rich Text Editor', () => {
|
|
|
144
144
|
cy.wrap(element).should('not.have.attr', 'target');
|
|
145
145
|
});
|
|
146
146
|
});
|
|
147
|
+
|
|
148
|
+
it('does not allow to open links in a new tab', () => {
|
|
149
|
+
cy.mount({
|
|
150
|
+
Component: <RichTextEditor editable openLinkInNewTab={false} />
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
cy.get('[data-testid="RichTextEditor"]').type('cypress');
|
|
154
|
+
cy.get('[data-testid="RichTextEditor"]').focus().type('{selectAll}');
|
|
155
|
+
|
|
156
|
+
cy.get('#link').click();
|
|
157
|
+
|
|
158
|
+
cy.findByLabelText('Saved link').should('have.text', 'https://');
|
|
159
|
+
|
|
160
|
+
cy.findByLabelText('Edit link').click();
|
|
161
|
+
cy.get('#InputLinkField').type('www.centreon.com').type('{enter}');
|
|
162
|
+
|
|
163
|
+
cy.findByText('cypress').parent().should('not.have.attr', 'target');
|
|
164
|
+
|
|
165
|
+
cy.get('[data-testid="RichTextEditor"]').focus().clear();
|
|
166
|
+
|
|
167
|
+
cy.get('[data-testid="RichTextEditor"]')
|
|
168
|
+
.focus()
|
|
169
|
+
.type('https://centreon.com');
|
|
170
|
+
|
|
171
|
+
cy.findByText('https://centreon.com')
|
|
172
|
+
.parent()
|
|
173
|
+
.should('not.have.attr', 'target');
|
|
174
|
+
});
|
|
147
175
|
});
|
|
148
176
|
|
|
149
|
-
describe('
|
|
177
|
+
describe('Rich Text Editor not editable', () => {
|
|
150
178
|
beforeEach(() => {
|
|
151
179
|
cy.mount({
|
|
152
180
|
Component: (
|
|
@@ -172,6 +200,33 @@ describe('Rich Text Editor', () => {
|
|
|
172
200
|
});
|
|
173
201
|
});
|
|
174
202
|
|
|
203
|
+
describe('Rich Text Editor is disabled', () => {
|
|
204
|
+
beforeEach(() => {
|
|
205
|
+
cy.mount({
|
|
206
|
+
Component: (
|
|
207
|
+
<RichTextEditor
|
|
208
|
+
disabled
|
|
209
|
+
editable
|
|
210
|
+
initialEditorState={mockInititalStateToEditor}
|
|
211
|
+
/>
|
|
212
|
+
)
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('displays editor when editable props is false and an initialState exist', () => {
|
|
217
|
+
cy.get('[data-testid="RichTextEditor"]')
|
|
218
|
+
.invoke('attr', 'contenteditable')
|
|
219
|
+
.should('eq', 'true');
|
|
220
|
+
cy.findByLabelText('Undo').should('be.disabled');
|
|
221
|
+
cy.findByLabelText('Redo').should('be.disabled');
|
|
222
|
+
cy.findByLabelText('bold').should('be.disabled');
|
|
223
|
+
cy.findByLabelText('italic').should('be.disabled');
|
|
224
|
+
cy.findByLabelText('underline').should('be.disabled');
|
|
225
|
+
cy.findByLabelText('strikethrough').should('be.disabled');
|
|
226
|
+
cy.findByLabelText('link').should('be.disabled');
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
175
230
|
describe('Custom style editable rich text editor ', () => {
|
|
176
231
|
beforeEach(() => {
|
|
177
232
|
cy.mount({
|
|
@@ -230,4 +285,57 @@ describe('Rich Text Editor', () => {
|
|
|
230
285
|
});
|
|
231
286
|
});
|
|
232
287
|
});
|
|
288
|
+
|
|
289
|
+
describe('Block type', () => {
|
|
290
|
+
beforeEach(() => {
|
|
291
|
+
cy.mount({
|
|
292
|
+
Component: <RichTextEditor editable />
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
const testCases = [
|
|
297
|
+
{
|
|
298
|
+
blockType: 'Heading 1',
|
|
299
|
+
tag: 'h1'
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
blockType: 'Heading 2',
|
|
303
|
+
tag: 'h2'
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
blockType: 'Heading 3',
|
|
307
|
+
tag: 'h3'
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
blockType: 'Heading 4',
|
|
311
|
+
tag: 'h4'
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
blockType: 'Heading 5',
|
|
315
|
+
tag: 'h5'
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
blockType: 'Heading 6',
|
|
319
|
+
tag: 'h6'
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
blockType: 'Bullet List',
|
|
323
|
+
tag: 'ul'
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
blockType: 'Number List',
|
|
327
|
+
tag: 'ol'
|
|
328
|
+
}
|
|
329
|
+
];
|
|
330
|
+
|
|
331
|
+
testCases.forEach(({ blockType, tag }) => {
|
|
332
|
+
it(`displays ${blockType} when the corresponding block type button is selected`, () => {
|
|
333
|
+
cy.get('[data-testid="RichTextEditor"]').type('Example');
|
|
334
|
+
cy.findByTestId('Block type').click();
|
|
335
|
+
cy.findByText(blockType).click();
|
|
336
|
+
|
|
337
|
+
cy.get('[data-testid="RichTextEditor"]').find(tag).should('be.visible');
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
});
|
|
233
341
|
});
|
|
@@ -3,11 +3,15 @@ import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
|
|
|
3
3
|
import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin';
|
|
4
4
|
import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary';
|
|
5
5
|
import { AutoLinkNode, LinkNode } from '@lexical/link';
|
|
6
|
+
import { HeadingNode } from '@lexical/rich-text';
|
|
7
|
+
import { ListItemNode, ListNode } from '@lexical/list';
|
|
6
8
|
import anylogger from 'anylogger';
|
|
7
9
|
import { makeStyles } from 'tss-react/mui';
|
|
8
10
|
import { EditorState } from 'lexical';
|
|
9
11
|
import { LinkPlugin } from '@lexical/react/LexicalLinkPlugin';
|
|
10
12
|
import { equals } from 'ramda';
|
|
13
|
+
import { ListPlugin } from '@lexical/react/LexicalListPlugin';
|
|
14
|
+
import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin';
|
|
11
15
|
|
|
12
16
|
import { Typography } from '@mui/material';
|
|
13
17
|
|
|
@@ -18,6 +22,7 @@ import FloatingLinkEditorPlugin from './plugins/FloatingLinkEditorPlugin';
|
|
|
18
22
|
|
|
19
23
|
export interface RichTextEditorProps {
|
|
20
24
|
contentClassName?: string;
|
|
25
|
+
disabled?: boolean;
|
|
21
26
|
displayMacrosButton?: boolean;
|
|
22
27
|
editable: boolean;
|
|
23
28
|
editorState?: string;
|
|
@@ -28,6 +33,7 @@ export interface RichTextEditorProps {
|
|
|
28
33
|
minInputHeight?: number;
|
|
29
34
|
namespace?: string;
|
|
30
35
|
onBlur?: (e: string) => void;
|
|
36
|
+
openLinkInNewTab?: boolean;
|
|
31
37
|
placeholder?: string;
|
|
32
38
|
resetEditorToInitialStateCondition?: () => boolean;
|
|
33
39
|
toolbarPositions?: 'start' | 'end';
|
|
@@ -94,7 +100,9 @@ const RichTextEditor = ({
|
|
|
94
100
|
error,
|
|
95
101
|
onBlur,
|
|
96
102
|
contentClassName,
|
|
97
|
-
displayMacrosButton = false
|
|
103
|
+
displayMacrosButton = false,
|
|
104
|
+
disabled = false,
|
|
105
|
+
openLinkInNewTab = true
|
|
98
106
|
}: RichTextEditorProps): JSX.Element => {
|
|
99
107
|
const { classes } = useStyles({ toolbarPositions });
|
|
100
108
|
|
|
@@ -106,7 +114,7 @@ const RichTextEditor = ({
|
|
|
106
114
|
editable,
|
|
107
115
|
editorState: initialEditorState,
|
|
108
116
|
namespace,
|
|
109
|
-
nodes: [AutoLinkNode, LinkNode],
|
|
117
|
+
nodes: [AutoLinkNode, LinkNode, HeadingNode, ListNode, ListItemNode],
|
|
110
118
|
onError,
|
|
111
119
|
theme: {
|
|
112
120
|
link: classes.link,
|
|
@@ -125,9 +133,9 @@ const RichTextEditor = ({
|
|
|
125
133
|
<div className={classes.container}>
|
|
126
134
|
<div className={classes.toolbar}>
|
|
127
135
|
<ToolbarPlugin
|
|
136
|
+
disabled={disabled}
|
|
128
137
|
displayMacrosButton={displayMacrosButton}
|
|
129
138
|
editable={editable}
|
|
130
|
-
getEditorState={getEditorState}
|
|
131
139
|
/>
|
|
132
140
|
</div>
|
|
133
141
|
<div>
|
|
@@ -136,6 +144,7 @@ const RichTextEditor = ({
|
|
|
136
144
|
contentEditable={
|
|
137
145
|
<ContentEditable
|
|
138
146
|
className={contentClassName || ''}
|
|
147
|
+
disabled={disabled}
|
|
139
148
|
editable={editable}
|
|
140
149
|
editorState={editorState}
|
|
141
150
|
error={error}
|
|
@@ -155,8 +164,13 @@ const RichTextEditor = ({
|
|
|
155
164
|
/>
|
|
156
165
|
<HistoryPlugin />
|
|
157
166
|
<LinkPlugin />
|
|
158
|
-
<
|
|
159
|
-
<
|
|
167
|
+
<ListPlugin />
|
|
168
|
+
<OnChangePlugin onChange={getEditorState} />
|
|
169
|
+
<AutoCompleteLinkPlugin openLinkInNewTab={openLinkInNewTab} />
|
|
170
|
+
<FloatingLinkEditorPlugin
|
|
171
|
+
editable={editable}
|
|
172
|
+
openLinkInNewTab={openLinkInNewTab}
|
|
173
|
+
/>
|
|
160
174
|
{error && <Typography className={classes.error}>{error}</Typography>}
|
|
161
175
|
</div>
|
|
162
176
|
</div>
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
AutoLinkPlugin,
|
|
3
|
+
LinkMatcher
|
|
4
|
+
} from '@lexical/react/LexicalAutoLinkPlugin';
|
|
2
5
|
|
|
3
6
|
interface LinkAttributes {
|
|
4
7
|
rel?: null | string;
|
|
@@ -19,13 +22,15 @@ const URL_MATCHER =
|
|
|
19
22
|
const EMAIL_MATCHER =
|
|
20
23
|
/(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))/;
|
|
21
24
|
|
|
22
|
-
const
|
|
25
|
+
const getMatchers = (openLinkInNewTab: boolean): Array<LinkMatcher> => [
|
|
23
26
|
(text: string): LinkMatcherResult | null => {
|
|
24
27
|
const match = URL_MATCHER.exec(text);
|
|
25
28
|
|
|
26
29
|
return (
|
|
27
30
|
match && {
|
|
28
|
-
attributes:
|
|
31
|
+
attributes: openLinkInNewTab
|
|
32
|
+
? { rel: 'noreferrer', target: '_blank' }
|
|
33
|
+
: undefined,
|
|
29
34
|
index: match.index,
|
|
30
35
|
length: match[0].length,
|
|
31
36
|
text: match[0],
|
|
@@ -47,8 +52,12 @@ const MATCHERS = [
|
|
|
47
52
|
}
|
|
48
53
|
];
|
|
49
54
|
|
|
50
|
-
const AutoCompleteLinkPlugin = (
|
|
51
|
-
|
|
55
|
+
const AutoCompleteLinkPlugin = ({
|
|
56
|
+
openLinkInNewTab
|
|
57
|
+
}: {
|
|
58
|
+
openLinkInNewTab: boolean;
|
|
59
|
+
}): JSX.Element => {
|
|
60
|
+
return <AutoLinkPlugin matchers={getMatchers(openLinkInNewTab)} />;
|
|
52
61
|
};
|
|
53
62
|
|
|
54
63
|
export default AutoCompleteLinkPlugin;
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
|
|
29
29
|
interface FloatingLinkEditorPluginProps {
|
|
30
30
|
editable: boolean;
|
|
31
|
+
openLinkInNewTab: boolean;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
interface UseFloatingLinkEditorProps extends FloatingLinkEditorPluginProps {
|
|
@@ -36,6 +37,7 @@ interface UseFloatingLinkEditorProps extends FloatingLinkEditorPluginProps {
|
|
|
36
37
|
|
|
37
38
|
interface FloatingLinkEditorProps {
|
|
38
39
|
editor: LexicalEditor;
|
|
40
|
+
openLinkInNewTab: boolean;
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
interface TooltipPosition {
|
|
@@ -44,7 +46,8 @@ interface TooltipPosition {
|
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
const FloatingLinkEditor = ({
|
|
47
|
-
editor
|
|
49
|
+
editor,
|
|
50
|
+
openLinkInNewTab
|
|
48
51
|
}: FloatingLinkEditorProps): JSX.Element | null => {
|
|
49
52
|
const nativeSelection = window.getSelection();
|
|
50
53
|
const rootElement = editor.getRootElement();
|
|
@@ -67,7 +70,7 @@ const FloatingLinkEditor = ({
|
|
|
67
70
|
|
|
68
71
|
if (value !== '') {
|
|
69
72
|
editor.dispatchCommand(TOGGLE_LINK_COMMAND, {
|
|
70
|
-
target: '_blank',
|
|
73
|
+
target: openLinkInNewTab ? '_blank' : undefined,
|
|
71
74
|
url: value
|
|
72
75
|
});
|
|
73
76
|
}
|
|
@@ -144,7 +147,7 @@ const FloatingLinkEditor = ({
|
|
|
144
147
|
aria-label={labelSavedLink}
|
|
145
148
|
href={linkUrl}
|
|
146
149
|
rel="noreferrer noopener"
|
|
147
|
-
target=
|
|
150
|
+
target={openLinkInNewTab ? '_blank' : undefined}
|
|
148
151
|
variant="button"
|
|
149
152
|
>
|
|
150
153
|
{linkUrl}
|
|
@@ -166,7 +169,8 @@ const FloatingLinkEditor = ({
|
|
|
166
169
|
|
|
167
170
|
const useFloatingTextFormatToolbar = ({
|
|
168
171
|
editor,
|
|
169
|
-
editable
|
|
172
|
+
editable,
|
|
173
|
+
openLinkInNewTab
|
|
170
174
|
}: UseFloatingLinkEditorProps): JSX.Element | null => {
|
|
171
175
|
const [isText, setIsText] = useState(false);
|
|
172
176
|
const isInsertingLink = useAtomValue(isInsertingLinkAtom);
|
|
@@ -241,15 +245,18 @@ const useFloatingTextFormatToolbar = ({
|
|
|
241
245
|
return null;
|
|
242
246
|
}
|
|
243
247
|
|
|
244
|
-
return
|
|
248
|
+
return (
|
|
249
|
+
<FloatingLinkEditor editor={editor} openLinkInNewTab={openLinkInNewTab} />
|
|
250
|
+
);
|
|
245
251
|
};
|
|
246
252
|
|
|
247
253
|
const FloatingActionsToolbarPlugin = ({
|
|
248
|
-
editable
|
|
254
|
+
editable,
|
|
255
|
+
openLinkInNewTab
|
|
249
256
|
}: FloatingLinkEditorPluginProps): JSX.Element | null => {
|
|
250
257
|
const [editor] = useLexicalComposerContext();
|
|
251
258
|
|
|
252
|
-
return useFloatingTextFormatToolbar({ editable, editor });
|
|
259
|
+
return useFloatingTextFormatToolbar({ editable, editor, openLinkInNewTab });
|
|
253
260
|
};
|
|
254
261
|
|
|
255
262
|
export default FloatingActionsToolbarPlugin;
|