@madie/madie-design-system 1.2.55 → 1.2.57

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.
@@ -1,4 +1,4 @@
1
- import React, { useEffect, useState } from "react";
1
+ import React, { useEffect, useState, useMemo } from "react";
2
2
 
3
3
  import ErrorIcon from "@mui/icons-material/Error"; // warning
4
4
  import InfoIcon from "@mui/icons-material/Info"; // info
@@ -6,6 +6,7 @@ import CancelIcon from "@mui/icons-material/Cancel"; // error
6
6
  import CheckCircleIcon from "@mui/icons-material/CheckCircle"; //success
7
7
  import ContentCopyIcon from "@mui/icons-material/ContentCopy"; //copy
8
8
  import FullscreenExitRoundedIcon from '@mui/icons-material/FullscreenExitRounded';
9
+ import WarningRoundedIcon from '@mui/icons-material/WarningRounded';
9
10
  import Tooltip from "@mui/material/Tooltip";
10
11
  import { IconButton } from "@mui/material";
11
12
  import ClearIcon from "@mui/icons-material/Clear";
@@ -13,184 +14,338 @@ import Toast from "../Toast/index";
13
14
  import classNames from "classnames";
14
15
  import PropTypes from "prop-types";
15
16
 
16
- // warning, info, error, success
17
- const MadieAlert = ({
18
- type = "warning",
19
- visible = true,
20
- content,
21
- canClose = true,
22
- minimizeAlerts = false, // pass the minimizeAlerts featureflag for now, since it will be standard later
23
- alertProps, // props to pass to outer component
24
- closeButtonProps, // props to pass to close button
25
- copyButton,
26
- }) => {
27
- const [copyText, setCopyText] = useState("");
28
- const [toastOpen, setToastOpen] = useState(false);
29
- const [minimized, setMinimized] = useState(false);
30
-
31
- const copyButtonBuilder = (content) => {
32
- const traversal = (contentNode, parentNode = true) => {
33
- if (!contentNode) return "";
34
- let result = contentNode.type === "ul" ? "\n" : "";
35
- if (Array.isArray(contentNode.props?.children)) {
36
- contentNode.props.children.forEach((child, index) => {
37
- result += traversal(child, false);
38
- if (
39
- (parentNode && index == 1) ||
40
- contentNode.type === "ul"
41
- ) {
42
- result += "\n";
43
- }
44
- });
45
- } else if (typeof contentNode.props?.children === "object") {
46
- result += traversal(contentNode.props.children, false);
47
- } else if (
48
- typeof contentNode.props?.children === "string" ||
49
- typeof contentNode.props?.children === "number"
50
- ) {
51
- result += contentNode.props.children.toString();
52
- } else if (
53
- typeof contentNode === "string" ||
54
- typeof contentNode === "number"
55
- ) {
56
- result += contentNode.toString();
57
- }
58
- return result;
59
- };
60
- return traversal(content, false).trim();
17
+ // Define icon mapping outside component to avoid recreation on each render
18
+ const typeToIconMap = {
19
+ warning: ErrorIcon,
20
+ info: InfoIcon,
21
+ error: CancelIcon,
22
+ success: CheckCircleIcon,
23
+ };
24
+
25
+ // Common button styles
26
+ const buttonBaseSx = {
27
+ marginLeft: "auto",
28
+ };
29
+
30
+ const buttonWithDividerSx = {
31
+ ...buttonBaseSx,
32
+ "&:after": {
33
+ content: `''`,
34
+ position: "absolute",
35
+ left: "0px",
36
+ width: "1px",
37
+ height: "40px",
38
+ backgroundColor: "#B0B0B0",
39
+ pointerEvents: "none",
40
+ }
41
+ };
42
+
43
+ // Extract alert content processing to a separate function
44
+ const processContent = (content) => {
45
+ // Counts errors/warnings in ul/li elements
46
+ const countUlLiChildren = (element) => {
47
+ const counts = { ul: 0, li: 0 };
48
+
49
+ const traverse = (el) => {
50
+ if (!el || typeof el !== 'object') return;
51
+
52
+ if (el.type === 'ul' || el.type === 'li') {
53
+ const children = el.props?.children;
54
+ const childCount = Array.isArray(children) ? children.length : (children ? 1 : 0);
55
+ counts[el.type] += childCount;
56
+ }
57
+
58
+ const children = el.props?.children;
59
+ if (Array.isArray(children)) {
60
+ children.forEach(traverse);
61
+ } else if (children && typeof children === 'object') {
62
+ traverse(children);
63
+ }
61
64
  };
65
+
66
+ traverse(element);
67
+
68
+ return counts.li > counts.ul ? counts.li : counts.ul;
69
+ };
62
70
 
63
- useEffect(() => {
64
- if (content && copyButton) {
65
- setCopyText(copyButtonBuilder(content));
71
+ // Converts content to text for copying - improved to avoid unwanted characters
72
+ const buildCopyText = (contentNode, parentNode = true) => {
73
+ if (!contentNode) return "";
74
+
75
+ // Initialize result string
76
+ let result = "";
77
+
78
+ // Handle specific node types that need special formatting
79
+ if (contentNode.type === "ul") {
80
+ // Start ul with a newline
81
+ result = "\n";
82
+ }
83
+
84
+ // Process children based on their type
85
+ if (Array.isArray(contentNode.props?.children)) {
86
+ contentNode.props.children.forEach((child, index) => {
87
+ const childText = buildCopyText(child, false);
88
+ // Only add non-empty text
89
+ if (childText.trim()) {
90
+ result += childText;
91
+
92
+ // Add line breaks after certain elements
93
+ if ((parentNode && index === 1) || contentNode.type === "ul") {
94
+ result += "\n";
95
+ }
66
96
  }
67
- }, [content]);
68
- // we have four states to render for
69
- const typeSelect = {
70
- warning: ErrorIcon,
71
- info: InfoIcon,
72
- error: CancelIcon,
73
- success: CheckCircleIcon,
74
- };
75
- const Icon = typeSelect[type];
76
- const alertClass = classNames("madie-alert", type);
77
- const iconClass = classNames("alert-icon", type);
97
+ });
98
+ } else if (typeof contentNode.props?.children === "object") {
99
+ result += buildCopyText(contentNode.props.children, false);
100
+ } else if (typeof contentNode.props?.children === "string" ||
101
+ typeof contentNode.props?.children === "number") {
102
+ // Clean the string by trimming and normalizing whitespace
103
+ result += contentNode.props.children.toString().replace(/\s+/g, ' ').trim();
104
+ } else if (typeof contentNode === "string" || typeof contentNode === "number") {
105
+ // Clean the string by trimming and normalizing whitespace
106
+ result += contentNode.toString().replace(/\s+/g, ' ').trim();
107
+ }
108
+
109
+ return result;
110
+ };
111
+
112
+ // Process content and ensure we trim the final result
113
+ const rawText = buildCopyText(content, false);
114
+ return {
115
+ errorCount: countUlLiChildren(content),
116
+ copyText: rawText.replace(/\s+\n/g, '\n').replace(/\n\s+/g, '\n').trim()
117
+ };
118
+ };
78
119
 
120
+ // Define ActionButton as a separate component outside MadieAlert
121
+ const ActionButton = ({ tooltip, onClick, icon, testId, sx }) => (
122
+ <Tooltip title={tooltip} data-testid={`${testId}-tooltip`} arrow>
123
+ <IconButton
124
+ onClick={onClick}
125
+ sx={sx}
126
+ data-testid={testId}
127
+ >
128
+ {icon}
129
+ </IconButton>
130
+ </Tooltip>
131
+ );
132
+
133
+ // Add PropTypes for ActionButton
134
+ ActionButton.propTypes = {
135
+ tooltip: PropTypes.string.isRequired,
136
+ onClick: PropTypes.func.isRequired,
137
+ icon: PropTypes.node.isRequired,
138
+ testId: PropTypes.string.isRequired,
139
+ sx: PropTypes.object
140
+ };
141
+
142
+ const MadieAlert = ({
143
+ type = "warning",
144
+ visible = true,
145
+ content,
146
+ canClose = true,
147
+ minimizeAlerts = false,
148
+ alertProps,
149
+ closeButtonProps,
150
+ copyButton,
151
+ alerts = null,
152
+ }) => {
153
+ // Consolidate related state
154
+ const [state, setState] = useState({
155
+ toastOpen: false,
156
+ copyText: "",
157
+ minimizedAlerts: {},
158
+ individualErrors: {},
159
+ });
160
+
161
+ // Create alias for readability
162
+ const { toastOpen, copyText, minimizedAlerts, individualErrors } = state;
163
+
164
+ // Use a single update function
165
+ const updateState = (updates) => {
166
+ setState(prev => ({ ...prev, ...updates }));
167
+ };
168
+
169
+ // Prepare alerts array once
170
+ const alertsArray = useMemo(() => {
171
+ return alerts || [{
172
+ type, visible, content, canClose, alertProps, closeButtonProps, copyButton
173
+ }];
174
+ }, [alerts, type, visible, content, canClose, alertProps, closeButtonProps, copyButton]);
175
+
176
+ // Process all alerts content once
177
+ useEffect(() => {
178
+ const newIndividualErrors = {};
179
+ let totalCopyText = "";
180
+
181
+ alertsArray.forEach((alert, index) => {
182
+ if (alert.content && alert.visible !== false) {
183
+ const processed = processContent(alert.content);
184
+ newIndividualErrors[index] = processed.errorCount;
185
+
186
+ if (alert.copyButton) {
187
+ totalCopyText += processed.copyText + "\n\n";
188
+ }
189
+ }
190
+ });
191
+
192
+ updateState({
193
+ individualErrors: newIndividualErrors,
194
+ copyText: totalCopyText.trim(),
195
+ });
196
+ }, [alertsArray]);
197
+
198
+ // Utility functions for managing minimized state
199
+ const minimizeAlert = (index) => {
200
+ updateState({
201
+ minimizedAlerts: { ...minimizedAlerts, [index]: true }
202
+ });
203
+ };
204
+
205
+ const restoreAllAlerts = () => {
206
+ updateState({ minimizedAlerts: {} });
207
+ };
208
+
209
+ // Calculate minimized alerts info
210
+ const minimizedIndices = Object.keys(minimizedAlerts)
211
+ .filter(key => minimizedAlerts[key])
212
+ .map(key => parseInt(key));
213
+
214
+ const totalMinimizedErrors = minimizedIndices.reduce(
215
+ (sum, index) => sum + (individualErrors[index] || 0),
216
+ 0
217
+ );
218
+
219
+ // Render alert content
220
+ const renderAlert = (alert, index) => {
221
+ const {
222
+ type = "warning",
223
+ visible = true,
224
+ content,
225
+ canClose = true,
226
+ alertProps = {},
227
+ closeButtonProps = {},
228
+ copyButton = false
229
+ } = alert;
230
+
231
+ if (!visible || minimizedAlerts[index]) return null;
232
+
233
+ const Icon = typeToIconMap[type];
234
+ const alertClass = classNames("madie-alert", type);
235
+
79
236
  return (
80
- visible && !minimized && (
81
- <div className={alertClass} {...alertProps}>
82
- <Toast
83
- toastKey="copy-success-toast"
84
- data-testid="copy-success"
85
- toastType="success"
86
- open={toastOpen}
87
- message="Copied to clipboard!"
88
- onClose={() => {
89
- setToastOpen(false);
90
- }}
91
- autoHideDuration={1500}
92
- />
93
- <Icon className={iconClass} />
94
- <div id="content">{content && content}</div>
95
- {
96
- // minimizeAlerts is a feature flag for now, since it will be standard later
97
- }
98
- {minimizeAlerts && (
99
- <Tooltip title={"Minimize"} arrow>
100
- <IconButton sx={{
101
- marginLeft: "auto",
102
- "&:after": {
103
- content: `''`,
104
- position: "absolute",
105
- left: "0px",
106
- width: "1px",
107
- height: "40px",
108
- backgroundColor: "#B0B0B0",
109
- pointerEvents: "none",
110
- },
111
- }}
112
- >
113
- <FullscreenExitRoundedIcon
114
- onClick={(e) => {
115
- e.preventDefault();
116
- setMinimized(true);
117
- }}
118
- sx={{ color: "#242424" }}
119
- />
120
- </IconButton>
121
- </Tooltip>
122
- )}
123
- {copyButton && (
124
- <Tooltip
125
- data-testid="copy-button-tooltip"
126
- title={"Copy Text"}
127
- arrow
128
- >
129
- <IconButton
130
- sx={{
131
- marginLeft: "auto",
132
- ...(!minimizeAlerts && {
133
- "&:after": {
134
- content: `''`,
135
- position: "absolute",
136
- left: "0px",
137
- width: "1px",
138
- height: "40px",
139
- backgroundColor: "#B0B0B0",
140
- pointerEvents: "none",
141
- }
142
- })
143
- }}
144
- >
145
- <ContentCopyIcon
146
- onClick={(e) => {
147
- e.preventDefault();
148
- navigator.clipboard.writeText(copyText);
149
- setToastOpen(true);
150
- }}
151
- sx={{ color: "#242424" }}
152
- />
153
- </IconButton>
154
- </Tooltip>
155
- )}
156
-
157
- {canClose && (
158
- <IconButton
159
- sx={{
160
- marginLeft: "auto",
161
- "&:after": {
162
- content: `''`,
163
- position: "absolute",
164
- left: "0px",
165
- width: "1px",
166
- height: "40px",
167
- backgroundColor: "#B0B0B0",
168
- pointerEvents: "none",
169
- },
170
- }}
171
- {...closeButtonProps}
172
- >
173
- <ClearIcon sx={{ color: "#242424" }} />
174
- </IconButton>
175
- )}
176
- </div>
177
- )
237
+ <div key={index} className={alertClass} {...alertProps}>
238
+ <Icon className={classNames("alert-icon", type)} />
239
+ <div id="content" data-alert-index={index}>{content}</div>
240
+
241
+ {minimizeAlerts && (
242
+ <ActionButton
243
+ tooltip="Minimize"
244
+ onClick={(e) => {
245
+ e.preventDefault();
246
+ minimizeAlert(index);
247
+ }}
248
+ icon={<FullscreenExitRoundedIcon sx={{ color: "#242424" }} />}
249
+ testId={`minimize-button-${index}`}
250
+ sx={buttonWithDividerSx}
251
+ />
252
+ )}
253
+
254
+ {copyButton && (
255
+ <ActionButton
256
+ tooltip="Copy"
257
+ onClick={(e) => {
258
+ e.preventDefault();
259
+ navigator.clipboard.writeText(copyButton ? processContent(content).copyText : copyText);
260
+ updateState({ toastOpen: true });
261
+ }}
262
+ icon={<ContentCopyIcon sx={{ color: "#242424" }} />}
263
+ testId="copy-button"
264
+ sx={!minimizeAlerts ? buttonWithDividerSx : buttonBaseSx}
265
+ />
266
+ )}
267
+
268
+ {canClose && (
269
+ <IconButton
270
+ sx={buttonWithDividerSx}
271
+ {...closeButtonProps}
272
+ >
273
+ <ClearIcon sx={{ color: "#242424" }} />
274
+ </IconButton>
275
+ )}
276
+ </div>
178
277
  );
278
+ };
279
+
280
+ return (
281
+ <div>
282
+ <Toast
283
+ toastKey="copy-success-toast"
284
+ data-testid="copy-success"
285
+ toastType="success"
286
+ open={toastOpen}
287
+ message="Copied to clipboard!"
288
+ onClose={() => updateState({ toastOpen: false })}
289
+ autoHideDuration={1500}
290
+ />
291
+
292
+ {/* Render visible alerts */}
293
+ {alertsArray.map((alert, index) => renderAlert(alert, index))}
294
+
295
+ {/* Show minimized indicator if any alerts are minimized */}
296
+ {minimizedIndices.length > 0 && (
297
+ <div
298
+ style={{
299
+ position: "absolute",
300
+ right: "32px",
301
+ top: "10px",
302
+ display: "flex",
303
+ alignItems: "center",
304
+ cursor: "pointer"
305
+ }}
306
+ data-testid="minimized-alert"
307
+ onClick={(e) => {
308
+ e.preventDefault();
309
+ restoreAllAlerts();
310
+ }}
311
+ >
312
+ <WarningRoundedIcon sx={{ color: "yellow", marginRight: "5px" }}/>
313
+ <span data-testid="minimized-alert-text" style={{ color: "white", fontSize: "16px" }}>
314
+ Display Alerts {totalMinimizedErrors > 0 ? `(${totalMinimizedErrors})` : ""}
315
+ </span>
316
+ </div>
317
+ )}
318
+ </div>
319
+ );
179
320
  };
180
321
 
322
+ // PropTypes definition
181
323
  MadieAlert.propTypes = {
182
- type: PropTypes.string,
183
- visible: PropTypes.bool,
184
- content: PropTypes.node,
185
- canClose: PropTypes.bool,
186
- alertProps: PropTypes.object,
187
- closeButtonProps: PropTypes.object,
188
- copyButton: PropTypes.bool,
189
- minimizeAlerts: PropTypes.bool,
324
+ type: PropTypes.string,
325
+ visible: PropTypes.bool,
326
+ content: PropTypes.node,
327
+ canClose: PropTypes.bool,
328
+ alertProps: PropTypes.object,
329
+ closeButtonProps: PropTypes.object,
330
+ copyButton: PropTypes.bool,
331
+ minimizeAlerts: PropTypes.bool,
332
+ alerts: PropTypes.arrayOf(
333
+ PropTypes.shape({
334
+ type: PropTypes.string,
335
+ visible: PropTypes.bool,
336
+ content: PropTypes.node,
337
+ canClose: PropTypes.bool,
338
+ alertProps: PropTypes.object,
339
+ closeButtonProps: PropTypes.object,
340
+ copyButton: PropTypes.bool,
341
+ })
342
+ )
190
343
  };
344
+
191
345
  MadieAlert.defaultProps = {
192
- copyButton: false,
193
- minimizeAlerts: false,
346
+ copyButton: false,
347
+ minimizeAlerts: false,
348
+ alerts: null,
194
349
  };
195
350
 
196
351
  export default MadieAlert;
@@ -41,3 +41,27 @@ export const DeleteDialog = () => {
41
41
  </div>
42
42
  );
43
43
  };
44
+
45
+ export const DeleteDialogNoWarning = () => {
46
+ const [open, setOpen] = useState(false);
47
+ const onClose = () => {
48
+ setOpen(false);
49
+ };
50
+ const onContinue = () => {
51
+ setOpen(false);
52
+ };
53
+
54
+ return (
55
+ <div className="qpp-u-padding--16" style={{ width: 300 }}>
56
+ <Button variant="cyan" onClick={() => setOpen(true)}>
57
+ open Dialog
58
+ </Button>
59
+ <MadieDeleteDialog
60
+ open={open}
61
+ onContinue={onContinue}
62
+ onClose={onClose}
63
+ hideWarning={true}
64
+ />
65
+ </div>
66
+ );
67
+ };
@@ -37,10 +37,12 @@ const MadieDeleteDialog = ({
37
37
  <span className="strong">{otherDialogProps.name}</span>?
38
38
  </p>
39
39
  </section>
40
- <section className="dialog-warning-action">
41
- <ErrorIcon />
42
- <p>This Action cannot be undone.</p>
43
- </section>
40
+ {otherDialogProps.hideWarning !== true && (
41
+ <section className="dialog-warning-action">
42
+ <ErrorIcon />
43
+ <p>This Action cannot be undone.</p>
44
+ </section>
45
+ )}
44
46
  </div>
45
47
  </MadieDialog>
46
48
  );
@@ -179,6 +179,7 @@ const RichTextEditor = ({
179
179
  {
180
180
  extensions: [
181
181
  StarterKit,
182
+ Markdown,
182
183
  Gapcursor,
183
184
  Table.configure({
184
185
  resizable: true,
@@ -187,7 +188,6 @@ const RichTextEditor = ({
187
188
  TableHeader,
188
189
  TableCell,
189
190
  Underline,
190
- Markdown,
191
191
  ],
192
192
  shouldRerenderOnTransaction: false,
193
193
  content,
@@ -200,7 +200,7 @@ const RichTextEditor = ({
200
200
  [content]
201
201
  );
202
202
  return (
203
- <>
203
+ <div className="rich-text-editor">
204
204
  <InputLabel
205
205
  shrink
206
206
  required={required}
@@ -243,7 +243,7 @@ const RichTextEditor = ({
243
243
  </InputLabel>
244
244
  <MenuBar editor={editor} />
245
245
  <EditorContent editor={editor} />
246
- </>
246
+ </div>
247
247
  );
248
248
  };
249
249