@sudobility/entity-components-rn 1.0.1
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/dist/index.js +76 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1509 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +63 -0
- package/src/EntityCard.tsx +137 -0
- package/src/EntityList.tsx +96 -0
- package/src/EntitySelector.tsx +200 -0
- package/src/InvitationForm.tsx +163 -0
- package/src/InvitationList.tsx +240 -0
- package/src/MemberList.tsx +254 -0
- package/src/MemberRoleSelector.tsx +174 -0
- package/src/__tests__/EntityCard.test.tsx +103 -0
- package/src/__tests__/InvitationForm.test.tsx +153 -0
- package/src/__tests__/MemberRoleSelector.test.tsx +125 -0
- package/src/index.ts +29 -0
- package/src/nativewind.d.ts +27 -0
- package/src/types.ts +226 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react-native';
|
|
3
|
+
import { MemberRoleSelector } from '../MemberRoleSelector';
|
|
4
|
+
|
|
5
|
+
describe('MemberRoleSelector', () => {
|
|
6
|
+
it('renders the selected role label', () => {
|
|
7
|
+
render(
|
|
8
|
+
<MemberRoleSelector selectedRole='admin' onRoleChange={jest.fn()} />
|
|
9
|
+
);
|
|
10
|
+
expect(screen.getByText('Admin')).toBeTruthy();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('renders role description when showDescriptions is true', () => {
|
|
14
|
+
render(
|
|
15
|
+
<MemberRoleSelector
|
|
16
|
+
selectedRole='admin'
|
|
17
|
+
onRoleChange={jest.fn()}
|
|
18
|
+
showDescriptions
|
|
19
|
+
/>
|
|
20
|
+
);
|
|
21
|
+
expect(
|
|
22
|
+
screen.getByText('Can manage members and settings')
|
|
23
|
+
).toBeTruthy();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('has correct accessibility label for trigger', () => {
|
|
27
|
+
render(
|
|
28
|
+
<MemberRoleSelector selectedRole='member' onRoleChange={jest.fn()} />
|
|
29
|
+
);
|
|
30
|
+
const trigger = screen.getByLabelText('Selected role: Member');
|
|
31
|
+
expect(trigger).toBeTruthy();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('opens modal when trigger is pressed', () => {
|
|
35
|
+
render(
|
|
36
|
+
<MemberRoleSelector selectedRole='member' onRoleChange={jest.fn()} />
|
|
37
|
+
);
|
|
38
|
+
fireEvent.press(screen.getByLabelText('Selected role: Member'));
|
|
39
|
+
expect(screen.getByText('Select Role')).toBeTruthy();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('does not open modal when disabled', () => {
|
|
43
|
+
render(
|
|
44
|
+
<MemberRoleSelector
|
|
45
|
+
selectedRole='member'
|
|
46
|
+
onRoleChange={jest.fn()}
|
|
47
|
+
disabled
|
|
48
|
+
/>
|
|
49
|
+
);
|
|
50
|
+
fireEvent.press(screen.getByLabelText('Selected role: Member'));
|
|
51
|
+
expect(screen.queryByText('Select Role')).toBeNull();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('shows only available roles in modal', () => {
|
|
55
|
+
render(
|
|
56
|
+
<MemberRoleSelector
|
|
57
|
+
selectedRole='member'
|
|
58
|
+
onRoleChange={jest.fn()}
|
|
59
|
+
availableRoles={['member', 'viewer']}
|
|
60
|
+
/>
|
|
61
|
+
);
|
|
62
|
+
fireEvent.press(screen.getByLabelText('Selected role: Member'));
|
|
63
|
+
// Should show member and viewer
|
|
64
|
+
expect(screen.getByLabelText(/^Member:/)).toBeTruthy();
|
|
65
|
+
expect(screen.getByLabelText(/^Viewer:/)).toBeTruthy();
|
|
66
|
+
// Should not show admin, owner, or guest
|
|
67
|
+
expect(screen.queryByLabelText(/^Admin:/)).toBeNull();
|
|
68
|
+
expect(screen.queryByLabelText(/^Owner:/)).toBeNull();
|
|
69
|
+
expect(screen.queryByLabelText(/^Guest:/)).toBeNull();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('calls onRoleChange when a role is selected', () => {
|
|
73
|
+
const onRoleChange = jest.fn();
|
|
74
|
+
render(
|
|
75
|
+
<MemberRoleSelector selectedRole='member' onRoleChange={onRoleChange} />
|
|
76
|
+
);
|
|
77
|
+
fireEvent.press(screen.getByLabelText('Selected role: Member'));
|
|
78
|
+
fireEvent.press(screen.getByLabelText(/^Viewer:/));
|
|
79
|
+
expect(onRoleChange).toHaveBeenCalledWith('viewer');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('closes modal after selecting a role', () => {
|
|
83
|
+
render(
|
|
84
|
+
<MemberRoleSelector selectedRole='member' onRoleChange={jest.fn()} />
|
|
85
|
+
);
|
|
86
|
+
fireEvent.press(screen.getByLabelText('Selected role: Member'));
|
|
87
|
+
expect(screen.getByText('Select Role')).toBeTruthy();
|
|
88
|
+
fireEvent.press(screen.getByLabelText(/^Viewer:/));
|
|
89
|
+
expect(screen.queryByText('Select Role')).toBeNull();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('closes modal when Cancel is pressed', () => {
|
|
93
|
+
render(
|
|
94
|
+
<MemberRoleSelector selectedRole='member' onRoleChange={jest.fn()} />
|
|
95
|
+
);
|
|
96
|
+
fireEvent.press(screen.getByLabelText('Selected role: Member'));
|
|
97
|
+
expect(screen.getByText('Select Role')).toBeTruthy();
|
|
98
|
+
fireEvent.press(screen.getByText('Cancel'));
|
|
99
|
+
expect(screen.queryByText('Select Role')).toBeNull();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('shows checkmark next to the currently selected role', () => {
|
|
103
|
+
render(
|
|
104
|
+
<MemberRoleSelector selectedRole='admin' onRoleChange={jest.fn()} />
|
|
105
|
+
);
|
|
106
|
+
fireEvent.press(screen.getByLabelText('Selected role: Admin'));
|
|
107
|
+
// The admin option should have selected state
|
|
108
|
+
const adminOption = screen.getByLabelText(/^Admin:/);
|
|
109
|
+
expect(adminOption.props.accessibilityState).toEqual(
|
|
110
|
+
expect.objectContaining({ selected: true })
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('defaults availableRoles to admin, member, viewer, guest', () => {
|
|
115
|
+
render(
|
|
116
|
+
<MemberRoleSelector selectedRole='member' onRoleChange={jest.fn()} />
|
|
117
|
+
);
|
|
118
|
+
fireEvent.press(screen.getByLabelText('Selected role: Member'));
|
|
119
|
+
expect(screen.getByLabelText(/^Admin:/)).toBeTruthy();
|
|
120
|
+
expect(screen.getByLabelText(/^Member:/)).toBeTruthy();
|
|
121
|
+
expect(screen.getByLabelText(/^Viewer:/)).toBeTruthy();
|
|
122
|
+
expect(screen.getByLabelText(/^Guest:/)).toBeTruthy();
|
|
123
|
+
expect(screen.queryByLabelText(/^Owner:/)).toBeNull();
|
|
124
|
+
});
|
|
125
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Entity Components for React Native
|
|
2
|
+
// @sudobility/entity-components-rn
|
|
3
|
+
|
|
4
|
+
// Types
|
|
5
|
+
export type {
|
|
6
|
+
Entity,
|
|
7
|
+
Member,
|
|
8
|
+
Invitation,
|
|
9
|
+
EntityRole,
|
|
10
|
+
RoleConfig,
|
|
11
|
+
EntityCardProps,
|
|
12
|
+
EntityListProps,
|
|
13
|
+
EntitySelectorProps,
|
|
14
|
+
MemberListProps,
|
|
15
|
+
MemberRoleSelectorProps,
|
|
16
|
+
InvitationFormProps,
|
|
17
|
+
InvitationListProps,
|
|
18
|
+
} from './types';
|
|
19
|
+
|
|
20
|
+
export { DEFAULT_ROLE_CONFIGS } from './types';
|
|
21
|
+
|
|
22
|
+
// Components
|
|
23
|
+
export { EntityCard } from './EntityCard';
|
|
24
|
+
export { EntityList } from './EntityList';
|
|
25
|
+
export { EntitySelector } from './EntitySelector';
|
|
26
|
+
export { MemberList } from './MemberList';
|
|
27
|
+
export { MemberRoleSelector } from './MemberRoleSelector';
|
|
28
|
+
export { InvitationForm } from './InvitationForm';
|
|
29
|
+
export { InvitationList } from './InvitationList';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/// <reference types="nativewind/types" />
|
|
2
|
+
|
|
3
|
+
import 'react-native';
|
|
4
|
+
|
|
5
|
+
declare module 'react-native' {
|
|
6
|
+
interface ViewProps {
|
|
7
|
+
className?: string;
|
|
8
|
+
}
|
|
9
|
+
interface TextProps {
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
interface ImageProps {
|
|
13
|
+
className?: string;
|
|
14
|
+
}
|
|
15
|
+
interface ScrollViewProps {
|
|
16
|
+
className?: string;
|
|
17
|
+
}
|
|
18
|
+
interface TextInputProps {
|
|
19
|
+
className?: string;
|
|
20
|
+
}
|
|
21
|
+
interface PressableProps {
|
|
22
|
+
className?: string;
|
|
23
|
+
}
|
|
24
|
+
interface TouchableOpacityProps {
|
|
25
|
+
className?: string;
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import type { ComponentType, ReactElement, ReactNode } from 'react';
|
|
2
|
+
import type { ViewStyle } from 'react-native';
|
|
3
|
+
|
|
4
|
+
/** Type matching FlatList's ListHeaderComponent/ListFooterComponent */
|
|
5
|
+
type ListComponentProp = ComponentType<any> | ReactElement | null;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Entity role types
|
|
9
|
+
*/
|
|
10
|
+
export type EntityRole = 'owner' | 'admin' | 'member' | 'viewer' | 'guest';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Entity type
|
|
14
|
+
*/
|
|
15
|
+
export interface Entity {
|
|
16
|
+
id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
description?: string;
|
|
19
|
+
avatarUrl?: string;
|
|
20
|
+
role?: EntityRole;
|
|
21
|
+
memberCount?: number;
|
|
22
|
+
createdAt?: string;
|
|
23
|
+
updatedAt?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Member type
|
|
28
|
+
*/
|
|
29
|
+
export interface Member {
|
|
30
|
+
id: string;
|
|
31
|
+
name: string;
|
|
32
|
+
email: string;
|
|
33
|
+
avatarUrl?: string;
|
|
34
|
+
role: EntityRole;
|
|
35
|
+
joinedAt?: string;
|
|
36
|
+
status?: 'active' | 'inactive' | 'pending';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Invitation type
|
|
41
|
+
*/
|
|
42
|
+
export interface Invitation {
|
|
43
|
+
id: string;
|
|
44
|
+
email: string;
|
|
45
|
+
role: EntityRole;
|
|
46
|
+
status: 'pending' | 'accepted' | 'declined' | 'expired';
|
|
47
|
+
invitedBy?: string;
|
|
48
|
+
invitedAt: string;
|
|
49
|
+
expiresAt?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* EntityCard props
|
|
54
|
+
*/
|
|
55
|
+
export interface EntityCardProps {
|
|
56
|
+
entity: Entity;
|
|
57
|
+
onPress?: (entity: Entity) => void;
|
|
58
|
+
onLongPress?: (entity: Entity) => void;
|
|
59
|
+
selected?: boolean;
|
|
60
|
+
showRole?: boolean;
|
|
61
|
+
showMemberCount?: boolean;
|
|
62
|
+
showDescription?: boolean;
|
|
63
|
+
className?: string;
|
|
64
|
+
style?: ViewStyle;
|
|
65
|
+
testID?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* EntityList props
|
|
70
|
+
*/
|
|
71
|
+
export interface EntityListProps {
|
|
72
|
+
entities: Entity[];
|
|
73
|
+
onEntityPress?: (entity: Entity) => void;
|
|
74
|
+
onEntityLongPress?: (entity: Entity) => void;
|
|
75
|
+
selectedEntityId?: string;
|
|
76
|
+
showRoles?: boolean;
|
|
77
|
+
showMemberCounts?: boolean;
|
|
78
|
+
showDescriptions?: boolean;
|
|
79
|
+
emptyMessage?: string;
|
|
80
|
+
emptyIcon?: ReactNode;
|
|
81
|
+
loading?: boolean;
|
|
82
|
+
refreshing?: boolean;
|
|
83
|
+
onRefresh?: () => void;
|
|
84
|
+
ListHeaderComponent?: ListComponentProp;
|
|
85
|
+
ListFooterComponent?: ListComponentProp;
|
|
86
|
+
className?: string;
|
|
87
|
+
style?: ViewStyle;
|
|
88
|
+
testID?: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* EntitySelector props
|
|
93
|
+
*/
|
|
94
|
+
export interface EntitySelectorProps {
|
|
95
|
+
entities: Entity[];
|
|
96
|
+
selectedEntity?: Entity | null;
|
|
97
|
+
onSelect: (entity: Entity) => void;
|
|
98
|
+
placeholder?: string;
|
|
99
|
+
disabled?: boolean;
|
|
100
|
+
loading?: boolean;
|
|
101
|
+
showAvatars?: boolean;
|
|
102
|
+
showRoles?: boolean;
|
|
103
|
+
className?: string;
|
|
104
|
+
style?: ViewStyle;
|
|
105
|
+
testID?: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* MemberList props
|
|
110
|
+
*/
|
|
111
|
+
export interface MemberListProps {
|
|
112
|
+
members: Member[];
|
|
113
|
+
onMemberPress?: (member: Member) => void;
|
|
114
|
+
onRoleChange?: (member: Member, newRole: EntityRole) => void;
|
|
115
|
+
onRemoveMember?: (member: Member) => void;
|
|
116
|
+
currentUserRole?: EntityRole;
|
|
117
|
+
canEditRoles?: boolean;
|
|
118
|
+
canRemoveMembers?: boolean;
|
|
119
|
+
emptyMessage?: string;
|
|
120
|
+
emptyIcon?: ReactNode;
|
|
121
|
+
loading?: boolean;
|
|
122
|
+
refreshing?: boolean;
|
|
123
|
+
onRefresh?: () => void;
|
|
124
|
+
ListHeaderComponent?: ListComponentProp;
|
|
125
|
+
ListFooterComponent?: ListComponentProp;
|
|
126
|
+
className?: string;
|
|
127
|
+
style?: ViewStyle;
|
|
128
|
+
testID?: string;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* MemberRoleSelector props
|
|
133
|
+
*/
|
|
134
|
+
export interface MemberRoleSelectorProps {
|
|
135
|
+
selectedRole: EntityRole;
|
|
136
|
+
onRoleChange: (role: EntityRole) => void;
|
|
137
|
+
availableRoles?: EntityRole[];
|
|
138
|
+
disabled?: boolean;
|
|
139
|
+
showDescriptions?: boolean;
|
|
140
|
+
className?: string;
|
|
141
|
+
style?: ViewStyle;
|
|
142
|
+
testID?: string;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* InvitationForm props
|
|
147
|
+
*/
|
|
148
|
+
export interface InvitationFormProps {
|
|
149
|
+
onSubmit: (email: string, role: EntityRole) => void | Promise<void>;
|
|
150
|
+
availableRoles?: EntityRole[];
|
|
151
|
+
defaultRole?: EntityRole;
|
|
152
|
+
loading?: boolean;
|
|
153
|
+
disabled?: boolean;
|
|
154
|
+
placeholder?: string;
|
|
155
|
+
submitLabel?: string;
|
|
156
|
+
className?: string;
|
|
157
|
+
style?: ViewStyle;
|
|
158
|
+
testID?: string;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* InvitationList props
|
|
163
|
+
*/
|
|
164
|
+
export interface InvitationListProps {
|
|
165
|
+
invitations: Invitation[];
|
|
166
|
+
onResend?: (invitation: Invitation) => void;
|
|
167
|
+
onCancel?: (invitation: Invitation) => void;
|
|
168
|
+
canResend?: boolean;
|
|
169
|
+
canCancel?: boolean;
|
|
170
|
+
emptyMessage?: string;
|
|
171
|
+
emptyIcon?: ReactNode;
|
|
172
|
+
loading?: boolean;
|
|
173
|
+
refreshing?: boolean;
|
|
174
|
+
onRefresh?: () => void;
|
|
175
|
+
ListHeaderComponent?: ListComponentProp;
|
|
176
|
+
ListFooterComponent?: ListComponentProp;
|
|
177
|
+
className?: string;
|
|
178
|
+
style?: ViewStyle;
|
|
179
|
+
testID?: string;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Role configuration
|
|
184
|
+
*/
|
|
185
|
+
export interface RoleConfig {
|
|
186
|
+
role: EntityRole;
|
|
187
|
+
label: string;
|
|
188
|
+
description: string;
|
|
189
|
+
color: string;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Default role configurations
|
|
194
|
+
*/
|
|
195
|
+
export const DEFAULT_ROLE_CONFIGS: RoleConfig[] = [
|
|
196
|
+
{
|
|
197
|
+
role: 'owner',
|
|
198
|
+
label: 'Owner',
|
|
199
|
+
description: 'Full access and ownership rights',
|
|
200
|
+
color: 'purple',
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
role: 'admin',
|
|
204
|
+
label: 'Admin',
|
|
205
|
+
description: 'Can manage members and settings',
|
|
206
|
+
color: 'blue',
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
role: 'member',
|
|
210
|
+
label: 'Member',
|
|
211
|
+
description: 'Can view and contribute',
|
|
212
|
+
color: 'green',
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
role: 'viewer',
|
|
216
|
+
label: 'Viewer',
|
|
217
|
+
description: 'Can only view content',
|
|
218
|
+
color: 'gray',
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
role: 'guest',
|
|
222
|
+
label: 'Guest',
|
|
223
|
+
description: 'Limited access',
|
|
224
|
+
color: 'yellow',
|
|
225
|
+
},
|
|
226
|
+
];
|