@centreon/ui 23.10.52 → 23.10.54
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/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/utils/index.ts +1 -0
- package/src/utils/translatedLabel.ts +2 -0
- package/src/utils/useLicenseExpirationWarning.cypress.spec.tsx +137 -0
- package/src/utils/useLicenseExpirationWarning.ts +52 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@centreon/ui",
|
|
3
|
-
"version": "23.10.
|
|
3
|
+
"version": "23.10.54",
|
|
4
4
|
"description": "Centreon UI Components",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"eslint": "eslint ./src --ext .js,.jsx,.ts,.tsx --max-warnings 0",
|
|
@@ -102,7 +102,9 @@
|
|
|
102
102
|
},
|
|
103
103
|
"dependencies": {
|
|
104
104
|
"@lexical/link": "0.11.3",
|
|
105
|
+
"@lexical/list": "^0.11.3",
|
|
105
106
|
"@lexical/react": "0.11.3",
|
|
107
|
+
"@lexical/rich-text": "^0.11.3",
|
|
106
108
|
"@lexical/selection": "0.11.3",
|
|
107
109
|
"@lexical/utils": "0.11.3",
|
|
108
110
|
"@visx/curve": "^2.1.0",
|
|
@@ -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;
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
4
|
+
import {
|
|
5
|
+
$isListNode,
|
|
6
|
+
INSERT_ORDERED_LIST_COMMAND,
|
|
7
|
+
INSERT_UNORDERED_LIST_COMMAND,
|
|
8
|
+
ListNode,
|
|
9
|
+
REMOVE_LIST_COMMAND
|
|
10
|
+
} from '@lexical/list';
|
|
11
|
+
import {
|
|
12
|
+
$createParagraphNode,
|
|
13
|
+
$getSelection,
|
|
14
|
+
$isRangeSelection,
|
|
15
|
+
$isRootOrShadowRoot,
|
|
16
|
+
COMMAND_PRIORITY_CRITICAL,
|
|
17
|
+
SELECTION_CHANGE_COMMAND
|
|
18
|
+
} from 'lexical';
|
|
19
|
+
import {
|
|
20
|
+
$createHeadingNode,
|
|
21
|
+
$isHeadingNode,
|
|
22
|
+
HeadingTagType
|
|
23
|
+
} from '@lexical/rich-text';
|
|
24
|
+
import { $setBlocksType } from '@lexical/selection';
|
|
25
|
+
import { T, always, cond, equals, isNil } from 'ramda';
|
|
26
|
+
import {
|
|
27
|
+
$findMatchingParent,
|
|
28
|
+
$getNearestNodeOfType,
|
|
29
|
+
mergeRegister
|
|
30
|
+
} from '@lexical/utils';
|
|
31
|
+
|
|
32
|
+
import { SingleAutocompleteField } from '../../..';
|
|
33
|
+
|
|
34
|
+
import { useBlockButtonsStyles } from './ToolbarPlugin.styles';
|
|
35
|
+
|
|
36
|
+
interface Props {
|
|
37
|
+
disabled: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const blockTypeToBlockName = {
|
|
41
|
+
bullet: 'Bullet List',
|
|
42
|
+
h1: 'Heading 1',
|
|
43
|
+
h2: 'Heading 2',
|
|
44
|
+
h3: 'Heading 3',
|
|
45
|
+
h4: 'Heading 4',
|
|
46
|
+
h5: 'Heading 5',
|
|
47
|
+
h6: 'Heading 6',
|
|
48
|
+
number: 'Number List',
|
|
49
|
+
paragraph: 'Normal'
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const blockTypes = [
|
|
53
|
+
'h1',
|
|
54
|
+
'h2',
|
|
55
|
+
'h3',
|
|
56
|
+
'h4',
|
|
57
|
+
'h5',
|
|
58
|
+
'h6',
|
|
59
|
+
'bullet',
|
|
60
|
+
'number',
|
|
61
|
+
'paragraph'
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
const blockTypeOptions = blockTypes.map((blockType) => ({
|
|
65
|
+
id: blockType,
|
|
66
|
+
name: blockTypeToBlockName[blockType]
|
|
67
|
+
}));
|
|
68
|
+
|
|
69
|
+
const BlockButtons = ({ disabled }: Props): JSX.Element => {
|
|
70
|
+
const { classes } = useBlockButtonsStyles();
|
|
71
|
+
|
|
72
|
+
const [blockType, setBlockType] =
|
|
73
|
+
useState<keyof typeof blockTypeToBlockName>('paragraph');
|
|
74
|
+
|
|
75
|
+
const [editor] = useLexicalComposerContext();
|
|
76
|
+
|
|
77
|
+
const formatParagraph = (): void => {
|
|
78
|
+
editor.update(() => {
|
|
79
|
+
const selection = $getSelection();
|
|
80
|
+
$setBlocksType(selection, () => $createParagraphNode());
|
|
81
|
+
});
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const formatBulletList = (): void => {
|
|
85
|
+
if (blockType !== 'bullet') {
|
|
86
|
+
editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined);
|
|
87
|
+
} else {
|
|
88
|
+
editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const formatNumberedList = (): void => {
|
|
93
|
+
if (blockType !== 'number') {
|
|
94
|
+
editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined);
|
|
95
|
+
} else {
|
|
96
|
+
editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const formatHeading = (headingSize: HeadingTagType): void => {
|
|
101
|
+
if (blockType !== headingSize) {
|
|
102
|
+
editor.update(() => {
|
|
103
|
+
const selection = $getSelection();
|
|
104
|
+
if ($isRangeSelection(selection)) {
|
|
105
|
+
$setBlocksType(selection, () => $createHeadingNode(headingSize));
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const changeBlockType = (_, newBlockType): void => {
|
|
112
|
+
const formatFunction = cond<Array<string>, (value?) => void>([
|
|
113
|
+
[equals('bullet'), always(formatBulletList)],
|
|
114
|
+
[equals('number'), always(formatNumberedList)],
|
|
115
|
+
[equals('h1'), always(() => formatHeading('h1'))],
|
|
116
|
+
[equals('h2'), always(() => formatHeading('h2'))],
|
|
117
|
+
[equals('h3'), always(() => formatHeading('h3'))],
|
|
118
|
+
[equals('h4'), always(() => formatHeading('h4'))],
|
|
119
|
+
[equals('h5'), always(() => formatHeading('h5'))],
|
|
120
|
+
[equals('h6'), always(() => formatHeading('h6'))],
|
|
121
|
+
[T, always(formatParagraph)]
|
|
122
|
+
])(newBlockType?.id || '');
|
|
123
|
+
|
|
124
|
+
formatFunction();
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const updateToolbar = useCallback(() => {
|
|
128
|
+
const selection = $getSelection();
|
|
129
|
+
const anchorNode = selection?.anchor.getNode();
|
|
130
|
+
const element = equals(anchorNode?.getKey(), 'root')
|
|
131
|
+
? anchorNode
|
|
132
|
+
: $findMatchingParent(anchorNode, (e) => {
|
|
133
|
+
const parent = e.getParent();
|
|
134
|
+
|
|
135
|
+
return parent !== null && $isRootOrShadowRoot(parent);
|
|
136
|
+
}) || anchorNode?.getTopLevelElementOrThrow();
|
|
137
|
+
|
|
138
|
+
const elementKey = element?.getKey();
|
|
139
|
+
const elementDOM = editor.getElementByKey(elementKey);
|
|
140
|
+
|
|
141
|
+
if (isNil(elementDOM)) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if ($isListNode(element)) {
|
|
146
|
+
const parentList = $getNearestNodeOfType(anchorNode, ListNode);
|
|
147
|
+
const type = parentList
|
|
148
|
+
? parentList.getListType()
|
|
149
|
+
: element.getListType();
|
|
150
|
+
setBlockType(type);
|
|
151
|
+
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const type = $isHeadingNode(element) ? element.getTag() : element.getType();
|
|
155
|
+
if (type in blockTypeToBlockName) {
|
|
156
|
+
setBlockType(type as keyof typeof blockTypeToBlockName);
|
|
157
|
+
}
|
|
158
|
+
}, [editor]);
|
|
159
|
+
|
|
160
|
+
const value = blockTypeOptions.find((option) => option.id === blockType);
|
|
161
|
+
|
|
162
|
+
useEffect(() => {
|
|
163
|
+
return editor.registerCommand(
|
|
164
|
+
SELECTION_CHANGE_COMMAND,
|
|
165
|
+
() => {
|
|
166
|
+
updateToolbar();
|
|
167
|
+
|
|
168
|
+
return false;
|
|
169
|
+
},
|
|
170
|
+
COMMAND_PRIORITY_CRITICAL
|
|
171
|
+
);
|
|
172
|
+
}, [editor, updateToolbar]);
|
|
173
|
+
|
|
174
|
+
useEffect(() => {
|
|
175
|
+
return mergeRegister(
|
|
176
|
+
editor.registerUpdateListener(({ editorState }) => {
|
|
177
|
+
editorState.read(() => {
|
|
178
|
+
updateToolbar();
|
|
179
|
+
});
|
|
180
|
+
})
|
|
181
|
+
);
|
|
182
|
+
}, [editor, updateToolbar]);
|
|
183
|
+
|
|
184
|
+
return (
|
|
185
|
+
<SingleAutocompleteField
|
|
186
|
+
className={classes.autocomplete}
|
|
187
|
+
dataTestId="Block type"
|
|
188
|
+
disabled={disabled}
|
|
189
|
+
label=""
|
|
190
|
+
options={blockTypeOptions}
|
|
191
|
+
value={value}
|
|
192
|
+
onChange={changeBlockType}
|
|
193
|
+
/>
|
|
194
|
+
);
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
export default BlockButtons;
|
|
@@ -5,7 +5,6 @@ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext
|
|
|
5
5
|
import {
|
|
6
6
|
$getSelection,
|
|
7
7
|
$isRangeSelection,
|
|
8
|
-
EditorState,
|
|
9
8
|
FORMAT_TEXT_COMMAND,
|
|
10
9
|
RangeSelection,
|
|
11
10
|
SELECTION_CHANGE_COMMAND,
|
|
@@ -61,10 +60,10 @@ const getSelectedNode = (selection: RangeSelection): ElementNode | TextNode => {
|
|
|
61
60
|
};
|
|
62
61
|
|
|
63
62
|
interface Props {
|
|
64
|
-
|
|
63
|
+
disabled: boolean;
|
|
65
64
|
}
|
|
66
65
|
|
|
67
|
-
const FormatButtons = ({
|
|
66
|
+
const FormatButtons = ({ disabled }: Props): JSX.Element => {
|
|
68
67
|
const { classes, cx } = useStyles();
|
|
69
68
|
|
|
70
69
|
const [isBold, setIsBold] = useState(false);
|
|
@@ -104,13 +103,6 @@ const FormatButtons = ({ getEditorState }: Props): JSX.Element => {
|
|
|
104
103
|
|
|
105
104
|
useEffect(() => {
|
|
106
105
|
return mergeRegister(
|
|
107
|
-
editor.registerUpdateListener(({ editorState }) => {
|
|
108
|
-
editorState.read(() => {
|
|
109
|
-
updateToolbar();
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
getEditorState?.(editorState);
|
|
113
|
-
}),
|
|
114
106
|
editor.registerCommand(
|
|
115
107
|
SELECTION_CHANGE_COMMAND,
|
|
116
108
|
() => {
|
|
@@ -169,6 +161,7 @@ const FormatButtons = ({ getEditorState }: Props): JSX.Element => {
|
|
|
169
161
|
<IconButton
|
|
170
162
|
ariaLabel={type}
|
|
171
163
|
className={cx(isSelected && classes.buttonSelected)}
|
|
164
|
+
disabled={disabled}
|
|
172
165
|
key={type}
|
|
173
166
|
size="medium"
|
|
174
167
|
title={type}
|
|
@@ -11,6 +11,10 @@ import { useStyles } from '.';
|
|
|
11
11
|
|
|
12
12
|
const LowPriority = 1;
|
|
13
13
|
|
|
14
|
+
interface Props {
|
|
15
|
+
disabled: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
14
18
|
export const standardMacros = [
|
|
15
19
|
'{{SHORTDATETIME}}',
|
|
16
20
|
'{{LONGDATETIME}}',
|
|
@@ -22,7 +26,7 @@ export const standardMacros = [
|
|
|
22
26
|
'{{STATETYPE}}'
|
|
23
27
|
];
|
|
24
28
|
|
|
25
|
-
const MacrosButton = (): JSX.Element => {
|
|
29
|
+
const MacrosButton = ({ disabled }: Props): JSX.Element => {
|
|
26
30
|
const { classes } = useStyles();
|
|
27
31
|
const [arMacrosDiplayed, setAreMacrosDiplayed] = useState(false);
|
|
28
32
|
|
|
@@ -65,6 +69,7 @@ const MacrosButton = (): JSX.Element => {
|
|
|
65
69
|
<IconButton
|
|
66
70
|
ariaLabel="Macros"
|
|
67
71
|
className={classes.macrosButton}
|
|
72
|
+
disabled={disabled}
|
|
68
73
|
title="Macros"
|
|
69
74
|
tooltipPlacement="top"
|
|
70
75
|
onClick={displayMacros}
|
|
@@ -18,7 +18,11 @@ import { labelRedo, labelUndo } from '../../translatedLabels';
|
|
|
18
18
|
|
|
19
19
|
const LowPriority = 1;
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
interface Props {
|
|
22
|
+
disabled: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const UndoRedoButtons = ({ disabled }: Props): JSX.Element => {
|
|
22
26
|
const { t } = useTranslation();
|
|
23
27
|
|
|
24
28
|
const [canUndo, setCanUndo] = useState(false);
|
|
@@ -64,7 +68,7 @@ const UndoRedoButtons = (): JSX.Element => {
|
|
|
64
68
|
<>
|
|
65
69
|
<IconButton
|
|
66
70
|
ariaLabel={translatedUndo}
|
|
67
|
-
disabled={!canUndo}
|
|
71
|
+
disabled={!canUndo || disabled}
|
|
68
72
|
title={translatedUndo}
|
|
69
73
|
tooltipPlacement="top"
|
|
70
74
|
onClick={undo}
|
|
@@ -73,7 +77,7 @@ const UndoRedoButtons = (): JSX.Element => {
|
|
|
73
77
|
</IconButton>
|
|
74
78
|
<IconButton
|
|
75
79
|
ariaLabel={translatedRedo}
|
|
76
|
-
disabled={!canRedo}
|
|
80
|
+
disabled={!canRedo || disabled}
|
|
77
81
|
title={translatedRedo}
|
|
78
82
|
tooltipPlacement="top"
|
|
79
83
|
onClick={redo}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { EditorState } from 'lexical';
|
|
2
1
|
import { makeStyles } from 'tss-react/mui';
|
|
3
2
|
|
|
4
3
|
import { Divider } from '@mui/material';
|
|
@@ -6,11 +5,12 @@ import { Divider } from '@mui/material';
|
|
|
6
5
|
import FormatButtons from './FormatButtons';
|
|
7
6
|
import UndoRedoButtons from './UndoRedoButtons';
|
|
8
7
|
import MacrosButton from './MacrosButton';
|
|
8
|
+
import BlockButtons from './BlockButtons';
|
|
9
9
|
|
|
10
10
|
interface Props {
|
|
11
|
+
disabled: boolean;
|
|
11
12
|
displayMacrosButton?: boolean;
|
|
12
13
|
editable: boolean;
|
|
13
|
-
getEditorState?: (editorState: EditorState) => void;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export const useStyles = makeStyles()((theme) => ({
|
|
@@ -31,21 +31,23 @@ export const useStyles = makeStyles()((theme) => ({
|
|
|
31
31
|
}));
|
|
32
32
|
|
|
33
33
|
const ToolbarPlugin = ({
|
|
34
|
-
getEditorState,
|
|
35
34
|
editable,
|
|
36
|
-
displayMacrosButton
|
|
35
|
+
displayMacrosButton,
|
|
36
|
+
disabled
|
|
37
37
|
}: Props): JSX.Element | null => {
|
|
38
38
|
const { classes } = useStyles();
|
|
39
39
|
|
|
40
40
|
return editable ? (
|
|
41
41
|
<div className={classes.container}>
|
|
42
|
-
<UndoRedoButtons />
|
|
42
|
+
<UndoRedoButtons disabled={disabled} />
|
|
43
43
|
<Divider flexItem orientation="vertical" />
|
|
44
|
-
<
|
|
44
|
+
<BlockButtons disabled={disabled} />
|
|
45
|
+
<Divider flexItem orientation="vertical" />
|
|
46
|
+
<FormatButtons disabled={disabled} />
|
|
45
47
|
{displayMacrosButton && (
|
|
46
48
|
<>
|
|
47
49
|
<Divider flexItem orientation="vertical" />
|
|
48
|
-
<MacrosButton />
|
|
50
|
+
<MacrosButton disabled={disabled} />
|
|
49
51
|
</>
|
|
50
52
|
)}
|
|
51
53
|
</div>
|
package/src/utils/index.ts
CHANGED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import dayjs from 'dayjs';
|
|
2
|
+
|
|
3
|
+
import { TestQueryProvider, Method, SnackbarProvider } from '@centreon/ui';
|
|
4
|
+
|
|
5
|
+
import { labelLicenseWarning } from './translatedLabel';
|
|
6
|
+
|
|
7
|
+
import { useLicenseExpirationWarning } from '.';
|
|
8
|
+
|
|
9
|
+
const getMockedResponse = (expirationDate): object => ({
|
|
10
|
+
result: {
|
|
11
|
+
module: {
|
|
12
|
+
entities: [
|
|
13
|
+
{
|
|
14
|
+
description: 'Centreon Auto Discovery',
|
|
15
|
+
id: 'centreon-autodiscovery-server',
|
|
16
|
+
label: 'Centreon',
|
|
17
|
+
license: {
|
|
18
|
+
expiration_date: expirationDate,
|
|
19
|
+
host_limit: -1,
|
|
20
|
+
host_usage: 1,
|
|
21
|
+
is_valid: true,
|
|
22
|
+
required: true
|
|
23
|
+
},
|
|
24
|
+
type: 'module',
|
|
25
|
+
version: {
|
|
26
|
+
available: '23.10.0',
|
|
27
|
+
current: '23.10.0',
|
|
28
|
+
installed: true,
|
|
29
|
+
outdated: false
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
description: 'Centreon License Manager',
|
|
34
|
+
id: 'centreon-license-manager',
|
|
35
|
+
label: 'Centreon',
|
|
36
|
+
license: {
|
|
37
|
+
required: false
|
|
38
|
+
},
|
|
39
|
+
type: 'module',
|
|
40
|
+
version: {
|
|
41
|
+
available: '23.10.0',
|
|
42
|
+
current: '23.10.0',
|
|
43
|
+
installed: true,
|
|
44
|
+
outdated: false
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
description: 'Centreon Monitoring Connectors Manager',
|
|
49
|
+
id: 'centreon-pp-manager',
|
|
50
|
+
label: 'Centreon',
|
|
51
|
+
license: {
|
|
52
|
+
required: false
|
|
53
|
+
},
|
|
54
|
+
type: 'module',
|
|
55
|
+
version: {
|
|
56
|
+
available: '23.10.0',
|
|
57
|
+
current: '23.10.0',
|
|
58
|
+
installed: true,
|
|
59
|
+
outdated: false
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
],
|
|
63
|
+
pagination: {
|
|
64
|
+
limit: 3,
|
|
65
|
+
offset: 0,
|
|
66
|
+
total: 3
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
widget: {
|
|
70
|
+
entities: [],
|
|
71
|
+
pagination: {
|
|
72
|
+
limit: 0,
|
|
73
|
+
offset: 0,
|
|
74
|
+
total: 0
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
status: true
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const mockRequest = ({ expirationDate }: { expirationDate }): void => {
|
|
82
|
+
cy.interceptAPIRequest({
|
|
83
|
+
alias: 'getLicenseInformations',
|
|
84
|
+
method: Method.GET,
|
|
85
|
+
path: '**internal.php?object=centreon_module&action=list',
|
|
86
|
+
response: getMockedResponse(expirationDate)
|
|
87
|
+
});
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const TestComponent = (): JSX.Element => {
|
|
91
|
+
useLicenseExpirationWarning({
|
|
92
|
+
module: 'centreon-autodiscovery-server'
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return <div />;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const TestWithQueryProvider = (): JSX.Element => {
|
|
99
|
+
return (
|
|
100
|
+
<TestQueryProvider>
|
|
101
|
+
<SnackbarProvider>
|
|
102
|
+
<TestComponent />
|
|
103
|
+
</SnackbarProvider>
|
|
104
|
+
</TestQueryProvider>
|
|
105
|
+
);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const initialize = (): void => {
|
|
109
|
+
cy.viewport('macbook-13');
|
|
110
|
+
|
|
111
|
+
cy.mount({
|
|
112
|
+
Component: <TestWithQueryProvider />
|
|
113
|
+
});
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const currentDate = dayjs();
|
|
117
|
+
|
|
118
|
+
describe('License', () => {
|
|
119
|
+
beforeEach(initialize);
|
|
120
|
+
|
|
121
|
+
it('does not display any warning message when the license expires in more than 15 days from the current date', () => {
|
|
122
|
+
mockRequest({ expirationDate: currentDate.add(20, 'day') });
|
|
123
|
+
|
|
124
|
+
cy.waitForRequest('@getLicenseInformations');
|
|
125
|
+
|
|
126
|
+
cy.findByText(
|
|
127
|
+
labelLicenseWarning('centreon-autodiscovery-server', 20)
|
|
128
|
+
).should('not.exist');
|
|
129
|
+
});
|
|
130
|
+
it('displays a warning message when the license expires within 15 days', () => {
|
|
131
|
+
mockRequest({ expirationDate: currentDate.add(10.5, 'day') });
|
|
132
|
+
|
|
133
|
+
cy.findByText(labelLicenseWarning('centreon-autodiscovery-server', 10));
|
|
134
|
+
|
|
135
|
+
cy.matchImageSnapshot();
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
import dayjs from 'dayjs';
|
|
4
|
+
import { path, pipe, propEq, find, lt, isNil } from 'ramda';
|
|
5
|
+
import { useTranslation } from 'react-i18next';
|
|
6
|
+
|
|
7
|
+
import { useFetchQuery, useSnackbar } from '..';
|
|
8
|
+
|
|
9
|
+
import { labelLicenseWarning } from './translatedLabel';
|
|
10
|
+
|
|
11
|
+
const legacyBaseEndpoint = './api/internal.php';
|
|
12
|
+
const extensionsEndpoint = `${legacyBaseEndpoint}?object=centreon_module&action=list`;
|
|
13
|
+
|
|
14
|
+
interface Props {
|
|
15
|
+
module: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const useLicenseExpirationWarning = ({ module }: Props): void => {
|
|
19
|
+
const { t } = useTranslation();
|
|
20
|
+
const { showWarningMessage } = useSnackbar();
|
|
21
|
+
|
|
22
|
+
const { fetchQuery } = useFetchQuery({
|
|
23
|
+
getEndpoint: () => extensionsEndpoint,
|
|
24
|
+
getQueryKey: () => [module]
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const currentDate = dayjs();
|
|
28
|
+
|
|
29
|
+
const getExpirationDate = pipe(
|
|
30
|
+
path(['result', 'module', 'entities']),
|
|
31
|
+
find(propEq('id', module)),
|
|
32
|
+
path(['license', 'expiration_date'])
|
|
33
|
+
) as (data) => string;
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
fetchQuery().then((response) => {
|
|
37
|
+
const expirationDate = getExpirationDate(response);
|
|
38
|
+
if (isNil(expirationDate)) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const daysUntilExpiration = dayjs(expirationDate).diff(
|
|
43
|
+
currentDate,
|
|
44
|
+
'day'
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
if (lt(daysUntilExpiration, 15)) {
|
|
48
|
+
showWarningMessage(t(labelLicenseWarning(module, daysUntilExpiration)));
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}, []);
|
|
52
|
+
};
|