@eohjsc/react-native-smart-city 0.4.97 → 0.4.99

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 (25) hide show
  1. package/package.json +1 -1
  2. package/src/commons/ActionGroup/OnOffTemplate/index.js +21 -17
  3. package/src/commons/ActionGroup/TerminalBoxTemplate.js +182 -0
  4. package/src/commons/ActionGroup/TerminalBoxTemplateStyle.js +54 -0
  5. package/src/commons/ActionGroup/TextBoxTemplate.js +11 -5
  6. package/src/commons/ActionGroup/__test__/TerminalBoxTemplate.test.js +232 -0
  7. package/src/commons/ActionGroup/__test__/TextBoxTemplate.test.js +1 -1
  8. package/src/commons/ActionGroup/index.js +3 -1
  9. package/src/commons/Device/ConnectedViewHeader.js +1 -2
  10. package/src/commons/OneTapTemplate/SliderRangeActionTemplate.js +3 -3
  11. package/src/commons/OneTapTemplate/TerminalBoxActionTemplate.js +87 -0
  12. package/src/commons/OneTapTemplate/TextBoxActionTemplate.js +2 -2
  13. package/src/commons/OneTapTemplate/__test__/SliderRangeActionTemplate.test.js +1 -1
  14. package/src/commons/OneTapTemplate/__test__/TerminalBoxActionTemplate.test.js +118 -0
  15. package/src/commons/Sharing/WrapHeaderScrollable.js +1 -1
  16. package/src/commons/UnitSummary/ConfigHistoryChart/index.js +15 -16
  17. package/src/configs/API.js +1 -0
  18. package/src/configs/AccessibilityLabel.js +2 -0
  19. package/src/configs/Colors.js +1 -0
  20. package/src/screens/Automate/AddNewAction/ChooseAction.js +10 -0
  21. package/src/screens/Automate/ScriptDetail/index.js +2 -1
  22. package/src/screens/Device/components/SensorDisplayItem.js +1 -0
  23. package/src/screens/Device/detail.js +23 -18
  24. package/src/utils/I18n/translations/en.js +1 -1
  25. package/src/utils/I18n/translations/vi.js +1 -1
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.4.97",
4
+ "version": "0.4.99",
5
5
  "description": "TODO",
6
6
  "main": "index.js",
7
7
  "files": [
@@ -9,8 +9,6 @@ import OnOffButtonTemplate from './OnOffButtonTemplate';
9
9
  import OnOffSimpleTemplate from './OnOffSimpleTemplate';
10
10
  import styles from './styles';
11
11
 
12
- let temp;
13
-
14
12
  const getComponent = (template) => {
15
13
  switch (template) {
16
14
  case 'on_off_button_action_template': // todo refactor later with backend
@@ -53,15 +51,18 @@ const OnOffTemplate = memo(({ item = {}, doAction, sensor = {} }) => {
53
51
 
54
52
  const [isOn, setIsOn] = useState(false);
55
53
  const [tempIsOn, setTempIsOn] = useState(getIsOnValue());
56
-
57
- const updateStatusFromPusher = useCallback(() => {
58
- setTimeout(() => {
59
- setTempIsOn(temp);
60
- }, 3000);
54
+ const [timeoutId, setTimeoutId] = useState();
55
+ const [needUpdateTempIsOn, setNeedUpdateTempIsOn] = useState(false);
56
+
57
+ const refreshTempIsOn = useCallback(() => {
58
+ const timeout = setTimeout(() => {
59
+ setNeedUpdateTempIsOn(true);
60
+ }, 7000);
61
+ setTimeoutId(timeout);
61
62
  }, []);
62
63
 
63
64
  const triggerAction = useCallback(async () => {
64
- const action_data = isOn ? action_off_data : action_on_data;
65
+ const action_data = tempIsOn ? action_off_data : action_on_data;
65
66
  if (!action_data) {
66
67
  return;
67
68
  }
@@ -76,10 +77,11 @@ const OnOffTemplate = memo(({ item = {}, doAction, sensor = {} }) => {
76
77
  config_value: isOn ? 0 : 1,
77
78
  };
78
79
  }
79
- setTempIsOn((prev) => !prev);
80
+ clearTimeout(timeoutId);
80
81
  await doAction(action_data, data);
81
- updateStatusFromPusher(); // todo Bang read about this magic
82
-
82
+ setTempIsOn((prev) => !prev);
83
+ setNeedUpdateTempIsOn(false);
84
+ refreshTempIsOn();
83
85
  if (
84
86
  is_managed_by_backend &&
85
87
  config &&
@@ -88,15 +90,17 @@ const OnOffTemplate = memo(({ item = {}, doAction, sensor = {} }) => {
88
90
  watchMultiConfigs([config]);
89
91
  }
90
92
  }, [
91
- isOn,
93
+ tempIsOn,
92
94
  action_off_data,
93
95
  action_on_data,
94
- device_type,
95
- is_managed_by_backend,
96
96
  allow_config_store_value,
97
97
  config,
98
+ device_type,
99
+ timeoutId,
98
100
  doAction,
99
- updateStatusFromPusher,
101
+ refreshTempIsOn,
102
+ is_managed_by_backend,
103
+ isOn,
100
104
  ]);
101
105
 
102
106
  useUnwatchLGDeviceConfigControl(sensor, [config]);
@@ -106,8 +110,8 @@ const OnOffTemplate = memo(({ item = {}, doAction, sensor = {} }) => {
106
110
  }, [getIsOnValue]);
107
111
 
108
112
  useEffect(() => {
109
- temp = isOn;
110
- }, [isOn]);
113
+ needUpdateTempIsOn && setTempIsOn(getIsOnValue());
114
+ }, [getIsOnValue, needUpdateTempIsOn]);
111
115
 
112
116
  useEffect(() => {
113
117
  if (device_type !== DEVICE_TYPE.LG_THINQ) {
@@ -0,0 +1,182 @@
1
+ import React, {
2
+ useCallback,
3
+ useEffect,
4
+ useMemo,
5
+ useRef,
6
+ useState,
7
+ } from 'react';
8
+ import { View, TouchableOpacity } from 'react-native';
9
+ import { IconOutline } from '@ant-design/icons-react-native';
10
+ import Text from '../../commons/Text';
11
+ import { useConfigGlobalState } from '../../iot/states';
12
+ import styles from './TerminalBoxTemplateStyle';
13
+ import _TextInput from '../Form/TextInput';
14
+ import AccessibilityLabel from '../../configs/AccessibilityLabel';
15
+ import moment from 'moment';
16
+ import { useFetchConfigHistory } from '../UnitSummary/ConfigHistoryChart';
17
+ import { ScrollView } from 'react-native';
18
+ import { API } from '../../configs';
19
+
20
+ const useNewMessage = (
21
+ newValue,
22
+ latestUpdatedFromMessage,
23
+ setAllMessages,
24
+ setLatestUpdatedFromMessage,
25
+ type
26
+ ) => {
27
+ useEffect(() => {
28
+ if (
29
+ !newValue ||
30
+ !newValue.last_updated ||
31
+ moment(newValue.last_updated).isSameOrBefore(latestUpdatedFromMessage)
32
+ ) {
33
+ return;
34
+ }
35
+ setAllMessages((prev) => [
36
+ ...prev,
37
+ { x: moment(), y: newValue.value, type },
38
+ ]);
39
+ setLatestUpdatedFromMessage(moment(newValue.last_updated));
40
+ }, [
41
+ newValue,
42
+ latestUpdatedFromMessage,
43
+ setAllMessages,
44
+ setLatestUpdatedFromMessage,
45
+ type,
46
+ ]);
47
+ };
48
+
49
+ const TerminalBoxTemplate = ({ item, doAction, isWidgetOrder }) => {
50
+ const { label, configuration } = item;
51
+ const { action_data, from_config, to_config } = configuration;
52
+ const [configValues] = useConfigGlobalState('configValues');
53
+ const [value, setValue] = useState();
54
+ const [allMessages, setAllMessages] = useState([]);
55
+ const [latestUpdatedFromMessage, setLatestUpdatedFromMessage] = useState(
56
+ moment()
57
+ );
58
+ const [latestUpdatedToMessage, setLatestUpdatedToMessage] = useState(
59
+ moment()
60
+ );
61
+ const scrollViewRef = useRef();
62
+ const sendCommand = useCallback(() => {
63
+ doAction(action_data, JSON.stringify({ value: value }));
64
+ setValue('');
65
+ }, [doAction, action_data, value]);
66
+
67
+ const onInputChange = (e) => {
68
+ setValue(e);
69
+ };
70
+
71
+ const fromValue = useMemo(() => {
72
+ return configValues[from_config?.id];
73
+ }, [configValues, from_config]);
74
+
75
+ const toValue = useMemo(() => {
76
+ return configValues[to_config?.id];
77
+ }, [configValues, to_config]);
78
+
79
+ const configs = useMemo(() => {
80
+ if (!from_config || !to_config) {
81
+ return [];
82
+ }
83
+ return [from_config, to_config];
84
+ }, [from_config, to_config]);
85
+
86
+ const scrollToBottom = () => {
87
+ setTimeout(() => {
88
+ scrollViewRef.current.scrollToEnd({ animated: true });
89
+ }, 100);
90
+ };
91
+
92
+ const setMessages = useCallback(
93
+ (configMessages) => {
94
+ let messages = [];
95
+ configMessages.map((configMessage) => {
96
+ const type = configMessage.id === from_config.id ? 'from' : 'to';
97
+ messages = [
98
+ ...messages,
99
+ ...configMessage.data.map((item) => {
100
+ return { x: item.x, y: item.y, type };
101
+ }),
102
+ ];
103
+ });
104
+ messages = messages.sort((a, b) => {
105
+ return moment(a.x).diff(moment(b.x));
106
+ });
107
+ setAllMessages(messages);
108
+ scrollToBottom();
109
+ },
110
+ [from_config]
111
+ );
112
+
113
+ const fetchDataDisplayHistory = useFetchConfigHistory(
114
+ configs,
115
+ setMessages,
116
+ API.CONFIG.DISPLAY_HISTORY_V4()
117
+ );
118
+
119
+ useEffect(() => {
120
+ fetchDataDisplayHistory(moment().subtract(7, 'days'), moment());
121
+ }, [fetchDataDisplayHistory]);
122
+
123
+ useNewMessage(
124
+ fromValue,
125
+ latestUpdatedFromMessage,
126
+ setAllMessages,
127
+ setLatestUpdatedFromMessage,
128
+ 'from'
129
+ );
130
+
131
+ useNewMessage(
132
+ toValue,
133
+ latestUpdatedToMessage,
134
+ setAllMessages,
135
+ setLatestUpdatedToMessage,
136
+ 'to'
137
+ );
138
+ return (
139
+ <View style={(isWidgetOrder && styles.wrapOrderItem) || styles.wrap}>
140
+ <View>
141
+ <Text type="H4">{label}</Text>
142
+ </View>
143
+ <ScrollView
144
+ ref={scrollViewRef}
145
+ onContentSizeChange={() =>
146
+ scrollViewRef.current.scrollToEnd({ animated: true })
147
+ }
148
+ style={styles.scrollView}
149
+ >
150
+ {allMessages.map((item, index) => {
151
+ return (
152
+ <View
153
+ key={index}
154
+ style={item.type === 'to' ? styles.to : styles.from}
155
+ >
156
+ <Text>{item.y}</Text>
157
+ <Text>{moment(item.x).format('DD/MM/YYYY HH:mm:ss')}</Text>
158
+ </View>
159
+ );
160
+ })}
161
+ </ScrollView>
162
+ <View style={styles.iconAndText}>
163
+ <_TextInput
164
+ wrapStyle={styles.wrapInputStyle}
165
+ value={value}
166
+ onChange={onInputChange}
167
+ maxLength={255}
168
+ />
169
+ <TouchableOpacity
170
+ style={styles.iconAndTextOption}
171
+ onPress={sendCommand}
172
+ accessibilityLabel={AccessibilityLabel.TERMINAL_BOX_BUTTON_SEND}
173
+ disabled={!value}
174
+ >
175
+ <IconOutline name="send" size={20} />
176
+ </TouchableOpacity>
177
+ </View>
178
+ </View>
179
+ );
180
+ };
181
+
182
+ export default TerminalBoxTemplate;
@@ -0,0 +1,54 @@
1
+ import { StyleSheet } from 'react-native';
2
+ import { Colors } from '../../configs';
3
+
4
+ export default StyleSheet.create({
5
+ wrapOrderItem: {
6
+ flex: 1,
7
+ flexDirection: 'row',
8
+ justifyContent: 'space-between',
9
+ padding: 16,
10
+ },
11
+ wrap: {
12
+ padding: 16,
13
+ marginHorizontal: 16,
14
+ marginBottom: 16,
15
+ borderWidth: 1,
16
+ borderColor: Colors.Gray4,
17
+ borderRadius: 10,
18
+ flex: 1,
19
+ flexDirection: 'column',
20
+ },
21
+ iconAndText: {
22
+ flexDirection: 'row',
23
+ alignItems: 'center',
24
+ width: '100%',
25
+ },
26
+ iconAndTextOption: {
27
+ marginTop: 25,
28
+ },
29
+ wrapInputStyle: {
30
+ marginRight: 14,
31
+ width: '90%',
32
+ },
33
+ scrollView: {
34
+ marginTop: 13,
35
+ height: 250,
36
+ },
37
+ to: {
38
+ padding: 5,
39
+ borderWidth: 1,
40
+ borderColor: Colors.Gray4,
41
+ borderRadius: 10,
42
+ marginBottom: 5,
43
+ },
44
+ from: {
45
+ alignItems: 'flex-end',
46
+ marginRight: 0,
47
+ backgroundColor: Colors.Blue18,
48
+ padding: 5,
49
+ borderWidth: 1,
50
+ borderColor: Colors.Gray4,
51
+ borderRadius: 10,
52
+ marginBottom: 5,
53
+ },
54
+ });
@@ -1,4 +1,4 @@
1
- import React, { useCallback, useState } from 'react';
1
+ import React, { useCallback, useMemo, useState } from 'react';
2
2
  import { View, TouchableOpacity } from 'react-native';
3
3
  import { IconOutline } from '@ant-design/icons-react-native';
4
4
  import Text from '../../commons/Text';
@@ -10,14 +10,15 @@ import styles from './TextBoxTemplateStyle';
10
10
  import { useTranslations } from '../../hooks/Common/useTranslations';
11
11
  import _TextInput from '../Form/TextInput';
12
12
  import AccessibilityLabel from '../../configs/AccessibilityLabel';
13
+ import useKeyboardAnimated from '../../hooks/Explore/useKeyboardAnimated';
13
14
 
14
15
  const TextBoxTemplate = ({ item, doAction, isWidgetOrder }) => {
15
16
  const t = useTranslations();
17
+ const transY = useKeyboardAnimated();
16
18
  const { label, configuration } = item;
17
19
  const { action_data, config } = configuration;
18
20
  const [configValues] = useConfigGlobalState('configValues');
19
21
  const [value, setValue] = useState();
20
-
21
22
  const { stateAlert, hideAlertAction, onShowAlert } = useDropdownAction();
22
23
 
23
24
  const onDone = useCallback(() => {
@@ -25,9 +26,13 @@ const TextBoxTemplate = ({ item, doAction, isWidgetOrder }) => {
25
26
  hideAlertAction();
26
27
  }, [doAction, action_data, value, hideAlertAction]);
27
28
 
28
- const onInputChange = (value) => {
29
- setValue(value);
29
+ const onInputChange = (e) => {
30
+ setValue(e);
30
31
  };
32
+ const valueText = useMemo(() => {
33
+ return configValues[config.id]?.value || '';
34
+ }, [config.id, configValues]);
35
+
31
36
  return (
32
37
  <View style={(isWidgetOrder && styles.wrapOrderItem) || styles.wrap}>
33
38
  <View>
@@ -35,7 +40,7 @@ const TextBoxTemplate = ({ item, doAction, isWidgetOrder }) => {
35
40
  </View>
36
41
 
37
42
  <View style={styles.iconAndText}>
38
- <Text type="H4">{configValues[config.id]?.value}</Text>
43
+ <Text type="H4">{valueText}</Text>
39
44
  <TouchableOpacity
40
45
  style={styles.iconAndTextOption}
41
46
  onPress={onShowAlert}
@@ -53,6 +58,7 @@ const TextBoxTemplate = ({ item, doAction, isWidgetOrder }) => {
53
58
  rightButtonTitle={stateAlert.rightButton}
54
59
  rightButtonClick={onDone}
55
60
  rightDisabled={!value}
61
+ transY={transY}
56
62
  >
57
63
  <_TextInput
58
64
  wrapStyle={styles.wrapInputStyle}
@@ -0,0 +1,232 @@
1
+ import React from 'react';
2
+ import { TouchableOpacity, View } from 'react-native';
3
+ import { act, create } from 'react-test-renderer';
4
+ import { watchMultiConfigs } from '../../../iot/Monitor';
5
+ import { AccessibilityLabel } from '../../../configs/Constants';
6
+ import { SCProvider } from '../../../context';
7
+ import { mockSCStore } from '../../../context/mockStore';
8
+
9
+ import _TextInput from '../../Form/TextInput';
10
+ import { API, Colors } from '../../../configs';
11
+ import TerminalBoxTemplate from '../TerminalBoxTemplate';
12
+ import MockAdapter from 'axios-mock-adapter';
13
+ import api from '../../../utils/Apis/axios';
14
+ import { ScrollView } from 'react-native';
15
+
16
+ const mock = new MockAdapter(api.axiosInstance);
17
+ const wrapComponent = (item, mockDoAction, isWidgetOrder = false) => (
18
+ <SCProvider initState={mockSCStore({})}>
19
+ <TerminalBoxTemplate
20
+ item={item}
21
+ doAction={mockDoAction}
22
+ isWidgetOrder={isWidgetOrder}
23
+ />
24
+ </SCProvider>
25
+ );
26
+
27
+ jest.mock('../../../iot/Monitor');
28
+
29
+ jest.mock('../../../iot/states', () => ({
30
+ useConfigGlobalState: () => [{ 5: { value: 2 } }, null],
31
+ }));
32
+
33
+ jest.mock('@react-navigation/native', () => {
34
+ return {
35
+ ...jest.requireActual('@react-navigation/native'),
36
+ useFocusEffect: jest.fn(),
37
+ };
38
+ });
39
+
40
+ describe('Test TerminalBoxTemplate', () => {
41
+ let displayItem;
42
+ let sensor;
43
+ let wrapper;
44
+ let data;
45
+
46
+ beforeEach(() => {
47
+ watchMultiConfigs.mockClear();
48
+ jest.useFakeTimers();
49
+ displayItem = {
50
+ id: 33909,
51
+ configuration_error: null,
52
+ is_configuration_ready: true,
53
+ configuration: {
54
+ from_config: {
55
+ id: 35490,
56
+ sensor_id: 18310,
57
+ chip_id: 4129,
58
+ color: '#000000',
59
+ standard: '',
60
+ measure: '',
61
+ unit: '',
62
+ data: [
63
+ {
64
+ x: '2024-02-28T07:53:57.828537Z',
65
+ y: null,
66
+ },
67
+ ],
68
+ },
69
+ to_config: {
70
+ id: 35491,
71
+ sensor_id: 18310,
72
+ chip_id: 4129,
73
+ color: '#000000',
74
+ standard: '',
75
+ measure: '',
76
+ unit: '',
77
+ data: [
78
+ {
79
+ x: '2024-02-28T07:53:57.828537Z',
80
+ y: null,
81
+ },
82
+ ],
83
+ },
84
+ action: 'e8d1de74-7609-4e04-b63d-e6a19389d608',
85
+ action_data: {
86
+ id: 33283,
87
+ name: 'String V4',
88
+ key: 'e8d1de74-7609-4e04-b63d-e6a19389d608',
89
+ icon: '',
90
+ icon_kit: null,
91
+ color: '#00979D',
92
+ home_assistant_action: null,
93
+ lg_action: null,
94
+ arduino_action: {
95
+ id: 8025,
96
+ pin: null,
97
+ value: 0,
98
+ },
99
+ modbus_action: null,
100
+ zigbee_action: null,
101
+ command_prefer_over_internet: true,
102
+ command_prefer_over_bluetooth: false,
103
+ command_prefer_over_googlehome: false,
104
+ allow_config_store_value_id: null,
105
+ end_device: {
106
+ id: 10161,
107
+ name: 'Terminal Box',
108
+ device_type: '',
109
+ },
110
+ sensor_id: 18310,
111
+ chip_id: 4129,
112
+ is_only_bluetooth: false,
113
+ googlehome_action: null,
114
+ },
115
+ },
116
+ template: 'TerminalBoxTemplate',
117
+ order: 0,
118
+ label: 'String Box',
119
+ };
120
+ sensor = {
121
+ name: 'Sensor name',
122
+ is_managed_by_backend: false,
123
+ };
124
+ data = {
125
+ configs: [
126
+ {
127
+ id: 35490,
128
+ head: [
129
+ {
130
+ x: '2024-02-28T07:53:57.787788Z',
131
+ y: null,
132
+ },
133
+ ],
134
+ tail: [
135
+ {
136
+ x: '2024-02-29T07:30:01.940166Z',
137
+ y: null,
138
+ },
139
+ ],
140
+ middle: {
141
+ ready: [],
142
+ not_ready: [],
143
+ channel: 'cache-4e37a226-c61c-4708-a00c-c22bd192b845',
144
+ },
145
+ },
146
+ {
147
+ id: 35491,
148
+ head: [
149
+ {
150
+ x: '2024-02-28T07:53:57.828537Z',
151
+ y: null,
152
+ },
153
+ ],
154
+ tail: [
155
+ {
156
+ x: '2024-02-29T01:42:14.488233Z',
157
+ y: null,
158
+ },
159
+ ],
160
+ middle: {
161
+ ready: [],
162
+ not_ready: [],
163
+ channel: 'cache-2d0806d7-5c15-4af0-8443-6ec6b54ba9cf',
164
+ },
165
+ },
166
+ ],
167
+ };
168
+ });
169
+
170
+ it('render template', async () => {
171
+ const mockDoAction = jest.fn();
172
+ mock.onGet(API.CONFIG.DISPLAY_HISTORY_V4()).reply(200, data);
173
+ await act(async () => {
174
+ wrapper = await create(wrapComponent(displayItem, mockDoAction));
175
+ });
176
+ jest.runAllTimers();
177
+
178
+ const instance = wrapper.root;
179
+ const views = instance.findAllByType(View);
180
+
181
+ expect(views[0].props.style).toEqual({
182
+ padding: 16,
183
+ marginHorizontal: 16,
184
+ marginBottom: 16,
185
+ borderWidth: 1,
186
+ borderColor: Colors.Gray4,
187
+ borderRadius: 10,
188
+ flex: 1,
189
+ flexDirection: 'column',
190
+ });
191
+ const scroll = instance.findByType(ScrollView);
192
+ await act(async () => {
193
+ await scroll.props.onContentSizeChange();
194
+ });
195
+ const textInput = instance.findByType(_TextInput);
196
+ await act(async () => {
197
+ await textInput.props.onChange('123');
198
+ });
199
+ const buttonSend = instance.find(
200
+ (el) =>
201
+ el.props.accessibilityLabel ===
202
+ AccessibilityLabel.TERMINAL_BOX_BUTTON_SEND &&
203
+ el.type === TouchableOpacity
204
+ );
205
+ await act(async () => {
206
+ await buttonSend.props.onPress();
207
+ });
208
+
209
+ expect(mockDoAction).toHaveBeenCalledWith(
210
+ displayItem.configuration.action_data,
211
+ JSON.stringify({ value: '123' })
212
+ );
213
+ });
214
+ it('render template isWidgetOrder change position', async () => {
215
+ const mockDoAction = jest.fn();
216
+ sensor.is_managed_by_backend = true;
217
+ displayItem.configuration.from_config = null;
218
+ mock.onGet(API.CONFIG.DISPLAY_HISTORY_V4()).reply(200, data);
219
+ await act(async () => {
220
+ wrapper = await create(wrapComponent(displayItem, mockDoAction, true));
221
+ });
222
+ const instance = wrapper.root;
223
+ const views = instance.findAllByType(View);
224
+
225
+ expect(views[0].props.style).toEqual({
226
+ flex: 1,
227
+ flexDirection: 'row',
228
+ justifyContent: 'space-between',
229
+ padding: 16,
230
+ });
231
+ });
232
+ });
@@ -34,7 +34,7 @@ jest.mock('@react-navigation/native', () => {
34
34
  };
35
35
  });
36
36
 
37
- describe('Test OptionsDropdownActionTemplate', () => {
37
+ describe('Test TextBoxTemplate', () => {
38
38
  const actionData = {
39
39
  color: '#00979D',
40
40
  command_prefer_over_bluetooth: true,
@@ -14,6 +14,7 @@ import OnOffSmartLock from './OnOffSmartLock/OnOffSmartLock';
14
14
  import TwoButtonTemplate from './TwoButtonTemplate';
15
15
  import SwitchButtonTemplate from './OnOffTemplate/SwitchButtonTemplate';
16
16
  import TextBoxTemplate from './TextBoxTemplate';
17
+ import TerminalBoxTemplate from './TerminalBoxTemplate';
17
18
 
18
19
  export const getActionComponent = (template) => {
19
20
  switch (template) {
@@ -49,6 +50,8 @@ export const getActionComponent = (template) => {
49
50
  return SwitchButtonTemplate;
50
51
  case 'TextBoxTemplate':
51
52
  return TextBoxTemplate;
53
+ case 'TerminalBoxTemplate':
54
+ return TerminalBoxTemplate;
52
55
  default:
53
56
  return null;
54
57
  }
@@ -56,7 +59,6 @@ export const getActionComponent = (template) => {
56
59
 
57
60
  const ActionGroup = (params = {}) => {
58
61
  const { item } = params;
59
-
60
62
  const ButtonGroupComponent = useMemo(() => {
61
63
  return getActionComponent(item?.template);
62
64
  }, [item?.template]);
@@ -1,5 +1,5 @@
1
1
  import React, { memo } from 'react';
2
- import { Platform, StyleSheet, View } from 'react-native';
2
+ import { StyleSheet, View } from 'react-native';
3
3
  import { IconOutline } from '@ant-design/icons-react-native';
4
4
  import { useTranslations } from '../../hooks/Common/useTranslations';
5
5
  import { Colors } from '../../configs';
@@ -49,7 +49,6 @@ const styles = StyleSheet.create({
49
49
  flexDirection: 'column',
50
50
  justifyContent: 'center',
51
51
  alignItems: 'center',
52
- marginTop: Platform.OS === 'ios' ? 25 : 0,
53
52
  },
54
53
  connectStatus: {
55
54
  flexDirection: 'row',
@@ -20,8 +20,8 @@ const SliderRangeActionTemplate = ({ device, item, onSelectAction }) => {
20
20
  const [configValues] = useConfigGlobalState('configValues');
21
21
  const [value, setValue] = useState();
22
22
 
23
- const onInputChange = (value) => {
24
- setValue(value);
23
+ const onInputChange = (e) => {
24
+ setValue(e);
25
25
  };
26
26
 
27
27
  useEffect(() => {
@@ -30,7 +30,7 @@ const SliderRangeActionTemplate = ({ device, item, onSelectAction }) => {
30
30
  }, [configValues, config]);
31
31
 
32
32
  const onPressDone = useCallback(() => {
33
- let actionData = { value: value };
33
+ let actionData = { value: parseFloat(value) };
34
34
  setValue(value);
35
35
  onSelectAction &&
36
36
  onSelectAction({
@@ -0,0 +1,87 @@
1
+ import React, { memo, useState, useCallback } from 'react';
2
+ import { View, TouchableOpacity } from 'react-native';
3
+ import { useTranslations } from '../../hooks/Common/useTranslations';
4
+ import styles from './TextBoxActionTemplateStyles';
5
+ import { Colors } from '../../configs';
6
+ import SelectActionCard from '../SelectActionCard';
7
+ import Text from '../Text';
8
+ import { AccessibilityLabel } from '../../configs/Constants';
9
+ import { ModalCustom } from '../Modal';
10
+ import _TextInput from '../Form/TextInput';
11
+
12
+ const TerminalBoxActionTemplate = ({ device, item, onSelectAction }) => {
13
+ const t = useTranslations();
14
+ const [visible, setVisible] = useState(false);
15
+ const onClose = () => setVisible(false);
16
+ const onPress = () => setVisible(true);
17
+ const { configuration, template, title } = item;
18
+ const { action } = configuration;
19
+ const [value, setValue] = useState();
20
+ const onInputChange = (e) => {
21
+ setValue(e);
22
+ };
23
+
24
+ const onPressDone = useCallback(() => {
25
+ let actionData = { value: value };
26
+ setValue(value);
27
+ onSelectAction &&
28
+ onSelectAction({
29
+ index: item.index,
30
+ action,
31
+ data: actionData,
32
+ template,
33
+ });
34
+ onClose();
35
+ }, [value, onSelectAction, item.index, action, template]);
36
+
37
+ return (
38
+ <>
39
+ <SelectActionCard onPress={onPress} action={value} title={title} />
40
+ <ModalCustom
41
+ isVisible={visible}
42
+ onBackButtonPress={onClose}
43
+ onBackdropPress={onClose}
44
+ >
45
+ <View style={styles.popoverStyle}>
46
+ <View>
47
+ <Text type="H4" bold style={styles.textWithLine}>
48
+ {t('enter_parameters')}
49
+ </Text>
50
+ <View style={styles.modalContent}>
51
+ <_TextInput
52
+ wrapStyle={styles.wrapInputStyle}
53
+ value={value}
54
+ onChange={onInputChange}
55
+ />
56
+ </View>
57
+ <View style={styles.wrapButton}>
58
+ <TouchableOpacity
59
+ onPress={onClose}
60
+ accessibilityLabel={AccessibilityLabel.TEXT_BOX_BUTTON_CANCEL}
61
+ >
62
+ <Text type="H4" bold color={Colors.Primary}>
63
+ {t('cancel')}
64
+ </Text>
65
+ </TouchableOpacity>
66
+ <TouchableOpacity
67
+ onPress={onPressDone}
68
+ accessibilityLabel={AccessibilityLabel.TEXT_BOX_BUTTON_DONE}
69
+ disabled={!value}
70
+ >
71
+ <Text
72
+ type="H4"
73
+ bold
74
+ color={!!value ? Colors.Primary : Colors.Gray}
75
+ >
76
+ {t('done')}
77
+ </Text>
78
+ </TouchableOpacity>
79
+ </View>
80
+ </View>
81
+ </View>
82
+ </ModalCustom>
83
+ </>
84
+ );
85
+ };
86
+
87
+ export default memo(TerminalBoxActionTemplate);
@@ -20,8 +20,8 @@ const TextBoxActionTemplate = ({ device, item, onSelectAction }) => {
20
20
  const [configValues] = useConfigGlobalState('configValues');
21
21
  const [value, setValue] = useState();
22
22
 
23
- const onInputChange = (value) => {
24
- setValue(value);
23
+ const onInputChange = (e) => {
24
+ setValue(e);
25
25
  };
26
26
 
27
27
  useEffect(() => {
@@ -64,7 +64,7 @@ describe('Test SliderRangeActionTemplate', () => {
64
64
  });
65
65
  expect(mockOnSelectAction).toHaveBeenCalledWith({
66
66
  action: 'b498234c-6c1a-452d-a1d1-87a314c20528',
67
- data: { value: '123' },
67
+ data: { value: 123 },
68
68
  index: undefined,
69
69
  template: 'slider_range_template',
70
70
  });
@@ -0,0 +1,118 @@
1
+ import React from 'react';
2
+ import { create, act } from 'react-test-renderer';
3
+ import { SCProvider } from '../../../context';
4
+ import { mockSCStore } from '../../../context/mockStore';
5
+ import { TouchableOpacity } from 'react-native';
6
+ import { AccessibilityLabel } from '../../../configs/Constants';
7
+ import SelectActionCard from '../../SelectActionCard';
8
+ import _TextInput from '../../Form/TextInput';
9
+ import TerminalBoxActionTemplate from '../TerminalBoxActionTemplate';
10
+
11
+ const mockOnSelectAction = jest.fn();
12
+
13
+ const wrapComponent = (item, params = {}) => (
14
+ <SCProvider initState={mockSCStore({})}>
15
+ <TerminalBoxActionTemplate
16
+ item={item}
17
+ onSelectAction={mockOnSelectAction}
18
+ {...params}
19
+ />
20
+ </SCProvider>
21
+ );
22
+
23
+ describe('Test TerminalBoxActionTemplate', () => {
24
+ let tree;
25
+ let data = {
26
+ title: '',
27
+ template: 'TerminalBoxTemplate',
28
+ configuration: {
29
+ action_data: {
30
+ id: 33283,
31
+ key: 'e8d1de74-7609-4e04-b63d-e6a19389d608',
32
+ icon: '',
33
+ name: 'String',
34
+ color: '#00979D',
35
+ },
36
+ from_config: {
37
+ id: 35490,
38
+ data: [],
39
+ unit: '',
40
+ color: '#000000',
41
+ chip_id: 4129,
42
+ },
43
+ to_config: {
44
+ id: 35491,
45
+ data: [],
46
+ unit: '',
47
+ color: '#000000',
48
+ chip_id: 4129,
49
+ },
50
+ action: 'b498234c-6c1a-452d-a1d1-87a314c20528',
51
+ },
52
+ };
53
+
54
+ beforeEach(() => {
55
+ mockOnSelectAction.mockClear();
56
+ });
57
+
58
+ const renderOptions = async (params = {}) => {
59
+ await act(async () => {
60
+ tree = await create(wrapComponent(data, params));
61
+ });
62
+ const instance = tree.root;
63
+ const card = instance.findByType(SelectActionCard);
64
+ await act(async () => {
65
+ card.props.onPress();
66
+ });
67
+ return instance;
68
+ };
69
+
70
+ it('Test onPress Done', async () => {
71
+ const instance = await renderOptions();
72
+ const textInput = instance.findByType(_TextInput);
73
+ await act(async () => {
74
+ await textInput.props.onChange('123');
75
+ });
76
+ const touchableOpacity = instance.find(
77
+ (item) =>
78
+ item.props.accessibilityLabel ===
79
+ AccessibilityLabel.TEXT_BOX_BUTTON_DONE &&
80
+ item.type === TouchableOpacity
81
+ );
82
+ await act(async () => {
83
+ touchableOpacity.props.onPress();
84
+ });
85
+ expect(mockOnSelectAction).toHaveBeenCalledWith({
86
+ action: 'b498234c-6c1a-452d-a1d1-87a314c20528',
87
+ data: { value: '123' },
88
+ index: undefined,
89
+ template: 'TerminalBoxTemplate',
90
+ });
91
+ });
92
+
93
+ it('Test onPress Cancel', async () => {
94
+ const instance = await renderOptions();
95
+ const textInput = instance.findByType(_TextInput);
96
+ await act(async () => {
97
+ await textInput.props.onChange('');
98
+ });
99
+ const touchableDone = instance.find(
100
+ (item) =>
101
+ item.props.accessibilityLabel ===
102
+ AccessibilityLabel.TEXT_BOX_BUTTON_DONE &&
103
+ item.type === TouchableOpacity
104
+ );
105
+ expect(touchableDone.props.disabled).toBeTruthy();
106
+
107
+ const touchableOpacity = instance.find(
108
+ (item) =>
109
+ item.props.accessibilityLabel ===
110
+ AccessibilityLabel.TEXT_BOX_BUTTON_CANCEL &&
111
+ item.type === TouchableOpacity
112
+ );
113
+ await act(async () => {
114
+ touchableOpacity.props.onPress();
115
+ });
116
+ expect(mockOnSelectAction).not.toHaveBeenCalled();
117
+ });
118
+ });
@@ -119,7 +119,7 @@ const styles = StyleSheet.create({
119
119
  }),
120
120
  },
121
121
  contentContainerStyle: {
122
- paddingBottom: heightHeader,
122
+ paddingBottom: heightHeader + 230,
123
123
  },
124
124
  bottomLoading: {
125
125
  height: 32,
@@ -17,7 +17,11 @@ const fetchDataS3 = async (url) => {
17
17
  return data;
18
18
  };
19
19
 
20
- export const useFetchConfigHistory = (configs, setChartData) => {
20
+ export const useFetchConfigHistory = (
21
+ configs,
22
+ setChartData,
23
+ endpoint = API.CONFIG.DISPLAY_HISTORY_V3()
24
+ ) => {
21
25
  const fetchDataDisplayHistory = useCallback(
22
26
  async (startDate, endDate) => {
23
27
  if (!configs.length || !startDate || !endDate) {
@@ -33,24 +37,19 @@ export const useFetchConfigHistory = (configs, setChartData) => {
33
37
  params.append('configs', item.id);
34
38
  });
35
39
 
36
- params.append(
37
- 'date_from',
38
- startDate.subtract(7, 'hours').format('YYYY-MM-DDTHH:mm:ss')
39
- );
40
- params.append(
41
- 'date_to',
42
- endDate.subtract(7, 'hours').format('YYYY-MM-DDTHH:mm:ss')
43
- );
40
+ const timezone = moment().utcOffset();
41
+ startDate = startDate.subtract(timezone, 'minutes');
42
+ endDate = endDate.subtract(timezone, 'minutes');
44
43
 
45
- const { success, data } = await axiosGet(
46
- API.CONFIG.DISPLAY_HISTORY_V3(),
47
- {
48
- params,
49
- }
50
- );
44
+ params.append('date_from', startDate.format('YYYY-MM-DDTHH:mm:ss'));
45
+ params.append('date_to', endDate.format('YYYY-MM-DDTHH:mm:ss'));
46
+
47
+ const { success, data } = await axiosGet(endpoint, {
48
+ params,
49
+ });
51
50
  await updateConfigChart(success, data, configs, setChartData);
52
51
  },
53
- [configs, setChartData]
52
+ [configs, endpoint, setChartData]
54
53
  );
55
54
  return fetchDataDisplayHistory;
56
55
  };
@@ -83,6 +83,7 @@ const API = {
83
83
  },
84
84
  CONFIG: {
85
85
  DISPLAY_HISTORY_V3: () => '/chip_manager/configs/value_history_v3/',
86
+ DISPLAY_HISTORY_V4: () => '/chip_manager/configs/value_history_v4/',
86
87
  },
87
88
  AUTOMATE: {
88
89
  ACTION_ONE_TAP: (id) => `/property_manager/automate/${id}/action_one_tap/`,
@@ -564,6 +564,8 @@ export default {
564
564
  TEXT_BOX_BUTTON_EDIT: 'TEXT_BOX_BUTTON_EDIT',
565
565
  TEXT_BOX_BUTTON_CANCEL: 'TEXT_BOX_BUTTON_CANCEL',
566
566
  TEXT_BOX_BUTTON_DONE: 'TEXT_BOX_BUTTON_DONE',
567
+ //TerminalBoxTemplate
568
+ TERMINAL_BOX_BUTTON_SEND: 'TERMINAL_BOX_BUTTON_SEND',
567
569
  // OnOffButtonAction
568
570
  ON_OFF_BUTTON_ACTION_TITLE: 'ON_OFF_BUTTON_ACTION_TITLE',
569
571
 
@@ -122,6 +122,7 @@ export const Colors = {
122
122
  Blue15: '#99CCFF',
123
123
  Blue16: '#0059B2',
124
124
  Blue17: '#00A4B2',
125
+ Blue18: '#BAE7FF',
125
126
  //Range Volcano:
126
127
  Volcano3: '#FFBB96',
127
128
  Summer: '#FFB24A',
@@ -15,6 +15,7 @@ import moment from 'moment';
15
15
  import { ToastBottomHelper } from '../../../utils/Utils';
16
16
  import TextBoxActionTemplate from '../../../commons/OneTapTemplate/TextBoxActionTemplate';
17
17
  import SliderRangeActionTemplate from '../../../commons/OneTapTemplate/SliderRangeActionTemplate';
18
+ import TerminalBoxActionTemplate from '../../../commons/OneTapTemplate/TerminalBoxActionTemplate';
18
19
 
19
20
  const RenderActionItem = ({ device, item, handleOnSelectAction, index, t }) => {
20
21
  item.index = index;
@@ -78,6 +79,15 @@ const RenderActionItem = ({ device, item, handleOnSelectAction, index, t }) => {
78
79
  onSelectAction={handleOnSelectAction}
79
80
  />
80
81
  );
82
+ case 'TerminalBoxTemplate':
83
+ return (
84
+ <TerminalBoxActionTemplate
85
+ key={item.id}
86
+ device={device}
87
+ item={item}
88
+ onSelectAction={handleOnSelectAction}
89
+ />
90
+ );
81
91
  default:
82
92
  ToastBottomHelper.error(
83
93
  t('template_not_supported', { template: item.template }),
@@ -324,6 +324,7 @@ const Item = ({ item, index, enableScript, t }) => {
324
324
  station_name,
325
325
  sensor_name,
326
326
  action_name,
327
+ data,
327
328
  } = action_script;
328
329
  return (
329
330
  <View style={styles.wrapItem}>
@@ -358,7 +359,7 @@ const Item = ({ item, index, enableScript, t }) => {
358
359
  {sensor_name}
359
360
  </Text>
360
361
  <Text numberOfLines={1} type="H4" color={color}>
361
- {action_name}
362
+ {action_name} {data}
362
363
  </Text>
363
364
  </View>
364
365
  </View>
@@ -171,6 +171,7 @@ export const SensorDisplayItem = ({
171
171
  case 'two_button_action_template':
172
172
  case 'switch_button_action_template':
173
173
  case 'TextBoxTemplate':
174
+ case 'TerminalBoxTemplate':
174
175
  return (
175
176
  <ActionGroup
176
177
  accessibilityLabel={AccessibilityLabel.DEVICE_DETAIL_ACTION_GROUP}
@@ -500,29 +500,34 @@ const DeviceDetail = ({ route }) => {
500
500
  useFocusEffect(
501
501
  useCallback(() => {
502
502
  let params = new URLSearchParams();
503
- let configIds = [];
504
- // todo Bang refactor widgets like dashboard
505
- display.items.map((item) => {
506
- if (!item.configuration) {
503
+ let configIdsSet = new Set();
504
+
505
+ display.items.forEach((item) => {
506
+ const { configuration } = item;
507
+
508
+ if (!configuration) {
507
509
  return;
508
510
  }
509
- item.configuration.configs?.map((config) => {
510
- if (!configIds.includes(config.id)) {
511
- configIds.push(config.id);
512
- }
513
- });
514
- item.configuration.options?.map((option) => {
515
- if (!configIds.includes(option.config)) {
516
- configIds.push(option.config);
517
- }
518
- });
519
- if (item.configuration.config) {
520
- const config = item.configuration.config;
521
- const configId = config?.id ? config?.id : config;
522
- !configIds.includes(configId) && configIds.push(configId);
511
+
512
+ const { configs, options, config, from_config, to_config } =
513
+ configuration;
514
+
515
+ configs?.forEach((config) => configIdsSet.add(config.id));
516
+ options?.forEach((option) => configIdsSet.add(option.config));
517
+
518
+ if (config) {
519
+ const configId = config.id || config;
520
+ configIdsSet.add(configId);
521
+ }
522
+ if (from_config) {
523
+ configIdsSet.add(from_config.id);
524
+ }
525
+ if (to_config) {
526
+ configIdsSet.add(to_config.id);
523
527
  }
524
528
  });
525
529
 
530
+ let configIds = Array.from(configIdsSet);
526
531
  configIds = configIds.filter(Boolean);
527
532
  configIdsTemp.current = configIds;
528
533
 
@@ -1467,7 +1467,7 @@ export default {
1467
1467
  'customize...': 'Customize...',
1468
1468
  uri_invalid: 'URI invalid',
1469
1469
  when_value_is: 'When value is',
1470
- template_not_supported: '"{template}" not yet supported',
1470
+ template_not_supported: 'Widget "{template}" not yet supported',
1471
1471
  invited_user: 'Invited user {user}',
1472
1472
  enter_value: 'Enter value',
1473
1473
  enter_parameters: 'Enter parameters',
@@ -1477,7 +1477,7 @@ export default {
1477
1477
  'customize...': 'Tùy chỉnh...',
1478
1478
  uri_invalid: 'URI không hợp lệ',
1479
1479
  when_value_is: 'Khi giá trị là',
1480
- template_not_supported: '"{template}" chưa được hỗ trợ',
1480
+ template_not_supported: 'Tiện ích "{template}" chưa được hỗ trợ',
1481
1481
  invited_user: 'Đã mời người dùng {user}',
1482
1482
  enter_value: 'Nhập giá trị',
1483
1483
  enter_parameters: 'Nhập thông số',