@eohjsc/react-native-smart-city 0.7.14 → 0.7.15

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.7.14",
4
+ "version": "0.7.15",
5
5
  "description": "TODO",
6
6
  "main": "index.js",
7
7
  "files": [
@@ -29,13 +29,13 @@ const IFrame = memo(({ item = {} }) => {
29
29
  }, [id, url]);
30
30
 
31
31
  return (
32
- <View>
32
+ <View style={styles.viewIframe}>
33
33
  <TouchableOpacity style={styles.reloadButton} onClick={reload}>
34
34
  <IconComponent size={20} icon="ReloadOutlined" />
35
35
  </TouchableOpacity>
36
36
  <WebView
37
37
  source={{ uri: urlWithEnv }}
38
- style={styles.iframe}
38
+ className={styles.iframe}
39
39
  ref={ref}
40
40
  title={title}
41
41
  javaScriptEnabled
@@ -30,4 +30,9 @@ export const styles = StyleSheet.create({
30
30
  settingWidget: {
31
31
  width: 256,
32
32
  },
33
+ viewIframe: {
34
+ width: '100%',
35
+ display: 'flex',
36
+ height: 200,
37
+ },
33
38
  });
@@ -21,7 +21,8 @@ const API = {
21
21
  AUTOMATE: (id) => `/property_manager/units/${id}/automate/`,
22
22
  DEVICE_CONTROL: (id) => `/property_manager/units/${id}/device_control/`,
23
23
  DEVICE_SENSOR: (id) => `/property_manager/units/${id}/device_sensor/`,
24
- SENSORS_STATUS: (id) => `/property_manager/units/${id}/sensors_status/`,
24
+ END_DEVICES_STATUS: (id) =>
25
+ `/property_manager/units/${id}/end_devices_status/`,
25
26
  CHANGE_OWNER: (id) => `/property_manager/units/${id}/change_owner/`,
26
27
  FAVOURITE_DEVICES: (id) =>
27
28
  `/property_manager/units/${id}/favourite_devices/`,
@@ -1,36 +1,51 @@
1
1
  import { useFocusEffect, useNavigation } from '@react-navigation/native';
2
2
  import { useCallback, useRef } from 'react';
3
- import { BackHandler } from 'react-native';
3
+ import { BackHandler, Platform } from 'react-native';
4
4
 
5
5
  export const useBlockBack = (actionBack) => {
6
6
  const navigation = useNavigation();
7
- const isBeforeRemoveHandled = useRef(false);
7
+ const isListening = useRef(false);
8
8
 
9
9
  const blockBack = useCallback(() => {
10
10
  actionBack && actionBack();
11
11
  return true;
12
12
  }, [actionBack]);
13
13
 
14
+ const blockBeforeRemove = useCallback(
15
+ (e) => {
16
+ e.preventDefault();
17
+ blockBack();
18
+ },
19
+ [blockBack]
20
+ );
21
+
14
22
  useFocusEffect(
15
23
  useCallback(() => {
16
- const onBeforeRemove = (e) => {
17
- if (!isBeforeRemoveHandled.current) {
18
- isBeforeRemoveHandled.current = true;
19
- e.preventDefault();
20
- blockBack();
21
- }
22
- };
23
-
24
- BackHandler.addEventListener('hardwareBackPress', blockBack);
25
- const unsubscribe = navigation.addListener(
26
- 'beforeRemove',
27
- onBeforeRemove
28
- );
29
-
30
- return () => {
31
- BackHandler.removeEventListener('hardwareBackPress', blockBack);
32
- unsubscribe();
33
- };
34
- }, [blockBack, navigation])
24
+ if (isListening.current) {
25
+ return;
26
+ }
27
+ isListening.current = true;
28
+
29
+ if (Platform.OS === 'ios') {
30
+ const unsubscribe = navigation.addListener(
31
+ 'beforeRemove',
32
+ blockBeforeRemove
33
+ );
34
+
35
+ return () => {
36
+ unsubscribe();
37
+ isListening.current = false;
38
+ };
39
+ }
40
+
41
+ if (Platform.OS === 'android') {
42
+ BackHandler.addEventListener('hardwareBackPress', blockBack);
43
+
44
+ return () => {
45
+ BackHandler.removeEventListener('hardwareBackPress', blockBack);
46
+ isListening.current = false;
47
+ };
48
+ }
49
+ }, [blockBack, blockBeforeRemove, navigation])
35
50
  );
36
51
  };
@@ -1,7 +1,7 @@
1
1
  import { useCallback, useContext, useEffect, useRef } from 'react';
2
2
  import { useIsFocused } from '@react-navigation/native';
3
3
  import { SCContext, useSCContextSelector } from '../../context';
4
- import { axiosGet } from '../../utils/Apis/axios';
4
+ import { axiosPost } from '../../utils/Apis/axios';
5
5
  import { API } from '../../configs';
6
6
  import { Action } from '../../context/actionType';
7
7
 
@@ -13,7 +13,7 @@ const useDevicesStatus = (unit, devices) => {
13
13
  (state) => state.app.isNetworkConnected
14
14
  );
15
15
  const isFocused = useIsFocused();
16
- const hasFetched = useRef(false); // Track if data has been fetched
16
+ const hasFetched = useRef([]); // Track if data has been fetched
17
17
 
18
18
  const getDevicesStatus = useCallback(
19
19
  async (_unit, _devices) => {
@@ -21,24 +21,29 @@ const useDevicesStatus = (unit, devices) => {
21
21
  clearTimeout(timeoutId);
22
22
  timeoutId = null;
23
23
  }
24
- const params = new URLSearchParams();
25
- _devices.forEach((device) => {
26
- params.append('sensors', device.id);
27
- });
28
- const { success, data } = await axiosGet(
29
- API.UNIT.SENSORS_STATUS(_unit.id),
24
+ const end_device_ids = _devices.map((item) => item.id);
25
+
26
+ if (hasFetched.current.toString() === end_device_ids.toString()) {
27
+ return;
28
+ }
29
+ const { success, data } = await axiosPost(
30
+ API.UNIT.END_DEVICES_STATUS(_unit.id),
30
31
  {
31
- params: params,
32
+ end_device_ids,
32
33
  }
33
34
  );
34
- success && setAction(Action.SET_DEVICES_STATUS, data);
35
+
36
+ if (success) {
37
+ hasFetched.current = end_device_ids;
38
+ setAction(Action.SET_DEVICES_STATUS, data);
39
+ }
35
40
  timeoutId = setTimeout(() => getDevicesStatus(_unit, _devices), 10000);
36
41
  },
37
42
  [setAction]
38
43
  );
39
44
 
40
45
  useEffect(() => {
41
- if (!isFocused || !isNetworkConnected || hasFetched.current) {
46
+ if (!isFocused || !isNetworkConnected) {
42
47
  return;
43
48
  }
44
49
  if (!devices?.length) {
@@ -53,7 +58,6 @@ const useDevicesStatus = (unit, devices) => {
53
58
  }
54
59
 
55
60
  getDevicesStatus(unit, devices);
56
- hasFetched.current = true; // Mark as fetched
57
61
  return () => {
58
62
  if (timeoutId) {
59
63
  clearTimeout(timeoutId);
@@ -65,7 +69,6 @@ const useDevicesStatus = (unit, devices) => {
65
69
  useEffect(() => {
66
70
  // Reset the hasFetched flag when the component is no longer focused
67
71
  if (!isFocused) {
68
- hasFetched.current = false;
69
72
  if (timeoutId) {
70
73
  clearTimeout(timeoutId); // Clear timeout when losing focus
71
74
  timeoutId = null;
@@ -221,23 +221,21 @@ const ScanDeviceLocal = ({ route }) => {
221
221
  title={t('device_scaned')}
222
222
  isShowSeparator
223
223
  />
224
- {!deviceList.length ? (
224
+
225
+ <View style={styles.rowContainer}>
226
+ <Text style={styles.subTitle} type="Body">
227
+ {t('select_device_and_connect')}
228
+ </Text>
225
229
  <ActivityIndicator style={styles.containerLoading} />
226
- ) : (
227
- <>
228
- <Text style={styles.subTitle} type="Body">
229
- {t('select_device_and_connect')}
230
- </Text>
231
- <FlatList
232
- style={styles.listContainer}
233
- keyExtractor={(item) => item?.host}
234
- data={deviceList}
235
- renderItem={renderItem}
236
- extraData={deviceList}
237
- numColumns={1}
238
- />
239
- </>
240
- )}
230
+ </View>
231
+ <FlatList
232
+ style={styles.listContainer}
233
+ keyExtractor={(item) => item?.host}
234
+ data={deviceList}
235
+ renderItem={renderItem}
236
+ extraData={deviceList}
237
+ numColumns={1}
238
+ />
241
239
 
242
240
  <ViewButtonBottom
243
241
  leftTitle={t('cancel')}
@@ -6,6 +6,10 @@ export default StyleSheet.create({
6
6
  flex: 1,
7
7
  backgroundColor: Colors.Gray2,
8
8
  },
9
+ rowContainer: {
10
+ flexDirection: 'row',
11
+ alignItems: 'center',
12
+ },
9
13
  title: {
10
14
  marginVertical: 16,
11
15
  marginLeft: 16,
@@ -53,6 +57,7 @@ export default StyleSheet.create({
53
57
  containerLoading: {
54
58
  flex: 1,
55
59
  justifyContent: 'center',
56
- alignItems: 'center',
60
+ alignItems: 'flex-start',
61
+ marginLeft: 16,
57
62
  },
58
63
  });
@@ -105,6 +105,9 @@ describe('test ScanDeviceLocal', () => {
105
105
  );
106
106
  expect(zeroconfMock.on).toHaveBeenCalledWith('error', expect.any(Function));
107
107
  expect(zeroconfMock.scan).toHaveBeenCalledWith('plugandplay', 'tcp');
108
+
109
+ const activityIndicator = instance.findByType(ActivityIndicator);
110
+ expect(activityIndicator).toBeTruthy();
108
111
  };
109
112
 
110
113
  it('test connect gateway and navigate', async () => {
@@ -382,17 +385,6 @@ describe('test ScanDeviceLocal', () => {
382
385
  expect(mockedGoBack).toHaveBeenCalled();
383
386
  });
384
387
 
385
- it('test renders loading indicator when device list is empty', async () => {
386
- const route = { params: { unit: 1 } };
387
- await act(async () => {
388
- tree = await renderer.create(wrapComponent(route));
389
- });
390
- const instance = tree.root;
391
-
392
- const activityIndicator = instance.findByType(ActivityIndicator);
393
- expect(activityIndicator).toBeTruthy();
394
- });
395
-
396
388
  it('test renders device list when devices are available', async () => {
397
389
  const route = { params: { unit: 1 } };
398
390
  await act(async () => {
@@ -451,8 +443,7 @@ describe('test ScanDeviceLocal', () => {
451
443
  await removeCallback('Device 1');
452
444
  });
453
445
 
454
- const flatListAfter = instance.findAllByType(FlatList);
455
- expect(flatListAfter.length).toBe(0);
446
+ expect(flatList.props.data).toEqual([]);
456
447
 
457
448
  const activityIndicator = instance.findByType(ActivityIndicator);
458
449
  expect(activityIndicator).toBeTruthy();
@@ -1,4 +1,5 @@
1
1
  import React from 'react';
2
+ import { BackHandler, Platform } from 'react-native';
2
3
  import { create, act } from 'react-test-renderer';
3
4
  import Toast from 'react-native-toast-message';
4
5
  import { useNavigation } from '@react-navigation/native';
@@ -40,10 +41,12 @@ describe('Test ScriptDetail', () => {
40
41
  const mockGoBack = useNavigation().goBack;
41
42
  const mockAddListener = useNavigation().addListener;
42
43
  const mockedNavigate = useNavigation().navigate;
44
+ const mockedDispatch = useNavigation().dispatch;
43
45
  beforeEach(() => {
44
46
  mockGoBack.mockClear();
45
47
  mockAddListener.mockClear();
46
48
  mockedNavigate.mockClear();
49
+ mockedDispatch.mockClear();
47
50
  global.mockedNavigate.mockClear();
48
51
  mock.reset();
49
52
  route = {
@@ -596,11 +599,35 @@ describe('Test ScriptDetail', () => {
596
599
  menuActionMore.props.onItemClick({ doAction: jest.fn() });
597
600
  menuActionMore.props.listMenuItem[2].doAction();
598
601
  });
599
- expect(global.mockedNavigate).toBeCalledWith(Routes.AddUnknownTypeSmart, {
600
- automate,
601
- closeScreen: undefined,
602
+
603
+ const mockState = {
604
+ routes: [{ name: 'Home' }],
605
+ };
606
+ const updatedState = mockedDispatch.mock.calls[0][0](mockState);
607
+ expect(updatedState).toEqual({
608
+ type: 'RESET',
609
+ payload: {
610
+ index: 2,
611
+ routes: [
612
+ {
613
+ name: 'Home',
614
+ },
615
+ {
616
+ name: Routes.ScriptDetail,
617
+ params: route.params,
618
+ },
619
+ {
620
+ name: Routes.AddUnknownTypeSmart,
621
+ params: {
622
+ automate: automate,
623
+ closeScreen: undefined,
624
+ },
625
+ },
626
+ ],
627
+ },
602
628
  });
603
- global.mockedNavigate.mockClear();
629
+ expect(mockedDispatch).toHaveBeenCalledTimes(1);
630
+
604
631
  expect(menuActionMore.props.listMenuItem[0].text).toEqual('Device display');
605
632
  await act(async () => {
606
633
  menuActionMore.props.listMenuItem[0].doAction();
@@ -705,6 +732,7 @@ describe('Test ScriptDetail', () => {
705
732
  });
706
733
 
707
734
  it('test navigate to UnitDetail on event beforeRemove', async () => {
735
+ Platform.OS = 'ios';
708
736
  route.params.closeScreen = Routes.UnitDetail;
709
737
  route.params.preAutomate.unit = 2;
710
738
 
@@ -729,4 +757,31 @@ describe('Test ScriptDetail', () => {
729
757
  unitId: 2,
730
758
  });
731
759
  });
760
+
761
+ it('test navigate to UnitDetail on event hardwareBackPress', async () => {
762
+ Platform.OS = 'android';
763
+ jest.spyOn(BackHandler, 'addEventListener');
764
+ route.params.closeScreen = Routes.UnitDetail;
765
+ route.params.preAutomate.unit = 2;
766
+
767
+ await act(async () => {
768
+ await create(wrapComponent(route));
769
+ });
770
+
771
+ expect(BackHandler.addEventListener).toHaveBeenCalledWith(
772
+ 'hardwareBackPress',
773
+ expect.any(Function)
774
+ );
775
+ const backPressHandler = BackHandler.addEventListener.mock.calls.find(
776
+ ([eventName]) => eventName === 'hardwareBackPress'
777
+ )[1];
778
+
779
+ await act(async () => {
780
+ backPressHandler();
781
+ });
782
+
783
+ expect(mockedNavigate).toHaveBeenCalledWith(Routes.UnitDetail, {
784
+ unitId: 2,
785
+ });
786
+ });
732
787
  });
@@ -10,7 +10,11 @@ import { PopoverMode } from 'react-native-popover-view';
10
10
  import { IconFill, IconOutline } from '@ant-design/icons-react-native';
11
11
  import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
12
12
 
13
- import { useIsFocused, useNavigation } from '@react-navigation/native';
13
+ import {
14
+ CommonActions,
15
+ useIsFocused,
16
+ useNavigation,
17
+ } from '@react-navigation/native';
14
18
  import Add from '../../../../assets/images/Add.svg';
15
19
  import Delay from '../../../../assets/images/Delay.svg';
16
20
  import Notify from '../../../../assets/images/Notify.svg';
@@ -39,7 +43,7 @@ import ItemConditionScriptDetail from '../../../commons/Automate/ItemConditionSc
39
43
  const PreventDoubleTouch = withPreventDoubleClick(TouchableOpacity);
40
44
 
41
45
  const ScriptDetail = ({ route }) => {
42
- const { navigate, goBack } = useNavigation();
46
+ const { dispatch, navigate, goBack } = useNavigation();
43
47
  const { params = {} } = route;
44
48
  const refMenuAction = useRef();
45
49
  const { childRef, showingPopover, showPopoverWithRef, hidePopover } =
@@ -56,8 +60,9 @@ const ScriptDetail = ({ route }) => {
56
60
  preAutomate = {}, // pre-loaded automate data
57
61
  newActionsList, // updated actions list
58
62
  closeScreen,
63
+ automate: currentAutomate, // when edit and close
59
64
  } = params;
60
- const [automate, setAutomate] = useState(preAutomate);
65
+ const [automate, setAutomate] = useState(currentAutomate || preAutomate);
61
66
  const isFocused = useIsFocused();
62
67
  const [data, setData] = useState([]);
63
68
  const [isShowRename, setIsShowRename] = useState(false);
@@ -103,11 +108,42 @@ const ScriptDetail = ({ route }) => {
103
108
  ToastBottomHelper.error(t('this_script_has_been_disabled'));
104
109
  return;
105
110
  }
106
- navigate(Routes.AddUnknownTypeSmart, {
107
- automate,
108
- closeScreen: route.name,
111
+
112
+ // Reset stack and navigate to AddUnknownTypeSmart
113
+ dispatch((state) => {
114
+ const index = state.routes.findIndex((r) => r.name === closeScreen);
115
+ let routes = [];
116
+ if (!closeScreen) {
117
+ routes = state.routes;
118
+ } else if (index >= 0) {
119
+ routes = state.routes.slice(0, index + 1);
120
+ }
121
+ const hasScriptDetail = routes.some(
122
+ (r) => r.name === Routes.ScriptDetail
123
+ );
124
+
125
+ if (!hasScriptDetail) {
126
+ routes.push({
127
+ name: Routes.ScriptDetail,
128
+ params: route.params,
129
+ });
130
+ }
131
+
132
+ routes.push({
133
+ name: Routes.AddUnknownTypeSmart,
134
+ params: {
135
+ automate,
136
+ closeScreen: route.name,
137
+ },
138
+ });
139
+
140
+ return CommonActions.reset({
141
+ ...state,
142
+ routes: routes,
143
+ index: routes.length - 1,
144
+ });
109
145
  });
110
- }, [automate, can_edit, enableScript, navigate, route.name, t]);
146
+ }, [automate, can_edit, closeScreen, enableScript, dispatch, route, t]);
111
147
 
112
148
  const listMenuItem = useMemo(
113
149
  () => [
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import { create, act } from 'react-test-renderer';
3
- import { TouchableOpacity } from 'react-native';
3
+ import { FlatList, RefreshControl, TouchableOpacity } from 'react-native';
4
4
  import Modal from 'react-native-modal';
5
5
  import MockAdapter from 'axios-mock-adapter';
6
6
 
@@ -67,4 +67,25 @@ describe('test SharedUnit', () => {
67
67
  });
68
68
  expect(tabHeader.props.textFilter).toEqual(t('text_latest_date'));
69
69
  });
70
+
71
+ it('should trigger refresh when pulling down', async () => {
72
+ mock
73
+ .onGet(API.UNIT.FILTER_SHARED_UNITS())
74
+ .reply(200, [{ id: 1, is_star: false, name: 'Unit 1' }]);
75
+
76
+ await act(async () => {
77
+ tree = await create(wrapComponent());
78
+ });
79
+
80
+ const instance = tree.root;
81
+ const flatList = instance.findByType(FlatList);
82
+
83
+ const refreshControl = flatList.findByType(RefreshControl);
84
+ expect(refreshControl.props.refreshing).toBe(false);
85
+
86
+ await act(async () => {
87
+ refreshControl.props.onRefresh();
88
+ });
89
+ expect(mock.history.get.length).toBeGreaterThan(0);
90
+ });
70
91
  });
@@ -46,7 +46,7 @@ const UnitMemberList = ({ route }) => {
46
46
  return;
47
47
  }
48
48
 
49
- if (permissions?.max_members_per_unit <= dataMembers.length) {
49
+ if (permissions?.max_members_per_unit < dataMembers.length) {
50
50
  ToastBottomHelper.error(
51
51
  t('reach_max_members_per_unit', {
52
52
  length: permissions?.max_members_per_unit,
@@ -81,6 +81,16 @@ describe('test MemberList', () => {
81
81
  });
82
82
 
83
83
  it('add new member but reach limit', async () => {
84
+ const dataMember = [
85
+ {
86
+ id: 1,
87
+ name: 'owner',
88
+ avatar: 'https://image1.jpg',
89
+ phone_number: '0933123456',
90
+ },
91
+ ];
92
+ mockAxios.onGet(API.SHARE.UNITS_MEMBERS(1)).reply(200, dataMember);
93
+
84
94
  let tree;
85
95
  await act(async () => {
86
96
  tree = await create(
@@ -59,7 +59,7 @@ describe('Test UnitDetail', () => {
59
59
 
60
60
  const detailUnitApiUrl = API.UNIT.UNIT_DETAIL(1);
61
61
  const summaryUnitApiUrl = API.UNIT.UNIT_SUMMARY(1);
62
- const sensorStatusApiUrl = API.UNIT.SENSORS_STATUS(1);
62
+ const sensorStatusApiUrl = API.UNIT.END_DEVICES_STATUS(1);
63
63
  const getAutomates = API.UNIT.AUTOMATE(1);
64
64
 
65
65
  let tree;