@eohjsc/react-native-smart-city 0.7.30 → 0.7.32
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/android/build.gradle +5 -5
- package/package.json +1 -1
- package/src/configs/API.js +2 -0
- package/src/configs/AccessibilityLabel.js +2 -0
- package/src/screens/Automate/AddNewAction/ReceiverSelect.js +5 -3
- package/src/screens/Automate/EditActionsList/UpdateReceiverEmailScript.js +4 -3
- package/src/screens/Automate/EditActionsList/UpdateReceiverSmsScript.js +5 -4
- package/src/screens/Automate/ScriptDetail/Styles/indexStyles.js +1 -1
- package/src/screens/Automate/ScriptDetail/__test__/index.test.js +116 -2
- package/src/screens/Automate/ScriptDetail/index.js +41 -3
- package/src/utils/I18n/translations/en.js +1 -0
- package/src/utils/I18n/translations/vi.js +1 -0
package/android/build.gradle
CHANGED
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
// original location:
|
|
11
11
|
// - https://github.com/facebook/react-native/blob/0.58-stable/local-cli/templates/HelloWorld/android/app/build.gradle
|
|
12
12
|
|
|
13
|
-
def DEFAULT_COMPILE_SDK_VERSION =
|
|
14
|
-
def DEFAULT_BUILD_TOOLS_VERSION = '
|
|
13
|
+
def DEFAULT_COMPILE_SDK_VERSION = 34
|
|
14
|
+
def DEFAULT_BUILD_TOOLS_VERSION = '34.0.0'
|
|
15
15
|
def DEFAULT_MIN_SDK_VERSION = 24
|
|
16
|
-
def DEFAULT_TARGET_SDK_VERSION =
|
|
16
|
+
def DEFAULT_TARGET_SDK_VERSION = 34
|
|
17
17
|
|
|
18
18
|
def safeExtGet(prop, fallback) {
|
|
19
19
|
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
|
@@ -111,12 +111,12 @@ afterEvaluate { project ->
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
task androidJavadocJar(type: Jar, dependsOn: androidJavadoc) {
|
|
114
|
-
|
|
114
|
+
archiveClassifier = 'javadoc'
|
|
115
115
|
from androidJavadoc.destinationDir
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
task androidSourcesJar(type: Jar) {
|
|
119
|
-
|
|
119
|
+
archiveClassifier = 'sources'
|
|
120
120
|
from android.sourceSets.main.java.srcDirs
|
|
121
121
|
include '**/*.java'
|
|
122
122
|
}
|
package/package.json
CHANGED
package/src/configs/API.js
CHANGED
|
@@ -149,6 +149,8 @@ const API = {
|
|
|
149
149
|
CREATE_AUTOMATE_V2: () => '/property_manager/automate_v2/',
|
|
150
150
|
UPDATE_AUTOMATE: (automateId) =>
|
|
151
151
|
`/property_manager/automate/${automateId}/`,
|
|
152
|
+
ENABLE_NOTIFICATIONS: (automateId) =>
|
|
153
|
+
`/property_manager/automate/${automateId}/enable_notifications/`,
|
|
152
154
|
ENABLE_SCRIPT: (automateId) =>
|
|
153
155
|
`/property_manager/automate/${automateId}/enable_script/`,
|
|
154
156
|
STAR_SCRIPT: (id) => `/property_manager/automate/${id}/star_script/`,
|
|
@@ -262,6 +262,8 @@ export default {
|
|
|
262
262
|
AUTOMATE_SELECT_CONDITION: 'AUTOMATE_SELECT_CONDITION',
|
|
263
263
|
AUTOMATE_DELETE_CONDITION: 'AUTOMATE_DELETE_CONDITION',
|
|
264
264
|
AUTOMATE_NUMBER_CONDITION: 'AUTOMATE_NUMBER_CONDITION',
|
|
265
|
+
SWITCH_ENABLE_SCRIPT: 'SWITCH_ENABLE_SCRIPT',
|
|
266
|
+
SWITCH_ENABLE_NOTIFICATIONS_SCRIPT: 'SWITCH_ENABLE_NOTIFICATIONS_SCRIPT',
|
|
265
267
|
|
|
266
268
|
// Parking input maunaly spot
|
|
267
269
|
PARKING_SPOT_INFO_BUTTON: 'PARKING_SPOT_INFO_BUTTON',
|
|
@@ -10,6 +10,7 @@ import BottomButtonView from '../../../commons/BottomButtonView';
|
|
|
10
10
|
import { Search } from '../../../commons/DevMode';
|
|
11
11
|
import { convertToSlug } from '../../../utils/Functions/Search';
|
|
12
12
|
import { axiosGet } from '../../../utils/Apis/axios';
|
|
13
|
+
import { shortEmailName } from '../../../utils/Utils';
|
|
13
14
|
import { API, Colors } from '../../../configs';
|
|
14
15
|
import { useSCContextSelector } from '../../../context';
|
|
15
16
|
import { Image } from 'react-native';
|
|
@@ -111,7 +112,7 @@ const ReceiverSelect = ({
|
|
|
111
112
|
);
|
|
112
113
|
|
|
113
114
|
const RowMember = memo(({ member, index, onValueChange }) => {
|
|
114
|
-
const { id, name, avatar, share_id, label, invalidLabel, disabled } =
|
|
115
|
+
const { id, name, email, avatar, share_id, label, invalidLabel, disabled } =
|
|
115
116
|
member;
|
|
116
117
|
const [role, roleColor] = useMemo(() => {
|
|
117
118
|
if (!share_id) {
|
|
@@ -124,8 +125,9 @@ const ReceiverSelect = ({
|
|
|
124
125
|
}, [share_id, id]);
|
|
125
126
|
|
|
126
127
|
const firstWordsInName = useMemo(() => {
|
|
127
|
-
|
|
128
|
-
|
|
128
|
+
const nameTemp = name || shortEmailName(email);
|
|
129
|
+
return nameTemp?.charAt() || '';
|
|
130
|
+
}, [email, name]);
|
|
129
131
|
|
|
130
132
|
const circleColor = arrColor[index % arrColor.length];
|
|
131
133
|
|
|
@@ -8,7 +8,7 @@ import { useTranslations } from '../../../hooks/Common/useTranslations';
|
|
|
8
8
|
import BottomButtonView from '../../../commons/BottomButtonView';
|
|
9
9
|
import { axiosPut, axiosGet } from '../../../utils/Apis/axios';
|
|
10
10
|
import { API, Colors } from '../../../configs';
|
|
11
|
-
import { ToastBottomHelper } from '../../../utils/Utils';
|
|
11
|
+
import { ToastBottomHelper, shortEmailName } from '../../../utils/Utils';
|
|
12
12
|
import Routes from '../../../utils/Route';
|
|
13
13
|
import moment from 'moment';
|
|
14
14
|
import CheckBox from '@react-native-community/checkbox';
|
|
@@ -110,8 +110,9 @@ const UpdateReceiverEmailScript = ({ route }) => {
|
|
|
110
110
|
}, [share_id, id]);
|
|
111
111
|
|
|
112
112
|
const firstWordsInName = useMemo(() => {
|
|
113
|
-
|
|
114
|
-
|
|
113
|
+
const nameTemp = name || shortEmailName(email);
|
|
114
|
+
return nameTemp?.charAt() || '';
|
|
115
|
+
}, [email, name]);
|
|
115
116
|
|
|
116
117
|
const circleColor = arrColor[index % arrColor.length];
|
|
117
118
|
|
|
@@ -8,7 +8,7 @@ import { useTranslations } from '../../../hooks/Common/useTranslations';
|
|
|
8
8
|
import BottomButtonView from '../../../commons/BottomButtonView/index.js';
|
|
9
9
|
import { axiosPut, axiosGet } from '../../../utils/Apis/axios.js';
|
|
10
10
|
import { API, Colors } from '../../../configs/index.js';
|
|
11
|
-
import { ToastBottomHelper } from '../../../utils/Utils.js';
|
|
11
|
+
import { ToastBottomHelper, shortEmailName } from '../../../utils/Utils.js';
|
|
12
12
|
import Routes from '../../../utils/Route/index.js';
|
|
13
13
|
import moment from 'moment';
|
|
14
14
|
import CheckBox from '@react-native-community/checkbox';
|
|
@@ -96,7 +96,7 @@ const UpdateReceiverSmsScript = ({ route }) => {
|
|
|
96
96
|
);
|
|
97
97
|
|
|
98
98
|
const RowMember = memo(({ member, index, onValueChange }) => {
|
|
99
|
-
const { id, name, avatar, share_id, phone_number } = member;
|
|
99
|
+
const { id, name, email, avatar, share_id, phone_number } = member;
|
|
100
100
|
const [role, roleColor] = useMemo(() => {
|
|
101
101
|
if (!share_id) {
|
|
102
102
|
return [t('owner'), Colors.Primary];
|
|
@@ -108,8 +108,9 @@ const UpdateReceiverSmsScript = ({ route }) => {
|
|
|
108
108
|
}, [share_id, id]);
|
|
109
109
|
|
|
110
110
|
const firstWordsInName = useMemo(() => {
|
|
111
|
-
|
|
112
|
-
|
|
111
|
+
const nameTemp = name || shortEmailName(email);
|
|
112
|
+
return nameTemp?.charAt() || '';
|
|
113
|
+
}, [email, name]);
|
|
113
114
|
|
|
114
115
|
const circleColor = arrColor[index % arrColor.length];
|
|
115
116
|
|
|
@@ -3,7 +3,7 @@ import { BackHandler, Platform } from 'react-native';
|
|
|
3
3
|
import { create, act } from 'react-test-renderer';
|
|
4
4
|
import Toast from 'react-native-toast-message';
|
|
5
5
|
import { useNavigation } from '@react-navigation/native';
|
|
6
|
-
import {
|
|
6
|
+
import { TouchableOpacity } from 'react-native';
|
|
7
7
|
import MockAdapter from 'axios-mock-adapter';
|
|
8
8
|
|
|
9
9
|
import { SCProvider } from '../../../../context';
|
|
@@ -92,6 +92,7 @@ describe('Test ScriptDetail', () => {
|
|
|
92
92
|
script: {
|
|
93
93
|
name: 'name',
|
|
94
94
|
enable: true,
|
|
95
|
+
enabled_notifications: true,
|
|
95
96
|
},
|
|
96
97
|
},
|
|
97
98
|
},
|
|
@@ -920,7 +921,9 @@ describe('Test ScriptDetail', () => {
|
|
|
920
921
|
tree = await create(wrapComponent(route));
|
|
921
922
|
});
|
|
922
923
|
const instance = tree.root;
|
|
923
|
-
const switchButton = instance.
|
|
924
|
+
const switchButton = instance.findByProps({
|
|
925
|
+
accessibilityLabel: AccessibilityLabel.SWITCH_ENABLE_SCRIPT,
|
|
926
|
+
});
|
|
924
927
|
await act(async () => {
|
|
925
928
|
await switchButton.props.onValueChange(false);
|
|
926
929
|
});
|
|
@@ -939,6 +942,116 @@ describe('Test ScriptDetail', () => {
|
|
|
939
942
|
el.type === TouchableOpacity
|
|
940
943
|
);
|
|
941
944
|
expect(buttonEditScript).toHaveLength(0);
|
|
945
|
+
expect(mock.history.post).toHaveLength(1);
|
|
946
|
+
expect(mock.history.post[0].url).toEqual(API.AUTOMATE.ENABLE_SCRIPT(1));
|
|
947
|
+
});
|
|
948
|
+
|
|
949
|
+
it('test press disable script false', async () => {
|
|
950
|
+
mock.onGet(API.AUTOMATE.SCRIPT_ITEMS(1)).reply(200, data);
|
|
951
|
+
mock.onPost(API.AUTOMATE.ENABLE_SCRIPT(1)).reply(400);
|
|
952
|
+
await act(async () => {
|
|
953
|
+
tree = await create(wrapComponent(route));
|
|
954
|
+
});
|
|
955
|
+
const instance = tree.root;
|
|
956
|
+
const switchButton = instance.findByProps({
|
|
957
|
+
accessibilityLabel: AccessibilityLabel.SWITCH_ENABLE_SCRIPT,
|
|
958
|
+
});
|
|
959
|
+
expect(switchButton.props.value).toBeTruthy();
|
|
960
|
+
|
|
961
|
+
await act(async () => {
|
|
962
|
+
await switchButton.props.onValueChange(false);
|
|
963
|
+
});
|
|
964
|
+
expect(switchButton.props.value).toBeTruthy();
|
|
965
|
+
const buttonAddScript = instance.findAll(
|
|
966
|
+
(el) =>
|
|
967
|
+
el.props.accessibilityLabel ===
|
|
968
|
+
AccessibilityLabel.BUTTON_ADD_SCRIPT_ACTION &&
|
|
969
|
+
el.type === TouchableOpacity
|
|
970
|
+
);
|
|
971
|
+
expect(buttonAddScript).toHaveLength(1);
|
|
972
|
+
|
|
973
|
+
const buttonEditScript = instance.findAll(
|
|
974
|
+
(el) =>
|
|
975
|
+
el.props.accessibilityLabel ===
|
|
976
|
+
AccessibilityLabel.BUTTON_EDIT_SCRIPT_ACTION &&
|
|
977
|
+
el.type === TouchableOpacity
|
|
978
|
+
);
|
|
979
|
+
expect(buttonEditScript).toHaveLength(1);
|
|
980
|
+
expect(mock.history.post).toHaveLength(1);
|
|
981
|
+
expect(mock.history.post[0].url).toEqual(API.AUTOMATE.ENABLE_SCRIPT(1));
|
|
982
|
+
});
|
|
983
|
+
|
|
984
|
+
it('test enable notifications for script', async () => {
|
|
985
|
+
mock.onGet(API.AUTOMATE.SCRIPT_ITEMS(1)).reply(200, data);
|
|
986
|
+
mock.onPost(API.AUTOMATE.ENABLE_NOTIFICATIONS(1)).reply(200);
|
|
987
|
+
await act(async () => {
|
|
988
|
+
tree = await create(wrapComponent(route));
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
const instance = tree.root;
|
|
992
|
+
const enableNotificationsSwitch = instance.findByProps({
|
|
993
|
+
accessibilityLabel: AccessibilityLabel.SWITCH_ENABLE_NOTIFICATIONS_SCRIPT,
|
|
994
|
+
});
|
|
995
|
+
expect(enableNotificationsSwitch.props.value).toBeTruthy();
|
|
996
|
+
|
|
997
|
+
await act(async () => {
|
|
998
|
+
await enableNotificationsSwitch.props.onValueChange(false);
|
|
999
|
+
});
|
|
1000
|
+
expect(enableNotificationsSwitch.props.value).toBeFalsy();
|
|
1001
|
+
expect(mock.history.post).toHaveLength(1);
|
|
1002
|
+
expect(mock.history.post[0].url).toEqual(
|
|
1003
|
+
API.AUTOMATE.ENABLE_NOTIFICATIONS(1)
|
|
1004
|
+
);
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
it('test enable notifications for script fail', async () => {
|
|
1008
|
+
mock.onGet(API.AUTOMATE.SCRIPT_ITEMS(1)).reply(200, data);
|
|
1009
|
+
mock.onPost(API.AUTOMATE.ENABLE_NOTIFICATIONS(1)).reply(400);
|
|
1010
|
+
await act(async () => {
|
|
1011
|
+
tree = await create(wrapComponent(route));
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
const instance = tree.root;
|
|
1015
|
+
const enableNotificationsSwitch = instance.findByProps({
|
|
1016
|
+
accessibilityLabel: AccessibilityLabel.SWITCH_ENABLE_NOTIFICATIONS_SCRIPT,
|
|
1017
|
+
});
|
|
1018
|
+
expect(enableNotificationsSwitch.props.value).toBeTruthy();
|
|
1019
|
+
|
|
1020
|
+
await act(async () => {
|
|
1021
|
+
await enableNotificationsSwitch.props.onValueChange(false);
|
|
1022
|
+
});
|
|
1023
|
+
expect(enableNotificationsSwitch.props.value).toBeTruthy();
|
|
1024
|
+
expect(mock.history.post).toHaveLength(1);
|
|
1025
|
+
expect(mock.history.post[0].url).toEqual(
|
|
1026
|
+
API.AUTOMATE.ENABLE_NOTIFICATIONS(1)
|
|
1027
|
+
);
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
it('test hidden enable notifications switch when disable script', async () => {
|
|
1031
|
+
mock.onGet(API.AUTOMATE.SCRIPT_ITEMS(1)).reply(200, data);
|
|
1032
|
+
mock.onPost(API.AUTOMATE.ENABLE_SCRIPT(1)).reply(200, data);
|
|
1033
|
+
await act(async () => {
|
|
1034
|
+
tree = await create(wrapComponent(route));
|
|
1035
|
+
});
|
|
1036
|
+
|
|
1037
|
+
const instance = tree.root;
|
|
1038
|
+
const enableSwitch = instance.findByProps({
|
|
1039
|
+
accessibilityLabel: AccessibilityLabel.SWITCH_ENABLE_SCRIPT,
|
|
1040
|
+
});
|
|
1041
|
+
let enableNotificationsSwitchs = instance.findAllByProps({
|
|
1042
|
+
accessibilityLabel: AccessibilityLabel.SWITCH_ENABLE_NOTIFICATIONS_SCRIPT,
|
|
1043
|
+
});
|
|
1044
|
+
expect(enableSwitch.props.value).toBeTruthy();
|
|
1045
|
+
expect(enableNotificationsSwitchs).toHaveLength(1);
|
|
1046
|
+
|
|
1047
|
+
await act(async () => {
|
|
1048
|
+
await enableSwitch.props.onValueChange(false);
|
|
1049
|
+
});
|
|
1050
|
+
expect(enableSwitch.props.value).toBeFalsy();
|
|
1051
|
+
enableNotificationsSwitchs = instance.findAllByProps({
|
|
1052
|
+
accessibilityLabel: AccessibilityLabel.SWITCH_ENABLE_NOTIFICATIONS_SCRIPT,
|
|
1053
|
+
});
|
|
1054
|
+
expect(enableNotificationsSwitchs).toHaveLength(0);
|
|
942
1055
|
});
|
|
943
1056
|
|
|
944
1057
|
it('test press add action reach limit', async () => {
|
|
@@ -1068,6 +1181,7 @@ describe('Test ScriptDetail', () => {
|
|
|
1068
1181
|
script: {
|
|
1069
1182
|
name: 'name',
|
|
1070
1183
|
enable: true,
|
|
1184
|
+
enabled_notifications: true,
|
|
1071
1185
|
},
|
|
1072
1186
|
};
|
|
1073
1187
|
route.params = {
|
|
@@ -108,10 +108,14 @@ const ScriptDetail = ({ route }) => {
|
|
|
108
108
|
chip_local_control,
|
|
109
109
|
is_local_control,
|
|
110
110
|
chip_id_local_control,
|
|
111
|
+
enabled_notifications,
|
|
111
112
|
} = script || {};
|
|
112
113
|
|
|
113
114
|
const [local_control, setLocalControl] = useState({});
|
|
114
115
|
const [enableScript, setEnableScript] = useState(enable);
|
|
116
|
+
const [enableNotificationsScript, setEnableNotificationsScript] = useState(
|
|
117
|
+
enabled_notifications
|
|
118
|
+
);
|
|
115
119
|
const [listMenuItemCondition, setListMenuItemCondition] = useState([]);
|
|
116
120
|
const [isShowAddCondition, setIsShowAddCondition] = useState(false);
|
|
117
121
|
const permissions = useBackendPermission();
|
|
@@ -289,6 +293,7 @@ const ScriptDetail = ({ route }) => {
|
|
|
289
293
|
if (success) {
|
|
290
294
|
setAutomate(automateData);
|
|
291
295
|
setEnableScript(automateData.script.enable);
|
|
296
|
+
setEnableNotificationsScript(automateData.script.enabled_notifications);
|
|
292
297
|
setNeedAllCondition(automateData.is_need_all_conditions);
|
|
293
298
|
}
|
|
294
299
|
}, [automateId]);
|
|
@@ -330,7 +335,6 @@ const ScriptDetail = ({ route }) => {
|
|
|
330
335
|
|
|
331
336
|
const onChangeSwitch = useCallback(
|
|
332
337
|
async (checked) => {
|
|
333
|
-
setEnableScript(checked);
|
|
334
338
|
const { success } = await axiosPost(
|
|
335
339
|
API.AUTOMATE.ENABLE_SCRIPT(automateId),
|
|
336
340
|
{
|
|
@@ -338,8 +342,24 @@ const ScriptDetail = ({ route }) => {
|
|
|
338
342
|
}
|
|
339
343
|
);
|
|
340
344
|
if (success) {
|
|
341
|
-
ToastBottomHelper.success(t('update_successfully'));
|
|
342
345
|
setEnableScript(checked);
|
|
346
|
+
ToastBottomHelper.success(t('update_successfully'));
|
|
347
|
+
}
|
|
348
|
+
},
|
|
349
|
+
[automateId, t]
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
const onChangeNotificationsSwitch = useCallback(
|
|
353
|
+
async (checked) => {
|
|
354
|
+
const { success } = await axiosPost(
|
|
355
|
+
API.AUTOMATE.ENABLE_NOTIFICATIONS(automateId),
|
|
356
|
+
{
|
|
357
|
+
enable: checked,
|
|
358
|
+
}
|
|
359
|
+
);
|
|
360
|
+
if (success) {
|
|
361
|
+
setEnableNotificationsScript(checked);
|
|
362
|
+
ToastBottomHelper.success(t('update_successfully'));
|
|
343
363
|
}
|
|
344
364
|
},
|
|
345
365
|
[automateId, t]
|
|
@@ -680,7 +700,25 @@ const ScriptDetail = ({ route }) => {
|
|
|
680
700
|
<Text type="H3" semibold>
|
|
681
701
|
{t('enable_this_script')}
|
|
682
702
|
</Text>
|
|
683
|
-
<Switch
|
|
703
|
+
<Switch
|
|
704
|
+
value={enableScript}
|
|
705
|
+
onValueChange={onChangeSwitch}
|
|
706
|
+
accessibilityLabel={AccessibilityLabel.SWITCH_ENABLE_SCRIPT}
|
|
707
|
+
/>
|
|
708
|
+
</View>
|
|
709
|
+
)}
|
|
710
|
+
{!!enableScript && (
|
|
711
|
+
<View style={styles.row}>
|
|
712
|
+
<Text type="H3" semibold>
|
|
713
|
+
{t('enable_notifications_for_this_script')}
|
|
714
|
+
</Text>
|
|
715
|
+
<Switch
|
|
716
|
+
value={enableNotificationsScript}
|
|
717
|
+
onValueChange={onChangeNotificationsSwitch}
|
|
718
|
+
accessibilityLabel={
|
|
719
|
+
AccessibilityLabel.SWITCH_ENABLE_NOTIFICATIONS_SCRIPT
|
|
720
|
+
}
|
|
721
|
+
/>
|
|
684
722
|
</View>
|
|
685
723
|
)}
|
|
686
724
|
{renderLocalControl}
|
|
@@ -1109,6 +1109,7 @@ export default {
|
|
|
1109
1109
|
automate: 'Automate',
|
|
1110
1110
|
smart: 'Smart',
|
|
1111
1111
|
enable_this_script: 'Enable this script',
|
|
1112
|
+
enable_notifications_for_this_script: 'Enable notifications for this script',
|
|
1112
1113
|
local_control: 'Local control',
|
|
1113
1114
|
local_control_update_success: 'Local control update successful',
|
|
1114
1115
|
choose_gateway: 'Choose gateway',
|
|
@@ -1118,6 +1118,7 @@ export default {
|
|
|
1118
1118
|
automate: 'Tự động',
|
|
1119
1119
|
smart: 'Thông minh',
|
|
1120
1120
|
enable_this_script: 'Kích hoạt kịch bản này',
|
|
1121
|
+
enable_notifications_for_this_script: 'Bật thông báo cho kịch bản này',
|
|
1121
1122
|
local_control: 'Điều khiển tại biên',
|
|
1122
1123
|
local_control_update_success: 'Cập nhật điều khiển tại biên thành công',
|
|
1123
1124
|
choose_gateway: 'Chọn gateway',
|