@iobroker/dm-gui-components 10.0.23 → 10.1.0

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.
Files changed (46) hide show
  1. package/README.md +10 -0
  2. package/build/AnimatedIcons.d.ts +37 -0
  3. package/build/AnimatedIcons.js +108 -0
  4. package/build/AnimatedIcons.js.map +1 -0
  5. package/build/Communication.d.ts +7 -0
  6. package/build/Communication.js +43 -11
  7. package/build/Communication.js.map +1 -1
  8. package/build/DeviceCard.d.ts +25 -13
  9. package/build/DeviceCard.js +177 -282
  10. package/build/DeviceCard.js.map +1 -1
  11. package/build/DeviceControl.js +38 -26
  12. package/build/DeviceControl.js.map +1 -1
  13. package/build/DeviceImageUpload.d.ts +5 -1
  14. package/build/DeviceImageUpload.js +79 -62
  15. package/build/DeviceImageUpload.js.map +1 -1
  16. package/build/DeviceList.d.ts +8 -0
  17. package/build/DeviceList.js +74 -59
  18. package/build/DeviceList.js.map +1 -1
  19. package/build/DeviceStatus.d.ts +20 -4
  20. package/build/DeviceStatus.js +146 -191
  21. package/build/DeviceStatus.js.map +1 -1
  22. package/build/StatusIndicator.js +9 -37
  23. package/build/StatusIndicator.js.map +1 -1
  24. package/build/StatusPill.d.ts +18 -0
  25. package/build/StatusPill.js +30 -0
  26. package/build/StatusPill.js.map +1 -0
  27. package/build/Switch.d.ts +3 -0
  28. package/build/Switch.js +36 -26
  29. package/build/Switch.js.map +1 -1
  30. package/build/Utils.js +92 -60
  31. package/build/Utils.js.map +1 -1
  32. package/build/i18n/de.json +3 -0
  33. package/build/i18n/en.json +3 -0
  34. package/build/i18n/es.json +3 -0
  35. package/build/i18n/fr.json +3 -0
  36. package/build/i18n/it.json +3 -0
  37. package/build/i18n/nl.json +3 -0
  38. package/build/i18n/pl.json +3 -0
  39. package/build/i18n/pt.json +3 -0
  40. package/build/i18n/ru.json +3 -0
  41. package/build/i18n/uk.json +3 -0
  42. package/build/i18n/zh-cn.json +3 -0
  43. package/build/styles.d.ts +17 -0
  44. package/build/styles.js +226 -0
  45. package/build/styles.js.map +1 -0
  46. package/package.json +10 -10
package/README.md CHANGED
@@ -197,6 +197,16 @@ const indicator = {
197
197
  -->
198
198
 
199
199
  ## Changelog
200
+ ### 10.1.0 (2026-11-05)
201
+ - (@GermanBluefox) Redesigned the device cards (big and small): device tile with a live connection LED, compact status pills that are only tinted when a value needs attention, a facts grid and an action footer
202
+ - (@GermanBluefox) Added animations: the animated icons of `@iobroker/gui-components` (refresh, filter, delete, upload, folder, copy, play/pause, result) and own ones (battery, signal and update icons, hover animations of the actions, value ticks, LED pulse on status changes, snapping switch)
203
+ - (@GermanBluefox) Added: a successful action is confirmed with a check mark, the error toast shows an animated cross
204
+ - (@GermanBluefox) Changed: "No response from the backend" and the copy confirmations no longer use `window.alert`
205
+ - (@GermanBluefox) Added: "Reset filters" button in the toolbar and in the empty list
206
+ - (@GermanBluefox) Redesigned the instance selection and the empty states
207
+ - (@GermanBluefox) Animations respect `prefers-reduced-motion`
208
+ - (@GermanBluefox) Corrected: a slider or select control without `stateId` never got its range or options and showed only "..."
209
+
200
210
  ### 10.0.23 (2026-09-21)
201
211
  - (@BenAhrdt) Supported explicit line breaks in status indicator tooltips
202
212
 
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Own animated icons of the device manager, in the style of the animated icons of `@iobroker/gui-components`:
3
+ * plain CSS/SVG, `currentColor`, the size of the MUI icons and less motion if the user asked for it.
4
+ */
5
+ import React, { type CSSProperties } from 'react';
6
+ interface OwnIconProps {
7
+ className?: string;
8
+ style?: CSSProperties;
9
+ /** Accessible name. Without it, the icon is decorative. */
10
+ ariaLabel?: string;
11
+ }
12
+ /** Color of the charge of a battery with this level */
13
+ export declare function batteryColor(level: number): string;
14
+ export interface IconBatteryProps extends OwnIconProps {
15
+ /** Charge in percent. Without it the battery is shown without a level (e.g. for a voltage). */
16
+ level?: number;
17
+ /** The battery is charging: the charge fills up again and again */
18
+ charging?: boolean;
19
+ }
20
+ /** Battery whose charge slides to the new level when it changes */
21
+ export declare function IconBattery(props: IconBatteryProps): React.JSX.Element;
22
+ export interface IconSignalProps extends OwnIconProps {
23
+ /** Signal strength (RSSI) in dBm */
24
+ rssi: number;
25
+ }
26
+ /** Four bars for the signal strength. When it changes, the bars light up or go out one after another. */
27
+ export declare function IconSignal(props: IconSignalProps): React.JSX.Element;
28
+ /** An update can be installed: the arrow drops into the tray while the mouse is over the icon or its button */
29
+ export declare function IconUpdate(props: OwnIconProps): React.JSX.Element;
30
+ /**
31
+ * Lets the new value of a status slide in when it changes (not on the first render).
32
+ *
33
+ * @param value the shown value
34
+ * @returns the ref for the element that shows the value
35
+ */
36
+ export declare function useValueTick<T extends HTMLElement>(value: unknown): React.RefObject<T | null>;
37
+ export {};
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Own animated icons of the device manager, in the style of the animated icons of `@iobroker/gui-components`:
3
+ * plain CSS/SVG, `currentColor`, the size of the MUI icons and less motion if the user asked for it.
4
+ */
5
+ import React, { useEffect, useRef } from 'react';
6
+ import { useDmStyles } from './styles';
7
+ function svgProps(props, className) {
8
+ return {
9
+ viewBox: '0 0 24 24',
10
+ width: '1em',
11
+ height: '1em',
12
+ className: `${className}${props.className ? ` ${props.className}` : ''}`,
13
+ style: { fontSize: '1.5rem', flexShrink: 0, ...props.style },
14
+ role: props.ariaLabel ? 'img' : undefined,
15
+ 'aria-label': props.ariaLabel,
16
+ 'aria-hidden': props.ariaLabel ? undefined : true,
17
+ };
18
+ }
19
+ /** True if the user asked the system for less motion */
20
+ function prefersReducedMotion() {
21
+ return !!window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
22
+ }
23
+ /** Color of the charge of a battery with this level */
24
+ export function batteryColor(level) {
25
+ if (level > 55) {
26
+ return 'var(--dm-ok, #00ac00)';
27
+ }
28
+ if (level > 25) {
29
+ return '#e6b800';
30
+ }
31
+ if (level > 10) {
32
+ return 'var(--dm-warn, #da8200)';
33
+ }
34
+ return 'var(--dm-err, #ff0000)';
35
+ }
36
+ /** Battery whose charge slides to the new level when it changes */
37
+ export function IconBattery(props) {
38
+ useDmStyles();
39
+ const known = typeof props.level === 'number' && !props.charging;
40
+ const level = known ? Math.max(0, Math.min(100, props.level)) : 100;
41
+ // An empty battery still shows a sliver, so that its color is visible
42
+ const scale = known ? Math.max(0.08, level / 100) : 1;
43
+ let className = 'dm-bat';
44
+ if (props.charging) {
45
+ className += ' dm-bat-charging';
46
+ }
47
+ else if (!known) {
48
+ className += ' dm-bat-unknown';
49
+ }
50
+ const svg = svgProps(props, className);
51
+ return (React.createElement("svg", { ...svg, style: { ...svg.style, '--dm-bat': known ? batteryColor(level) : undefined } },
52
+ React.createElement("rect", { className: "dm-bat-body", x: "2.8", y: "6.8", width: "16.4", height: "10.4", rx: "2.2" }),
53
+ React.createElement("rect", { className: "dm-bat-cap", x: "19.9", y: "9.9", width: "2", height: "4.2", rx: "0.8" }),
54
+ React.createElement("rect", { className: "dm-bat-fill", x: "4.6", y: "8.6", width: "12.8", height: "6.8", rx: "0.9", style: { transform: `scaleX(${scale})` } }),
55
+ props.charging ? (React.createElement("path", { className: "dm-bat-bolt", d: "M11.9 7.4 8.4 12.6h2.9l-.6 4 3.6-5.3h-2.9z" })) : null));
56
+ }
57
+ /** Number of the lit bars for a signal strength in dBm */
58
+ function signalBars(rssi) {
59
+ if (rssi >= -55) {
60
+ return 4;
61
+ }
62
+ if (rssi >= -67) {
63
+ return 3;
64
+ }
65
+ if (rssi >= -80) {
66
+ return 2;
67
+ }
68
+ return 1;
69
+ }
70
+ /** Four bars for the signal strength. When it changes, the bars light up or go out one after another. */
71
+ export function IconSignal(props) {
72
+ useDmStyles();
73
+ const bars = signalBars(props.rssi);
74
+ return (React.createElement("svg", { ...svgProps(props, 'dm-sig') }, [0, 1, 2, 3].map(i => (React.createElement("rect", { key: i, className: i < bars ? 'dm-sig-on' : undefined, x: 3 + i * 5, y: 16 - i * 4, width: "3.2", height: 4 + i * 4, rx: "1" })))));
75
+ }
76
+ /** An update can be installed: the arrow drops into the tray while the mouse is over the icon or its button */
77
+ export function IconUpdate(props) {
78
+ useDmStyles();
79
+ return (React.createElement("svg", { ...svgProps(props, 'dm-upd') },
80
+ React.createElement("path", { d: "M4 14.5v3.2A2.3 2.3 0 0 0 6.3 20h11.4a2.3 2.3 0 0 0 2.3-2.3v-3.2" }),
81
+ React.createElement("path", { className: "dm-upd-arrow", d: "M12 3.5v10.5M7.6 9.8 12 14.2l4.4-4.4" })));
82
+ }
83
+ /**
84
+ * Lets the new value of a status slide in when it changes (not on the first render).
85
+ *
86
+ * @param value the shown value
87
+ * @returns the ref for the element that shows the value
88
+ */
89
+ export function useValueTick(value) {
90
+ const ref = useRef(null);
91
+ const previous = useRef(value);
92
+ useEffect(() => {
93
+ if (Object.is(previous.current, value)) {
94
+ return;
95
+ }
96
+ const goesUp = typeof value === 'number' && typeof previous.current === 'number' && value > previous.current;
97
+ previous.current = value;
98
+ if (!ref.current?.animate || prefersReducedMotion()) {
99
+ return;
100
+ }
101
+ ref.current.animate([
102
+ { transform: `translateY(${goesUp ? 45 : -45}%)`, opacity: 0 },
103
+ { transform: 'none', opacity: 1 },
104
+ ], { duration: 300, easing: 'cubic-bezier(0.2, 0.8, 0.3, 1)' });
105
+ }, [value]);
106
+ return ref;
107
+ }
108
+ //# sourceMappingURL=AnimatedIcons.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AnimatedIcons.js","sourceRoot":"./src/","sources":["AnimatedIcons.tsx"],"names":[],"mappings":"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","sourcesContent":["/**\n * Own animated icons of the device manager, in the style of the animated icons of `@iobroker/gui-components`:\n * plain CSS/SVG, `currentColor`, the size of the MUI icons and less motion if the user asked for it.\n */\nimport React, { useEffect, useRef, type CSSProperties } from 'react';\n\nimport { useDmStyles } from './styles';\n\ninterface OwnIconProps {\n className?: string;\n style?: CSSProperties;\n /** Accessible name. Without it, the icon is decorative. */\n ariaLabel?: string;\n}\n\nfunction svgProps(props: OwnIconProps, className: string): React.SVGProps<SVGSVGElement> {\n return {\n viewBox: '0 0 24 24',\n width: '1em',\n height: '1em',\n className: `${className}${props.className ? ` ${props.className}` : ''}`,\n style: { fontSize: '1.5rem', flexShrink: 0, ...props.style },\n role: props.ariaLabel ? 'img' : undefined,\n 'aria-label': props.ariaLabel,\n 'aria-hidden': props.ariaLabel ? undefined : true,\n };\n}\n\n/** True if the user asked the system for less motion */\nfunction prefersReducedMotion(): boolean {\n return !!window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;\n}\n\n/** Color of the charge of a battery with this level */\nexport function batteryColor(level: number): string {\n if (level > 55) {\n return 'var(--dm-ok, #00ac00)';\n }\n if (level > 25) {\n return '#e6b800';\n }\n if (level > 10) {\n return 'var(--dm-warn, #da8200)';\n }\n return 'var(--dm-err, #ff0000)';\n}\n\nexport interface IconBatteryProps extends OwnIconProps {\n /** Charge in percent. Without it the battery is shown without a level (e.g. for a voltage). */\n level?: number;\n /** The battery is charging: the charge fills up again and again */\n charging?: boolean;\n}\n\n/** Battery whose charge slides to the new level when it changes */\nexport function IconBattery(props: IconBatteryProps): React.JSX.Element {\n useDmStyles();\n const known = typeof props.level === 'number' && !props.charging;\n const level = known ? Math.max(0, Math.min(100, props.level!)) : 100;\n // An empty battery still shows a sliver, so that its color is visible\n const scale = known ? Math.max(0.08, level / 100) : 1;\n let className = 'dm-bat';\n if (props.charging) {\n className += ' dm-bat-charging';\n } else if (!known) {\n className += ' dm-bat-unknown';\n }\n const svg = svgProps(props, className);\n return (\n <svg\n {...svg}\n style={{ ...svg.style, '--dm-bat': known ? batteryColor(level) : undefined } as CSSProperties}\n >\n <rect\n className=\"dm-bat-body\"\n x=\"2.8\"\n y=\"6.8\"\n width=\"16.4\"\n height=\"10.4\"\n rx=\"2.2\"\n />\n <rect\n className=\"dm-bat-cap\"\n x=\"19.9\"\n y=\"9.9\"\n width=\"2\"\n height=\"4.2\"\n rx=\"0.8\"\n />\n <rect\n className=\"dm-bat-fill\"\n x=\"4.6\"\n y=\"8.6\"\n width=\"12.8\"\n height=\"6.8\"\n rx=\"0.9\"\n style={{ transform: `scaleX(${scale})` }}\n />\n {props.charging ? (\n <path\n className=\"dm-bat-bolt\"\n d=\"M11.9 7.4 8.4 12.6h2.9l-.6 4 3.6-5.3h-2.9z\"\n />\n ) : null}\n </svg>\n );\n}\n\n/** Number of the lit bars for a signal strength in dBm */\nfunction signalBars(rssi: number): number {\n if (rssi >= -55) {\n return 4;\n }\n if (rssi >= -67) {\n return 3;\n }\n if (rssi >= -80) {\n return 2;\n }\n return 1;\n}\n\nexport interface IconSignalProps extends OwnIconProps {\n /** Signal strength (RSSI) in dBm */\n rssi: number;\n}\n\n/** Four bars for the signal strength. When it changes, the bars light up or go out one after another. */\nexport function IconSignal(props: IconSignalProps): React.JSX.Element {\n useDmStyles();\n const bars = signalBars(props.rssi);\n return (\n <svg {...svgProps(props, 'dm-sig')}>\n {[0, 1, 2, 3].map(i => (\n <rect\n key={i}\n className={i < bars ? 'dm-sig-on' : undefined}\n x={3 + i * 5}\n y={16 - i * 4}\n width=\"3.2\"\n height={4 + i * 4}\n rx=\"1\"\n />\n ))}\n </svg>\n );\n}\n\n/** An update can be installed: the arrow drops into the tray while the mouse is over the icon or its button */\nexport function IconUpdate(props: OwnIconProps): React.JSX.Element {\n useDmStyles();\n return (\n <svg {...svgProps(props, 'dm-upd')}>\n <path d=\"M4 14.5v3.2A2.3 2.3 0 0 0 6.3 20h11.4a2.3 2.3 0 0 0 2.3-2.3v-3.2\" />\n <path\n className=\"dm-upd-arrow\"\n d=\"M12 3.5v10.5M7.6 9.8 12 14.2l4.4-4.4\"\n />\n </svg>\n );\n}\n\n/**\n * Lets the new value of a status slide in when it changes (not on the first render).\n *\n * @param value the shown value\n * @returns the ref for the element that shows the value\n */\nexport function useValueTick<T extends HTMLElement>(value: unknown): React.RefObject<T | null> {\n const ref = useRef<T>(null);\n const previous = useRef(value);\n useEffect(() => {\n if (Object.is(previous.current, value)) {\n return;\n }\n const goesUp = typeof value === 'number' && typeof previous.current === 'number' && value > previous.current;\n previous.current = value;\n if (!ref.current?.animate || prefersReducedMotion()) {\n return;\n }\n ref.current.animate(\n [\n { transform: `translateY(${goesUp ? 45 : -45}%)`, opacity: 0 },\n { transform: 'none', opacity: 1 },\n ],\n { duration: 300, easing: 'cubic-bezier(0.2, 0.8, 0.3, 1)' },\n );\n }, [value]);\n return ref;\n}\n"]}
@@ -49,6 +49,8 @@ export type CommunicationState = {
49
49
  showInput: InputAction | null;
50
50
  inputValue: string | boolean | number | null;
51
51
  selectedInstance: string;
52
+ /** Changes after each successful action: a check mark confirms it for a moment. 0 - nothing to show */
53
+ resultFlash?: number;
52
54
  };
53
55
  /**
54
56
  * Communication Component
@@ -56,12 +58,15 @@ export type CommunicationState = {
56
58
  export default class Communication<P extends CommunicationProps, S extends CommunicationState> extends Component<P, S> {
57
59
  private protocol;
58
60
  private responseTimeout;
61
+ private resultFlashTimeout;
59
62
  instanceHandler: (action: ActionBase) => () => void;
60
63
  deviceHandler: (deviceId: DeviceId, action: ActionBase) => () => void;
61
64
  controlHandler: (deviceId: DeviceId, control: ControlBase, state: ControlState) => () => Promise<ioBroker.State | null>;
62
65
  controlStateHandler: (deviceId: DeviceId, control: ControlBase) => () => Promise<ioBroker.State | null>;
63
66
  constructor(props: P);
64
67
  componentWillUnmount(): void;
68
+ /** Confirms a successful action: the spinner is followed by a check mark that draws itself */
69
+ showResultFlash(): void;
65
70
  loadAllData(): Promise<void>;
66
71
  loadDeviceList(): void;
67
72
  updateDevice(_update: DeviceInfo): void;
@@ -76,7 +81,9 @@ export default class Communication<P extends CommunicationProps, S extends Commu
76
81
  loadInstanceInfos(): Promise<InstanceDetails>;
77
82
  renderMessageDialog(): React.JSX.Element | null;
78
83
  renderConfirmDialog(): React.JSX.Element | null;
84
+ /** The toast shows errors: the cross draws itself and shakes */
79
85
  renderSnackbar(): React.JSX.Element;
86
+ renderResultFlash(): React.JSX.Element | null;
80
87
  getOkButton(button?: ActionButton | 'apply' | 'cancel' | 'close'): React.JSX.Element;
81
88
  getCancelButton(button?: ActionButton | 'apply' | 'cancel' | 'close'): React.JSX.Element;
82
89
  renderFormDialog(): React.JSX.Element | null;
@@ -1,5 +1,5 @@
1
- import { I18n, Icon, Utils, } from '@iobroker/gui-components';
2
- import { Check, Close, ContentCopy } from '@mui/icons-material';
1
+ import { I18n, Icon, IconCopyToClipboard, IconResult, Utils, } from '@iobroker/gui-components';
2
+ import { Check, Close } from '@mui/icons-material';
3
3
  import { Backdrop, Box, Button, Checkbox, CircularProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, FormControl, FormControlLabel, Grid, IconButton, Input, InputAdornment, InputLabel, LinearProgress, MenuItem, Select, Slider, Snackbar, TextField, ThemeProvider, Typography, } from '@mui/material';
4
4
  import React, { Component } from 'react';
5
5
  import JsonConfig from './JsonConfig';
@@ -8,12 +8,16 @@ import { DmProtocolV2 } from './protocol/DmProtocolV2';
8
8
  import { DmProtocolV3 } from './protocol/DmProtocolV3';
9
9
  import { UnknownDmProtocol } from './protocol/UnknownDmProtocol';
10
10
  import { getTranslation } from './Utils';
11
+ import { ensureDmStyles, getThemeVars } from './styles';
12
+ /** How long the check mark after a successful action is shown (the CSS animation takes as long) */
13
+ const RESULT_FLASH_DURATION = 1250;
11
14
  /**
12
15
  * Communication Component
13
16
  */
14
17
  export default class Communication extends Component {
15
18
  protocol = new UnknownDmProtocol();
16
19
  responseTimeout = null;
20
+ resultFlashTimeout = null;
17
21
  // eslint-disable-next-line react/no-unused-class-component-methods
18
22
  instanceHandler;
19
23
  // eslint-disable-next-line react/no-unused-class-component-methods
@@ -24,6 +28,7 @@ export default class Communication extends Component {
24
28
  controlStateHandler;
25
29
  constructor(props) {
26
30
  super(props);
31
+ ensureDmStyles();
27
32
  this.state = {
28
33
  showSpinner: false,
29
34
  showToast: null,
@@ -34,6 +39,7 @@ export default class Communication extends Component {
34
39
  showConfirmation: null,
35
40
  showInput: null,
36
41
  inputValue: null,
42
+ resultFlash: 0,
37
43
  selectedInstance: this.props.selectedInstance ??
38
44
  this.props.instance ??
39
45
  (window.localStorage.getItem('dmSelectedInstance') || ''),
@@ -76,6 +82,21 @@ export default class Communication extends Component {
76
82
  clearTimeout(this.responseTimeout);
77
83
  this.responseTimeout = null;
78
84
  }
85
+ if (this.resultFlashTimeout) {
86
+ clearTimeout(this.resultFlashTimeout);
87
+ this.resultFlashTimeout = null;
88
+ }
89
+ }
90
+ /** Confirms a successful action: the spinner is followed by a check mark that draws itself */
91
+ showResultFlash() {
92
+ if (this.resultFlashTimeout) {
93
+ clearTimeout(this.resultFlashTimeout);
94
+ }
95
+ this.setState({ resultFlash: Date.now() });
96
+ this.resultFlashTimeout = setTimeout(() => {
97
+ this.resultFlashTimeout = null;
98
+ this.setState({ resultFlash: 0 });
99
+ }, RESULT_FLASH_DURATION);
79
100
  }
80
101
  // eslint-disable-next-line class-methods-use-this
81
102
  loadAllData() {
@@ -98,8 +119,7 @@ export default class Communication extends Component {
98
119
  const send = async () => {
99
120
  this.setState({ showSpinner: true });
100
121
  this.responseTimeout = setTimeout(() => {
101
- this.setState({ showSpinner: false });
102
- window.alert(I18n.t('ra_No response from the backend'));
122
+ this.setState({ showSpinner: false, showToast: I18n.t('ra_No response from the backend') });
103
123
  }, messageToSend.timeout || 5000);
104
124
  const response = await this.protocol.sendAction(command, messageToSend);
105
125
  if (this.responseTimeout) {
@@ -219,6 +239,7 @@ export default class Communication extends Component {
219
239
  }
220
240
  else {
221
241
  this.setState({ showSpinner: false });
242
+ this.showResultFlash();
222
243
  }
223
244
  break;
224
245
  default:
@@ -295,8 +316,18 @@ export default class Communication extends Component {
295
316
  React.createElement(Button, { variant: "contained", color: "primary", onClick: () => confirm.handleClose(true), autoFocus: true }, getTranslation('yesButtonText')),
296
317
  React.createElement(Button, { variant: "contained", color: "grey", onClick: () => confirm.handleClose(false), autoFocus: true }, getTranslation('noButtonText')))));
297
318
  }
319
+ /** The toast shows errors: the cross draws itself and shakes */
298
320
  renderSnackbar() {
299
- return (React.createElement(Snackbar, { open: !!this.state.showToast, autoHideDuration: 6_000, onClose: () => this.setState({ showToast: null }), message: this.state.showToast }));
321
+ return (React.createElement(Snackbar, { open: !!this.state.showToast, autoHideDuration: 6_000, onClose: () => this.setState({ showToast: null }), message: this.state.showToast ? (React.createElement("span", { className: "dm-toast" },
322
+ React.createElement(IconResult, { type: "error", replay: this.state.showToast }),
323
+ this.state.showToast)) : null }));
324
+ }
325
+ renderResultFlash() {
326
+ if (!this.state.resultFlash) {
327
+ return null;
328
+ }
329
+ return (React.createElement("div", { key: this.state.resultFlash, className: "dm-result", style: getThemeVars(this.props.theme) },
330
+ React.createElement(IconResult, { type: "success" })));
300
331
  }
301
332
  getOkButton(button) {
302
333
  if (typeof button === 'string') {
@@ -359,7 +390,9 @@ export default class Communication extends Component {
359
390
  ? button.color
360
391
  : undefined,
361
392
  ...(button.style || undefined),
362
- }, onClick: () => {
393
+ },
394
+ // The copy symbol confirms the copy with a check mark
395
+ onClick: () => {
363
396
  if (button.copyToClipboardAttr && form.data) {
364
397
  const val = form.data[button.copyToClipboardAttr];
365
398
  if (typeof val === 'string') {
@@ -368,16 +401,14 @@ export default class Communication extends Component {
368
401
  else {
369
402
  Utils.copyToClipboard(JSON.stringify(val, null, 2));
370
403
  }
371
- window.alert(I18n.t('copied'));
372
404
  }
373
405
  else if (form.data) {
374
406
  Utils.copyToClipboard(JSON.stringify(form.data, null, 2));
375
- window.alert(I18n.t('copied'));
376
407
  }
377
408
  else {
378
- window.alert(I18n.t('nothingToCopy'));
409
+ this.setState({ showToast: I18n.t('nothingToCopy') });
379
410
  }
380
- }, startIcon: button?.icon ? React.createElement(Icon, { src: button?.icon }) : React.createElement(ContentCopy, null) }, getTranslation(button?.label || 'ctcButtonText', button?.noTranslation)));
411
+ }, startIcon: button?.icon ? React.createElement(Icon, { src: button?.icon }) : React.createElement(IconCopyToClipboard, null) }, getTranslation(button?.label || 'ctcButtonText', button?.noTranslation)));
381
412
  }
382
413
  else if (button === 'apply' || button.type === 'apply') {
383
414
  buttons.push(this.getOkButton(button));
@@ -587,7 +618,8 @@ export default class Communication extends Component {
587
618
  this.renderProgressDialog(),
588
619
  this.renderConfirmationDialog(),
589
620
  this.renderInputDialog(),
590
- this.renderSpinner()));
621
+ this.renderSpinner(),
622
+ this.renderResultFlash()));
591
623
  }
592
624
  }
593
625
  //# sourceMappingURL=Communication.js.map