@eohjsc/react-native-smart-city 0.3.19 → 0.3.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json 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.3.19",
4
+ "version": "0.3.22",
5
5
  "description": "TODO",
6
6
  "main": "index.js",
7
7
  "files": [
@@ -55,7 +55,6 @@ const OnOffTemplate = memo(({ actionGroup, doAction, sensor }) => {
55
55
  );
56
56
 
57
57
  const triggerAction = useCallback(async () => {
58
- setTempIsOn((prev) => !prev);
59
58
  switch (sensor?.device_type) {
60
59
  case DEVICE_TYPE.ZIGBEE:
61
60
  if (action_on_data && action_off_data) {
@@ -75,11 +74,10 @@ const OnOffTemplate = memo(({ actionGroup, doAction, sensor }) => {
75
74
  }
76
75
  break;
77
76
  case DEVICE_TYPE.LG_THINQ:
77
+ setTempIsOn((prev) => !prev);
78
78
  if (action_data) {
79
79
  await doAction(action_data, JSON.stringify({ value: !isOn }));
80
80
  }
81
-
82
- updateStatusFromPusher();
83
81
  break;
84
82
  default:
85
83
  if (action_data) {
@@ -98,6 +96,7 @@ const OnOffTemplate = memo(({ actionGroup, doAction, sensor }) => {
98
96
  }
99
97
  break;
100
98
  }
99
+ updateStatusFromPusher();
101
100
  if (sensor?.is_managed_by_backend) {
102
101
  configuration?.config &&
103
102
  sensor?.device_type !== 'GOOGLE_HOME' &&
@@ -130,7 +129,13 @@ const OnOffTemplate = memo(({ actionGroup, doAction, sensor }) => {
130
129
  if (sensor?.is_managed_by_backend && sensor.device_type !== 'GOOGLE_HOME') {
131
130
  watchMultiConfigs([configuration.config]);
132
131
  }
133
- }, [sensor, configuration.config]);
132
+ }, [sensor, configuration.config, getIsOnValue]);
133
+
134
+ useEffect(() => {
135
+ if (sensor?.device_type !== DEVICE_TYPE.LG_THINQ) {
136
+ setTempIsOn(getIsOnValue());
137
+ }
138
+ }, [getIsOnValue, sensor?.device_type]);
134
139
 
135
140
  const Component = useMemo(() => {
136
141
  return getComponent(actionGroup.template);
@@ -24,7 +24,7 @@ const SubUnitFavorites = ({
24
24
  const { getStatus, serverDown } = useSensorsStatus(unit, favoriteDevices);
25
25
 
26
26
  const handleOnAddNew = () => {
27
- navigate(Routes.SelectFavoritesDevices, {
27
+ navigate(Routes.SelectAddToFavorites, {
28
28
  unitId: unit.id,
29
29
  });
30
30
  };
@@ -58,6 +58,7 @@ const SubUnitFavorites = ({
58
58
  isOwner={isOwner}
59
59
  automate={automate}
60
60
  unit={unit}
61
+ wrapStyle={wrapItemStyle}
61
62
  />
62
63
  ))}
63
64
  <ItemAddNew
@@ -29,9 +29,14 @@ const API = {
29
29
  CHANGE_OWNER: (id) => `/property_manager/units/${id}/change_owner/`,
30
30
  FAVOURITE_DEVICES: (id) =>
31
31
  `/property_manager/units/${id}/favourite_devices/`,
32
- DEVICES: (id) => `/property_manager/units/${id}/devices/`,
32
+ DEVICES_NOT_FAVORITES: (id) =>
33
+ `/property_manager/units/${id}/devices_not_favourites/`,
33
34
  ADD_DEVICES_TO_FAVORITES: (id) =>
34
35
  `/property_manager/units/${id}/add_devices_to_favourites/`,
36
+ AUTOMATE_SCRIPTS_NOT_STARRED: (id) =>
37
+ `/property_manager/units/${id}/automate_scripts_not_starred/`,
38
+ STAR_AUTOMATE_SCRIPTS: (id) =>
39
+ `/property_manager/units/${id}/star_automate_scripts/`,
35
40
  },
36
41
  SUB_UNIT: {
37
42
  REMOVE_SUB_UNIT: (unitId, id) =>
@@ -15,8 +15,8 @@ export const Action = {
15
15
  ADD_DEVICES_TO_FAVORITES: 'ADD_DEVICE_TO_FAVORITES',
16
16
  REMOVE_DEVICES_FROM_FAVORITES: 'REMOVE_DEVICE_FROM_FAVORITES',
17
17
  SET_STARRED_SCRIPTS: 'SET_STARRED_SCRIPTS',
18
- STAR_SCRIPT: 'STAR_SCRIPT',
19
- UNSTAR_SCRIPT: 'UNSTAR_SCRIPT',
18
+ STAR_SCRIPTS: 'STAR_SCRIPTS',
19
+ UNSTAR_SCRIPTS: 'UNSTAR_SCRIPTS',
20
20
  CONNECTING_GOOGLE_HOME: 'CONNECTING_GOOGLE_HOME',
21
21
  SET_GOOGLE_HOME_CONNECTIONS: 'SET_GOOGLE_HOME_CONNECTIONS',
22
22
  CHANGE_GOOGLE_HOME_CONN_STATE: 'CHANGE_GOOGLE_HOME_CONN_STATE',
@@ -235,7 +235,7 @@ export const reducer = (currentState: ContextData, action: Action) => {
235
235
  starredScriptIds: payload,
236
236
  },
237
237
  };
238
- case Action.STAR_SCRIPT:
238
+ case Action.STAR_SCRIPTS:
239
239
  return {
240
240
  ...currentState,
241
241
  automate: {
@@ -245,13 +245,13 @@ export const reducer = (currentState: ContextData, action: Action) => {
245
245
  ),
246
246
  },
247
247
  };
248
- case Action.UNSTAR_SCRIPT:
248
+ case Action.UNSTAR_SCRIPTS:
249
249
  return {
250
250
  ...currentState,
251
251
  automate: {
252
252
  ...currentState.automate,
253
253
  starredScriptIds: currentState.automate.starredScriptIds.filter(
254
- (scriptId) => scriptId !== payload
254
+ (scriptId) => !payload.includes(scriptId)
255
255
  ),
256
256
  },
257
257
  };
@@ -1,4 +1,4 @@
1
- import { useCallback, useContext, useEffect } from 'react';
1
+ import { useCallback, useContext, useEffect, useState } from 'react';
2
2
  import { API } from '../../configs';
3
3
  import { SCContext, useSCContextSelector } from '../../context';
4
4
  import { Action } from '../../context/actionType';
@@ -6,12 +6,14 @@ import { axiosGet } from '../../utils/Apis/axios';
6
6
 
7
7
  const useValueEvaluations = (unitId) => {
8
8
  const { setAction } = useContext(SCContext);
9
+ const [fetching, setFetching] = useState(false);
9
10
 
10
11
  const fetchConfigValueEvaluations = useCallback(
11
12
  async (page = 1) => {
12
13
  if (!unitId) {
13
14
  return;
14
15
  }
16
+ setFetching(true);
15
17
  const params = new URLSearchParams();
16
18
  params.append('config__end_device__station__unit', unitId);
17
19
  params.append('page', page);
@@ -24,9 +26,15 @@ const useValueEvaluations = (unitId) => {
24
26
  data: data.results,
25
27
  });
26
28
  if (data.next) {
27
- await fetchConfigValueEvaluations(page + 1);
29
+ return await fetchConfigValueEvaluations(page + 1);
28
30
  }
31
+ } else {
32
+ setAction(Action.UPDATE_VALUE_EVALUATIONS, {
33
+ unitId,
34
+ data: [],
35
+ });
29
36
  }
37
+ setFetching(false);
30
38
  },
31
39
  [unitId, setAction]
32
40
  );
@@ -36,10 +44,15 @@ const useValueEvaluations = (unitId) => {
36
44
  });
37
45
 
38
46
  useEffect(() => {
39
- if (!(fetchedValueEvaluationUnits.indexOf(unitId) !== -1)) {
47
+ if (!fetching && !(fetchedValueEvaluationUnits.indexOf(unitId) !== -1)) {
40
48
  fetchConfigValueEvaluations();
41
49
  }
42
- }, [unitId, fetchConfigValueEvaluations, fetchedValueEvaluationUnits]);
50
+ }, [
51
+ unitId,
52
+ fetching,
53
+ fetchConfigValueEvaluations,
54
+ fetchedValueEvaluationUnits,
55
+ ]);
43
56
  };
44
57
 
45
58
  export default useValueEvaluations;
@@ -51,7 +51,7 @@ import EmergencySetting from '../screens/EmergencySetting';
51
51
  import ConfirmUnitDeletion from '../screens/ConfirmUnitDeletion';
52
52
  import InfoMemberUnit from '../screens/Sharing/InfoMemberUnit';
53
53
  import EnterPassword from '../screens/EnterPassword';
54
- import SelectFavoritesDevices from '../screens/Unit/SelectFavoritesDevices';
54
+ import SelectAddToFavorites from '../screens/Unit/SelectAddToFavorites';
55
55
  import { HanetCameraStack } from './HanetCameraStack';
56
56
  import { axiosGet } from '../utils/Apis/axios';
57
57
  import { API } from '../configs';
@@ -402,8 +402,8 @@ export const UnitStack = memo((props) => {
402
402
  }}
403
403
  />
404
404
  <Stack.Screen
405
- name={Route.SelectFavoritesDevices}
406
- component={SelectFavoritesDevices}
405
+ name={Route.SelectAddToFavorites}
406
+ component={SelectAddToFavorites}
407
407
  options={{
408
408
  headerShown: false,
409
409
  }}
@@ -148,7 +148,7 @@ const DeviceDetail = ({ route }) => {
148
148
  );
149
149
 
150
150
  const canManageSubUnit = useMemo(() => {
151
- return currentUserId === unit?.user_id;
151
+ return Number(currentUserId) === Number(unit?.user_id);
152
152
  }, [currentUserId, unit]);
153
153
 
154
154
  const fetchUnitDetail = useCallback(async () => {
@@ -721,16 +721,19 @@ const DeviceDetail = ({ route }) => {
721
721
  isNetworkConnected !== null &&
722
722
  renderSensorConnected()}
723
723
  </View>
724
- {isShowSetupEmergencyContact && canManageSubUnit && (
725
- <BottomButtonView
726
- style={styles.bottomButtonEmergencyContact}
727
- mainIcon={<Icon name="plus" size={16} color={Colors.Primary} />}
728
- mainTitle={t('setup_my_emergency_contact')}
729
- onPressMain={onSetupContacts}
730
- typeMain="primaryBorder"
731
- semiboldMain={false}
732
- />
733
- )}
724
+ {isNetworkConnected &&
725
+ isConnected &&
726
+ isShowSetupEmergencyContact &&
727
+ canManageSubUnit && (
728
+ <BottomButtonView
729
+ style={styles.bottomButtonEmergencyContact}
730
+ mainIcon={<Icon name="plus" size={16} color={Colors.Primary} />}
731
+ mainTitle={t('setup_my_emergency_contact')}
732
+ onPressMain={onSetupContacts}
733
+ typeMain="primaryBorder"
734
+ semiboldMain={false}
735
+ />
736
+ )}
734
737
  <AlertSendConfirm
735
738
  showAlertConfirm={showAlertConfirm && !lockShowing}
736
739
  countDown={countDown}
@@ -1,20 +1,19 @@
1
1
  import React, { useCallback, useState } from 'react';
2
- import { SafeAreaView, StyleSheet, TextInput } from 'react-native';
2
+ import { SafeAreaView, StyleSheet, TextInput, View } from 'react-native';
3
3
  import { useNavigation } from '@react-navigation/native';
4
4
  import { useTranslations } from '../../hooks/Common/useTranslations';
5
5
  import { Section, ViewButtonBottom } from '../../commons';
6
6
  import WrapHeaderScrollable from '../../commons/Sharing/WrapHeaderScrollable';
7
7
  import { API, Colors } from '../../configs';
8
- import { useKeyboardShow } from '../../hooks/Common';
9
8
  import { TESTID } from '../../configs/Constants';
10
9
  import { axiosPost } from '../../utils/Apis/axios';
11
10
  import { ToastBottomHelper } from '../../utils/Utils';
11
+ import { isValidPhoneNumber } from '../../utils/Validation';
12
12
 
13
13
  export const EmergencyContactsAddNew = ({ route }) => {
14
14
  const t = useTranslations();
15
15
  const { group } = route.params;
16
16
  const { goBack } = useNavigation();
17
- const { keyboardBottomPadding } = useKeyboardShow();
18
17
  const [textName, setTextName] = useState('');
19
18
  const [textPhone, setTextPhone] = useState('');
20
19
  const onTextNameChange = useCallback(
@@ -33,22 +32,28 @@ export const EmergencyContactsAddNew = ({ route }) => {
33
32
  goBack();
34
33
  }, [goBack]);
35
34
  const onSave = useCallback(async () => {
36
- const { success } = await axiosPost(API.EMERGENCY_BUTTON.CREATE_CONTACT(), {
37
- group: group.id,
38
- phone_number: textPhone,
39
- name: textName,
40
- });
41
- if (success) {
42
- goBack();
35
+ if (isValidPhoneNumber(textPhone)) {
36
+ const { success } = await axiosPost(
37
+ API.EMERGENCY_BUTTON.CREATE_CONTACT(),
38
+ {
39
+ group: group.id,
40
+ phone_number: textPhone,
41
+ name: textName,
42
+ }
43
+ );
44
+ if (success) {
45
+ goBack();
46
+ ToastBottomHelper.success(t('create_contact_success'));
47
+ } else {
48
+ ToastBottomHelper.error(t('create_contact_failed'));
49
+ }
43
50
  } else {
44
- ToastBottomHelper.error(t('create_contact_failed'));
51
+ ToastBottomHelper.error(t('invalid_phone_number'));
45
52
  }
46
53
  }, [goBack, group.id, t, textName, textPhone]);
47
54
 
48
55
  return (
49
- <SafeAreaView
50
- style={[styles.wrap, { marginBottom: keyboardBottomPadding }]}
51
- >
56
+ <SafeAreaView style={styles.wrap}>
52
57
  <WrapHeaderScrollable title={t('create_contact')}>
53
58
  <Section type={'border'}>
54
59
  <TextInput
@@ -58,6 +63,7 @@ export const EmergencyContactsAddNew = ({ route }) => {
58
63
  placeholder={t('text_name')}
59
64
  underlineColorAndroid={null}
60
65
  onChangeText={onTextNameChange}
66
+ maxLength={64}
61
67
  />
62
68
  <TextInput
63
69
  testID={TESTID.ON_CHANGE_PHONE_EMERGENCY_CONTACT}
@@ -67,17 +73,20 @@ export const EmergencyContactsAddNew = ({ route }) => {
67
73
  underlineColorAndroid={null}
68
74
  keyboardType={'phone-pad'}
69
75
  onChangeText={onTextPhoneChange}
76
+ maxLength={64}
70
77
  />
71
78
  </Section>
72
79
  </WrapHeaderScrollable>
73
- <ViewButtonBottom
74
- leftTitle={t('cancel')}
75
- leftDisabled={false}
76
- onLeftClick={onCancel}
77
- rightTitle={t('save')}
78
- rightDisabled={!textPhone || !textName}
79
- onRightClick={onSave}
80
- />
80
+ <View style={styles.viewButtonBottom}>
81
+ <ViewButtonBottom
82
+ leftTitle={t('cancel')}
83
+ leftDisabled={false}
84
+ onLeftClick={onCancel}
85
+ rightTitle={t('save')}
86
+ rightDisabled={!textPhone || !textName}
87
+ onRightClick={onSave}
88
+ />
89
+ </View>
81
90
  </SafeAreaView>
82
91
  );
83
92
  };
@@ -95,4 +104,8 @@ const styles = StyleSheet.create({
95
104
  fontFamily: 'SFProDisplay-Regular',
96
105
  lineHeight: 24,
97
106
  },
107
+ viewButtonBottom: {
108
+ borderTopWidth: 1,
109
+ borderTopColor: Colors.Gray4,
110
+ },
98
111
  });
@@ -27,8 +27,9 @@ export const EmergencyContactsSelectContacts = ({ route }) => {
27
27
  setLoading(true);
28
28
  const { success, data } = await axiosGet(API.SHARE.UNITS_MEMBERS(id));
29
29
  if (success) {
30
+ const result = data.filter((item) => item?.name && item?.phone_number);
30
31
  setLoading(false);
31
- setDataContact(data);
32
+ setDataContact(result);
32
33
  }
33
34
  }, []);
34
35
 
@@ -115,22 +115,40 @@ describe('test EmergencyContactAddNew', () => {
115
115
  const instance = tree.root;
116
116
  const viewButtonBottom = instance.findByType(ViewButtonBottom);
117
117
 
118
+ const TextInput_ = instance.find(
119
+ (el) =>
120
+ el.props.testID === TESTID.ON_CHANGE_PHONE_EMERGENCY_CONTACT &&
121
+ el.type === TextInput
122
+ );
123
+
118
124
  await act(async () => {
125
+ await TextInput_.props.onChangeText('0901603859');
119
126
  await viewButtonBottom.props.onRightClick();
120
127
  });
121
128
 
122
129
  expect(mockedGoBack).toHaveBeenCalledTimes(1);
123
130
  });
124
131
 
125
- test('onSave fail', async () => {
132
+ test('onSave failed', async () => {
126
133
  await act(async () => {
127
134
  tree = await create(wrapComponent(route));
128
135
  });
129
136
  const instance = tree.root;
130
137
  const viewButtonBottom = instance.findByType(ViewButtonBottom);
131
138
 
139
+ const TextInput_ = instance.find(
140
+ (el) =>
141
+ el.props.testID === TESTID.ON_CHANGE_PHONE_EMERGENCY_CONTACT &&
142
+ el.type === TextInput
143
+ );
144
+
132
145
  mock.onPost(API.EMERGENCY_BUTTON.CREATE_CONTACT()).reply(400);
133
- await viewButtonBottom.props.onRightClick();
146
+
147
+ await act(async () => {
148
+ await TextInput_.props.onChangeText('0901603859');
149
+ await viewButtonBottom.props.onRightClick();
150
+ });
151
+
134
152
  expect(Toast.show).toHaveBeenCalledWith({
135
153
  type: 'error',
136
154
  position: 'bottom',
@@ -139,4 +157,20 @@ describe('test EmergencyContactAddNew', () => {
139
157
  });
140
158
  expect(mockedGoBack).not.toHaveBeenCalled();
141
159
  });
160
+
161
+ test('invalid phone number', async () => {
162
+ await act(async () => {
163
+ tree = await create(wrapComponent(route));
164
+ });
165
+ const instance = tree.root;
166
+ const viewButtonBottom = instance.findByType(ViewButtonBottom);
167
+
168
+ await viewButtonBottom.props.onRightClick();
169
+ expect(Toast.show).toHaveBeenCalledWith({
170
+ type: 'error',
171
+ position: 'bottom',
172
+ text1: getTranslate('en', 'invalid_phone_number'),
173
+ visibilityTime: 1000,
174
+ });
175
+ });
142
176
  });
@@ -14,14 +14,14 @@ export const useStarredScript = (automate) => {
14
14
 
15
15
  const starScript = useCallback(async () => {
16
16
  const { success } = await axiosPost(API.AUTOMATE.STAR_SCRIPT(automate?.id));
17
- success && setAction(Action.STAR_SCRIPT, automate?.script?.id);
17
+ success && setAction(Action.STAR_SCRIPTS, [automate?.script?.id]);
18
18
  }, [automate, setAction]);
19
19
 
20
20
  const unstarScript = useCallback(async () => {
21
21
  const { success } = await axiosPost(
22
22
  API.AUTOMATE.UNSTAR_SCRIPT(automate?.id)
23
23
  );
24
- success && setAction(Action.UNSTAR_SCRIPT, automate?.script?.id);
24
+ success && setAction(Action.UNSTAR_SCRIPTS, [automate?.script?.id]);
25
25
  }, [automate, setAction]);
26
26
 
27
27
  return {
@@ -15,14 +15,15 @@ import NavBar from '../../commons/NavBar';
15
15
  import BottomButtonView from '../../commons/BottomButtonView';
16
16
  import { FullLoading } from '../../commons';
17
17
  import Device from '../AddNewAction/Device';
18
+ import AutomateScript from './components/AutomateScript';
18
19
  import { useTranslations } from '../../hooks/Common/useTranslations';
19
20
  import { SCContext } from '../../context';
20
21
  import { Action } from '../../context/actionType';
21
22
  import { axiosGet, axiosPost } from '../../utils/Apis/axios';
22
23
  import { API, Colors } from '../../configs';
23
- import styles from './SelectFavoritesDevicesStyles';
24
+ import styles from './SelectAddToFavoritesStyles';
24
25
 
25
- const SelectFavoritesDevices = memo(({ route }) => {
26
+ const SelectAddToFavorites = memo(({ route }) => {
26
27
  const t = useTranslations();
27
28
  const { goBack } = useNavigation();
28
29
  const { unitId } = route.params;
@@ -31,43 +32,69 @@ const SelectFavoritesDevices = memo(({ route }) => {
31
32
  const [listMenuItem, setListMenuItem] = useState([]);
32
33
  const [indexStation, setIndexStation] = useState(0);
33
34
  const [stations, setStations] = useState([]);
34
- const [selectedIds, setSelectedIds] = useState([]);
35
+ const [automates, setAutomates] = useState([]);
36
+
37
+ const [selectedDeviceIds, setSelectedDeviceIds] = useState([]);
38
+ const [selectedScriptIds, setSelectedScriptIds] = useState([]);
35
39
  const [loading, setLoading] = useState(true);
36
40
 
37
41
  const fetchData = useCallback(async () => {
38
42
  setLoading(true);
39
- const { success, data } = await axiosGet(API.UNIT.DEVICES(unitId));
40
- if (success) {
41
- const newData = data.filter((item) => item.devices.length > 0);
43
+ const { success: successDevice, data: dataDevice } = await axiosGet(
44
+ API.UNIT.DEVICES_NOT_FAVORITES(unitId)
45
+ );
46
+ const { success: successAutomate, data: dataAutomate } = await axiosGet(
47
+ API.UNIT.AUTOMATE_SCRIPTS_NOT_STARRED(unitId)
48
+ );
49
+ if (successDevice && successAutomate) {
50
+ const newData = dataDevice.filter((item) => item.devices.length > 0);
51
+ if (dataAutomate.length) {
52
+ newData.unshift({
53
+ isSmart: true,
54
+ name: t('smart'),
55
+ });
56
+ }
42
57
  const listMenu = newData.map((item, index) => ({
43
58
  text: item.name,
44
59
  station: item,
45
60
  index: index,
46
61
  }));
47
62
  setStations(newData);
63
+ setAutomates(dataAutomate);
48
64
  setListMenuItem(listMenu);
49
65
  setListStation(listMenu);
50
66
  }
51
67
  setLoading(false);
52
- }, [unitId]);
68
+ }, [unitId, t]);
53
69
 
54
- const addDevicesToFavorites = useCallback(async () => {
55
- if (selectedIds.length === 0) {
70
+ const addToFavorites = useCallback(async () => {
71
+ if (!selectedDeviceIds.length && !selectedScriptIds.length) {
56
72
  return;
57
73
  }
58
74
  setLoading(true);
59
- const { success } = await axiosPost(
60
- API.UNIT.ADD_DEVICES_TO_FAVORITES(unitId),
61
- {
62
- devices: selectedIds,
63
- }
64
- );
65
- if (success) {
66
- setAction(Action.ADD_DEVICES_TO_FAVORITES, selectedIds);
75
+
76
+ let response, success1, success2;
77
+
78
+ if (selectedDeviceIds.length) {
79
+ response = await axiosPost(API.UNIT.ADD_DEVICES_TO_FAVORITES(unitId), {
80
+ devices: selectedDeviceIds,
81
+ });
82
+ success1 = response.success;
83
+ }
84
+ if (selectedScriptIds.length) {
85
+ response = await axiosPost(API.UNIT.STAR_AUTOMATE_SCRIPTS(unitId), {
86
+ scripts: selectedScriptIds,
87
+ });
88
+ success2 = response.success;
89
+ }
90
+ success1 && setAction(Action.ADD_DEVICES_TO_FAVORITES, selectedDeviceIds);
91
+ success2 && setAction(Action.STAR_SCRIPTS, selectedScriptIds);
92
+ if (success1 || success2) {
67
93
  goBack();
68
94
  }
95
+
69
96
  setLoading(false);
70
- }, [unitId, selectedIds, setAction, goBack]);
97
+ }, [unitId, selectedDeviceIds, selectedScriptIds, setAction, goBack]);
71
98
 
72
99
  useEffect(() => {
73
100
  fetchData();
@@ -83,7 +110,7 @@ const SelectFavoritesDevices = memo(({ route }) => {
83
110
 
84
111
  const onSelectDevice = useCallback(
85
112
  (device) => {
86
- setSelectedIds((ids) => {
113
+ setSelectedDeviceIds((ids) => {
87
114
  const index = ids.indexOf(device.id);
88
115
  if (index !== -1) {
89
116
  return ids.filter((id) => id !== device.id);
@@ -91,7 +118,20 @@ const SelectFavoritesDevices = memo(({ route }) => {
91
118
  return [...ids, device.id];
92
119
  });
93
120
  },
94
- [setSelectedIds]
121
+ [setSelectedDeviceIds]
122
+ );
123
+
124
+ const onSelectAutomateScript = useCallback(
125
+ (automate) => {
126
+ setSelectedScriptIds((ids) => {
127
+ const index = ids.indexOf(automate?.script?.id);
128
+ if (index !== -1) {
129
+ return ids.filter((id) => id !== automate?.script?.id);
130
+ }
131
+ return [...ids, automate?.script?.id];
132
+ });
133
+ },
134
+ [setSelectedScriptIds]
95
135
  );
96
136
 
97
137
  const rightComponent = useMemo(
@@ -132,13 +172,24 @@ const SelectFavoritesDevices = memo(({ route }) => {
132
172
  )}
133
173
 
134
174
  <View style={styles.boxDevices}>
135
- {stations[indexStation]?.devices &&
136
- stations[indexStation].devices.map((device) => (
175
+ {stations[indexStation]?.isSmart &&
176
+ automates.map((automate, index) => (
177
+ <AutomateScript
178
+ key={`automate_script_${index}`}
179
+ automate={automate}
180
+ onPress={onSelectAutomateScript}
181
+ isSelected={selectedScriptIds.includes(automate?.script?.id)}
182
+ />
183
+ ))}
184
+ {!stations[indexStation]?.isSmart &&
185
+ stations[indexStation]?.devices &&
186
+ stations[indexStation].devices.map((device, index) => (
137
187
  <Device
188
+ key={`device_${index}`}
138
189
  svgMain={device.icon || 'sensor'}
139
190
  title={device.name}
140
191
  sensor={device}
141
- isSelectDevice={selectedIds.includes(device.id)}
192
+ isSelectDevice={selectedDeviceIds.includes(device.id)}
142
193
  onPress={onSelectDevice}
143
194
  />
144
195
  ))}
@@ -148,12 +199,16 @@ const SelectFavoritesDevices = memo(({ route }) => {
148
199
  <BottomButtonView
149
200
  style={styles.bottomButtonView}
150
201
  mainTitle={t('done')}
151
- onPressMain={addDevicesToFavorites}
152
- typeMain={selectedIds.length === 0 ? 'disabled' : 'primary'}
202
+ onPressMain={addToFavorites}
203
+ typeMain={
204
+ !selectedDeviceIds.length && !selectedScriptIds.length
205
+ ? 'disabled'
206
+ : 'primary'
207
+ }
153
208
  />
154
209
  {loading && <FullLoading />}
155
210
  </View>
156
211
  );
157
212
  });
158
213
 
159
- export default SelectFavoritesDevices;
214
+ export default SelectAddToFavorites;
@@ -0,0 +1,267 @@
1
+ import React from 'react';
2
+ import { act, create } from 'react-test-renderer';
3
+ import MockAdapter from 'axios-mock-adapter';
4
+
5
+ import { SCProvider } from '../../../context';
6
+ import { mockSCStore } from '../../../context/mockStore';
7
+ import SelectAddToFavorites from '../SelectAddToFavorites';
8
+ import NavBar from '../../../commons/NavBar';
9
+ import Device from '../../AddNewAction/Device';
10
+ import AutomateScript from '../components/AutomateScript';
11
+ import BottomButtonView from '../../../commons/BottomButtonView';
12
+ import { API } from '../../../configs';
13
+ import api from '../../../utils/Apis/axios';
14
+
15
+ const wrapComponent = (route) => (
16
+ <SCProvider initState={mockSCStore({})}>
17
+ <SelectAddToFavorites route={route} />
18
+ </SCProvider>
19
+ );
20
+
21
+ const mock = new MockAdapter(api.axiosInstance);
22
+
23
+ const mockGoBack = jest.fn();
24
+ jest.mock('@react-navigation/native', () => {
25
+ return {
26
+ ...jest.requireActual('@react-navigation/native'),
27
+ useNavigation: () => ({
28
+ goBack: mockGoBack,
29
+ }),
30
+ };
31
+ });
32
+
33
+ jest.mock('react', () => {
34
+ return {
35
+ ...jest.requireActual('react'),
36
+ memo: (x) => x,
37
+ };
38
+ });
39
+
40
+ describe('Test SelectAddToFavorites', () => {
41
+ let tree, route;
42
+ let dataDevice = [
43
+ {
44
+ id: 1,
45
+ name: 'station 1',
46
+ devices: [
47
+ {
48
+ id: 1,
49
+ name: 'device 1',
50
+ icon: 'sensor',
51
+ icon_kit: null,
52
+ },
53
+ {
54
+ id: 2,
55
+ name: 'device 2',
56
+ icon: null,
57
+ icon_kit: 'icon',
58
+ },
59
+ ],
60
+ },
61
+ {
62
+ id: 2,
63
+ name: 'station 2',
64
+ devices: [],
65
+ },
66
+ ];
67
+ let dataAutomate = [
68
+ {
69
+ id: 1,
70
+ script: {
71
+ id: 1,
72
+ icon_kit: 'sensor',
73
+ name: 'script 1',
74
+ },
75
+ type: 'one_tap',
76
+ author: 'author',
77
+ },
78
+ {
79
+ id: 2,
80
+ script: {
81
+ id: 2,
82
+ icon_kit: 'sensor',
83
+ name: 'script 2',
84
+ },
85
+ type: 'value_change',
86
+ author: 'author',
87
+ },
88
+ {
89
+ id: 3,
90
+ script: {
91
+ id: 3,
92
+ icon_kit: 'sensor',
93
+ name: 'script 3',
94
+ },
95
+ type: 'schedule',
96
+ author: 'author',
97
+ },
98
+ {
99
+ id: 4,
100
+ script: {
101
+ id: 4,
102
+ icon_kit: 'sensor',
103
+ name: 'script 4',
104
+ },
105
+ type: 'event',
106
+ author: 'author',
107
+ },
108
+ ];
109
+
110
+ beforeEach(() => {
111
+ mockGoBack.mockClear();
112
+ mock.resetHistory();
113
+ route = {
114
+ params: {
115
+ unitId: 1,
116
+ },
117
+ };
118
+ });
119
+
120
+ test('test select then add devices to favorites, star scripts', async () => {
121
+ mock.onGet(API.UNIT.DEVICES_NOT_FAVORITES(1)).replyOnce(200, dataDevice);
122
+ mock
123
+ .onGet(API.UNIT.AUTOMATE_SCRIPTS_NOT_STARRED(1))
124
+ .replyOnce(200, dataAutomate);
125
+ mock.onPost(API.UNIT.ADD_DEVICES_TO_FAVORITES(1)).replyOnce(200);
126
+ mock.onPost(API.UNIT.STAR_AUTOMATE_SCRIPTS(1)).replyOnce(200);
127
+
128
+ await act(async () => {
129
+ tree = await create(wrapComponent(route));
130
+ });
131
+ expect(mock.history.get).toHaveLength(2);
132
+ const instance = tree.root;
133
+
134
+ const navbar = instance.findByType(NavBar);
135
+ const bottomButton = instance.findByType(BottomButtonView);
136
+
137
+ await act(async () => {
138
+ await bottomButton.props.onPressMain();
139
+ });
140
+ expect(mock.history.post).toHaveLength(0);
141
+
142
+ const scripts = instance.findAllByType(AutomateScript);
143
+ expect(scripts).toHaveLength(4);
144
+
145
+ await act(async () => {
146
+ await scripts[0].props.onPress({ script: { id: 1 } });
147
+ });
148
+ await act(async () => {
149
+ await scripts[0].props.onPress({ script: { id: 2 } });
150
+ });
151
+ await act(async () => {
152
+ await scripts[0].props.onPress({ script: { id: 1 } });
153
+ });
154
+
155
+ await act(async () => {
156
+ await navbar.props.onSnapToItem(null, 1);
157
+ });
158
+ const devices = instance.findAllByType(Device);
159
+ expect(devices).toHaveLength(2);
160
+
161
+ await act(async () => {
162
+ await devices[0].props.onPress({ id: 1 });
163
+ });
164
+ await act(async () => {
165
+ await devices[1].props.onPress({ id: 2 });
166
+ });
167
+ await act(async () => {
168
+ await devices[0].props.onPress({ id: 1 });
169
+ });
170
+
171
+ await act(async () => {
172
+ await bottomButton.props.onPressMain();
173
+ });
174
+ expect(mock.history.post).toHaveLength(2);
175
+ expect(mockGoBack).toBeCalled();
176
+ });
177
+
178
+ test('test select only devices then click done', async () => {
179
+ mock.onGet(API.UNIT.DEVICES_NOT_FAVORITES(1)).replyOnce(200, dataDevice);
180
+ mock
181
+ .onGet(API.UNIT.AUTOMATE_SCRIPTS_NOT_STARRED(1))
182
+ .replyOnce(200, dataAutomate);
183
+ mock.onPost(API.UNIT.ADD_DEVICES_TO_FAVORITES(1)).replyOnce(200);
184
+ mock.onPost(API.UNIT.STAR_AUTOMATE_SCRIPTS(1)).replyOnce(200);
185
+
186
+ await act(async () => {
187
+ tree = await create(wrapComponent(route));
188
+ });
189
+ expect(mock.history.get).toHaveLength(2);
190
+ const instance = tree.root;
191
+
192
+ const navbar = instance.findByType(NavBar);
193
+ const bottomButton = instance.findByType(BottomButtonView);
194
+
195
+ await act(async () => {
196
+ await navbar.props.onSnapToItem(null, 1);
197
+ });
198
+ const devices = instance.findAllByType(Device);
199
+ expect(devices).toHaveLength(2);
200
+
201
+ await act(async () => {
202
+ await devices[0].props.onPress({ id: 1 });
203
+ });
204
+ await act(async () => {
205
+ await bottomButton.props.onPressMain();
206
+ });
207
+ expect(mock.history.post).toHaveLength(1);
208
+ expect(mockGoBack).toBeCalled();
209
+ });
210
+
211
+ test('test select only scripts then click done', async () => {
212
+ mock.onGet(API.UNIT.DEVICES_NOT_FAVORITES(1)).replyOnce(200, dataDevice);
213
+ mock
214
+ .onGet(API.UNIT.AUTOMATE_SCRIPTS_NOT_STARRED(1))
215
+ .replyOnce(200, dataAutomate);
216
+ mock.onPost(API.UNIT.ADD_DEVICES_TO_FAVORITES(1)).replyOnce(200);
217
+ mock.onPost(API.UNIT.STAR_AUTOMATE_SCRIPTS(1)).replyOnce(200);
218
+
219
+ await act(async () => {
220
+ tree = await create(wrapComponent(route));
221
+ });
222
+ expect(mock.history.get).toHaveLength(2);
223
+ const instance = tree.root;
224
+
225
+ const bottomButton = instance.findByType(BottomButtonView);
226
+
227
+ const scripts = instance.findAllByType(AutomateScript);
228
+ expect(scripts).toHaveLength(4);
229
+
230
+ await act(async () => {
231
+ await scripts[0].props.onPress({ script: { id: 1 } });
232
+ });
233
+ await act(async () => {
234
+ await bottomButton.props.onPressMain();
235
+ });
236
+ expect(mock.history.post).toHaveLength(1);
237
+ expect(mockGoBack).toBeCalled();
238
+ });
239
+
240
+ test('test click done call api fail not goBack', async () => {
241
+ mock.onGet(API.UNIT.DEVICES_NOT_FAVORITES(1)).replyOnce(200, dataDevice);
242
+ mock
243
+ .onGet(API.UNIT.AUTOMATE_SCRIPTS_NOT_STARRED(1))
244
+ .replyOnce(200, dataAutomate);
245
+ mock.onPost(API.UNIT.ADD_DEVICES_TO_FAVORITES(1)).replyOnce(200);
246
+ mock.onPost(API.UNIT.STAR_AUTOMATE_SCRIPTS(1)).replyOnce(400);
247
+
248
+ await act(async () => {
249
+ tree = await create(wrapComponent(route));
250
+ });
251
+ expect(mock.history.get).toHaveLength(2);
252
+ const instance = tree.root;
253
+
254
+ const bottomButton = instance.findByType(BottomButtonView);
255
+
256
+ const scripts = instance.findAllByType(AutomateScript);
257
+ expect(scripts).toHaveLength(4);
258
+
259
+ await act(async () => {
260
+ await scripts[0].props.onPress({ script: { id: 1 } });
261
+ });
262
+ await act(async () => {
263
+ await bottomButton.props.onPressMain();
264
+ });
265
+ expect(mock.history.post).toHaveLength(1);
266
+ });
267
+ });
@@ -0,0 +1,65 @@
1
+ import React from 'react';
2
+ import { View, TouchableWithoutFeedback } from 'react-native';
3
+ import { IconOutline } from '@ant-design/icons-react-native';
4
+ import { useTranslations } from '../../../../hooks/Common/useTranslations';
5
+ import Text from '../../../../commons/Text';
6
+ import FImage from '../../../../commons/FImage';
7
+ import OneTap from '../../../../../assets/images/OneTap.svg';
8
+ import ValueChange from '../../../../../assets/images/ValueChange.svg';
9
+ import Schedule from '../../../../../assets/images/Schedule.svg';
10
+ import Event from '../../../../../assets/images/Event.svg';
11
+ import styles from './styles';
12
+ import { AUTOMATE_TYPE } from '../../../../configs/Constants';
13
+ import { Colors } from '../../../../configs';
14
+
15
+ const AutomateScript = ({ automate, onPress, isSelected }) => {
16
+ const t = useTranslations();
17
+ const { script, type, author = '' } = automate;
18
+
19
+ const _onPress = () => {
20
+ onPress && onPress(automate);
21
+ };
22
+
23
+ const displayIcon = () => {
24
+ const iconKit = script?.icon_kit;
25
+ if (iconKit) {
26
+ return <FImage source={{ uri: iconKit }} style={styles.iconSensor} />;
27
+ } else if (type === AUTOMATE_TYPE.ONE_TAP) {
28
+ return <OneTap />;
29
+ } else if (type === AUTOMATE_TYPE.VALUE_CHANGE) {
30
+ return <ValueChange />;
31
+ } else if (type === AUTOMATE_TYPE.EVENT) {
32
+ return <Event />;
33
+ } else {
34
+ return <Schedule />;
35
+ }
36
+ };
37
+
38
+ return (
39
+ <TouchableWithoutFeedback onPress={_onPress}>
40
+ <View style={[styles.container, isSelected && styles.active]}>
41
+ <View style={styles.boxIcon}>{displayIcon()}</View>
42
+ <View>
43
+ <Text
44
+ numberOfLines={1}
45
+ semibold
46
+ size={14}
47
+ color={Colors.Gray9}
48
+ type="Body"
49
+ style={styles.name}
50
+ >
51
+ {script?.name}
52
+ </Text>
53
+ <View style={styles.descriptionContainer}>
54
+ <Text numberOfLines={1} type={'Label'} color={Colors.Gray7}>
55
+ {`${t('create_by')} ${author}`}
56
+ </Text>
57
+ <IconOutline name="right" size={12} />
58
+ </View>
59
+ </View>
60
+ </View>
61
+ </TouchableWithoutFeedback>
62
+ );
63
+ };
64
+
65
+ export default AutomateScript;
@@ -0,0 +1,48 @@
1
+ import { StyleSheet } from 'react-native';
2
+ import { Constants, Colors } from '../../../../configs';
3
+
4
+ const marginItem = 12;
5
+ const marginHorizontal = 16;
6
+ const widthItem = (Constants.width - marginHorizontal * 2 - marginItem) / 2;
7
+ const heightItem = (widthItem / 166) * 96;
8
+
9
+ export default StyleSheet.create({
10
+ active: {
11
+ borderColor: Colors.Primary,
12
+ borderWidth: 2,
13
+ },
14
+ container: {
15
+ padding: 12,
16
+ borderRadius: 10,
17
+ shadowColor: Colors.Shadow,
18
+ shadowOffset: {
19
+ width: 0,
20
+ height: 2,
21
+ },
22
+ shadowOpacity: 0.1,
23
+ shadowRadius: 3,
24
+ elevation: 4,
25
+ width: widthItem,
26
+ height: heightItem,
27
+ backgroundColor: Colors.White,
28
+ justifyContent: 'space-between',
29
+ marginBottom: 8,
30
+ },
31
+ boxIcon: {
32
+ flexDirection: 'row',
33
+ justifyContent: 'space-between',
34
+ },
35
+ descriptionContainer: {
36
+ flexDirection: 'row',
37
+ justifyContent: 'space-between',
38
+ alignItems: 'center',
39
+ },
40
+ iconSensor: {
41
+ width: 40,
42
+ height: 40,
43
+ resizeMode: 'contain',
44
+ },
45
+ name: {
46
+ marginTop: 6,
47
+ },
48
+ });
@@ -997,5 +997,6 @@
997
997
  "activated": "Activated",
998
998
  "text_unit_add_to_favorites_no_devices": "You don't have any devices or all your devices was added to your favorites",
999
999
  "not_found": "Not found",
1000
- "not_activated": "Not activated"
1000
+ "not_activated": "Not activated",
1001
+ "create_contact_success": "Create contact success!"
1001
1002
  }
@@ -998,5 +998,6 @@
998
998
  "activated": "Được kích hoạt",
999
999
  "text_unit_add_to_favorites_no_devices": "Bạn không có thiết bị nào hoặc tất cả thiết bị của bạn đã được thêm vào yêu thích",
1000
1000
  "not_found": "Không tìm thấy",
1001
- "not_activated": "Không kích hoạt"
1001
+ "not_activated": "Không kích hoạt",
1002
+ "create_contact_success": "Tạo liên hệ thành công!"
1002
1003
  }
@@ -146,7 +146,7 @@ const Routes = {
146
146
  ItemPasscode: 'ItemPasscode',
147
147
  UnitMemberInformation: 'UnitMemberInformation',
148
148
  EnterPassword: 'EnterPassword',
149
- SelectFavoritesDevices: 'SelectFavoritesDevices',
149
+ SelectAddToFavorites: 'SelectAddToFavorites',
150
150
  };
151
151
 
152
152
  export default Routes;
@@ -1,110 +0,0 @@
1
- import React from 'react';
2
- import { act, create } from 'react-test-renderer';
3
- import MockAdapter from 'axios-mock-adapter';
4
-
5
- import { SCProvider } from '../../../context';
6
- import { mockSCStore } from '../../../context/mockStore';
7
- import SelectFavoritesDevices from '../SelectFavoritesDevices';
8
- import Device from '../../AddNewAction/Device';
9
- import BottomButtonView from '../../../commons/BottomButtonView';
10
- import { API } from '../../../configs';
11
- import api from '../../../utils/Apis/axios';
12
-
13
- const wrapComponent = (route) => (
14
- <SCProvider initState={mockSCStore({})}>
15
- <SelectFavoritesDevices route={route} />
16
- </SCProvider>
17
- );
18
-
19
- const mock = new MockAdapter(api.axiosInstance);
20
-
21
- const mockGoBack = jest.fn();
22
- jest.mock('@react-navigation/native', () => {
23
- return {
24
- ...jest.requireActual('@react-navigation/native'),
25
- useNavigation: () => ({
26
- goBack: mockGoBack,
27
- }),
28
- };
29
- });
30
-
31
- jest.mock('react', () => {
32
- return {
33
- ...jest.requireActual('react'),
34
- memo: (x) => x,
35
- };
36
- });
37
-
38
- describe('Test SelectFavoritesDevices', () => {
39
- let tree, route;
40
-
41
- beforeAll(() => {
42
- mockGoBack.mockClear();
43
- mock.resetHistory();
44
- route = {
45
- params: {
46
- unitId: 1,
47
- },
48
- };
49
- });
50
-
51
- test('test select then add devices to favorites', async () => {
52
- let data = [
53
- {
54
- id: 1,
55
- name: 'station 1',
56
- devices: [
57
- {
58
- id: 1,
59
- name: 'device 1',
60
- icon: 'sensor',
61
- icon_kit: null,
62
- },
63
- {
64
- id: 2,
65
- name: 'device 2',
66
- icon: null,
67
- icon_kit: 'icon',
68
- },
69
- ],
70
- },
71
- {
72
- id: 2,
73
- name: 'station 2',
74
- devices: [],
75
- },
76
- ];
77
- mock.onGet(API.UNIT.DEVICES(1)).replyOnce(200, data);
78
- mock.onPost(API.UNIT.ADD_DEVICES_TO_FAVORITES(1)).replyOnce(200);
79
-
80
- await act(async () => {
81
- tree = await create(wrapComponent(route));
82
- });
83
- const instance = tree.root;
84
-
85
- const bottomButton = instance.findByType(BottomButtonView);
86
-
87
- await act(async () => {
88
- await bottomButton.props.onPressMain();
89
- });
90
- expect(mock.history.post).toHaveLength(0);
91
-
92
- const devices = instance.findAllByType(Device);
93
- expect(devices).toHaveLength(2);
94
-
95
- await act(async () => {
96
- await devices[0].props.onPress({ id: 1 });
97
- });
98
- await act(async () => {
99
- await devices[1].props.onPress({ id: 2 });
100
- });
101
- await act(async () => {
102
- await devices[0].props.onPress({ id: 1 });
103
- });
104
- await act(async () => {
105
- await bottomButton.props.onPressMain();
106
- });
107
- expect(mock.history.post).toHaveLength(1);
108
- expect(mockGoBack).toBeCalled();
109
- });
110
- });