@eohjsc/react-native-smart-city 0.3.10 → 0.3.11
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 +1 -1
- package/src/commons/SubUnit/Favorites/index.js +6 -2
- package/src/configs/API.js +4 -0
- package/src/context/actionType.ts +3 -2
- package/src/context/mockStore.ts +17 -3
- package/src/context/reducer.ts +28 -4
- package/src/hooks/IoT/__test__/useValueEvaluation.test.js +58 -0
- package/src/hooks/IoT/index.js +2 -1
- package/src/hooks/IoT/useValueEvaluation.js +45 -0
- package/src/navigations/UnitStack.js +8 -0
- package/src/screens/AddNewAction/SelectAction.js +3 -2
- package/src/screens/AddNewAction/SetupSensor.js +3 -2
- package/src/screens/Device/detail.js +18 -27
- package/src/screens/Device/hooks/useEvaluateValue.js +97 -0
- package/src/screens/Device/hooks/useFavoriteDevice.js +2 -2
- package/src/screens/ScriptDetail/index.js +13 -3
- package/src/screens/Unit/Detail.js +9 -4
- package/src/screens/Unit/SelectAddress.js +1 -3
- package/src/screens/Unit/SelectDevices.js +158 -0
- package/src/screens/Unit/SelectDevicesStyles.js +40 -0
- package/src/screens/Unit/__test__/SelectAddress.test.js +90 -1
- package/src/screens/Unit/__test__/SelectDevices.test.js +110 -0
- package/src/utils/Apis/axios.js +6 -0
- package/src/utils/I18n/translations/en.json +2 -0
- package/src/utils/I18n/translations/vi.json +2 -0
- package/src/utils/Route/index.js +1 -0
- package/src/utils/Validation.js +3 -0
package/package.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { View } from 'react-native';
|
|
3
|
+
import { useNavigation } from '@react-navigation/native';
|
|
3
4
|
import { useTranslations } from '../../../hooks/Common/useTranslations';
|
|
4
5
|
import { useSensorsStatus } from '../../../hooks/Common';
|
|
5
6
|
|
|
@@ -8,7 +9,7 @@ import ItemDevice from '../../Device/ItemDevice';
|
|
|
8
9
|
import ItemOneTap from '../OneTap/ItemOneTap';
|
|
9
10
|
import ItemAddNew from '../../Device/ItemAddNew';
|
|
10
11
|
import styles from './styles';
|
|
11
|
-
import
|
|
12
|
+
import Routes from '../../../utils/Route';
|
|
12
13
|
|
|
13
14
|
const SubUnitFavorites = ({
|
|
14
15
|
isOwner,
|
|
@@ -18,11 +19,14 @@ const SubUnitFavorites = ({
|
|
|
18
19
|
wrapItemStyle,
|
|
19
20
|
}) => {
|
|
20
21
|
const t = useTranslations();
|
|
22
|
+
const { navigate } = useNavigation();
|
|
21
23
|
|
|
22
24
|
const { getStatus, serverDown } = useSensorsStatus(unit, favoriteDevices);
|
|
23
25
|
|
|
24
26
|
const handleOnAddNew = () => {
|
|
25
|
-
|
|
27
|
+
navigate(Routes.SelectDevices, {
|
|
28
|
+
unitId: unit.id,
|
|
29
|
+
});
|
|
26
30
|
};
|
|
27
31
|
|
|
28
32
|
return (
|
package/src/configs/API.js
CHANGED
|
@@ -29,6 +29,9 @@ 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/`,
|
|
33
|
+
ADD_DEVICES_TO_FAVORITES: (id) =>
|
|
34
|
+
`/property_manager/units/${id}/add_devices_to_favourites/`,
|
|
32
35
|
},
|
|
33
36
|
SUB_UNIT: {
|
|
34
37
|
REMOVE_SUB_UNIT: (unitId, id) =>
|
|
@@ -173,6 +176,7 @@ const API = {
|
|
|
173
176
|
`/notifications/eoh/?page=${page}&type=${type}`,
|
|
174
177
|
SET_READ: (id) => `/notifications/eoh/${id}/set_read/`,
|
|
175
178
|
},
|
|
179
|
+
VALUE_EVALUATIONS: () => '/property_manager/config_value_evaluations/',
|
|
176
180
|
EXTERNAL: {
|
|
177
181
|
GOOGLE_MAP: {
|
|
178
182
|
AUTO_COMPLETE:
|
|
@@ -12,14 +12,15 @@ export const Action = {
|
|
|
12
12
|
CAMERA_STATUS_CHANGE: 'CAMERA_STATUS_CHANGE',
|
|
13
13
|
CLOSE_ALL_CAMERA: 'CLOSE_ALL_CAMERA',
|
|
14
14
|
SET_FAVORITE_DEVICES: 'SET_FAVORITE_DEVICES',
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
ADD_DEVICES_TO_FAVORITES: 'ADD_DEVICE_TO_FAVORITES',
|
|
16
|
+
REMOVE_DEVICES_FROM_FAVORITES: 'REMOVE_DEVICE_FROM_FAVORITES',
|
|
17
17
|
SET_STARRED_SCRIPTS: 'SET_STARRED_SCRIPTS',
|
|
18
18
|
STAR_SCRIPT: 'STAR_SCRIPT',
|
|
19
19
|
UNSTAR_SCRIPT: 'UNSTAR_SCRIPT',
|
|
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',
|
|
23
|
+
UPDATE_VALUE_EVALUATIONS: 'UPDATE_VALUE_EVALUATIONS',
|
|
23
24
|
};
|
|
24
25
|
|
|
25
26
|
export type AuthData = {
|
package/src/context/mockStore.ts
CHANGED
|
@@ -31,11 +31,23 @@ export const mockDataStore: ContextData = {
|
|
|
31
31
|
automate: {
|
|
32
32
|
starredScriptIds: [],
|
|
33
33
|
},
|
|
34
|
+
app: {
|
|
35
|
+
isFirstOpenCamera: true,
|
|
36
|
+
isLavidaSource: false,
|
|
37
|
+
isConnectWifiGateway: false,
|
|
38
|
+
isBluetoothEnabled: true,
|
|
39
|
+
isNetworkConnected: true,
|
|
40
|
+
camera_opened: [],
|
|
41
|
+
},
|
|
34
42
|
iot: {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
43
|
+
googlehome: {
|
|
44
|
+
isFirstTimeConnect: true,
|
|
45
|
+
isConnecting: false,
|
|
46
|
+
connections: {},
|
|
47
|
+
},
|
|
38
48
|
},
|
|
49
|
+
valueEvaluations: {},
|
|
50
|
+
fetchedValueEvaluationUnits: [],
|
|
39
51
|
};
|
|
40
52
|
|
|
41
53
|
export const mockSCStore = (data: ContextData): ContextData => {
|
|
@@ -83,5 +95,7 @@ export const mockSCStore = (data: ContextData): ContextData => {
|
|
|
83
95
|
connections: {},
|
|
84
96
|
},
|
|
85
97
|
},
|
|
98
|
+
valueEvaluations: {},
|
|
99
|
+
fetchedValueEvaluationUnits: [],
|
|
86
100
|
};
|
|
87
101
|
};
|
package/src/context/reducer.ts
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
AppType,
|
|
13
13
|
IoTType,
|
|
14
14
|
} from './actionType';
|
|
15
|
-
import { uniq } from 'lodash';
|
|
15
|
+
import { uniq, reduce } from 'lodash';
|
|
16
16
|
|
|
17
17
|
export type ContextData = {
|
|
18
18
|
auth: AuthData;
|
|
@@ -24,6 +24,8 @@ export type ContextData = {
|
|
|
24
24
|
automate: AutomateType;
|
|
25
25
|
app: AppType;
|
|
26
26
|
iot: IoTType;
|
|
27
|
+
valueEvaluations: {};
|
|
28
|
+
fetchedValueEvaluationUnits: Array<number>;
|
|
27
29
|
};
|
|
28
30
|
|
|
29
31
|
export type Action = {
|
|
@@ -65,6 +67,8 @@ export const initialState = {
|
|
|
65
67
|
connections: {},
|
|
66
68
|
},
|
|
67
69
|
},
|
|
70
|
+
valueEvaluations: {},
|
|
71
|
+
fetchedValueEvaluationUnits: [],
|
|
68
72
|
};
|
|
69
73
|
|
|
70
74
|
export const reducer = (currentState: ContextData, action: Action) => {
|
|
@@ -201,7 +205,7 @@ export const reducer = (currentState: ContextData, action: Action) => {
|
|
|
201
205
|
favoriteDeviceIds: payload,
|
|
202
206
|
},
|
|
203
207
|
};
|
|
204
|
-
case Action.
|
|
208
|
+
case Action.ADD_DEVICES_TO_FAVORITES:
|
|
205
209
|
return {
|
|
206
210
|
...currentState,
|
|
207
211
|
unit: {
|
|
@@ -211,13 +215,13 @@ export const reducer = (currentState: ContextData, action: Action) => {
|
|
|
211
215
|
),
|
|
212
216
|
},
|
|
213
217
|
};
|
|
214
|
-
case Action.
|
|
218
|
+
case Action.REMOVE_DEVICES_FROM_FAVORITES:
|
|
215
219
|
return {
|
|
216
220
|
...currentState,
|
|
217
221
|
unit: {
|
|
218
222
|
...currentState.unit,
|
|
219
223
|
favoriteDeviceIds: currentState.unit.favoriteDeviceIds.filter(
|
|
220
|
-
(deviceId) => deviceId
|
|
224
|
+
(deviceId) => !payload.includes(deviceId)
|
|
221
225
|
),
|
|
222
226
|
},
|
|
223
227
|
};
|
|
@@ -294,6 +298,26 @@ export const reducer = (currentState: ContextData, action: Action) => {
|
|
|
294
298
|
},
|
|
295
299
|
};
|
|
296
300
|
|
|
301
|
+
case Action.UPDATE_VALUE_EVALUATIONS:
|
|
302
|
+
// eslint-disable-next-line no-case-declarations
|
|
303
|
+
const { data, unitId } = payload;
|
|
304
|
+
return {
|
|
305
|
+
...currentState,
|
|
306
|
+
fetchedValueEvaluationUnits:
|
|
307
|
+
currentState.fetchedValueEvaluationUnits.indexOf(unitId) !== -1
|
|
308
|
+
? currentState.fetchedValueEvaluationUnits
|
|
309
|
+
: [...currentState.fetchedValueEvaluationUnits, unitId],
|
|
310
|
+
valueEvaluations: reduce(
|
|
311
|
+
data,
|
|
312
|
+
(dict, item) => {
|
|
313
|
+
// eslint-disable-next-line no-param-reassign
|
|
314
|
+
dict[item.config] = item;
|
|
315
|
+
return dict;
|
|
316
|
+
},
|
|
317
|
+
currentState.valueEvaluations
|
|
318
|
+
),
|
|
319
|
+
};
|
|
320
|
+
|
|
297
321
|
default:
|
|
298
322
|
return currentState;
|
|
299
323
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { renderHook } from '@testing-library/react-hooks';
|
|
3
|
+
import MockAdapter from 'axios-mock-adapter';
|
|
4
|
+
import { SCProvider } from '../../../context';
|
|
5
|
+
import { mockSCStore } from '../../../context/mockStore';
|
|
6
|
+
import { useValueEvaluations } from '../index';
|
|
7
|
+
import api from '../../../utils/Apis/axios';
|
|
8
|
+
import { API } from '../../../configs';
|
|
9
|
+
|
|
10
|
+
const mock = new MockAdapter(api.axiosInstance);
|
|
11
|
+
|
|
12
|
+
const mockedSetAction = jest.fn();
|
|
13
|
+
|
|
14
|
+
const wrapper = ({ children }) => <SCProvider>{children}</SCProvider>;
|
|
15
|
+
|
|
16
|
+
const mockUseContext = jest.fn().mockImplementation(() => ({
|
|
17
|
+
stateData: mockSCStore({}),
|
|
18
|
+
setAction: mockedSetAction,
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
React.useContext = mockUseContext;
|
|
22
|
+
|
|
23
|
+
describe('Test useValueEvaluation', () => {
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
mock.resetHistory();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('test unitId null', async () => {
|
|
29
|
+
renderHook(() => useValueEvaluations(null), {
|
|
30
|
+
wrapper,
|
|
31
|
+
});
|
|
32
|
+
expect(mock.history.get.length).toBe(0);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('test unitId not null request success', async () => {
|
|
36
|
+
mock.onGet(API.VALUE_EVALUATIONS()).replyOnce(200, {
|
|
37
|
+
results: [],
|
|
38
|
+
next: 'link',
|
|
39
|
+
});
|
|
40
|
+
mock.onGet(API.VALUE_EVALUATIONS()).replyOnce(200, {
|
|
41
|
+
results: [],
|
|
42
|
+
});
|
|
43
|
+
renderHook(() => useValueEvaluations(1), {
|
|
44
|
+
wrapper,
|
|
45
|
+
});
|
|
46
|
+
expect(mock.history.get.length).toBe(1);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('test request failed', async () => {
|
|
50
|
+
mock.onGet(API.VALUE_EVALUATIONS()).replyOnce(400, {
|
|
51
|
+
results: [],
|
|
52
|
+
});
|
|
53
|
+
renderHook(() => useValueEvaluations(1), {
|
|
54
|
+
wrapper,
|
|
55
|
+
});
|
|
56
|
+
expect(mock.history.get.length).toBe(1);
|
|
57
|
+
});
|
|
58
|
+
});
|
package/src/hooks/IoT/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import useGGHomeConnection from './useGGHomeConnection';
|
|
2
2
|
import useRemoteControl from './useRemoteControl';
|
|
3
|
+
import useValueEvaluations from './useValueEvaluation';
|
|
3
4
|
|
|
4
|
-
export { useGGHomeConnection, useRemoteControl };
|
|
5
|
+
export { useGGHomeConnection, useRemoteControl, useValueEvaluations };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { useCallback, useContext, useEffect } from 'react';
|
|
2
|
+
import { API } from '../../configs';
|
|
3
|
+
import { SCContext, useSCContextSelector } from '../../context';
|
|
4
|
+
import { Action } from '../../context/actionType';
|
|
5
|
+
import { axiosGet } from '../../utils/Apis/axios';
|
|
6
|
+
|
|
7
|
+
const useValueEvaluations = (unitId) => {
|
|
8
|
+
const { setAction } = useContext(SCContext);
|
|
9
|
+
|
|
10
|
+
const fetchConfigValueEvaluations = useCallback(
|
|
11
|
+
async (page = 1) => {
|
|
12
|
+
if (!unitId) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const params = new URLSearchParams();
|
|
16
|
+
params.append('config__end_device__station__unit', unitId);
|
|
17
|
+
params.append('page', page);
|
|
18
|
+
const { success, data } = await axiosGet(API.VALUE_EVALUATIONS(), {
|
|
19
|
+
params,
|
|
20
|
+
});
|
|
21
|
+
if (success) {
|
|
22
|
+
setAction(Action.UPDATE_VALUE_EVALUATIONS, {
|
|
23
|
+
unitId,
|
|
24
|
+
data: data.results,
|
|
25
|
+
});
|
|
26
|
+
if (data.next) {
|
|
27
|
+
await fetchConfigValueEvaluations(page + 1);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
[unitId, setAction]
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const fetchedValueEvaluationUnits = useSCContextSelector((state) => {
|
|
35
|
+
return state.fetchedValueEvaluationUnits || [];
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (!(fetchedValueEvaluationUnits.indexOf(unitId) !== -1)) {
|
|
40
|
+
fetchConfigValueEvaluations();
|
|
41
|
+
}
|
|
42
|
+
}, [unitId, fetchConfigValueEvaluations, fetchedValueEvaluationUnits]);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export default useValueEvaluations;
|
|
@@ -51,6 +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 SelectDevices from '../screens/Unit/SelectDevices';
|
|
54
55
|
import { HanetCameraStack } from './HanetCameraStack';
|
|
55
56
|
import { axiosGet } from '../utils/Apis/axios';
|
|
56
57
|
import { API } from '../configs';
|
|
@@ -400,6 +401,13 @@ export const UnitStack = memo((props) => {
|
|
|
400
401
|
headerShown: false,
|
|
401
402
|
}}
|
|
402
403
|
/>
|
|
404
|
+
<Stack.Screen
|
|
405
|
+
name={Route.SelectDevices}
|
|
406
|
+
component={SelectDevices}
|
|
407
|
+
options={{
|
|
408
|
+
headerShown: false,
|
|
409
|
+
}}
|
|
410
|
+
/>
|
|
403
411
|
</Stack.Navigator>
|
|
404
412
|
);
|
|
405
413
|
});
|
|
@@ -134,6 +134,7 @@ const SelectAction = memo(({ route }) => {
|
|
|
134
134
|
value: itemTemp?.value,
|
|
135
135
|
config_id: itemTemp?.id,
|
|
136
136
|
config_name: itemTemp?.name,
|
|
137
|
+
sensor_type: itemTemp?.sensor_type,
|
|
137
138
|
},
|
|
138
139
|
scriptName,
|
|
139
140
|
});
|
|
@@ -300,9 +301,9 @@ const SelectAction = memo(({ route }) => {
|
|
|
300
301
|
if (isNumberValue) {
|
|
301
302
|
return `${item?.name} ${
|
|
302
303
|
item?.title ? item.title : t('is_below') + ' (<)'
|
|
303
|
-
}
|
|
304
|
+
} ${item?.value} ${item?.unit}`;
|
|
304
305
|
}
|
|
305
|
-
return t(stateConditionData?.stateValue[item?.value
|
|
306
|
+
return t(stateConditionData?.stateValue[item?.value]);
|
|
306
307
|
},
|
|
307
308
|
[t]
|
|
308
309
|
);
|
|
@@ -56,7 +56,7 @@ const SetUpSensor = () => {
|
|
|
56
56
|
modalNumberConditionData[0]
|
|
57
57
|
);
|
|
58
58
|
const [itemActiveStateModal, setItemActiveStateModal] = useState(
|
|
59
|
-
modalStateConditionData?.stateValue[
|
|
59
|
+
modalStateConditionData?.stateValue[1]
|
|
60
60
|
);
|
|
61
61
|
const [value, setValue] = useState(parseFloat(item?.value || 0));
|
|
62
62
|
const [minimum] = useState(isHasLimit ? parseInt(item?.range_min, 10) : 0);
|
|
@@ -226,7 +226,8 @@ const SetUpSensor = () => {
|
|
|
226
226
|
<View style={styles.modalContent}>
|
|
227
227
|
{(isNumberValue
|
|
228
228
|
? modalNumberConditionData
|
|
229
|
-
:
|
|
229
|
+
: /* Disable picking not_active/not_dectect option -> temporaly remove not_acitve option */
|
|
230
|
+
modalStateConditionData?.stateValue.slice(1)
|
|
230
231
|
).map((i, index) => (
|
|
231
232
|
<>
|
|
232
233
|
<TouchableOpacity
|
|
@@ -44,12 +44,13 @@ import {
|
|
|
44
44
|
useBoolean,
|
|
45
45
|
useGGHomeDeviceConnected,
|
|
46
46
|
} from '../../hooks/Common';
|
|
47
|
-
import { useGGHomeConnection } from '../../hooks/IoT';
|
|
47
|
+
import { useGGHomeConnection, useValueEvaluations } from '../../hooks/IoT';
|
|
48
48
|
import { SensorDisplayItem } from './components/SensorDisplayItem';
|
|
49
49
|
import { useSCContextSelector } from '../../context';
|
|
50
50
|
import { EmergencyCountdown } from './components/EmergencyCountdown';
|
|
51
51
|
import { SensorConnectStatusViewHeader } from './components/SensorConnectStatusViewHeader';
|
|
52
52
|
import { useDisconnectedDevice } from './hooks/useDisconnectedDevice';
|
|
53
|
+
import { useEvaluateValue } from './hooks/useEvaluateValue';
|
|
53
54
|
import { Card } from '../../commons/CardShadow';
|
|
54
55
|
import PreventAccess from '../../commons/PreventAccess';
|
|
55
56
|
import { notImplemented } from '../../utils/Utils';
|
|
@@ -117,6 +118,8 @@ const DeviceDetail = ({ route }) => {
|
|
|
117
118
|
});
|
|
118
119
|
}, [display]);
|
|
119
120
|
|
|
121
|
+
useValueEvaluations(unitId || unitData?.id);
|
|
122
|
+
|
|
120
123
|
useDisconnectedDevice(sensorName, isDeviceHasBle, serverDown);
|
|
121
124
|
|
|
122
125
|
const isShowSetupEmergencyContact = useMemo(
|
|
@@ -438,21 +441,32 @@ const DeviceDetail = ({ route }) => {
|
|
|
438
441
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
439
442
|
}, [sensor, unit, isNetworkConnected, fetchDataDeviceDetail]);
|
|
440
443
|
|
|
444
|
+
const evaluateValue = useEvaluateValue();
|
|
445
|
+
|
|
441
446
|
const getData = useCallback(
|
|
442
447
|
(item) => {
|
|
443
448
|
if (!item.configuration) {
|
|
444
449
|
return;
|
|
445
450
|
}
|
|
446
451
|
const data = item.configuration.configs.map((config) => {
|
|
447
|
-
const
|
|
448
|
-
|
|
452
|
+
const configValue = configValues[config.id];
|
|
453
|
+
const displayValue = displayValues.find((k) => k.id === config.id);
|
|
454
|
+
if (!configValue && !displayValue) {
|
|
449
455
|
return;
|
|
450
456
|
}
|
|
457
|
+
const value = configValue
|
|
458
|
+
? {
|
|
459
|
+
id: config.id,
|
|
460
|
+
value: configValue,
|
|
461
|
+
evaluate: evaluateValue(config.id, configValue),
|
|
462
|
+
}
|
|
463
|
+
: displayValue;
|
|
464
|
+
|
|
451
465
|
return { ...config, ...value };
|
|
452
466
|
});
|
|
453
467
|
return data.filter((value) => value);
|
|
454
468
|
},
|
|
455
|
-
[displayValues]
|
|
469
|
+
[configValues, displayValues, evaluateValue]
|
|
456
470
|
);
|
|
457
471
|
|
|
458
472
|
useEffect(() => {
|
|
@@ -509,29 +523,6 @@ const DeviceDetail = ({ route }) => {
|
|
|
509
523
|
}
|
|
510
524
|
}, [sensor, display]);
|
|
511
525
|
|
|
512
|
-
useEffect(() => {
|
|
513
|
-
setDisplayValues((currentDisplayValues) => {
|
|
514
|
-
for (const [configId, value] of Object.entries(configValues)) {
|
|
515
|
-
const intId = parseInt(configId, 10);
|
|
516
|
-
const index = currentDisplayValues.findIndex(
|
|
517
|
-
(element) => element.id === intId
|
|
518
|
-
);
|
|
519
|
-
|
|
520
|
-
const item = currentDisplayValues[index];
|
|
521
|
-
if (index !== -1) {
|
|
522
|
-
currentDisplayValues[index].value = value;
|
|
523
|
-
currentDisplayValues[index].evaluate = item.evaluate;
|
|
524
|
-
} else {
|
|
525
|
-
currentDisplayValues.push({
|
|
526
|
-
id: intId,
|
|
527
|
-
value: value,
|
|
528
|
-
});
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
return currentDisplayValues;
|
|
532
|
-
});
|
|
533
|
-
}, [configValues, setDisplayValues]);
|
|
534
|
-
|
|
535
526
|
const isShowEmergencyResolve =
|
|
536
527
|
display.items.filter(
|
|
537
528
|
(item) =>
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import { useSCContextSelector } from '../../../context';
|
|
3
|
+
|
|
4
|
+
const evaluateRange = (value, configuration) => {
|
|
5
|
+
/*
|
|
6
|
+
configuration: {
|
|
7
|
+
ranges: [
|
|
8
|
+
{ start: 0.5, end: 1.5, evaluate: 'On' },
|
|
9
|
+
{ start: -0.5, end: 0.49, evaluate: {text: 'Off'} },
|
|
10
|
+
]
|
|
11
|
+
}
|
|
12
|
+
*/
|
|
13
|
+
if (!value) {
|
|
14
|
+
// eslint-disable-next-line no-param-reassign
|
|
15
|
+
value = 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
for (let i = 0; i < configuration?.ranges?.length; i++) {
|
|
19
|
+
const range = configuration.ranges[i];
|
|
20
|
+
if (range.start <= value && value <= range.end) {
|
|
21
|
+
return range.evaluate;
|
|
22
|
+
}
|
|
23
|
+
if (!range.end && range.start <= value) {
|
|
24
|
+
return range.evaluate;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return { text: value };
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const evaluateBoolean = (value, configuration) => {
|
|
31
|
+
/*
|
|
32
|
+
configuration: {
|
|
33
|
+
'on': {
|
|
34
|
+
'value': 1,
|
|
35
|
+
'evaluate': {
|
|
36
|
+
'text': 'On',
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
'off': {
|
|
40
|
+
'value': 0,
|
|
41
|
+
|
|
42
|
+
'evaluate': {
|
|
43
|
+
'text': 'Off',
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
*/
|
|
48
|
+
if (!value) {
|
|
49
|
+
// eslint-disable-next-line no-param-reassign
|
|
50
|
+
value = 0;
|
|
51
|
+
}
|
|
52
|
+
if (value === configuration.on?.value) {
|
|
53
|
+
return configuration.on.evaluate;
|
|
54
|
+
}
|
|
55
|
+
if (value === configuration.off?.value) {
|
|
56
|
+
return configuration.off.evaluate;
|
|
57
|
+
}
|
|
58
|
+
return { text: value };
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const valueEvaluationFuncs = {
|
|
62
|
+
range: evaluateRange,
|
|
63
|
+
boolean: evaluateBoolean,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const useEvaluateValue = () => {
|
|
67
|
+
const valueEvaluations = useSCContextSelector((state) => {
|
|
68
|
+
return state.valueEvaluations;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const evaluateValue = useCallback(
|
|
72
|
+
(configId, value) => {
|
|
73
|
+
if (value === null || value === undefined) {
|
|
74
|
+
return { text: '--', color: null };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const valueEvaluation = valueEvaluations[configId];
|
|
78
|
+
if (!valueEvaluation) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const evaluateFunc = valueEvaluationFuncs[valueEvaluation.template];
|
|
83
|
+
if (!evaluateFunc) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
return evaluateFunc(value, valueEvaluation.configuration);
|
|
89
|
+
} catch (e) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
[valueEvaluations]
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
return evaluateValue;
|
|
97
|
+
};
|
|
@@ -16,14 +16,14 @@ export const useFavoriteDevice = (device) => {
|
|
|
16
16
|
const { success } = await axiosPost(
|
|
17
17
|
API.DEVICE.ADD_TO_FAVOURITES(device?.id)
|
|
18
18
|
);
|
|
19
|
-
success && setAction(Action.
|
|
19
|
+
success && setAction(Action.ADD_DEVICES_TO_FAVORITES, [device.id]);
|
|
20
20
|
}, [device, setAction]);
|
|
21
21
|
|
|
22
22
|
const removeFromFavorites = useCallback(async () => {
|
|
23
23
|
const { success } = await axiosPost(
|
|
24
24
|
API.DEVICE.REMOVE_FROM_FAVOURITES(device?.id)
|
|
25
25
|
);
|
|
26
|
-
success && setAction(Action.
|
|
26
|
+
success && setAction(Action.REMOVE_DEVICES_FROM_FAVORITES, [device.id]);
|
|
27
27
|
}, [device, setAction]);
|
|
28
28
|
|
|
29
29
|
return {
|
|
@@ -34,7 +34,11 @@ import Routes from '../../utils/Route';
|
|
|
34
34
|
import { ToastBottomHelper } from '../../utils/Utils';
|
|
35
35
|
import ItemAutomate from '../../commons/Automate/ItemAutomate';
|
|
36
36
|
import withPreventDoubleClick from '../../commons/WithPreventDoubleClick';
|
|
37
|
-
import {
|
|
37
|
+
import {
|
|
38
|
+
AUTOMATE_SELECT,
|
|
39
|
+
AUTOMATE_TYPE,
|
|
40
|
+
STATE_VALUE_SENSOR_TYPES,
|
|
41
|
+
} from '../../configs/Constants';
|
|
38
42
|
import { popAction } from '../../navigations/utils';
|
|
39
43
|
import { TESTID } from '../../configs/Constants';
|
|
40
44
|
import useKeyboardAnimated from '../../hooks/Explore/useKeyboardAnimated';
|
|
@@ -359,17 +363,23 @@ const ScriptDetail = ({ route }) => {
|
|
|
359
363
|
date_repeat,
|
|
360
364
|
time_repeat,
|
|
361
365
|
weekday_repeat,
|
|
366
|
+
sensor_type,
|
|
362
367
|
} = automate;
|
|
363
368
|
if (type === AUTOMATE_TYPE.VALUE_CHANGE) {
|
|
369
|
+
const stateConditionData = STATE_VALUE_SENSOR_TYPES.find(
|
|
370
|
+
(i) => i.type === sensor_type
|
|
371
|
+
);
|
|
372
|
+
const isNumberValue = !stateConditionData;
|
|
373
|
+
|
|
364
374
|
let text;
|
|
365
375
|
if (condition === '>') {
|
|
366
376
|
text = 'higher_than';
|
|
367
377
|
} else if (condition === '<') {
|
|
368
378
|
text = 'lower_than';
|
|
369
379
|
} else if (condition === '=') {
|
|
370
|
-
text = 'equal';
|
|
380
|
+
text = isNumberValue ? 'equal' : stateConditionData?.stateValue[value];
|
|
371
381
|
}
|
|
372
|
-
return `${config_name} ${t(text)} ${value}`;
|
|
382
|
+
return `${config_name} ${t(text)} ${isNumberValue ? value : ''}`;
|
|
373
383
|
} else if (type === AUTOMATE_TYPE.SCHEDULE) {
|
|
374
384
|
const time =
|
|
375
385
|
time_repeat?.length >= 8
|
|
@@ -4,8 +4,9 @@ import React, {
|
|
|
4
4
|
useState,
|
|
5
5
|
useRef,
|
|
6
6
|
useContext,
|
|
7
|
+
useMemo,
|
|
7
8
|
} from 'react';
|
|
8
|
-
import { AppState, RefreshControl, View } from 'react-native';
|
|
9
|
+
import { AppState, RefreshControl, View, Platform } from 'react-native';
|
|
9
10
|
import { useIsFocused } from '@react-navigation/native';
|
|
10
11
|
|
|
11
12
|
import { useTranslations } from '../../hooks/Common/useTranslations';
|
|
@@ -24,6 +25,7 @@ import {
|
|
|
24
25
|
} from '../../hooks/Common';
|
|
25
26
|
import { useFavorites } from './hook/useFavorites';
|
|
26
27
|
import { useUnitConnectRemoteDevices } from './hook/useUnitConnectRemoteDevices';
|
|
28
|
+
import { useValueEvaluations } from '../../hooks/IoT';
|
|
27
29
|
import { fetchWithCache, axiosGet } from '../../utils/Apis/axios';
|
|
28
30
|
import ShortDetailSubUnit from '../../commons/SubUnit/ShortDetail';
|
|
29
31
|
import NavBar from '../../commons/NavBar';
|
|
@@ -52,6 +54,7 @@ import MediaPlayerDetail from '../../commons/MediaPlayerDetail';
|
|
|
52
54
|
const UnitDetail = ({ route }) => {
|
|
53
55
|
const t = useTranslations();
|
|
54
56
|
const { setAction } = useContext(SCContext);
|
|
57
|
+
const isIOS = useMemo(() => Platform.OS === 'ios', []);
|
|
55
58
|
|
|
56
59
|
const {
|
|
57
60
|
unitId,
|
|
@@ -190,6 +193,8 @@ const UnitDetail = ({ route }) => {
|
|
|
190
193
|
};
|
|
191
194
|
}, [fetchDetails]);
|
|
192
195
|
|
|
196
|
+
useValueEvaluations(unitId);
|
|
197
|
+
|
|
193
198
|
useUnitConnectRemoteDevices(unit);
|
|
194
199
|
|
|
195
200
|
useEffect(() => {
|
|
@@ -292,13 +297,13 @@ const UnitDetail = ({ route }) => {
|
|
|
292
297
|
}, [user, onRefresh]);
|
|
293
298
|
|
|
294
299
|
useEffect(() => {
|
|
295
|
-
if (isFirstOpenCamera) {
|
|
300
|
+
if (isFirstOpenCamera && isIOS) {
|
|
296
301
|
const to = setTimeout(() => {
|
|
297
302
|
setAction(Action.IS_FIRST_OPEN_CAMERA, false);
|
|
298
303
|
clearTimeout(to);
|
|
299
304
|
}, 3000);
|
|
300
305
|
}
|
|
301
|
-
}, [isFirstOpenCamera, setAction]);
|
|
306
|
+
}, [isFirstOpenCamera, isIOS, setAction]);
|
|
302
307
|
|
|
303
308
|
return (
|
|
304
309
|
<WrapParallaxScrollView
|
|
@@ -315,7 +320,7 @@ const UnitDetail = ({ route }) => {
|
|
|
315
320
|
onBack={(isSuccessfullyConnected && Dashboard) || (routeName && onBack)}
|
|
316
321
|
>
|
|
317
322
|
{/* NOTE: This is a trick to fix camera not full screen on first open app */}
|
|
318
|
-
{isFirstOpenCamera && (
|
|
323
|
+
{isFirstOpenCamera && isIOS && (
|
|
319
324
|
<MediaPlayerDetail
|
|
320
325
|
uri={Constants.URL_STREAM_CAMERA_DEMO}
|
|
321
326
|
isPaused={false}
|
|
@@ -70,9 +70,7 @@ const SelectAddress = memo(({ route }) => {
|
|
|
70
70
|
API.EXTERNAL.GOOGLE_MAP.AUTO_COMPLETE,
|
|
71
71
|
config
|
|
72
72
|
);
|
|
73
|
-
|
|
74
|
-
setSearchData(data.predictions);
|
|
75
|
-
}
|
|
73
|
+
success && setSearchData(data.predictions);
|
|
76
74
|
// eslint-disable-next-line no-empty
|
|
77
75
|
} catch (error) {}
|
|
78
76
|
}, []);
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
memo,
|
|
3
|
+
useState,
|
|
4
|
+
useEffect,
|
|
5
|
+
useCallback,
|
|
6
|
+
useMemo,
|
|
7
|
+
useContext,
|
|
8
|
+
} from 'react';
|
|
9
|
+
import { View, ScrollView, TouchableOpacity } from 'react-native';
|
|
10
|
+
import { useNavigation } from '@react-navigation/native';
|
|
11
|
+
import { Icon } from '@ant-design/react-native';
|
|
12
|
+
import { HeaderCustom } from '../../commons/Header';
|
|
13
|
+
import Text from '../../commons/Text';
|
|
14
|
+
import NavBar from '../../commons/NavBar';
|
|
15
|
+
import BottomButtonView from '../../commons/BottomButtonView';
|
|
16
|
+
import { FullLoading } from '../../commons';
|
|
17
|
+
import Device from '../AddNewAction/Device';
|
|
18
|
+
import { useTranslations } from '../../hooks/Common/useTranslations';
|
|
19
|
+
import { SCContext } from '../../context';
|
|
20
|
+
import { Action } from '../../context/actionType';
|
|
21
|
+
import { axiosGet, axiosPost } from '../../utils/Apis/axios';
|
|
22
|
+
import { API, Colors } from '../../configs';
|
|
23
|
+
import styles from './SelectDevicesStyles';
|
|
24
|
+
|
|
25
|
+
const SelectDevices = memo(({ route }) => {
|
|
26
|
+
const t = useTranslations();
|
|
27
|
+
const { goBack } = useNavigation();
|
|
28
|
+
const { unitId } = route.params;
|
|
29
|
+
const { setAction } = useContext(SCContext);
|
|
30
|
+
const [listStation, setListStation] = useState([]);
|
|
31
|
+
const [listMenuItem, setListMenuItem] = useState([]);
|
|
32
|
+
const [indexStation, setIndexStation] = useState(0);
|
|
33
|
+
const [stations, setStations] = useState([]);
|
|
34
|
+
const [selectedIds, setSelectedIds] = useState([]);
|
|
35
|
+
const [loading, setLoading] = useState(false);
|
|
36
|
+
|
|
37
|
+
const fetchData = useCallback(async () => {
|
|
38
|
+
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);
|
|
42
|
+
const listMenu = newData.map((item, index) => ({
|
|
43
|
+
text: item.name,
|
|
44
|
+
station: item,
|
|
45
|
+
index: index,
|
|
46
|
+
}));
|
|
47
|
+
setStations(newData);
|
|
48
|
+
setListMenuItem(listMenu);
|
|
49
|
+
setListStation(listMenu);
|
|
50
|
+
}
|
|
51
|
+
setLoading(false);
|
|
52
|
+
}, [unitId]);
|
|
53
|
+
|
|
54
|
+
const addDevicesToFavorites = useCallback(async () => {
|
|
55
|
+
if (selectedIds.length === 0) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
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);
|
|
67
|
+
goBack();
|
|
68
|
+
}
|
|
69
|
+
setLoading(false);
|
|
70
|
+
}, [unitId, selectedIds, setAction, goBack]);
|
|
71
|
+
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
fetchData();
|
|
74
|
+
}, [fetchData]);
|
|
75
|
+
|
|
76
|
+
const onSnapToItem = useCallback(
|
|
77
|
+
(item, index) => {
|
|
78
|
+
setIndexStation(index);
|
|
79
|
+
},
|
|
80
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
81
|
+
[unitId, indexStation]
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const onSelectDevice = useCallback(
|
|
85
|
+
(device) => {
|
|
86
|
+
setSelectedIds((ids) => {
|
|
87
|
+
const index = ids.indexOf(device.id);
|
|
88
|
+
if (index !== -1) {
|
|
89
|
+
return ids.filter((id) => id !== device.id);
|
|
90
|
+
}
|
|
91
|
+
return [...ids, device.id];
|
|
92
|
+
});
|
|
93
|
+
},
|
|
94
|
+
[setSelectedIds]
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const rightComponent = useMemo(
|
|
98
|
+
() => (
|
|
99
|
+
<TouchableOpacity style={styles.buttonClose} onPress={goBack}>
|
|
100
|
+
<Icon name={'close'} size={24} color={Colors.Black} />
|
|
101
|
+
</TouchableOpacity>
|
|
102
|
+
),
|
|
103
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
104
|
+
[]
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<View style={styles.wrap}>
|
|
109
|
+
<HeaderCustom rightComponent={rightComponent} />
|
|
110
|
+
<ScrollView
|
|
111
|
+
style={styles.wrap}
|
|
112
|
+
contentContainerStyle={styles.contentContainerStyle}
|
|
113
|
+
scrollIndicatorInsets={{ right: 1 }}
|
|
114
|
+
>
|
|
115
|
+
<Text bold type="H2" style={styles.title}>
|
|
116
|
+
{t('select_device')}
|
|
117
|
+
</Text>
|
|
118
|
+
|
|
119
|
+
{listStation.length ? (
|
|
120
|
+
<NavBar
|
|
121
|
+
listStation={listStation}
|
|
122
|
+
listMenuItem={listMenuItem}
|
|
123
|
+
onSnapToItem={onSnapToItem}
|
|
124
|
+
indexStation={indexStation}
|
|
125
|
+
style={styles.navbar}
|
|
126
|
+
/>
|
|
127
|
+
) : (
|
|
128
|
+
<View style={styles.noneData}>
|
|
129
|
+
<Text center>{t('text_unit_add_to_favorites_no_devices')}</Text>
|
|
130
|
+
</View>
|
|
131
|
+
)}
|
|
132
|
+
|
|
133
|
+
<View style={styles.boxDevices}>
|
|
134
|
+
{stations[indexStation]?.devices &&
|
|
135
|
+
stations[indexStation].devices.map((device) => (
|
|
136
|
+
<Device
|
|
137
|
+
svgMain={device.icon || 'sensor'}
|
|
138
|
+
title={device.name}
|
|
139
|
+
sensor={device}
|
|
140
|
+
isSelectDevice={selectedIds.includes(device.id)}
|
|
141
|
+
onPress={onSelectDevice}
|
|
142
|
+
/>
|
|
143
|
+
))}
|
|
144
|
+
</View>
|
|
145
|
+
</ScrollView>
|
|
146
|
+
|
|
147
|
+
<BottomButtonView
|
|
148
|
+
style={styles.bottomButtonView}
|
|
149
|
+
mainTitle={t('done')}
|
|
150
|
+
onPressMain={addDevicesToFavorites}
|
|
151
|
+
typeMain={selectedIds.length === 0 ? 'disabled' : 'primary'}
|
|
152
|
+
/>
|
|
153
|
+
{loading && <FullLoading />}
|
|
154
|
+
</View>
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
export default SelectDevices;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { StyleSheet } from 'react-native';
|
|
2
|
+
import { getBottomSpace } from 'react-native-iphone-x-helper';
|
|
3
|
+
import { Colors, Constants } from '../../configs';
|
|
4
|
+
|
|
5
|
+
export default StyleSheet.create({
|
|
6
|
+
wrap: {
|
|
7
|
+
flex: 1,
|
|
8
|
+
backgroundColor: Colors.White,
|
|
9
|
+
},
|
|
10
|
+
contentContainerStyle: {
|
|
11
|
+
paddingBottom: getBottomSpace() + 100,
|
|
12
|
+
},
|
|
13
|
+
navbar: {
|
|
14
|
+
paddingTop: 16,
|
|
15
|
+
},
|
|
16
|
+
title: {
|
|
17
|
+
marginHorizontal: 16,
|
|
18
|
+
},
|
|
19
|
+
boxDevices: {
|
|
20
|
+
flexWrap: 'wrap',
|
|
21
|
+
flexDirection: 'row',
|
|
22
|
+
marginTop: 22,
|
|
23
|
+
justifyContent: 'space-between',
|
|
24
|
+
paddingLeft: 16,
|
|
25
|
+
paddingRight: 16,
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
bottomButtonView: {
|
|
29
|
+
paddingTop: 24,
|
|
30
|
+
paddingBottom: 32,
|
|
31
|
+
|
|
32
|
+
backgroundColor: Colors.White,
|
|
33
|
+
borderColor: Colors.ShadownTransparent,
|
|
34
|
+
borderTopWidth: 1,
|
|
35
|
+
},
|
|
36
|
+
noneData: {
|
|
37
|
+
paddingHorizontal: 16,
|
|
38
|
+
marginTop: Constants.height * 0.3,
|
|
39
|
+
},
|
|
40
|
+
});
|
|
@@ -103,6 +103,22 @@ describe('Test SelectAddress', () => {
|
|
|
103
103
|
updateLocation: mockUpdateLocation,
|
|
104
104
|
},
|
|
105
105
|
};
|
|
106
|
+
mockUpdateLocation.mockClear();
|
|
107
|
+
mockGoBack.mockClear();
|
|
108
|
+
mock.resetHistory();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('test not do anything then click done', async () => {
|
|
112
|
+
await act(async () => {
|
|
113
|
+
tree = await create(wrapComponent(route));
|
|
114
|
+
});
|
|
115
|
+
const instance = tree.root;
|
|
116
|
+
const bottomButton = instance.findByType(BottomButtonView);
|
|
117
|
+
await act(async () => {
|
|
118
|
+
await bottomButton.props.onPressMain();
|
|
119
|
+
});
|
|
120
|
+
expect(mockUpdateLocation).not.toBeCalled();
|
|
121
|
+
expect(mockGoBack).not.toBeCalled();
|
|
106
122
|
});
|
|
107
123
|
|
|
108
124
|
test('test search location', async () => {
|
|
@@ -151,7 +167,6 @@ describe('Test SelectAddress', () => {
|
|
|
151
167
|
mock
|
|
152
168
|
.onGet(API.EXTERNAL.GOOGLE_MAP.GET_LAT_LNG_BY_PLACE_ID)
|
|
153
169
|
.reply(200, response.data);
|
|
154
|
-
|
|
155
170
|
await act(async () => {
|
|
156
171
|
await rowLocations[0].props.onPress({ place_id: 1, description: '1' });
|
|
157
172
|
});
|
|
@@ -163,6 +178,52 @@ describe('Test SelectAddress', () => {
|
|
|
163
178
|
expect(mockGoBack).toBeCalled();
|
|
164
179
|
});
|
|
165
180
|
|
|
181
|
+
test('test get lat lng of location failed', async () => {
|
|
182
|
+
await act(async () => {
|
|
183
|
+
tree = await create(wrapComponent(route));
|
|
184
|
+
});
|
|
185
|
+
const instance = tree.root;
|
|
186
|
+
const searchBars = instance.findAllByType(SearchBarLocation);
|
|
187
|
+
expect(searchBars).toHaveLength(1);
|
|
188
|
+
|
|
189
|
+
let response = {
|
|
190
|
+
status: 200,
|
|
191
|
+
data: {
|
|
192
|
+
predictions: [
|
|
193
|
+
{ place_id: 1, description: '1' },
|
|
194
|
+
{ place_id: 2, description: '2' },
|
|
195
|
+
{ place_id: 3, description: '3' },
|
|
196
|
+
],
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
mock.onGet(API.EXTERNAL.GOOGLE_MAP.AUTO_COMPLETE).reply(200, response.data);
|
|
200
|
+
await act(async () => {
|
|
201
|
+
await searchBars[0].props.onTextInput('');
|
|
202
|
+
});
|
|
203
|
+
let rowLocations = instance.findAllByType(RowLocation);
|
|
204
|
+
expect(rowLocations).toHaveLength(0);
|
|
205
|
+
|
|
206
|
+
await act(async () => {
|
|
207
|
+
await searchBars[0].props.onTextInput('input');
|
|
208
|
+
});
|
|
209
|
+
rowLocations = instance.findAllByType(RowLocation);
|
|
210
|
+
expect(rowLocations).toHaveLength(3);
|
|
211
|
+
response = {
|
|
212
|
+
status: 404,
|
|
213
|
+
data: {},
|
|
214
|
+
};
|
|
215
|
+
mock
|
|
216
|
+
.onGet(API.EXTERNAL.GOOGLE_MAP.GET_LAT_LNG_BY_PLACE_ID)
|
|
217
|
+
.reply(404, response.data);
|
|
218
|
+
await act(async () => {
|
|
219
|
+
await rowLocations[0].props.onPress({ place_id: 1, description: '1' });
|
|
220
|
+
});
|
|
221
|
+
const bottomButton = instance.findByType(BottomButtonView);
|
|
222
|
+
await act(async () => {
|
|
223
|
+
await bottomButton.props.onPressMain();
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
166
227
|
test('test get current location success', async () => {
|
|
167
228
|
await act(async () => {
|
|
168
229
|
tree = await create(wrapComponent(route));
|
|
@@ -196,6 +257,34 @@ describe('Test SelectAddress', () => {
|
|
|
196
257
|
});
|
|
197
258
|
});
|
|
198
259
|
|
|
260
|
+
test('test get current location success get location name failed', async () => {
|
|
261
|
+
await act(async () => {
|
|
262
|
+
tree = await create(wrapComponent(route));
|
|
263
|
+
});
|
|
264
|
+
const instance = tree.root;
|
|
265
|
+
const button = instance.find(
|
|
266
|
+
(el) => el.props.testID === TESTID.BUTTON_YOUR_LOCATION
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
const response = {
|
|
270
|
+
status: 400,
|
|
271
|
+
data: {
|
|
272
|
+
results: [],
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
mock
|
|
276
|
+
.onGet(API.EXTERNAL.GOOGLE_MAP.GET_LOCATION_FROM_LAT_LNG)
|
|
277
|
+
.reply(400, response.data);
|
|
278
|
+
await act(async () => {
|
|
279
|
+
await button.props.onPress();
|
|
280
|
+
});
|
|
281
|
+
const bottomButton = instance.findByType(BottomButtonView);
|
|
282
|
+
await act(async () => {
|
|
283
|
+
await bottomButton.props.onPressMain();
|
|
284
|
+
});
|
|
285
|
+
expect(mockGoBack).toBeCalled();
|
|
286
|
+
});
|
|
287
|
+
|
|
199
288
|
test('test get current location failed permission denied', async () => {
|
|
200
289
|
await act(async () => {
|
|
201
290
|
tree = await create(wrapComponent(route));
|
|
@@ -0,0 +1,110 @@
|
|
|
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 SelectDevices from '../SelectDevices';
|
|
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
|
+
<SelectDevices 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 SelectDevices', () => {
|
|
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
|
+
});
|
package/src/utils/Apis/axios.js
CHANGED
|
@@ -5,6 +5,7 @@ import NetInfo from '@react-native-community/netinfo';
|
|
|
5
5
|
import { PROBLEM_CODE } from '../../configs/Constants';
|
|
6
6
|
import { getTranslate } from '../I18n';
|
|
7
7
|
import { SCConfig } from '../../configs';
|
|
8
|
+
import { isHTML } from '../Validation';
|
|
8
9
|
|
|
9
10
|
const api = create({
|
|
10
11
|
headers: {
|
|
@@ -46,6 +47,11 @@ const parseErrorResponse = async (error) => {
|
|
|
46
47
|
case PROBLEM_CODE.SERVER_ERROR:
|
|
47
48
|
message = getTranslate(SCConfig.language, 'server_error');
|
|
48
49
|
break;
|
|
50
|
+
case PROBLEM_CODE.CLIENT_ERROR:
|
|
51
|
+
if (error.status === 404 && isHTML(error.data)) {
|
|
52
|
+
message = getTranslate(SCConfig.language, 'not_found');
|
|
53
|
+
}
|
|
54
|
+
break;
|
|
49
55
|
}
|
|
50
56
|
ToastBottomHelper.error(message);
|
|
51
57
|
}
|
|
@@ -995,5 +995,7 @@
|
|
|
995
995
|
"detected": "Detected",
|
|
996
996
|
"not_detected": "Not detected",
|
|
997
997
|
"activated": "Activated",
|
|
998
|
+
"text_unit_add_to_favorites_no_devices": "You don't have any devices or all your devices was added to your favorites",
|
|
999
|
+
"not_found": "Not found",
|
|
998
1000
|
"not_activated": "Not activated"
|
|
999
1001
|
}
|
|
@@ -996,5 +996,7 @@
|
|
|
996
996
|
"detected": "Phát hiện",
|
|
997
997
|
"not_detected": "Không phát hiện",
|
|
998
998
|
"activated": "Được kích hoạt",
|
|
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
|
+
"not_found": "Không tìm thấy",
|
|
999
1001
|
"not_activated": "Không kích hoạt"
|
|
1000
1002
|
}
|
package/src/utils/Route/index.js
CHANGED
package/src/utils/Validation.js
CHANGED
|
@@ -7,3 +7,6 @@ export const isValidPhoneNumber = (phoneNumber) => {
|
|
|
7
7
|
export const isValidEmailAddress = (emailAddress) => {
|
|
8
8
|
return /^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/.test(emailAddress);
|
|
9
9
|
};
|
|
10
|
+
|
|
11
|
+
export const isHTML = (string) =>
|
|
12
|
+
/<[a-z]+\d?(\s+[\w-]+=("[^"]*"|'[^']*'))*\s*\/?>|&#?\w+;/i.test(string);
|