@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.
@@ -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
+ ];