@eohjsc/react-native-smart-city 0.6.0 → 0.6.2

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 (30) hide show
  1. package/README.md +44 -6
  2. package/package.json +1 -1
  3. package/src/commons/Device/ItemDevice.js +30 -50
  4. package/src/commons/Device/PowerConsumptionChart.js +6 -1
  5. package/src/commons/Device/ProgressBar/__test__/ProgressBar.test.js +2 -3
  6. package/src/commons/Device/ProgressBar/index.js +36 -38
  7. package/src/commons/Device/ProgressBar/styles.js +18 -24
  8. package/src/commons/Form/CurrencyInput.js +2 -0
  9. package/src/commons/SubUnit/Favorites/index.js +0 -1
  10. package/src/commons/SubUnit/ShortDetail.js +0 -1
  11. package/src/commons/Widgets/IFrame/IFrame.js +47 -0
  12. package/src/commons/Widgets/IFrame/IFrameStyles.js +33 -0
  13. package/src/commons/Widgets/IFrame/__tests__/IFrame.test.js +75 -0
  14. package/src/commons/Widgets/IFrame/index.js +0 -0
  15. package/src/commons/Widgets/IFrameWithConfig/IFrameWithConfig.js +163 -0
  16. package/src/commons/Widgets/IFrameWithConfig/IFrameWithConfigStyles.js +9 -0
  17. package/src/commons/Widgets/IFrameWithConfig/__tests__/IFrameWithConfig.test.js +284 -0
  18. package/src/commons/Widgets/IFrameWithConfig/index.js +0 -0
  19. package/src/commons/Widgets/Widget.js +0 -0
  20. package/src/commons/Widgets/index.js +0 -0
  21. package/src/configs/AccessibilityLabel.js +3 -0
  22. package/src/configs/Constants.js +8 -3
  23. package/src/hooks/Common/useDevicesStatus.js +0 -1
  24. package/src/screens/Automate/AddNewAction/SetupConfigCondition.js +7 -5
  25. package/src/screens/Automate/ScriptDetail/utils.js +1 -1
  26. package/src/screens/Device/components/SensorDisplayItem.js +23 -1
  27. package/src/screens/Device/components/__test__/VisualChart.test.js +2 -9
  28. package/src/screens/SubUnit/Detail.js +0 -1
  29. package/src/screens/UnitSummary/components/PowerConsumption/__test__/PowerConsumption.test.js +19 -0
  30. package/src/screens/UnitSummary/components/RunningDevices/index.js +0 -1
package/README.md CHANGED
@@ -21,6 +21,8 @@
21
21
 
22
22
  1. StackNavigator
23
23
 
24
+ - You can see the screens or components that the library is supporting in the folder `/@eohjsc/react-native-smart-city/index.js`
25
+
24
26
  ```javascript
25
27
  import {
26
28
  UnitStack,
@@ -38,12 +40,9 @@ import {
38
40
  import Config from 'react-native-config';
39
41
  import { createStackNavigator } from '@react-navigation/stack';
40
42
 
41
- // TODO: What to do with the module?
42
43
  const Stack = createStackNavigator();
43
44
 
44
45
  const YourStack = () => {
45
-
46
-
47
46
  useEffect(() => {
48
47
  const config = {
49
48
  apiRoot: Config.API_ROOT,
@@ -58,8 +57,8 @@ const YourStack = () => {
58
57
  language,
59
58
  setCurrentSensorDisplay,
60
59
  appName: Config.APP_NAME,
61
- packageName: Config.APP_PACKAGE_NAME, // Your package name is required
62
- versionCode: Config.APP_VERSION_CODE, // Your app version is required
60
+ packageName: Config.APP_PACKAGE_NAME, // Your package name is required
61
+ versionCode: Config.APP_VERSION_CODE, // Your app version is required
63
62
  };
64
63
  initSCConfig(config);
65
64
  }, [language, setCurrentSensorDisplay]);
@@ -132,7 +131,46 @@ const MyScreen = () => {
132
131
  };
133
132
  ```
134
133
 
135
- 3. Trigger quick action
134
+ 3. Use navigate to the screens included in the '@eohjsc/react-native-smart-city' library
135
+
136
+ - You can see the list of screens, currently supported by the library in the folder `/@eohjsc/react-native-smart-city/src/navigations/`
137
+
138
+ ```javascript
139
+ import { useNavigation } from '@react-navigation/native';
140
+ const { navigate } = useNavigation();
141
+ const automate_id = 1;
142
+ const unit_id = 1;
143
+ const sensor_id = 1;
144
+
145
+ const handleNavigateScriptDetail = () => {
146
+ navigate('UnitStack', {
147
+ screen: 'ScriptDetail',
148
+ params: {
149
+ id: automate_id,
150
+ },
151
+ });
152
+ };
153
+
154
+ const handleNavigateDeviceDetail = () => {
155
+ navigate('UnitStack', {
156
+ screen: 'DeviceDetail',
157
+ params: {
158
+ unitId: unit_id,
159
+ sensorId: sensor_id,
160
+ },
161
+ });
162
+ };
163
+
164
+ <TouchableOpacity onPress={handleNavigateScriptDetail}>
165
+ <Text>{'Navigate to the "ScriptDetail" screen'}</Text>
166
+ </TouchableOpacity>;
167
+
168
+ <TouchableOpacity onPress={handleNavigateDeviceDetail}>
169
+ <Text>{'Navigate to the "DeviceDetail" screen'}</Text>
170
+ </TouchableOpacity>;
171
+ ```
172
+
173
+ 4. Trigger quick action
136
174
 
137
175
  ```javascript
138
176
  import React from 'react';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@eohjsc/react-native-smart-city",
3
3
  "title": "React Native Smart Home",
4
- "version": "0.6.0",
4
+ "version": "0.6.2",
5
5
  "description": "TODO",
6
6
  "main": "index.js",
7
7
  "files": [
@@ -18,20 +18,19 @@ import IconComponent from '../IconComponent';
18
18
  import ItemDeviceWrapper from './ItemDeviceWrapper';
19
19
 
20
20
  const ItemDevice = memo(
21
- ({ svgMain, description, title, sensor, unit, station, wrapStyle }) => {
21
+ ({ description, title, sensor, unit, station, wrapStyle }) => {
22
22
  const t = useTranslations();
23
23
  const navigation = useNavigation();
24
+ const { is_managed_by_backend, device_type, icon_kit, icon } = sensor;
24
25
 
25
26
  const {
26
27
  isConnected: isEoHBackendConnected,
27
28
  isFetchingStatus: isFetchingStatusFromEoHBackend,
28
29
  } = useEoHBackendDeviceConnected(sensor);
29
-
30
30
  const {
31
31
  isConnected: isHomeAssistantConnected,
32
32
  isConnecting: isHomeAssistantConnecting,
33
33
  } = useHomeAssistantDeviceConnected(sensor);
34
-
35
34
  const { isConnected: isBluetoothConnected } =
36
35
  useBluetoothDeviceConnected(sensor);
37
36
 
@@ -45,64 +44,51 @@ const ItemDevice = memo(
45
44
  }, [navigation, sensor, station, title, unit]);
46
45
 
47
46
  const textConnected = useMemo(() => {
48
- if (!!sensor && sensor?.is_managed_by_backend) {
49
- if (sensor?.device_type === DEVICE_TYPE.LG_THINQ) {
50
- return t('connected');
51
- }
52
- if (isBluetoothConnected) {
53
- return t('connected');
54
- }
55
- if (isFetchingStatusFromEoHBackend) {
56
- return '';
57
- }
58
- if (isEoHBackendConnected) {
59
- return t('connected');
60
- }
61
- return t('disconnected');
62
- }
63
- // not managed by backend
64
- if (sensor?.device_type === DEVICE_TYPE.GOOGLE_HOME) {
65
- if (isHomeAssistantConnecting) {
66
- return t('connecting');
67
- }
68
- if (isHomeAssistantConnected) {
47
+ if (is_managed_by_backend) {
48
+ if (
49
+ device_type === DEVICE_TYPE.LG_THINQ ||
50
+ isBluetoothConnected ||
51
+ isEoHBackendConnected
52
+ ) {
69
53
  return t('connected');
70
54
  }
55
+ return isFetchingStatusFromEoHBackend ? '' : t('disconnected');
56
+ } else if (device_type === DEVICE_TYPE.GOOGLE_HOME) {
57
+ return isHomeAssistantConnecting
58
+ ? t('connecting')
59
+ : isHomeAssistantConnected
60
+ ? t('connected')
61
+ : t('disconnected');
71
62
  }
72
63
  return t('disconnected');
73
64
  }, [
65
+ device_type,
74
66
  isBluetoothConnected,
75
67
  isEoHBackendConnected,
76
68
  isFetchingStatusFromEoHBackend,
77
69
  isHomeAssistantConnected,
78
70
  isHomeAssistantConnecting,
79
- sensor,
71
+ is_managed_by_backend,
80
72
  t,
81
73
  ]);
82
74
 
83
75
  const canRenderQuickAction = useMemo(() => {
84
- if (!!sensor && sensor?.is_managed_by_backend) {
85
- if (sensor?.device_type === DEVICE_TYPE.LG_THINQ) {
86
- return true;
87
- }
88
- if (isBluetoothConnected) {
89
- return true;
90
- }
91
- if (isFetchingStatusFromEoHBackend) {
92
- return false;
93
- }
94
- return true;
95
- }
96
- // not managed by backend
97
- if (sensor?.device_type === DEVICE_TYPE.GOOGLE_HOME) {
98
- return isHomeAssistantConnected;
76
+ if (is_managed_by_backend) {
77
+ return (
78
+ device_type === DEVICE_TYPE.LG_THINQ ||
79
+ isBluetoothConnected ||
80
+ !isFetchingStatusFromEoHBackend
81
+ );
99
82
  }
100
- return true;
83
+ return device_type === DEVICE_TYPE.GOOGLE_HOME
84
+ ? isHomeAssistantConnected
85
+ : true;
101
86
  }, [
87
+ device_type,
102
88
  isBluetoothConnected,
103
89
  isFetchingStatusFromEoHBackend,
104
90
  isHomeAssistantConnected,
105
- sensor,
91
+ is_managed_by_backend,
106
92
  ]);
107
93
 
108
94
  return (
@@ -114,9 +100,9 @@ const ItemDevice = memo(
114
100
  >
115
101
  <View style={styles.boxIcon}>
116
102
  <TouchableOpacity onPress={goToSensorDisplay}>
117
- <IconComponent icon={sensor?.icon_kit || sensor?.icon} />
103
+ <IconComponent icon={icon_kit || icon} />
118
104
  </TouchableOpacity>
119
- {!!canRenderQuickAction && (
105
+ {canRenderQuickAction && (
120
106
  <ItemQuickAction
121
107
  sensor={sensor}
122
108
  unit={unit}
@@ -155,7 +141,6 @@ const ItemDevice = memo(
155
141
  );
156
142
 
157
143
  export default ItemDevice;
158
-
159
144
  const styles = StyleSheet.create({
160
145
  boxIcon: {
161
146
  flexDirection: 'row',
@@ -178,11 +163,6 @@ const styles = StyleSheet.create({
178
163
  lineHeight20: {
179
164
  lineHeight: 20,
180
165
  },
181
- iconSensor: {
182
- width: 40,
183
- height: 40,
184
- resizeMode: 'contain',
185
- },
186
166
  quickAction: {
187
167
  width: 32,
188
168
  height: 32,
@@ -11,6 +11,7 @@ import DateTimeRangeChange from '../DateTimeRangeChange';
11
11
  import HorizontalBarChart from './HorizontalBarChart';
12
12
  import ChartAggregationOption from '../ChartAggregationOption';
13
13
  import { formatMoney } from '../../utils/Utils';
14
+ import AccessibilityLabel from '../../configs/AccessibilityLabel';
14
15
 
15
16
  const PowerConsumptionChart = memo(
16
17
  ({
@@ -101,6 +102,7 @@ const PowerConsumptionChart = memo(
101
102
  onPress={onCalculateCost}
102
103
  style={styles.buttonCalculate}
103
104
  textSemiBold={false}
105
+ accessibilityLabel={AccessibilityLabel.BUTTON_CALCULATE_COST}
104
106
  />
105
107
  </View>
106
108
  </View>
@@ -108,7 +110,10 @@ const PowerConsumptionChart = memo(
108
110
  {renderChart}
109
111
  {!!chartConfig.price && (
110
112
  <Text type="H4">
111
- {t('total_power_price')} <Text bold>{formatMoney(totalPrice)}</Text>
113
+ {t('total_power_price')}{' '}
114
+ <Text bold accessibilityLabel={AccessibilityLabel.TOTAL_PRICE}>
115
+ {formatMoney(totalPrice)}
116
+ </Text>
112
117
  </Text>
113
118
  )}
114
119
  </View>
@@ -18,8 +18,7 @@ describe('Test ProgressBar', () => {
18
18
  it('render ProgressBar', async () => {
19
19
  const item = {
20
20
  label: 'Value bar',
21
- configuration: { max_value: 100 },
22
- value: 8,
21
+ configuration: { min_value: 0, max_value: 100 },
23
22
  };
24
23
  const data = [{ value: 10, unit: 'oC' }];
25
24
  await act(async () => {
@@ -28,6 +27,6 @@ describe('Test ProgressBar', () => {
28
27
  const instance = tree.root;
29
28
  const texts = instance.findAllByType(Text);
30
29
  expect(texts[0].props.children).toBe('Value bar');
31
- expect(texts[1].props.children).toEqual('Max value: 100 oC');
30
+ expect(texts[1].props.children).toEqual('0 oC');
32
31
  });
33
32
  });
@@ -3,53 +3,51 @@ import { View } from 'react-native';
3
3
  import * as Progress from 'react-native-progress';
4
4
 
5
5
  import { Colors } from '../../../configs';
6
- import { useTranslations } from '../../../hooks/Common/useTranslations';
7
6
  import Text from '../../Text';
8
7
  import styles from './styles';
9
8
 
10
9
  const ProgressBar = memo(({ data = [], item, isWidgetOrder }) => {
11
- const t = useTranslations();
12
-
13
- const minValue = useMemo(() => {
14
- return Number(item?.configuration?.min_value) || 0;
15
- }, [item?.configuration?.min_value]);
16
- const maxValue = useMemo(() => {
17
- return Number(item?.configuration?.max_value) || 60;
18
- }, [item?.configuration?.max_value]);
19
-
20
- let { value = 0, measure, unit } = data.length ? data[0] : {};
21
- const isNotValue = ['', null, undefined, NaN].includes(value);
22
-
23
- if (isNotValue) {
24
- value = minValue;
25
- }
10
+ const {
11
+ configuration: { max_value, min_value },
12
+ label,
13
+ } = item;
14
+ const { value = 0, unit } = data.length ? data[0] : {};
15
+ const isNotValue = useMemo(
16
+ () => ['', null, undefined, NaN].includes(value),
17
+ [value]
18
+ );
26
19
 
27
- const distance = maxValue - minValue;
28
- const percent = (value - minValue) / distance;
20
+ const displayedValue = isNotValue ? min_value : value;
21
+ const distance = max_value - min_value;
22
+ const percent = (displayedValue - min_value) / distance;
29
23
 
30
24
  return (
31
- <View style={(isWidgetOrder && styles.wrapOrderItem) || styles.container}>
32
- <Text size={16} style={styles.textLabel}>
33
- {item?.label || 'Label'}
25
+ <>
26
+ <Text type="H3" semibold color={Colors.Gray9} style={styles.textLabel}>
27
+ {label}
34
28
  </Text>
35
-
36
- <Text size={16} style={styles.textMaxValue}>
37
- {`${t('max_value')}: ${maxValue} ${unit || measure}`}
38
- </Text>
39
-
40
- <View style={styles.wrapProgressBar}>
41
- <Progress.Bar
42
- style={styles.progressBar}
43
- width={null}
44
- height={40}
45
- progress={percent}
46
- unfilledColor={Colors.Blue15}
47
- />
48
- <Text numberOfLines={1} style={styles.textValue}>
49
- {isNotValue ? '--' : value}
50
- </Text>
29
+ <View style={isWidgetOrder ? styles.wrapOrderItem : styles.container}>
30
+ <View style={styles.boxProgress}>
31
+ <View style={styles.boxMaxMinValue}>
32
+ <Text style={styles.textMaxMinValue}>{`${min_value} ${unit}`}</Text>
33
+ <Text style={styles.textMaxMinValue}>{`${max_value} ${unit}`}</Text>
34
+ </View>
35
+ <Progress.Bar
36
+ style={styles.progressBar}
37
+ width={null}
38
+ height={40}
39
+ progress={percent}
40
+ unfilledColor={Colors.Blue15}
41
+ />
42
+ </View>
43
+ <View>
44
+ <Text
45
+ bold
46
+ style={styles.textValue}
47
+ >{`${displayedValue} ${unit}`}</Text>
48
+ </View>
51
49
  </View>
52
- </View>
50
+ </>
53
51
  );
54
52
  });
55
53
 
@@ -4,44 +4,38 @@ import { Colors } from '../../../configs';
4
4
 
5
5
  export default StyleSheet.create({
6
6
  container: {
7
+ display: 'flex',
8
+ flexDirection: 'row',
9
+ },
10
+ boxProgress: {
7
11
  flex: 1,
8
- flexDirection: 'column', //column direction
9
- marginHorizontal: 30,
10
- borderWidth: 1,
11
- padding: 16,
12
- borderRadius: 8,
13
- borderColor: Colors.Gray4,
14
- marginBottom: 12,
12
+ flexDirection: 'column',
13
+ padding: 15,
14
+ },
15
+ boxMaxMinValue: {
16
+ flexDirection: 'row',
17
+ justifyContent: 'space-between',
15
18
  },
16
19
  wrapOrderItem: {
17
20
  flex: 1,
18
- padding: 16,
19
- flexDirection: 'column',
21
+ padding: 15,
22
+ flexDirection: 'row',
20
23
  },
21
24
  textLabel: {
22
- marginLeft: 8,
25
+ marginLeft: 15,
23
26
  paddingTop: 8,
24
27
  },
25
- textMaxValue: {
26
- marginTop: 20,
28
+ textMaxMinValue: {
27
29
  marginBottom: 2,
28
30
  textAlign: 'left',
29
- fontWeight: 'bold',
30
- textTransform: 'uppercase',
31
+ },
32
+ textValue: {
33
+ paddingTop: 48,
34
+ marginRight: 8,
31
35
  },
32
36
  progressBar: {
33
- flex: 1,
34
37
  color: Colors.Blue16,
35
38
  borderWidth: 0,
36
39
  borderRadius: 10,
37
40
  },
38
- wrapProgressBar: {
39
- flex: 1,
40
- flexDirection: 'row',
41
- justifyContent: 'flex-start',
42
- alignItems: 'center',
43
- },
44
- textValue: {
45
- marginLeft: 8,
46
- },
47
41
  });
@@ -15,6 +15,7 @@ import {
15
15
  import AndroidKeyboardAdjust from 'react-native-android-keyboard-adjust';
16
16
  import Text from '../Text';
17
17
  import { Colors } from '../../configs';
18
+ import AccessibilityLabel from '../../configs/AccessibilityLabel';
18
19
 
19
20
  const formatNumber = (input, options) => {
20
21
  const {
@@ -151,6 +152,7 @@ const CurrencyInput = ({
151
152
  onSubmitEditing={onSubmitEditing}
152
153
  style={styles.input}
153
154
  maxLength={10}
155
+ accessibilityLabel={AccessibilityLabel.INPUT_CALCULATE_COST}
154
156
  />
155
157
  <Text
156
158
  semibold
@@ -56,7 +56,6 @@ const SubUnitFavorites = ({
56
56
  <ItemDevice
57
57
  key={`device-${sensor.id}`}
58
58
  id={sensor.id}
59
- svgMain={sensor.icon || 'sensor'}
60
59
  statusIcon={sensor.action && sensor.action.icon}
61
60
  statusColor={sensor.action && sensor.action.color}
62
61
  description={sensor.value}
@@ -154,7 +154,6 @@ const ShortDetailSubUnit = ({ unit, station, isOwner }) => {
154
154
  <ItemDevice
155
155
  key={`sensor-${device.id}`}
156
156
  id={device.id}
157
- svgMain={device.icon || 'sensor'}
158
157
  statusIcon={device.action && device.action.icon}
159
158
  statusColor={device.action && device.action.color}
160
159
  description={device.value}
@@ -0,0 +1,47 @@
1
+ import React, { memo, useCallback, useEffect, useRef, useState } from 'react';
2
+
3
+ import { styles } from './IFrameStyles';
4
+ import IconComponent from '../../IconComponent';
5
+ import WebView from 'react-native-webview';
6
+ import { TouchableOpacity, View } from 'react-native';
7
+ import { SCConfig } from '../../../configs';
8
+
9
+ const IFrame = memo(({ item = {} }) => {
10
+ const ref = useRef();
11
+ const [urlWithEnv, setUrlWithEnv] = useState();
12
+
13
+ const { configuration, id, title } = item;
14
+ const { url } = configuration || {};
15
+
16
+ const reload = useCallback(() => {
17
+ ref.current.reload();
18
+ }, []);
19
+
20
+ useEffect(() => {
21
+ if (!url) {
22
+ return;
23
+ }
24
+ if (!url.includes('?')) {
25
+ setUrlWithEnv(`${url}?eraOrigin=${SCConfig.apiRoot}&eraWidget=${id}`);
26
+ } else {
27
+ setUrlWithEnv(`${url}&eraOrigin=${SCConfig.apiRoot}&eraWidget=${id}`);
28
+ }
29
+ }, [id, url]);
30
+
31
+ return (
32
+ <View>
33
+ <TouchableOpacity style={styles.reloadButton} onClick={reload}>
34
+ <IconComponent size={20} icon="ReloadOutlined" />
35
+ </TouchableOpacity>
36
+ <WebView
37
+ source={{ uri: urlWithEnv }}
38
+ style={styles.iframe}
39
+ ref={ref}
40
+ title={title}
41
+ javaScriptEnabled
42
+ />
43
+ </View>
44
+ );
45
+ });
46
+
47
+ export default IFrame;
@@ -0,0 +1,33 @@
1
+ import { StyleSheet } from 'react-native';
2
+ import { Colors } from '../../../configs';
3
+
4
+ export const styles = StyleSheet.create({
5
+ iframe: {
6
+ display: 'flex',
7
+ width: '100%',
8
+ height: '100%',
9
+ paddingHorizontal: 16,
10
+ },
11
+ reloadButton: {
12
+ position: 'absolute',
13
+ right: 16,
14
+ top: 5,
15
+ },
16
+ reloadButtonEditing: {
17
+ position: 'absolute',
18
+ right: 77,
19
+ top: 0,
20
+ width: 36,
21
+ marginLeft: 2,
22
+ paddingTop: 5,
23
+ paddingBottom: 5,
24
+ justifyContent: 'center',
25
+ alignItems: 'center',
26
+ borderRadius: 4,
27
+ backgroundColor: Colors.White,
28
+ border: '1px solid #E6E6E6',
29
+ },
30
+ settingWidget: {
31
+ width: 256,
32
+ },
33
+ });
@@ -0,0 +1,75 @@
1
+ import React, { useRef } from 'react';
2
+ import { act } from 'react-test-renderer';
3
+
4
+ import IFrame from '../IFrame';
5
+ import WebView from 'react-native-webview';
6
+ import { TouchableOpacity } from 'react-native';
7
+ import { render } from '../../../../../jest/render';
8
+ import { SCConfig } from '../../../../configs';
9
+
10
+ describe('Test IFrame', () => {
11
+ let item;
12
+ beforeEach(() => {
13
+ item = {
14
+ id: 1,
15
+ configuration: {
16
+ url: 'http://localhost:3000/',
17
+ },
18
+ };
19
+ });
20
+
21
+ test('test with widget box', async () => {
22
+ const { root } = await render(<IFrame isWidgetBox item={item} />);
23
+
24
+ const iframes = root.findAllByType(WebView);
25
+ expect(iframes).toHaveLength(1);
26
+
27
+ expect(iframes[0].props.source?.uri).toEqual(
28
+ `http://localhost:3000/?eraOrigin=${SCConfig.apiRoot}&eraWidget=1`
29
+ );
30
+ });
31
+
32
+ test('test with query string', async () => {
33
+ item.configuration.url = 'http://localhost:3000/?test=1';
34
+ const { root } = await render(<IFrame isWidgetBox item={item} />);
35
+
36
+ const iframes = root.findAllByType(WebView);
37
+ expect(iframes).toHaveLength(1);
38
+
39
+ expect(iframes[0].props.source?.uri).toEqual(
40
+ `http://localhost:3000/?test=1&eraOrigin=${SCConfig.apiRoot}&eraWidget=1`
41
+ );
42
+ });
43
+
44
+ test('test no url', async () => {
45
+ item.configuration.url = null;
46
+ const { root } = await render(<IFrame isWidgetBox item={item} />);
47
+
48
+ const iframes = root.findAllByType(WebView);
49
+ expect(iframes).toHaveLength(1);
50
+
51
+ expect(iframes[0].props.source?.uri).toEqual(undefined);
52
+ });
53
+
54
+ test('test reload', async () => {
55
+ const ref = {
56
+ current: {},
57
+ };
58
+ useRef.mockReturnValue(ref);
59
+
60
+ const mockReload = jest.fn();
61
+ const { root } = await render(<IFrame isWidgetBox item={item} />);
62
+ ref.current = {
63
+ reload: mockReload,
64
+ };
65
+
66
+ const buttons = root.findAllByType(TouchableOpacity);
67
+ expect(buttons).toHaveLength(1);
68
+
69
+ await act(async () => {
70
+ buttons[0].props.onClick();
71
+ });
72
+
73
+ expect(mockReload).toHaveBeenCalledTimes(1);
74
+ });
75
+ });
File without changes
@@ -0,0 +1,163 @@
1
+ import moment from 'moment';
2
+ import React, { memo, useCallback, useEffect, useRef, useState } from 'react';
3
+
4
+ import { styles as useIframeStyle } from '../IFrame/IFrameStyles';
5
+ import { styles } from './IFrameWithConfigStyles';
6
+ import IconComponent from '../../IconComponent';
7
+ import { useConfigGlobalState } from '../../../iot/states';
8
+ import { useFetchConfigHistory } from '../../UnitSummary/ConfigHistoryChart';
9
+ import API from '../../../configs/API';
10
+ import WebView from 'react-native-webview';
11
+ import { TouchableOpacity, View } from 'react-native';
12
+
13
+ const IFrameWithConfig = memo(
14
+ ({ item = {}, widgetStyle, isWidgetBox, isSetting, isEditing }) => {
15
+ const ref = useRef();
16
+
17
+ const { configuration } = item;
18
+ const { url } = configuration || {};
19
+
20
+ const [height, setHeight] = useState(200);
21
+ const [urlWithEnv, setUrlWithEnv] = useState();
22
+ const [isReady, setIsReady] = useState(false);
23
+
24
+ const [configValues] = useConfigGlobalState('configValues');
25
+ const [chartData, setChartData] = useState(configuration?.history_configs);
26
+ const fetchDataDisplayHistory = useFetchConfigHistory(
27
+ configuration?.history_configs,
28
+ setChartData,
29
+ API.CONFIG.DISPLAY_HISTORY_V4()
30
+ );
31
+
32
+ const postMessage = useCallback((message) => {
33
+ ref.current?.injectJavaScript(
34
+ 'window.postMessage(' + JSON.stringify(message) + ', "*")'
35
+ );
36
+ }, []);
37
+
38
+ useEffect(() => {
39
+ if (!isReady || !ref.current) {
40
+ return;
41
+ }
42
+ if (!configuration?.history_configs?.length) {
43
+ return;
44
+ }
45
+ postMessage({
46
+ source: 'eraIframeWidget',
47
+ type: 'histories',
48
+ data: chartData,
49
+ eraWidgetId: item.id,
50
+ });
51
+ }, [
52
+ chartData,
53
+ configuration?.history_configs?.length,
54
+ configuration.realtime_configs,
55
+ isReady,
56
+ item.id,
57
+ postMessage,
58
+ ]);
59
+
60
+ useEffect(() => {
61
+ const appendix = `eraOrigin=http://localhost:3000&eraWidget=${item?.id}`;
62
+ if (!url.includes('?')) {
63
+ setUrlWithEnv(`${url}?${appendix}`);
64
+ } else {
65
+ setUrlWithEnv(`${url}&${appendix}`);
66
+ }
67
+ }, [item?.id, url]);
68
+
69
+ const onMessage = useCallback(
70
+ async (event) => {
71
+ const eventData = event.nativeEvent.data
72
+ ? JSON.parse(event.nativeEvent.data)
73
+ : {};
74
+ if (
75
+ eventData?.source !== 'eraIframeWidget' ||
76
+ eventData?.eraWidgetId !== item.id
77
+ ) {
78
+ return;
79
+ }
80
+ const { type, data } = eventData;
81
+
82
+ switch (type) {
83
+ case 'ready':
84
+ postMessage({
85
+ source: 'eraIframeWidget',
86
+ type: 'configuration',
87
+ data: configuration,
88
+ eraWidgetId: item.id,
89
+ });
90
+ setIsReady(true);
91
+ break;
92
+ case 'requestHistories':
93
+ await fetchDataDisplayHistory(moment(data[0]), moment(data[1]));
94
+ break;
95
+ case 'requestAdjustMobileHeight':
96
+ setHeight(data);
97
+ break;
98
+ }
99
+ },
100
+ [configuration, fetchDataDisplayHistory, item.id, postMessage]
101
+ );
102
+
103
+ useEffect(() => {
104
+ if (!isReady || !ref?.current) {
105
+ return;
106
+ }
107
+ if (!configuration?.realtime_configs?.length) {
108
+ return;
109
+ }
110
+ const values = {};
111
+ configuration?.realtime_configs.map((config) => {
112
+ return (values[config.id] = configValues[config.id]);
113
+ });
114
+ postMessage({
115
+ source: 'eraIframeWidget',
116
+ type: 'values',
117
+ data: values,
118
+ eraWidgetId: item.id,
119
+ });
120
+ }, [
121
+ isReady,
122
+ configValues,
123
+ configuration?.realtime_configs,
124
+ item.id,
125
+ postMessage,
126
+ ]);
127
+
128
+ const reload = useCallback(() => {
129
+ setIsReady(false);
130
+ ref.current.reload();
131
+ }, []);
132
+
133
+ return (
134
+ <View
135
+ style={
136
+ isSetting
137
+ ? styles.settingWidget
138
+ : { ...styles.wrapper, height: height }
139
+ }
140
+ >
141
+ <TouchableOpacity
142
+ className={
143
+ isEditing
144
+ ? useIframeStyle.reloadButtonEditing
145
+ : useIframeStyle.reloadButton
146
+ }
147
+ onClick={reload}
148
+ >
149
+ <IconComponent name="ReloadOutlined" />
150
+ </TouchableOpacity>
151
+ <WebView
152
+ source={{ uri: urlWithEnv }}
153
+ className={useIframeStyle.iframe}
154
+ ref={ref}
155
+ title={item?.title}
156
+ onMessage={onMessage}
157
+ />
158
+ </View>
159
+ );
160
+ }
161
+ );
162
+
163
+ export default IFrameWithConfig;
@@ -0,0 +1,9 @@
1
+ export const styles = {
2
+ settingWidget: {
3
+ width: 256,
4
+ },
5
+ wrapper: {
6
+ width: '100%',
7
+ display: 'flex',
8
+ },
9
+ };
@@ -0,0 +1,284 @@
1
+ import React from 'react';
2
+ import { act } from 'react-test-renderer';
3
+
4
+ import IFrameWithConfig from '../IFrameWithConfig';
5
+ import { useConfigGlobalState } from '../../../../iot/states';
6
+ import { render } from '../../../../../jest/render';
7
+ import API from '../../../../configs/API';
8
+ import MockAdapter from 'axios-mock-adapter';
9
+ import api from '../../../../utils/Apis/axios';
10
+ import WebView from 'react-native-webview';
11
+ import { TouchableOpacity, View } from 'react-native';
12
+ import { refFunctions } from '../../../../../__mocks__/react-native-webview';
13
+
14
+ jest.mock('../../../../iot/states', () => {
15
+ return {
16
+ ...jest.requireActual('../../../../iot/states'),
17
+ useConfigGlobalState: jest.fn(),
18
+ };
19
+ });
20
+
21
+ describe('Test IFrame With Config', () => {
22
+ let item;
23
+ let configValues = {
24
+ 1: {
25
+ value: 0.5,
26
+ },
27
+ };
28
+
29
+ beforeEach(() => {
30
+ item = {
31
+ id: 1,
32
+ configuration: {
33
+ url: 'http://localhost:3000/',
34
+ realtime_configs: [{ id: 1 }],
35
+ },
36
+ };
37
+ useConfigGlobalState.mockImplementation(() => [configValues]);
38
+ refFunctions.injectJavaScript.mockClear();
39
+ });
40
+
41
+ test('test with widget box', async () => {
42
+ const { root } = await render(<IFrameWithConfig isWidgetBox item={item} />);
43
+
44
+ const iframes = root.findAllByType(WebView);
45
+ expect(iframes).toHaveLength(1);
46
+
47
+ expect(iframes[0].props.source?.uri).toEqual(
48
+ 'http://localhost:3000/?eraOrigin=http://localhost:3000&eraWidget=1'
49
+ );
50
+ });
51
+
52
+ test('test with query string', async () => {
53
+ item.configuration.url = 'http://localhost:3000/?test=1';
54
+ const { root } = await render(<IFrameWithConfig isWidgetBox item={item} />);
55
+
56
+ const iframes = root.findAllByType(WebView);
57
+ expect(iframes).toHaveLength(1);
58
+
59
+ expect(iframes[0].props.source?.uri).toEqual(
60
+ 'http://localhost:3000/?test=1&eraOrigin=http://localhost:3000&eraWidget=1'
61
+ );
62
+ });
63
+
64
+ test('test reload', async () => {
65
+ const { root } = await render(<IFrameWithConfig isWidgetBox item={item} />);
66
+
67
+ const buttons = root.findAllByType(TouchableOpacity);
68
+ expect(buttons).toHaveLength(1);
69
+
70
+ await act(async () => {
71
+ buttons[0].props.onClick();
72
+ });
73
+
74
+ expect(refFunctions.reload).toHaveBeenCalledTimes(1);
75
+ });
76
+
77
+ test('test iframe is ready', async () => {
78
+ const { root } = await render(<IFrameWithConfig isWidgetBox item={item} />);
79
+ const webview = root.findByType(WebView);
80
+
81
+ await act(async () => {
82
+ await webview.props.onMessage({
83
+ nativeEvent: {
84
+ data: JSON.stringify({
85
+ type: 'ready',
86
+ eraWidgetId: 1,
87
+ source: 'eraIframeWidget',
88
+ }),
89
+ origin: 'http://localhost',
90
+ },
91
+ });
92
+ });
93
+ expectSendMessage(
94
+ {
95
+ source: 'eraIframeWidget',
96
+ type: 'configuration',
97
+ data: item.configuration,
98
+ eraWidgetId: 1,
99
+ },
100
+ 'http://localhost:3000'
101
+ );
102
+ expectSendMessage(
103
+ {
104
+ source: 'eraIframeWidget',
105
+ type: 'values',
106
+ data: configValues,
107
+ eraWidgetId: 1,
108
+ },
109
+ 'http://localhost:3000'
110
+ );
111
+ unexpectSendMessage(
112
+ {
113
+ source: 'eraIframeWidget',
114
+ type: 'histories',
115
+ data: [{ id: 1 }],
116
+ eraWidgetId: 1,
117
+ },
118
+ 'http://localhost:3000'
119
+ );
120
+ });
121
+
122
+ test('test iframe request more height', async () => {
123
+ const { root } = await render(<IFrameWithConfig isWidgetBox item={item} />);
124
+ const webview = root.findByType(WebView);
125
+
126
+ await act(async () => {
127
+ await webview.props.onMessage({
128
+ nativeEvent: {
129
+ data: JSON.stringify({
130
+ type: 'requestAdjustMobileHeight',
131
+ data: 100,
132
+ eraWidgetId: 1,
133
+ source: 'eraIframeWidget',
134
+ }),
135
+ origin: 'http://localhost',
136
+ },
137
+ });
138
+ });
139
+
140
+ const wrapper = root.findAllByType(View)[0];
141
+ expect(wrapper.props.style.height).toEqual(100);
142
+ });
143
+
144
+ test('test iframe receive message from unexpected origin', async () => {
145
+ const { root } = await render(<IFrameWithConfig isWidgetBox item={item} />);
146
+ const webview = root.findByType(WebView);
147
+
148
+ await act(async () => {
149
+ webview.props.onMessage({
150
+ nativeEvent: {
151
+ data: JSON.stringify({
152
+ type: 'ready',
153
+ eraWidgetId: 2,
154
+ source: 'eraIframeWidget',
155
+ }),
156
+ origin: 'http://localhost',
157
+ },
158
+ });
159
+ });
160
+ expect(refFunctions.injectJavaScript).toHaveBeenCalledTimes(0);
161
+
162
+ await act(async () => {
163
+ webview.props.onMessage({
164
+ nativeEvent: {
165
+ data: JSON.stringify({
166
+ type: 'ready',
167
+ eraWidgetId: 1,
168
+ source: 'eraIframeWidget-wrong',
169
+ }),
170
+ origin: 'http://localhost',
171
+ },
172
+ });
173
+ });
174
+ expect(refFunctions.injectJavaScript).toHaveBeenCalledTimes(0);
175
+ });
176
+
177
+ test('test iframe init chart data', async () => {
178
+ item.configuration.history_configs = [{ id: 1 }];
179
+ item.configuration.realtime_configs = [];
180
+
181
+ const { root } = await render(<IFrameWithConfig isWidgetBox item={item} />);
182
+ const webview = root.findByType(WebView);
183
+
184
+ await act(async () => {
185
+ webview.props.onMessage({
186
+ nativeEvent: {
187
+ data: JSON.stringify({
188
+ type: 'ready',
189
+ eraWidgetId: 1,
190
+ source: 'eraIframeWidget',
191
+ }),
192
+ origin: 'http://localhost',
193
+ },
194
+ });
195
+ });
196
+ unexpectSendMessage(
197
+ {
198
+ source: 'eraIframeWidget',
199
+ type: 'configValues',
200
+ data: configValues,
201
+ eraWidgetId: 1,
202
+ },
203
+ 'http://localhost:3000'
204
+ );
205
+ expectSendMessage(
206
+ {
207
+ source: 'eraIframeWidget',
208
+ type: 'histories',
209
+ data: [{ id: 1 }],
210
+ eraWidgetId: 1,
211
+ },
212
+ 'http://localhost:3000'
213
+ );
214
+ });
215
+
216
+ test('test iframe request chart data', async () => {
217
+ item.configuration.history_configs = [{ id: 1 }];
218
+ item.configuration.realtime_configs = [];
219
+
220
+ const { root } = await render(<IFrameWithConfig isWidgetBox item={item} />);
221
+ const webview = root.findByType(WebView);
222
+
223
+ await act(async () => {
224
+ webview.props.onMessage({
225
+ nativeEvent: {
226
+ data: JSON.stringify({
227
+ type: 'ready',
228
+ eraWidgetId: 1,
229
+ source: 'eraIframeWidget',
230
+ }),
231
+ origin: 'http://localhost',
232
+ },
233
+ });
234
+ });
235
+
236
+ const mockAxios = new MockAdapter(api.axiosInstance);
237
+ mockAxios.onGet(API.CONFIG.DISPLAY_HISTORY_V4()).reply(200, {
238
+ configs: [
239
+ {
240
+ id: 1,
241
+ head: [],
242
+ tail: [{ x: 1000, y: 100 }],
243
+ middle: { ready: [], not_ready: [] },
244
+ },
245
+ ],
246
+ });
247
+ refFunctions.injectJavaScript.mockClear();
248
+
249
+ await act(async () => {
250
+ await webview.props.onMessage({
251
+ nativeEvent: {
252
+ data: JSON.stringify({
253
+ type: 'requestHistories',
254
+ eraWidgetId: 1,
255
+ source: 'eraIframeWidget',
256
+ data: [1, 2],
257
+ }),
258
+ origin: 'http://localhost',
259
+ },
260
+ });
261
+ });
262
+
263
+ expectSendMessage(
264
+ {
265
+ source: 'eraIframeWidget',
266
+ type: 'histories',
267
+ data: [{ id: 1, data: [{ x: 1000, y: 100 }] }],
268
+ eraWidgetId: 1,
269
+ },
270
+ 'http://localhost:3000'
271
+ );
272
+ });
273
+
274
+ const expectSendMessage = (message) => {
275
+ expect(refFunctions.injectJavaScript).toHaveBeenCalledWith(
276
+ 'window.postMessage(' + JSON.stringify(message) + ', "*")'
277
+ );
278
+ };
279
+ const unexpectSendMessage = (message) => {
280
+ expect(refFunctions.injectJavaScript).not.toHaveBeenCalledWith(
281
+ 'window.postMessage(' + JSON.stringify(message) + ', "*")'
282
+ );
283
+ };
284
+ });
File without changes
File without changes
File without changes
@@ -413,6 +413,9 @@ export default {
413
413
  COMPASS_VALUE: 'COMPASS_VALUE',
414
414
  GROUP_CHECKBOX_ITEM: 'GROUP_CHECKBOX_ITEM',
415
415
  TOTAL_POWER_BOX_TITLE: 'TOTAL_POWER_BOX_TITLE',
416
+ INPUT_CALCULATE_COST: 'INPUT_CALCULATE_COST',
417
+ BUTTON_CALCULATE_COST: 'BUTTON_CALCULATE_COST',
418
+ TOTAL_PRICE: 'TOTAL_PRICE',
416
419
  EMERGENCY_POPUP: 'EMERGENCY_POPUP',
417
420
  RESOLVED_EMERGENCY_POPUP: 'RESOLVED_EMERGENCY_POPUP',
418
421
  TOTAL_POWER_CONSUMPTION: 'TOTAL_POWER_CONSUMPTION',
@@ -281,7 +281,12 @@ export const BUTTON_TYPE = {
281
281
  export const CHART_TIME = {
282
282
  hour: 1,
283
283
  day: 24,
284
- week: 168,
285
- month: 672,
286
- year: 12 * 672,
284
+ week: 24 * 7,
285
+ month: 24 * 30,
286
+ year: 24 * 365,
287
+ };
288
+
289
+ export const WIDGET_TYPE = {
290
+ iframe: 'IFrame',
291
+ iframeWithConfig: 'IFrameWithConfig',
287
292
  };
@@ -33,7 +33,6 @@ const useDevicesStatus = (unit, devices) => {
33
33
  );
34
34
  success && setAction(Action.SET_DEVICES_STATUS, data);
35
35
  timeoutId = setTimeout(() => getDevicesStatus(_unit, _devices), 10000);
36
- // eslint-disable-next-line react-hooks/exhaustive-deps
37
36
  },
38
37
  [setAction]
39
38
  );
@@ -19,11 +19,13 @@ import { ToastBottomHelper } from '../../../utils/Utils';
19
19
 
20
20
  const valueEvaluationToOptions = (valueEvaluation) => {
21
21
  if (valueEvaluation.template === 'range') {
22
- return valueEvaluation.configuration.ranges.map((range, index) => ({
23
- title: range.evaluate?.text,
24
- condition: 'value_evaluation',
25
- value: [valueEvaluation.id, index],
26
- }));
22
+ return (
23
+ valueEvaluation.configuration?.ranges?.map((range, index) => ({
24
+ title: range.evaluate?.text,
25
+ condition: 'value_evaluation',
26
+ value: [valueEvaluation.id, index],
27
+ })) || []
28
+ );
27
29
  }
28
30
  if (valueEvaluation.template === 'boolean') {
29
31
  return [
@@ -47,7 +47,7 @@ const generateAutomationConditionValueEvaluation = (
47
47
  if (valueEvaluation) {
48
48
  if (valueEvaluation.template === 'range') {
49
49
  return `${config_name} ${t('is')} ${
50
- valueEvaluation.configuration.ranges[index]?.evaluate.text
50
+ valueEvaluation.configuration.ranges[index]?.evaluate?.text
51
51
  }`;
52
52
  }
53
53
  if (valueEvaluation.template === 'boolean') {
@@ -13,7 +13,7 @@ import ListQualityIndicator from '../../../commons/Device/WaterQualitySensor/Lis
13
13
  import Compass from '../../../commons/Device/WindDirection/Compass';
14
14
  import Anemometer from '../../../commons/Device/WindSpeed/Anemometer';
15
15
  import MediaPlayerDetail from '../../../commons/MediaPlayerDetail';
16
- import { AccessibilityLabel } from '../../../configs/Constants';
16
+ import { AccessibilityLabel, WIDGET_TYPE } from '../../../configs/Constants';
17
17
  import { useSCContextSelector } from '../../../context';
18
18
  import { useRemoteControl } from '../../../hooks/IoT';
19
19
  import { useConfigGlobalState } from '../../../iot/states';
@@ -24,6 +24,8 @@ import VisualChart from './VisualChart';
24
24
  import { standardizeCameraScreenSize } from '../../../utils/Utils';
25
25
  import { Device } from '../../../configs';
26
26
  import DonutCharts from './DonutChart';
27
+ import IFrame from '../../../commons/Widgets/IFrame/IFrame';
28
+ import IFrameWithConfig from '../../../commons/Widgets/IFrameWithConfig/IFrameWithConfig';
27
29
 
28
30
  const { standardizeWidth, standardizeHeight } = standardizeCameraScreenSize(
29
31
  Device.screenWidth - 32
@@ -175,6 +177,26 @@ export const SensorDisplayItem = ({
175
177
  isWidgetOrder={isWidgetOrder}
176
178
  />
177
179
  );
180
+ case WIDGET_TYPE.iframe:
181
+ return (
182
+ <IFrame
183
+ doAction={doAction}
184
+ sensor={sensor}
185
+ id={idTemplate}
186
+ item={item}
187
+ isWidgetOrder={isWidgetOrder}
188
+ />
189
+ );
190
+ case WIDGET_TYPE.iframeWithConfig:
191
+ return (
192
+ <IFrameWithConfig
193
+ doAction={doAction}
194
+ sensor={sensor}
195
+ id={idTemplate}
196
+ item={item}
197
+ isWidgetOrder={isWidgetOrder}
198
+ />
199
+ );
178
200
  case 'history':
179
201
  return (
180
202
  <VisualChart
@@ -1,7 +1,7 @@
1
1
  import React from 'react';
2
2
  import MockAdapter from 'axios-mock-adapter';
3
3
  import moment from 'moment';
4
- import { act, create } from 'react-test-renderer';
4
+ import { act } from 'react-test-renderer';
5
5
 
6
6
  import VisualChart from '../VisualChart';
7
7
  import { API } from '../../../../configs';
@@ -11,14 +11,7 @@ import api from '../../../../utils/Apis/axios';
11
11
  import ChartAggregationOption from '../../../../commons/ChartAggregationOption';
12
12
  import DateTimeRangeChange from '../../../../commons/DateTimeRangeChange';
13
13
  import Highcharts from '../../../../commons/Highcharts';
14
-
15
- const render = async (component) => {
16
- let tree;
17
- await act(async () => {
18
- tree = await create(component);
19
- });
20
- return tree;
21
- };
14
+ import { render } from '../../../../../jest/render';
22
15
 
23
16
  const mockAxios = new MockAdapter(api.axiosInstance);
24
17
 
@@ -116,7 +116,6 @@ const SubUnitDetail = ({ route }) => {
116
116
  <ItemDevice
117
117
  key={`sensor-${item.id}`}
118
118
  id={item.id}
119
- svgMain={item.icon || 'door'}
120
119
  statusIcon={item.action && item.action.icon}
121
120
  statusColor={item.action && item.action.color}
122
121
  title={item.name}
@@ -146,6 +146,25 @@ describe('Test PowerConsumption', () => {
146
146
  const instance = tree.root;
147
147
  const Todays = instance.findByType(Today);
148
148
  expect(Todays).toBeDefined();
149
+
150
+ const input_calculate_cost = instance.find(
151
+ (el) =>
152
+ el.props.accessibilityLabel === AccessibilityLabel.INPUT_CALCULATE_COST
153
+ );
154
+ await act(async () => {
155
+ input_calculate_cost.props.onChangeText('1000');
156
+ });
157
+ const button_calculate_cost = instance.find(
158
+ (el) =>
159
+ el.props.accessibilityLabel === AccessibilityLabel.BUTTON_CALCULATE_COST
160
+ );
161
+ await act(async () => {
162
+ button_calculate_cost.props.onPress();
163
+ });
164
+ const totalPrice = instance.find(
165
+ (el) => el.props.accessibilityLabel === AccessibilityLabel.TOTAL_PRICE
166
+ );
167
+ expect(totalPrice.props.children).toEqual('57.380 đ');
149
168
  });
150
169
 
151
170
  it('render with unsuccess fetch', async () => {
@@ -15,7 +15,6 @@ const RunningDevices = memo(({ unit, summaryDetail }) => {
15
15
  <ItemDevice
16
16
  key={`sensor-${item.id}`}
17
17
  id={item.id}
18
- svgMain={item.icon || 'door'}
19
18
  statusIcon={item.action && item.action.icon}
20
19
  statusColor={item.action && item.action.color}
21
20
  description={item.value}