@eohjsc/react-native-smart-city 0.7.12 → 0.7.13

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.12",
4
+ "version": "0.7.13",
5
5
  "description": "TODO",
6
6
  "main": "index.js",
7
7
  "files": [
@@ -42,7 +42,8 @@
42
42
  "pods": "cd example && pod-install --quiet",
43
43
  "bootstrap": "yarn example && yarn && yarn pods",
44
44
  "build": "syncdir ./src ../EohMobile/node_modules/@eohjsc/react-native-smart-city/src",
45
- "watch": "syncdir ./src ../EohMobile/node_modules/@eohjsc/react-native-smart-city/src --watch"
45
+ "watch": "syncdir ./src ../EohMobile/node_modules/@eohjsc/react-native-smart-city/src --watch",
46
+ "copy-files": "cp -rfv ./assets/* ../EohMobile/node_modules/@eohjsc/react-native-smart-city/assets && cp -rfv ./src/* ../EohMobile/node_modules/@eohjsc/react-native-smart-city/src"
46
47
  },
47
48
  "repository": {
48
49
  "type": "git",
@@ -87,7 +88,7 @@
87
88
  "@react-navigation/drawer": "^6.6.15",
88
89
  "@react-navigation/native": "^6.1.17",
89
90
  "@react-navigation/native-stack": "^6.11.0",
90
- "@sentry/react-native": "^5.24.1",
91
+ "@sentry/react-native": "^6.2.0",
91
92
  "@testing-library/react-hooks": "^6.0.0",
92
93
  "@types/jest": "^29.2.1",
93
94
  "@types/react": "^17.0.0",
@@ -41,7 +41,7 @@ export default StyleSheet.create({
41
41
  height: 20,
42
42
  },
43
43
  title: {
44
- textAlign: 'center',
44
+ alignSelf: 'center',
45
45
  fontSize: 16,
46
46
  lineHeight: 24,
47
47
  fontWeight: 'bold',
@@ -55,5 +55,6 @@ export default StyleSheet.create({
55
55
  },
56
56
  wrapTitle: {
57
57
  marginHorizontal: 12,
58
+ flex: 1,
58
59
  },
59
60
  });
@@ -1,49 +1,57 @@
1
1
  import React, { memo } from 'react';
2
2
  import { View, StyleSheet } from 'react-native';
3
3
  import { useTranslations } from '../../hooks/Common/useTranslations';
4
- import { Colors, Constants } from '../../configs';
4
+ import { Constants } from '../../configs';
5
5
  import Text from '../../commons/Text';
6
6
  import RowMember from './RowMember';
7
+ import { RefreshControl, FlatList } from 'react-native';
8
+ import { getBottomSpace } from 'react-native-iphone-x-helper';
7
9
 
8
10
  const MemberList = ({
9
11
  dataMember,
10
12
  ownerId,
11
13
  unit,
12
14
  currentUserId, //user is using app
15
+ isRefresh,
16
+ onRefresh,
13
17
  }) => {
14
18
  const t = useTranslations();
15
19
  return (
16
- <View style={styles.box}>
17
- {!!dataMember.length &&
18
- dataMember.map((item, index) => (
19
- <RowMember
20
- member={item}
21
- unit={unit}
22
- index={index}
23
- ownerId={ownerId}
24
- currentUserId={currentUserId}
25
- key={index.toString()}
26
- />
27
- ))}
28
- {!dataMember.length && (
20
+ <FlatList
21
+ contentContainerStyle={styles.contentContainer}
22
+ keyExtractor={(item) => `${item.id}`}
23
+ data={dataMember}
24
+ extraData={dataMember}
25
+ numColumns={1}
26
+ refreshControl={
27
+ <RefreshControl refreshing={isRefresh} onRefresh={onRefresh} />
28
+ }
29
+ renderItem={({ item, index }) => (
30
+ <RowMember
31
+ member={item}
32
+ unit={unit}
33
+ index={index}
34
+ ownerId={ownerId}
35
+ currentUserId={currentUserId}
36
+ />
37
+ )}
38
+ ListEmptyComponent={() => (
29
39
  <View style={styles.viewEmpty}>
30
- <Text style={styles.textCenter}>{t('no_member')}</Text>
40
+ <Text type="H4">{t('no_member')}</Text>
31
41
  </View>
32
42
  )}
33
- </View>
43
+ />
34
44
  );
35
45
  };
36
46
  export default memo(MemberList);
37
47
 
38
48
  const styles = StyleSheet.create({
39
- box: {
40
- backgroundColor: Colors.White,
41
- paddingTop: 20,
49
+ contentContainer: {
50
+ paddingBottom: getBottomSpace() + 50,
42
51
  },
43
52
  viewEmpty: {
44
53
  justifyContent: 'center',
45
54
  alignItems: 'center',
46
55
  height: Constants.height - 200,
47
- backgroundColor: Colors.White,
48
56
  },
49
57
  });
@@ -18,160 +18,108 @@ const arrColor = [
18
18
  Colors.Green3,
19
19
  Colors.Cyan2,
20
20
  ];
21
- const RowMember = memo(
22
- ({
23
- member,
24
- index,
25
- ownerId,
26
- unit,
27
- currentUserId,
28
- type,
29
- leftIcon = true,
30
- rightComponent = true,
31
- }) => {
32
- const t = useTranslations();
33
- const { navigate } = useNavigation();
34
- const { id, name, phone_number, email, share_id, avatar } = member;
35
- const [role, roleColor] = useMemo(
36
- () =>
37
- id === ownerId
38
- ? [t('owner'), Colors.Primary]
39
- : id === currentUserId
40
- ? [t('me'), Colors.Primary]
41
- : [t('member'), Colors.Gray6],
42
- [currentUserId, id, ownerId, t]
43
- );
44
- const firstWordsInName = useMemo(() => {
45
- const wordTemp = name || shortEmailName(email) || '';
46
- return wordTemp?.charAt();
47
- }, [email, name]);
21
+ const RowMember = memo(({ member, index, ownerId, unit, currentUserId }) => {
22
+ const t = useTranslations();
23
+ const { navigate } = useNavigation();
24
+ const { id, name, phone_number, email, avatar } = member;
25
+ const [role, roleColor] = useMemo(
26
+ () =>
27
+ id === ownerId
28
+ ? [t('owner'), Colors.Primary]
29
+ : id === currentUserId
30
+ ? [t('me'), Colors.Primary]
31
+ : [t('member'), Colors.Gray6],
32
+ [currentUserId, id, ownerId, t]
33
+ );
34
+ const firstWordsInName = useMemo(() => {
35
+ const wordTemp = name || shortEmailName(email) || '';
36
+ return wordTemp?.charAt();
37
+ }, [email, name]);
48
38
 
49
- if (id === ownerId && share_id) {
50
- return null;
51
- }
52
- const circleColorTypes = {
53
- primary: 'primary',
54
- disable: 'disable',
55
- noneBG: 'none',
56
- };
57
- const circleColor = type
58
- ? circleColorTypes[type]
59
- : arrColor[index % arrColor.length];
60
-
61
- const onPressInfo = () => {
62
- navigate(Routes.UnitMemberInformation, {
63
- member,
64
- ownerId,
65
- unit,
66
- });
67
- };
68
- return (
69
- <View style={styles.rowContainer}>
70
- <TouchableOpacity
71
- onPress={onPressInfo}
72
- disabled={type === 'disable'}
73
- accessibilityLabel={`${AccessibilityLabel.SELECT_MEMBER_UNIT}-${member?.id}`}
74
- >
75
- <View style={styles.Border}>
76
- {!!leftIcon && (
77
- <View style={styles.paddingLeft16}>
78
- {avatar ? (
79
- <Image source={{ uri: avatar }} style={styles.avatar} />
80
- ) : (
81
- <CircleView size={40} backgroundColor={circleColor} center>
82
- <Text color={Colors.White}>{firstWordsInName}</Text>
83
- </CircleView>
84
- )}
85
- </View>
86
- )}
87
- <View style={styles.columnFlex}>
88
- <Text style={styles.titleName}>
89
- {name || shortEmailName(email) || ''}
39
+ const onPressInfo = () => {
40
+ navigate(Routes.UnitMemberInformation, {
41
+ member,
42
+ ownerId,
43
+ unit,
44
+ });
45
+ };
46
+ return (
47
+ <TouchableOpacity
48
+ onPress={onPressInfo}
49
+ accessibilityLabel={`${AccessibilityLabel.SELECT_MEMBER_UNIT}-${member?.id}`}
50
+ >
51
+ <View style={styles.border}>
52
+ <View style={styles.paddingLeft16}>
53
+ {avatar ? (
54
+ <Image source={{ uri: avatar }} style={styles.avatar} />
55
+ ) : (
56
+ <CircleView
57
+ size={40}
58
+ backgroundColor={arrColor[index % arrColor.length]}
59
+ center
60
+ >
61
+ <Text
62
+ color={Colors.White}
63
+ accessibilityLabel={
64
+ AccessibilityLabel.UNIT_MEMBER.ROW_TEXT_AVATAR
65
+ }
66
+ >
67
+ {firstWordsInName}
90
68
  </Text>
91
- {/* Can't write test for this case, use !! to avoid app crash when phone_number is '' */}
92
- {!!phone_number && (
93
- <Text
94
- style={styles.status}
95
- accessibilityLabel={
96
- AccessibilityLabel.TEXT_PHONE_NUMBER_UNIT_MEMBER
97
- }
98
- >
99
- {phone_number}
100
- </Text>
101
- )}
102
- </View>
103
- {!!rightComponent && (
104
- <View style={styles.endFlex}>
105
- {!!role && (
106
- <Text style={[styles.textRole, { color: roleColor }]}>
107
- {role}
108
- </Text>
109
- )}
110
- </View>
111
- )}
112
- </View>
113
- </TouchableOpacity>
69
+ </CircleView>
70
+ )}
71
+ </View>
72
+ <View style={styles.columnFlex}>
73
+ <Text
74
+ style={styles.titleName}
75
+ accessibilityLabel={AccessibilityLabel.UNIT_MEMBER.ROW_TEXT_NAME}
76
+ >
77
+ {name || shortEmailName(email) || ''}
78
+ </Text>
79
+ {/* Can't write test for this case, use !! to avoid app crash when phone_number is '' */}
80
+ {!!phone_number && (
81
+ <Text
82
+ style={styles.status}
83
+ accessibilityLabel={AccessibilityLabel.UNIT_MEMBER.ROW_TEXT_PHONE}
84
+ >
85
+ {'******' + phone_number.slice(-4)}
86
+ </Text>
87
+ )}
88
+ </View>
89
+ <View style={styles.endFlex}>
90
+ {!!role && (
91
+ <Text
92
+ style={[styles.textRole, { color: roleColor }]}
93
+ accessibilityLabel={AccessibilityLabel.UNIT_MEMBER.ROW_TEXT_ROLE}
94
+ >
95
+ {role}
96
+ </Text>
97
+ )}
98
+ </View>
114
99
  </View>
115
- );
116
- }
117
- );
100
+ </TouchableOpacity>
101
+ );
102
+ });
118
103
 
119
104
  export default RowMember;
120
105
 
121
106
  const styles = StyleSheet.create({
122
- rowContainer: {
123
- flex: 1,
124
- },
125
- iconContainer: {
126
- alignItems: 'center',
127
- justifyContent: 'center',
128
- marginRight: 16,
129
- width: 40,
130
- height: 40,
131
- borderRadius: 20,
132
- },
133
- infoContainer: {
134
- flex: 1,
135
- paddingTop: 17,
136
- borderBottomWidth: 1,
137
- borderColor: Colors.Gray4,
138
- },
139
- textName: {
140
- fontSize: 16,
141
- lineHeight: 24,
142
- color: Colors.Gray9,
143
- marginBottom: 0,
144
- },
145
107
  textRole: {
146
108
  fontSize: 12,
147
109
  lineHeight: 20,
148
110
  },
149
- buttonRemove: {
150
- position: 'absolute',
151
- right: 0,
152
- top: 10,
153
- bottom: 10,
154
- paddingHorizontal: 10,
155
- justifyContent: 'center',
156
- },
157
- textCenter: {
158
- alignSelf: 'center',
159
- },
160
111
  columnFlex: {
161
112
  paddingLeft: 16,
162
113
  justifyContent: 'center',
163
114
  flexDirection: 'column',
164
115
  },
165
- rowFlex: {
166
- flexDirection: 'row',
167
- },
168
116
  endFlex: {
169
117
  flex: 1,
170
118
  justifyContent: 'flex-start',
171
119
  alignItems: 'flex-end',
172
120
  paddingRight: 16,
173
121
  },
174
- Border: {
122
+ border: {
175
123
  display: 'flex',
176
124
  width: '100%',
177
125
  borderWidth: 0,
@@ -196,9 +144,6 @@ const styles = StyleSheet.create({
196
144
  fontWeight: '400',
197
145
  color: Colors.Gray7,
198
146
  },
199
- container: {
200
- paddingTop: 0,
201
- },
202
147
  paddingLeft16: {
203
148
  paddingLeft: 16,
204
149
  },
@@ -1,31 +1,30 @@
1
- import React from 'react';
2
1
  import renderer, { act } from 'react-test-renderer';
2
+
3
3
  import MemberList from '../MemberList';
4
+ import React from 'react';
4
5
  import RowMember from '../RowMember';
5
- import Text from '../../Text';
6
6
  import { SCProvider } from '../../../context';
7
+ import Text from '../../Text';
7
8
  import { mockSCStore } from '../../../context/mockStore';
8
9
  import t from '../../../hooks/Common/useTranslations';
9
10
 
10
- const wrapComponent = (dataMember, ownerId, currentUserId, mockFunc) => (
11
+ const wrapComponent = (dataMember, ownerId, currentUserId) => (
11
12
  <SCProvider initState={mockSCStore({})}>
12
13
  <MemberList
13
14
  dataMember={dataMember}
14
15
  ownerId={ownerId}
15
16
  currentUserId={currentUserId}
16
- onPressRemove={mockFunc}
17
17
  />
18
18
  </SCProvider>
19
19
  );
20
20
 
21
21
  describe('MemberList', () => {
22
22
  let tree;
23
- const mockFunc = jest.fn();
24
23
 
25
24
  it('MemberList snapshot id dataMember === ownerId', async () => {
26
25
  const dataMember = [{ id: 1, name: 'CEO' }];
27
26
  await act(async () => {
28
- tree = await renderer.create(wrapComponent(dataMember, 1, 2, mockFunc));
27
+ tree = await renderer.create(wrapComponent(dataMember, 1, 2));
29
28
  });
30
29
  const instance = tree.root;
31
30
  const rowMember = instance.findByType(RowMember);
@@ -38,9 +37,7 @@ describe('MemberList', () => {
38
37
  it('MemberList snapshot id dataMember !== ownerId', async () => {
39
38
  const dataMember = [{ id: 1, name: 'CEO' }];
40
39
  await act(async () => {
41
- tree = await renderer.create(
42
- wrapComponent(dataMember, 2, null, mockFunc)
43
- );
40
+ tree = await renderer.create(wrapComponent(dataMember, 2, null));
44
41
  });
45
42
  const instance = tree.root;
46
43
  const rowMember = instance.findByType(RowMember);
@@ -53,7 +50,7 @@ describe('MemberList', () => {
53
50
  it('MemberList snapshot id dataMember === currentUserId', async () => {
54
51
  const dataMember = [{ id: 1, name: 'CEO' }];
55
52
  await act(async () => {
56
- tree = await renderer.create(wrapComponent(dataMember, 2, 1, mockFunc));
53
+ tree = await renderer.create(wrapComponent(dataMember, 2, 1));
57
54
  });
58
55
  const instance = tree.root;
59
56
  const rowMembers = instance.findAllByType(RowMember);
@@ -1,40 +1,137 @@
1
- import React from 'react';
1
+ import { Image, TouchableOpacity } from 'react-native';
2
2
  import renderer, { act } from 'react-test-renderer';
3
+
4
+ import AccessibilityLabel from '../../../configs/AccessibilityLabel';
5
+ import React from 'react';
6
+ import Routes from '../../../utils/Route';
3
7
  import RowMember from '../RowMember';
4
- import Text from '../../Text';
5
8
  import { SCProvider } from '../../../context';
9
+ import Text from '../../Text';
6
10
  import { mockSCStore } from '../../../context/mockStore';
11
+ import { useNavigation } from '@react-navigation/native';
7
12
 
8
- const wrapComponent = (member, ownerId, currentUserId, mockFunc) => (
13
+ const wrapComponent = ({ member, ownerId, currentUserId, index = 0 }) => (
9
14
  <SCProvider initState={mockSCStore({})}>
10
15
  <RowMember
11
16
  member={member}
12
17
  ownerId={ownerId}
13
18
  currentUserId={currentUserId}
14
- onPressRemove={mockFunc}
19
+ index={index}
15
20
  />
16
21
  </SCProvider>
17
22
  );
18
23
 
19
24
  describe('RowMember', () => {
20
25
  let tree;
21
- const mockFunc = jest.fn();
26
+ const mockedNavigate = useNavigation().navigate;
27
+ const assertTextItems = ({ instance, avatar, name, phone, role }) => {
28
+ const avatarText = instance.findAll(
29
+ (el) =>
30
+ el.props.accessibilityLabel ===
31
+ AccessibilityLabel.UNIT_MEMBER.ROW_TEXT_AVATAR && el.type === Text
32
+ );
33
+ const nameText = instance.findAll(
34
+ (el) =>
35
+ el.props.accessibilityLabel ===
36
+ AccessibilityLabel.UNIT_MEMBER.ROW_TEXT_NAME && el.type === Text
37
+ );
38
+ const phoneText = instance.findAll(
39
+ (el) =>
40
+ el.props.accessibilityLabel ===
41
+ AccessibilityLabel.UNIT_MEMBER.ROW_TEXT_PHONE && el.type === Text
42
+ );
43
+ const roleText = instance.findAll(
44
+ (el) =>
45
+ el.props.accessibilityLabel ===
46
+ AccessibilityLabel.UNIT_MEMBER.ROW_TEXT_ROLE && el.type === Text
47
+ );
48
+
49
+ avatar !== null
50
+ ? expect(avatarText[0].props.children).toEqual(avatar)
51
+ : expect(avatarText.length).toEqual(0);
52
+ name !== null
53
+ ? expect(nameText[0].props.children).toEqual(name)
54
+ : expect(nameText.length).toEqual(0);
55
+ phone !== null
56
+ ? expect(phoneText[0].props.children).toEqual(phone)
57
+ : expect(phoneText.length).toEqual(0);
58
+ role !== null
59
+ ? expect(roleText[0].props.children).toEqual(role)
60
+ : expect(roleText.length).toEqual(0);
61
+ };
62
+
63
+ beforeEach(() => {
64
+ mockedNavigate.mockClear();
65
+ });
66
+
22
67
  it('RowMember owner have name', async () => {
23
- const dataMember = { id: 1, name: 'CEO' };
68
+ const dataMember = {
69
+ id: 1,
70
+ name: 'CEO',
71
+ avatar: 'https://image.jpg',
72
+ phone_number: '0123456789',
73
+ };
24
74
  await act(async () => {
25
- tree = await renderer.create(wrapComponent(dataMember, 1, 1, mockFunc));
75
+ tree = await renderer.create(
76
+ wrapComponent({ member: dataMember, ownerId: 1, currentUserId: 1 })
77
+ );
26
78
  });
27
79
  const instance = tree.root;
28
- const textInputs = instance.findAllByType(Text);
29
- expect(textInputs[1].props.children).toEqual('CEO');
80
+ const image = instance.findByType(Image);
81
+ assertTextItems({
82
+ instance,
83
+ avatar: null,
84
+ name: 'CEO',
85
+ phone: '******6789',
86
+ role: 'Owner',
87
+ });
88
+ expect(image.props.source.uri).toEqual('https://image.jpg');
89
+
90
+ const touchable = instance.findByType(TouchableOpacity);
91
+ await act(async () => {
92
+ touchable.props.onPress();
93
+ });
94
+
95
+ expect(global.mockedNavigate).toHaveBeenCalledWith(
96
+ Routes.UnitMemberInformation,
97
+ {
98
+ member: dataMember,
99
+ ownerId: 1,
100
+ unit: undefined,
101
+ }
102
+ );
103
+ });
104
+ it('render RowMember with null name', async () => {
105
+ const dataMember = { id: 2, name: '', email: 'abc@gmail.com' };
106
+ await act(async () => {
107
+ tree = await renderer.create(
108
+ wrapComponent({ member: dataMember, ownerId: 1, currentUserId: 2 })
109
+ );
110
+ });
111
+ const instance = tree.root;
112
+
113
+ assertTextItems({
114
+ instance,
115
+ avatar: 'a',
116
+ name: 'abc',
117
+ phone: null,
118
+ role: 'Me',
119
+ });
30
120
  });
31
- it('RowMember owner dont have name show start of email ', async () => {
32
- const dataMember = { id: 1, name: '', email: 'abc@gmail.com' };
121
+ it('render RowMember with null name and email', async () => {
122
+ const dataMember = { id: 3, name: null, email: null };
33
123
  await act(async () => {
34
- tree = await renderer.create(wrapComponent(dataMember, 1, 1, mockFunc));
124
+ tree = await renderer.create(
125
+ wrapComponent({ member: dataMember, ownerId: 1, currentUserId: 1 })
126
+ );
35
127
  });
36
128
  const instance = tree.root;
37
- const textInputs = instance.findAllByType(Text);
38
- expect(textInputs[1].props.children).toEqual('abc');
129
+ assertTextItems({
130
+ instance,
131
+ avatar: '',
132
+ name: '',
133
+ phone: null,
134
+ role: 'Member',
135
+ });
39
136
  });
40
137
  });
@@ -50,8 +50,12 @@ export default {
50
50
  SHARING_MEMBER: 'SHARING_MEMBER',
51
51
  REMOVE_MEMBER: 'REMOVE_MEMBER',
52
52
  CHECK_BOX_CUSTOM: 'CHECK_BOX_CUSTOM',
53
- TEXT_PHONE_NUMBER_ITEM: 'TEXT_PHONE_NUMBER_ITEM',
54
- TEXT_PHONE_NUMBER_UNIT_MEMBER: 'TEXT_PHONE_NUMBER_UNIT_MEMBER',
53
+ UNIT_MEMBER: {
54
+ ROW_TEXT_AVATAR: 'ROW_TEXT_AVATAR',
55
+ ROW_TEXT_NAME: 'ROW_TEXT_NAME',
56
+ ROW_TEXT_PHONE: 'ROW_TEXT_PHONE',
57
+ ROW_TEXT_ROLE: 'ROW_TEXT_ROLE',
58
+ },
55
59
 
56
60
  // SmartTiviTemplate
57
61
  SMART_TIVI_TEMPLATE: {
@@ -6,7 +6,6 @@ export const useIsOwnerOfUnit = (unitOwnerId) => {
6
6
  (state) => state?.auth?.account?.user?.id
7
7
  );
8
8
  let isOwner = unitOwnerId && idUser === unitOwnerId;
9
-
10
9
  return {
11
10
  isOwner,
12
11
  };
@@ -104,12 +104,13 @@ const SetupScriptReceiverEmail = ({ route }) => {
104
104
 
105
105
  return (
106
106
  <View style={styles.rowContainer}>
107
- <View style={styles.Border}>
107
+ <View style={styles.border}>
108
108
  <CheckBox
109
109
  disabled={!email}
110
110
  lineWidth={4}
111
111
  value={listUser.includes(id)}
112
112
  onValueChange={onValueChange(id)}
113
+ style={styles.checkbox}
113
114
  />
114
115
  <View style={styles.paddingLeft16}>
115
116
  {avatar ? (
@@ -41,10 +41,12 @@ export default StyleSheet.create({
41
41
  justifyContent: 'flex-start',
42
42
  alignItems: 'flex-end',
43
43
  },
44
- Border: {
44
+ border: {
45
45
  flexDirection: 'row',
46
46
  paddingBottom: 16,
47
47
  paddingHorizontal: 16,
48
+ justifyContent: 'center',
49
+ alignItems: 'center',
48
50
  },
49
51
  titleName: {
50
52
  fontSize: 16,
@@ -76,4 +78,8 @@ export default StyleSheet.create({
76
78
  borderRadius: 40,
77
79
  backgroundColor: Colors.Primary,
78
80
  },
81
+ checkbox: {
82
+ width: 24,
83
+ height: 24,
84
+ },
79
85
  });
@@ -36,6 +36,8 @@ export default StyleSheet.create({
36
36
  flexDirection: 'row',
37
37
  paddingBottom: 16,
38
38
  paddingHorizontal: 16,
39
+ justifyContent: 'center',
40
+ alignItems: 'center',
39
41
  },
40
42
  paddingLeft16: {
41
43
  paddingLeft: 16,
@@ -75,4 +77,8 @@ export default StyleSheet.create({
75
77
  fontSize: 12,
76
78
  lineHeight: 20,
77
79
  },
80
+ checkbox: {
81
+ width: 24,
82
+ height: 24,
83
+ },
78
84
  });
@@ -12,6 +12,7 @@ export default StyleSheet.create({
12
12
  wrapContent: {
13
13
  flex: 1,
14
14
  paddingHorizontal: 20,
15
+ marginBottom: 100,
15
16
  },
16
17
  wrapItem: {
17
18
  flexDirection: 'row',
@@ -50,12 +51,14 @@ export default StyleSheet.create({
50
51
  noBorder: {
51
52
  borderWidth: 0,
52
53
  },
54
+ icon: {
55
+ alignItems: 'center',
56
+ justifyContent: 'center',
57
+ marginRight: 10,
58
+ },
53
59
  iconItem: {
54
60
  width: 35,
55
61
  height: 35,
56
- marginTop: 6,
57
- marginRight: 15,
58
- alignItems: 'center',
59
62
  },
60
63
  contentItem: {
61
64
  flex: 1,
@@ -20,7 +20,7 @@ const UpdateNotifyScript = ({
20
20
  updateIndex,
21
21
  setNeedRefresh,
22
22
  }) => {
23
- const { title, message } = notify_script;
23
+ const { title, message, unit_id, unit_name } = notify_script;
24
24
  const [newValue, setNewValue] = useState({
25
25
  newTitle: title,
26
26
  newMessage: message,
@@ -31,7 +31,7 @@ const UpdateNotifyScript = ({
31
31
  API.AUTOMATE.UPDATE_SCRIPT_NOTIFY(automateId),
32
32
  {
33
33
  id: scriptItemId,
34
- unit: unitId,
34
+ unit: unit_id,
35
35
  title: newValue.newTitle,
36
36
  message: newValue.newMessage,
37
37
  }
@@ -40,6 +40,7 @@ const UpdateNotifyScript = ({
40
40
  actionsList[updateIndex].notify_script = {
41
41
  title: newValue.newTitle,
42
42
  message: newValue.newMessage,
43
+ unit_name: unit_name,
43
44
  };
44
45
  setActionList(actionsList);
45
46
  onClosePopup();
@@ -56,7 +57,8 @@ const UpdateNotifyScript = ({
56
57
  setActionList,
57
58
  setNeedRefresh,
58
59
  t,
59
- unitId,
60
+ unit_id,
61
+ unit_name,
60
62
  updateIndex,
61
63
  ]);
62
64
 
@@ -123,6 +123,7 @@ const UpdateReceiverEmailScript = ({ route }) => {
123
123
  lineWidth={4}
124
124
  value={listUser.includes(id)}
125
125
  onValueChange={onValueChange(id)}
126
+ style={styles.checkbox}
126
127
  />
127
128
  <View style={styles.paddingLeft16}>
128
129
  {avatar ? (
@@ -124,7 +124,7 @@ const EditActionsList = () => {
124
124
  onLongPress={onPressUpdate}
125
125
  accessibilityLabel={AccessibilityLabel.BUTTON_UPDATE}
126
126
  >
127
- {icon}
127
+ <View style={styles.icon}>{icon}</View>
128
128
  <View style={styles.contentItem}>{content}</View>
129
129
  <TouchableOpacity
130
130
  accessibilityLabel={AccessibilityLabel.BUTTON_REMOVE_EDIT_ACTION_LIST}
@@ -183,7 +183,7 @@ const EditActionsList = () => {
183
183
  }
184
184
 
185
185
  if (notify_script) {
186
- const { title, message } = notify_script;
186
+ const { title, message, unit_name } = notify_script;
187
187
 
188
188
  return (
189
189
  <CommonItem
@@ -201,6 +201,9 @@ const EditActionsList = () => {
201
201
  <Text numberOfLines={1} type="H4" color={Colors.Gray9}>
202
202
  {message}
203
203
  </Text>
204
+ <Text numberOfLines={1} type="H4" color={Colors.Gray9}>
205
+ {unit_name}
206
+ </Text>
204
207
  </>
205
208
  }
206
209
  onPress={() => onPressRemove(item)}
@@ -89,6 +89,8 @@ export default StyleSheet.create({
89
89
  paddingHorizontal: 16,
90
90
  marginLeft: 4,
91
91
  flexDirection: 'row',
92
+ justifyContent: 'center',
93
+ alignItems: 'center',
92
94
  },
93
95
  leftItem: {
94
96
  width: 41,
@@ -179,5 +181,7 @@ export default StyleSheet.create({
179
181
  height: 35,
180
182
  marginTop: 6,
181
183
  marginRight: 15,
184
+ justifyContent: 'center',
185
+ alignItems: 'center',
182
186
  },
183
187
  });
@@ -447,7 +447,7 @@ const Item = ({ item, index, enableScript, t }) => {
447
447
  </View>
448
448
  );
449
449
  } else if (notify_script) {
450
- const { title, message } = notify_script;
450
+ const { title, message, unit_name } = notify_script;
451
451
  return (
452
452
  <View style={styles.wrapItem}>
453
453
  <View style={styles.leftItem}>
@@ -469,6 +469,9 @@ const Item = ({ item, index, enableScript, t }) => {
469
469
  </Text>
470
470
  </View>
471
471
  </View>
472
+ <Text numberOfLines={1} type="H4" color={color}>
473
+ {unit_name}
474
+ </Text>
472
475
  </View>
473
476
  </View>
474
477
  );
@@ -481,7 +484,7 @@ const Item = ({ item, index, enableScript, t }) => {
481
484
  {paddedIndex}
482
485
  </Text>
483
486
  </View>
484
- <View style={styles.rightItem}>
487
+ <View style={styles.rightItemAction}>
485
488
  <View style={styles.iconEndDevice}>
486
489
  <Delay />
487
490
  </View>
@@ -1,33 +1,41 @@
1
- import React, { useCallback, useEffect } from 'react';
2
- import { IconOutline } from '@ant-design/icons-react-native';
3
- import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
4
- import { useNavigation, useIsFocused } from '@react-navigation/native';
1
+ import React, {
2
+ Fragment,
3
+ useCallback,
4
+ useEffect,
5
+ useMemo,
6
+ useState,
7
+ } from 'react';
5
8
  import { StyleSheet, TouchableOpacity, View } from 'react-native';
6
- import { useTranslations } from '../../hooks/Common/useTranslations';
9
+ import { useDataMember, useStateAlertAction } from './hooks';
10
+ import { useIsFocused, useNavigation } from '@react-navigation/native';
7
11
 
12
+ import { AccessibilityLabel } from '../../configs/Constants';
13
+ import AlertAction from '../../commons/AlertAction';
8
14
  import { Colors } from '../../configs';
9
- import Routes from '../../utils/Route';
15
+ import { HeaderCustom } from '../../commons';
16
+ import { IconOutline } from '@ant-design/icons-react-native';
17
+ import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
10
18
  import MemberList from '../../commons/Sharing/MemberList';
11
- import WrapHeaderScrollable from '../../commons/Sharing/WrapHeaderScrollable';
19
+ import Routes from '../../utils/Route';
20
+ import { Search } from '../../commons/DevMode';
21
+ import { ToastBottomHelper } from '../../utils/Utils';
22
+ import { convertToSlug } from '../../utils/Functions/Search';
23
+ import { useBackendPermission } from '../../utils/Permission/backend';
12
24
  import { useIsOwnerOfUnit } from '../../hooks/Common';
13
- import AlertAction from '../../commons/AlertAction';
14
-
15
- import { useDataMember, useStateAlertAction } from './hooks';
16
- import { AccessibilityLabel } from '../../configs/Constants';
17
25
  import { useSCContextSelector } from '../../context';
18
- import { useBackendPermission } from '../../utils/Permission/backend';
19
- import { ToastBottomHelper } from '../../utils/Utils';
26
+ import { useTranslations } from '../../hooks/Common/useTranslations';
20
27
 
21
28
  const UnitMemberList = ({ route }) => {
22
29
  const t = useTranslations();
23
30
  const { navigate } = useNavigation();
24
31
  const isFocused = useIsFocused();
25
32
  const account = useSCContextSelector((state) => state.auth.account);
26
- const { unitId, unit } = route?.params || {};
33
+ const { unitId, unit } = route?.params;
27
34
  const { dataMembers, isRefresh, onRefresh, leaveUnit, loading } =
28
35
  useDataMember(unitId, unit?.user_id);
29
36
  const { isOwner } = useIsOwnerOfUnit(unit?.user_id);
30
37
  const permissions = useBackendPermission();
38
+ const [searchText, setSearchText] = useState('');
31
39
 
32
40
  const { stateAlertSharingMenu, hideStateAlertSharingMenu, stateLeaveUnit } =
33
41
  useStateAlertAction();
@@ -71,45 +79,59 @@ const UnitMemberList = ({ route }) => {
71
79
  }, [hideStateAlertSharingMenu, isOwner, leaveUnit, unit.name]);
72
80
 
73
81
  useEffect(() => {
74
- if (isFocused) {
75
- onRefresh();
76
- }
82
+ isFocused && onRefresh();
77
83
  }, [isFocused, onRefresh]);
78
84
 
79
- const rightHeader = (
80
- <TouchableOpacity
81
- accessibilityLabel={AccessibilityLabel.MEMBER_LIST_RIGHT_HEADER_TOUCH}
82
- onPress={onPressRightHeader}
83
- style={styles.rightHeader}
84
- >
85
- {isOwner ? (
86
- <IconOutline name={'plus'} size={27} color={Colors.Black} />
87
- ) : (
88
- <MaterialIcons name={'more-vert'} size={27} color={Colors.Black} />
89
- )}
90
- </TouchableOpacity>
85
+ const matchedMembers = useMemo(() => {
86
+ return dataMembers.filter((member) => {
87
+ const text = convertToSlug(searchText);
88
+ const isPhoneMatch = convertToSlug(member.phone_number ?? '').includes(
89
+ text
90
+ );
91
+ const isNameMatch = convertToSlug(member.name ?? '').includes(text);
92
+ return isPhoneMatch || isNameMatch;
93
+ });
94
+ }, [dataMembers, searchText]);
95
+ const rightComponent = useMemo(
96
+ () => (
97
+ <TouchableOpacity
98
+ accessibilityLabel={AccessibilityLabel.MEMBER_LIST_RIGHT_HEADER_TOUCH}
99
+ onPress={onPressRightHeader}
100
+ >
101
+ {isOwner ? (
102
+ <IconOutline name={'plus'} size={27} color={Colors.Black} />
103
+ ) : (
104
+ <MaterialIcons name={'more-vert'} size={27} color={Colors.Black} />
105
+ )}
106
+ </TouchableOpacity>
107
+ ),
108
+ [isOwner, onPressRightHeader]
91
109
  );
92
110
 
93
111
  return (
94
112
  <View style={styles.container}>
95
- <WrapHeaderScrollable
113
+ <HeaderCustom
96
114
  title={t('unit_member')}
97
- rightComponent={rightHeader}
98
- loading={isRefresh}
99
- onRefresh={onRefresh}
100
- headerAniStyle={styles.headerAniStyle}
101
- styleScrollView={{ backgroundColor: Colors.White }}
102
- >
115
+ isShowSeparator
116
+ rightComponent={rightComponent}
117
+ titleStyles={styles.headerTitle}
118
+ />
119
+ <View style={styles.content}>
103
120
  {!loading && (
104
- <MemberList
105
- accessibilityLabel={AccessibilityLabel.SHARING_MEMBER}
106
- dataMember={dataMembers}
107
- unit={unit}
108
- ownerId={unit.user_id}
109
- currentUserId={account.user.id}
110
- />
121
+ <Fragment>
122
+ <Search onSearch={setSearchText} />
123
+ <MemberList
124
+ accessibilityLabel={AccessibilityLabel.SHARING_MEMBER}
125
+ dataMember={matchedMembers}
126
+ unit={unit}
127
+ ownerId={unit.user_id}
128
+ currentUserId={account.user.id}
129
+ isRefresh={isRefresh}
130
+ onRefresh={onRefresh}
131
+ />
132
+ </Fragment>
111
133
  )}
112
- </WrapHeaderScrollable>
134
+ </View>
113
135
  <AlertAction
114
136
  visible={stateAlertSharingMenu.visible}
115
137
  hideModal={hideStateAlertSharingMenu}
@@ -130,18 +152,19 @@ export default UnitMemberList;
130
152
  const styles = StyleSheet.create({
131
153
  container: {
132
154
  flex: 1,
133
- backgroundColor: Colors.Gray2,
155
+ backgroundColor: Colors.White,
134
156
  },
135
- rightHeader: {
136
- alignItems: 'center',
137
- width: 44,
138
- justifyContent: 'center',
157
+ content: {
139
158
  flex: 1,
140
- },
141
- headerAniStyle: {
142
- borderBottomWidth: 0,
159
+ marginHorizontal: 16,
143
160
  },
144
161
  rightButtonStyle: {
145
162
  color: Colors.Red,
146
163
  },
164
+ headerTitle: {
165
+ alignSelf: 'flex-start',
166
+ marginLeft: 0,
167
+ fontSize: 20,
168
+ lineHeight: 28,
169
+ },
147
170
  });
@@ -1,25 +1,23 @@
1
- import React from 'react';
2
- import { create, act } from 'react-test-renderer';
1
+ import { act, create } from 'react-test-renderer';
3
2
 
4
- import UnitMemberList from '../UnitMemberList';
3
+ import { API } from '../../../configs';
4
+ import { AccessibilityLabel } from '../../../configs/Constants';
5
5
  import { AlertAction } from '../../../commons';
6
+ import MemberList from '../../../commons/Sharing/MemberList';
7
+ import MockAdapter from 'axios-mock-adapter';
8
+ import React from 'react';
9
+ import Routes from '../../../utils/Route';
6
10
  import { SCProvider } from '../../../context';
7
- import { mockSCStore } from '../../../context/mockStore';
11
+ import { Search } from '../../../commons/DevMode';
12
+ import { ToastBottomHelper } from '../../../utils/Utils';
8
13
  import { TouchableOpacity } from 'react-native';
9
- import Routes from '../../../utils/Route';
10
- import MockAdapter from 'axios-mock-adapter';
14
+ import UnitMemberList from '../UnitMemberList';
11
15
  import api from '../../../utils/Apis/axios';
12
- import { useNavigation } from '@react-navigation/native';
13
- import { ToastBottomHelper } from '../../../utils/Utils';
14
16
  import { getTranslate } from '../../../utils/I18n';
17
+ import { mockSCStore } from '../../../context/mockStore';
18
+ import { useNavigation } from '@react-navigation/native';
15
19
 
16
- new MockAdapter(api.axiosInstance);
17
-
18
- jest.mock('../../../hooks/Common', () => {
19
- return {
20
- useIsOwnerOfUnit: () => ({ isOwner: true }),
21
- };
22
- });
20
+ const mockAxios = new MockAdapter(api.axiosInstance);
23
21
 
24
22
  const wrapComponent = (route, state = {}) => (
25
23
  <SCProvider initState={mockSCStore(state)}>
@@ -29,21 +27,17 @@ const wrapComponent = (route, state = {}) => (
29
27
 
30
28
  describe('test MemberList', () => {
31
29
  let route;
32
- let localState = {
33
- auth: {
34
- account: {
35
- user: {
36
- id: 2,
37
- },
38
- },
39
- },
40
- };
41
-
42
- const mockedNavigate = useNavigation().navigate;
30
+ let localState;
43
31
 
32
+ const mockNavigate = useNavigation().navigate;
33
+ const spyToastError = jest.spyOn(ToastBottomHelper, 'error');
34
+ const spyToastSuccess = jest.spyOn(ToastBottomHelper, 'success');
44
35
  beforeEach(() => {
45
- mockedNavigate.mockClear();
46
-
36
+ mockNavigate.mockClear();
37
+ mockAxios.reset();
38
+ spyToastError.mockClear();
39
+ spyToastSuccess.mockClear();
40
+ jest.clearAllMocks();
47
41
  route = {
48
42
  params: {
49
43
  unitId: 1,
@@ -54,6 +48,16 @@ describe('test MemberList', () => {
54
48
  },
55
49
  },
56
50
  };
51
+
52
+ localState = {
53
+ auth: {
54
+ account: {
55
+ user: {
56
+ id: 2,
57
+ },
58
+ },
59
+ },
60
+ };
57
61
  });
58
62
 
59
63
  it('AlertAction rightButtonClick', async () => {
@@ -70,7 +74,7 @@ describe('test MemberList', () => {
70
74
  await act(async () => {
71
75
  await memberListButtons[1].props.onPress();
72
76
  });
73
- expect(global.mockedNavigate).toBeCalledWith(Routes.AddMemberStack, {
77
+ expect(mockNavigate).toHaveBeenCalledWith(Routes.AddMemberStack, {
74
78
  screen: Routes.SelectShareDevice,
75
79
  params: { unit: { id: 1 } },
76
80
  });
@@ -84,6 +88,7 @@ describe('test MemberList', () => {
84
88
  auth: {
85
89
  account: {
86
90
  user: {
91
+ id: 2,
87
92
  permissions: {
88
93
  max_members_per_unit: 0,
89
94
  },
@@ -94,16 +99,108 @@ describe('test MemberList', () => {
94
99
  );
95
100
  });
96
101
  const instance = tree.root;
97
- const spyToastError = jest.spyOn(ToastBottomHelper, 'error');
98
102
  const memberListButtons = instance.findAllByType(TouchableOpacity);
99
103
  await act(async () => {
100
104
  await memberListButtons[1].props.onPress();
101
105
  });
102
- expect(global.mockedNavigate).not.toBeCalled();
103
- expect(spyToastError).toBeCalledWith(
106
+ expect(mockNavigate).not.toHaveBeenCalled();
107
+ expect(spyToastError).toHaveBeenCalledWith(
104
108
  getTranslate('en', 'reach_max_members_per_unit', { length: 0 }),
105
109
  '',
106
110
  7000
107
111
  );
108
112
  });
113
+
114
+ it('test member leaves unit', async () => {
115
+ mockAxios.onDelete(API.SHARE.UNITS_MEMBER_DETAIL(1, 'me')).reply(200);
116
+
117
+ localState = {
118
+ auth: {
119
+ account: {
120
+ user: {
121
+ id: 5,
122
+ },
123
+ },
124
+ },
125
+ };
126
+
127
+ let tree;
128
+ await act(async () => {
129
+ tree = await create(wrapComponent(route, localState));
130
+ });
131
+ const instance = tree.root;
132
+
133
+ const alertAction = instance.findByType(AlertAction);
134
+ await act(async () => {
135
+ alertAction.props.rightButtonClick();
136
+ });
137
+
138
+ const rightHeaderButton = instance.find(
139
+ (el) =>
140
+ el.type === TouchableOpacity &&
141
+ el.props.accessibilityLabel ===
142
+ AccessibilityLabel.MEMBER_LIST_RIGHT_HEADER_TOUCH
143
+ );
144
+ await act(async () => {
145
+ await rightHeaderButton.props.onPress();
146
+ });
147
+ expect(mockNavigate).toHaveBeenCalledWith(Routes.Dashboard);
148
+ expect(spyToastSuccess).toHaveBeenCalledTimes(1);
149
+ });
150
+
151
+ it('test search member by name and phone', async () => {
152
+ const dataMember = [
153
+ {
154
+ id: 1,
155
+ name: 'user 1',
156
+ avatar: 'https://image1.jpg',
157
+ phone_number: '0933123456',
158
+ },
159
+ {
160
+ id: 2,
161
+ name: 'user 2',
162
+ avatar: 'https://image1.jpg',
163
+ phone_number: '0933777111',
164
+ },
165
+ {
166
+ id: 3,
167
+ name: null,
168
+ avatar: 'https://image1.jpg',
169
+ phone_number: null,
170
+ },
171
+ ];
172
+
173
+ mockAxios.onGet(API.SHARE.UNITS_MEMBERS(1)).reply(200, dataMember);
174
+
175
+ let tree;
176
+ await act(async () => {
177
+ tree = await create(wrapComponent(route, localState));
178
+ });
179
+ const instance = tree.root;
180
+
181
+ const search = instance.findByType(Search);
182
+ const memberList = instance.findByType(MemberList);
183
+ await act(async () => {
184
+ search.props.onSearch('user 1');
185
+ });
186
+
187
+ expect(memberList.props.dataMember).toEqual([dataMember[0]]);
188
+
189
+ await act(async () => {
190
+ search.props.onSearch('3777');
191
+ });
192
+
193
+ expect(memberList.props.dataMember).toEqual([dataMember[1]]);
194
+
195
+ await act(async () => {
196
+ search.props.onSearch('');
197
+ });
198
+
199
+ //move owner to top
200
+ expect(memberList.props.dataMember).toEqual([
201
+ dataMember[1],
202
+ dataMember[0],
203
+ dataMember[2],
204
+ ]);
205
+ });
109
206
  });
@@ -1,12 +1,13 @@
1
- import React from 'react';
2
1
  import { act, renderHook } from '@testing-library/react-hooks';
3
- import MockAdapter from 'axios-mock-adapter';
4
- import { useDataMember } from '..';
2
+
5
3
  import API from '../../../../configs/API';
6
- import api from '../../../../utils/Apis/axios';
4
+ import MockAdapter from 'axios-mock-adapter';
5
+ import React from 'react';
6
+ import Routes from '../../../../utils/Route';
7
7
  import { SCProvider } from '../../../../context';
8
+ import api from '../../../../utils/Apis/axios';
8
9
  import { mockSCStore } from '../../../../context/mockStore';
9
- import Routes from '../../../../utils/Route';
10
+ import { useDataMember } from '..';
10
11
 
11
12
  const mock = new MockAdapter(api.axiosInstance);
12
13
 
@@ -1,12 +1,13 @@
1
+ import { axiosDelete, axiosGet } from '../../../utils/Apis/axios';
1
2
  import { useCallback, useState } from 'react';
2
- import { useTranslations } from '../../../hooks/Common/useTranslations';
3
- import { useNavigation } from '@react-navigation/native';
3
+
4
4
  import { API } from '../../../configs';
5
5
  import Routes from '../../../utils/Route';
6
- import { axiosDelete, axiosGet } from '../../../utils/Apis/axios';
7
6
  import { ToastBottomHelper } from '../../../utils/Utils';
8
- import { useSCContextSelector } from '../../../context';
9
7
  import { useIsOwnerOfUnit } from '../../../hooks/Common';
8
+ import { useNavigation } from '@react-navigation/native';
9
+ import { useSCContextSelector } from '../../../context';
10
+ import { useTranslations } from '../../../hooks/Common/useTranslations';
10
11
 
11
12
  const useDataMember = (unitId, userUnitId = undefined) => {
12
13
  const t = useTranslations();