@makolabs/ripple 1.2.4 → 1.2.9

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.
@@ -324,7 +324,7 @@
324
324
  {#if template === 'full' && showPageNumbers}
325
325
  {#if totalPages <= maxVisiblePages}
326
326
  <!-- Show all pages if total is less than or equal to maxVisiblePages -->
327
- {#each Array(totalPages) as page, i (page + i)}
327
+ {#each Array(totalPages) as page, i (page + '-' + i)}
328
328
  {@const pageNum = i + 1}
329
329
  <button
330
330
  onclick={() => goToPage(pageNum)}
@@ -44,7 +44,6 @@
44
44
  headerActions
45
45
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
46
46
  }: TableProps<any> = $props();
47
-
48
47
  // Determine if we should use Card wrapper
49
48
  const hasHeader = $derived(title !== undefined || subtitle !== undefined);
50
49
 
@@ -19,33 +19,33 @@ export const table = tv({
19
19
  variants: {
20
20
  size: {
21
21
  xs: {
22
- th: 'px-2 py-1.5 text-xs',
22
+ th: 'px-2 py-1.5',
23
23
  td: 'px-2 py-1.5 text-xs'
24
24
  },
25
25
  sm: {
26
- th: 'px-3 py-2 text-xs',
26
+ th: 'px-3 py-2',
27
27
  td: 'px-3 py-2 text-sm'
28
28
  },
29
29
  base: {
30
- th: 'px-4 py-3 text-sm',
30
+ th: 'px-3 py-2',
31
31
  td: 'px-4 py-3 text-sm'
32
32
  },
33
33
  lg: {
34
- th: 'px-6 py-4 text-sm',
34
+ th: 'px-4 py-3',
35
35
  td: 'px-6 py-4 text-base'
36
36
  },
37
37
  xl: {
38
- th: 'px-8 py-5 text-base',
38
+ th: 'px-5 py-4',
39
39
  td: 'px-8 py-5 text-base'
40
40
  },
41
41
  '2xl': {
42
- th: 'px-10 py-6 text-lg',
42
+ th: 'px-6 py-5',
43
43
  td: 'px-10 py-6 text-lg'
44
44
  }
45
45
  },
46
46
  color: {
47
47
  default: {
48
- th: 'text-default-700 bg-default-50'
48
+ th: 'bg-gray-50'
49
49
  },
50
50
  [Color.PRIMARY]: {
51
51
  th: 'text-primary-700 bg-primary-50',
@@ -1,29 +1,31 @@
1
1
  <script lang="ts">
2
+ import { onMount } from 'svelte';
2
3
  import { PageHeader, MetricCard, Button, cn } from '../index.js';
3
4
  import UserTable from './UserTable.svelte';
4
5
  import UserModal from './UserModal.svelte';
5
6
  import UserViewModal from './UserViewModal.svelte';
6
- import type { User, UserManagementProps } from './user-management.js';
7
+ import type { User, UserManagementProps, Role, Permission } from './user-management.js';
7
8
  import { SvelteSet } from 'svelte/reactivity';
9
+ import * as UserManagementAdapter from './adapters/UserManagement.remote.js';
8
10
 
9
11
  let {
10
- users = [],
11
- totalUsers = 0,
12
- loading = false,
13
- currentPage = 1,
14
- pageSize = 10,
15
- roles = [],
16
- permissions = [],
17
- onPageChange,
18
- onPageSizeChange,
19
- onSort,
20
- onCreateUser,
21
- onUpdateUser,
22
- onDeleteUser,
23
- onDeleteUsers,
12
+ adapter = UserManagementAdapter,
13
+ roles: initialRoles = [],
14
+ permissions: initialPermissions = [],
24
15
  class: className
25
16
  }: UserManagementProps = $props();
26
17
 
18
+ // Internal state
19
+ let users = $state<User[]>([]);
20
+ let totalUsers = $state(0);
21
+ let loading = $state(false);
22
+ let currentPage = $state(1);
23
+ let pageSize = $state(10);
24
+ let sortBy = $state<string | null>(null);
25
+ let sortOrder = $state<'asc' | 'desc'>('desc');
26
+ let roles = $state<Role[]>(initialRoles);
27
+ let permissions = $state<Permission[]>(initialPermissions);
28
+
27
29
  // Modal states
28
30
  let showEditCreateModal = $state(false);
29
31
  let showViewModal = $state(false);
@@ -37,15 +39,78 @@
37
39
  const hasSelectedUsers = $derived(selectedUsers.size > 0);
38
40
  const activeUsers = $derived(users.filter((u) => !!u.id));
39
41
 
42
+ // Load users from adapter (remote function)
43
+ async function loadUsers() {
44
+ try {
45
+ loading = true;
46
+ const result = await adapter.getUsers({
47
+ page: currentPage,
48
+ pageSize,
49
+ sortBy: sortBy || undefined,
50
+ sortOrder: sortOrder || 'desc'
51
+ });
52
+ users = result.users;
53
+ totalUsers = result.totalUsers;
54
+ } catch (error) {
55
+ console.error('Error loading users:', error);
56
+ } finally {
57
+ loading = false;
58
+ }
59
+ }
60
+
61
+ // Handlers
62
+ async function handlePageChange(page: number) {
63
+ currentPage = page;
64
+ await loadUsers();
65
+ }
66
+
67
+ async function handlePageSizeChange(size: number) {
68
+ pageSize = size;
69
+ currentPage = 1; // Reset to first page when changing page size
70
+ await loadUsers();
71
+ }
72
+
73
+ async function handleSort(state: { column: string | null; direction: 'asc' | 'desc' | null }) {
74
+ if (state.column && state.direction) {
75
+ sortBy = state.column;
76
+ sortOrder = state.direction;
77
+ await loadUsers();
78
+ }
79
+ }
80
+
40
81
  // Modal handlers
41
- function openViewModal(user: User) {
82
+ async function openViewModal(user: User) {
42
83
  selectedUser = user;
43
84
  showViewModal = true;
85
+ // Fetch fresh permissions for the user
86
+ if (user.id) {
87
+ try {
88
+ const permissions = await adapter.getUserPermissions(user.id);
89
+ selectedUser = {
90
+ ...user,
91
+ permissions
92
+ };
93
+ } catch (error) {
94
+ console.error('Error fetching user permissions:', error);
95
+ }
96
+ }
44
97
  }
45
98
 
46
- function openEditModal(user: User) {
99
+ async function openEditModal(user: User) {
47
100
  selectedUser = user;
48
101
  showEditCreateModal = true;
102
+ // Fetch fresh permissions for the user
103
+ if (user.id) {
104
+ try {
105
+ const permissions = await adapter.getUserPermissions(user.id);
106
+ selectedUser = {
107
+ ...user,
108
+ permissions
109
+ };
110
+ } catch (error) {
111
+ console.error('Error fetching user permissions:', error);
112
+ }
113
+ }
49
114
  }
50
115
 
51
116
  function openCreateModal() {
@@ -62,10 +127,16 @@
62
127
 
63
128
  // Save handlers
64
129
  async function handleUserSave(user: User, mode: 'create' | 'edit') {
65
- if (mode === 'create' && onCreateUser) {
66
- await onCreateUser(user);
67
- } else if (mode === 'edit' && onUpdateUser) {
68
- await onUpdateUser(user.id, user);
130
+ try {
131
+ if (mode === 'create') {
132
+ await adapter.createUser(user);
133
+ } else {
134
+ await adapter.updateUser({ userId: user.id, userData: user });
135
+ }
136
+ await loadUsers();
137
+ } catch (error) {
138
+ console.error('Error saving user:', error);
139
+ throw error;
69
140
  }
70
141
  }
71
142
 
@@ -74,8 +145,12 @@
74
145
  if (!confirm('Are you sure you want to delete this user? This action cannot be undone.')) {
75
146
  return;
76
147
  }
77
- if (onDeleteUser) {
78
- await onDeleteUser(userId);
148
+ try {
149
+ await adapter.deleteUser(userId);
150
+ await loadUsers();
151
+ } catch (error) {
152
+ console.error('Error deleting user:', error);
153
+ throw error;
79
154
  }
80
155
  }
81
156
 
@@ -89,13 +164,23 @@
89
164
  return;
90
165
  }
91
166
 
92
- if (bulkAction === 'delete' && onDeleteUsers) {
93
- await onDeleteUsers(userIds);
94
- selectedUsers.clear();
95
- selectedUsers = new SvelteSet(selectedUsers);
96
- bulkAction = '';
167
+ if (bulkAction === 'delete') {
168
+ try {
169
+ await adapter.deleteUsers(userIds);
170
+ selectedUsers.clear();
171
+ bulkAction = '';
172
+ await loadUsers();
173
+ } catch (error) {
174
+ console.error('Error deleting users:', error);
175
+ throw error;
176
+ }
97
177
  }
98
178
  }
179
+
180
+ // Initialize on mount
181
+ onMount(async () => {
182
+ await loadUsers();
183
+ });
99
184
  </script>
100
185
 
101
186
  {#snippet PlusIcon()}
@@ -112,7 +197,7 @@
112
197
  layout="horizontal"
113
198
  class="mb-6"
114
199
  >
115
- <Button onclick={openCreateModal} color="primary" disabled={!onCreateUser}>
200
+ <Button onclick={openCreateModal} color="primary">
116
201
  {@render PlusIcon()}
117
202
  Add User
118
203
  </Button>
@@ -156,9 +241,9 @@
156
241
  {currentPage}
157
242
  {pageSize}
158
243
  {totalUsers}
159
- {onPageChange}
160
- {onPageSizeChange}
161
- {onSort}
244
+ onPageChange={handlePageChange}
245
+ onPageSizeChange={handlePageSizeChange}
246
+ onSort={handleSort}
162
247
  onView={openViewModal}
163
248
  onEdit={openEditModal}
164
249
  onDelete={handleDeleteUser}
@@ -1,47 +1,33 @@
1
1
  <script lang="ts">
2
+ import { onMount } from 'svelte';
2
3
  import UserManagement from './UserManagement.svelte';
3
4
  import type { UserManagementProps } from './user-management.js';
5
+ import * as mockAdapter from './adapters/mockUserManagement.js';
6
+ import { resetState } from './adapters/mockUserManagement.js';
7
+ import type { User, Role, Permission } from './user-management.js';
4
8
 
5
- interface Props extends UserManagementProps {
9
+ interface Props extends Omit<UserManagementProps, 'adapter'> {
6
10
  testId?: string;
11
+ initialUsers?: User[];
12
+ roles?: Role[];
13
+ permissions?: Permission[];
14
+ simulateDelay?: boolean;
15
+ delayMs?: number;
7
16
  }
8
17
 
9
- let {
10
- users = [],
11
- totalUsers = 0,
12
- loading = false,
13
- currentPage = 1,
14
- pageSize = 10,
15
- roles = [],
16
- permissions = [],
17
- onPageChange = () => {},
18
- onPageSizeChange = () => {},
19
- onSort,
20
- onCreateUser,
21
- onUpdateUser,
22
- onDeleteUser,
23
- onDeleteUsers,
24
- testId,
25
- ...rest
26
- }: Props = $props();
18
+ let { testId, initialUsers, roles, permissions, simulateDelay, delayMs, ...rest }: Props =
19
+ $props();
20
+
21
+ // Initialize mock adapter with provided data
22
+ onMount(async () => {
23
+ await resetState({
24
+ initialUsers,
25
+ simulateDelay,
26
+ delayMs
27
+ });
28
+ });
27
29
  </script>
28
30
 
29
31
  <div data-testid={testId}>
30
- <UserManagement
31
- {users}
32
- {totalUsers}
33
- {loading}
34
- {currentPage}
35
- {pageSize}
36
- {roles}
37
- {permissions}
38
- {onPageChange}
39
- {onPageSizeChange}
40
- {onSort}
41
- {onCreateUser}
42
- {onUpdateUser}
43
- {onDeleteUser}
44
- {onDeleteUsers}
45
- {...rest}
46
- />
32
+ <UserManagement adapter={mockAdapter} {roles} {permissions} {...rest} />
47
33
  </div>
@@ -1,6 +1,12 @@
1
1
  import type { UserManagementProps } from './user-management.js';
2
- interface Props extends UserManagementProps {
2
+ import type { User, Role, Permission } from './user-management.js';
3
+ interface Props extends Omit<UserManagementProps, 'adapter'> {
3
4
  testId?: string;
5
+ initialUsers?: User[];
6
+ roles?: Role[];
7
+ permissions?: Permission[];
8
+ simulateDelay?: boolean;
9
+ delayMs?: number;
4
10
  }
5
11
  declare const UserManagementTestWrapper: import("svelte").Component<Props, {}, "">;
6
12
  type UserManagementTestWrapper = ReturnType<typeof UserManagementTestWrapper>;
@@ -25,23 +25,75 @@
25
25
  let formData = $state<Partial<User>>({
26
26
  first_name: '',
27
27
  last_name: '',
28
- username: '',
29
28
  email_addresses: [{ email_address: '' }],
30
29
  phone_numbers: [],
31
30
  role: '',
32
31
  permissions: []
33
32
  });
34
33
 
34
+ // Helper function to detect role from permissions
35
+ function detectRoleFromPermissions(userPermissions: string[]): string {
36
+ if (!roles || roles.length === 0 || !userPermissions || userPermissions.length === 0) {
37
+ return '';
38
+ }
39
+
40
+ // First, try to find an exact match (all role permissions match user permissions)
41
+ for (const role of roles) {
42
+ if (role.permissions && role.permissions.length > 0) {
43
+ const rolePermissions = role.permissions;
44
+ // Check if all role permissions are present in user permissions
45
+ const allPermissionsMatch = rolePermissions.every((perm) => userPermissions.includes(perm));
46
+ // Also check if the counts match (to avoid partial matches)
47
+ if (allPermissionsMatch && rolePermissions.length === userPermissions.length) {
48
+ return role.value;
49
+ }
50
+ }
51
+ }
52
+
53
+ // If no exact match, try to find a role where all of its permissions are in user's permissions
54
+ // This handles cases where user has additional permissions beyond the role
55
+ for (const role of roles) {
56
+ if (role.permissions && role.permissions.length > 0) {
57
+ const allRolePermissionsInUser = role.permissions.every((perm) =>
58
+ userPermissions.includes(perm)
59
+ );
60
+ if (allRolePermissionsInUser) {
61
+ return role.value;
62
+ }
63
+ }
64
+ }
65
+
66
+ // Last resort: find any role where at least one permission matches
67
+ // (for cases where permissions might be partially matching)
68
+ for (const role of roles) {
69
+ if (role.permissions && role.permissions.length > 0) {
70
+ const hasMatchingPermission = role.permissions.some((perm) =>
71
+ userPermissions.includes(perm)
72
+ );
73
+ if (hasMatchingPermission) {
74
+ return role.value;
75
+ }
76
+ }
77
+ }
78
+
79
+ return '';
80
+ }
81
+
35
82
  // Initialize form data when user changes
36
83
  $effect(() => {
37
84
  if (open && user) {
85
+ // Detect role from permissions if role is not already set
86
+ let detectedRole = user.role || '';
87
+ if (!detectedRole && user.permissions && user.permissions.length > 0) {
88
+ detectedRole = detectRoleFromPermissions(user.permissions);
89
+ }
90
+
38
91
  formData = {
39
92
  first_name: user.first_name || '',
40
93
  last_name: user.last_name || '',
41
- username: user.username || '',
42
94
  email_addresses: user.email_addresses || [{ email_address: '' }],
43
95
  phone_numbers: user.phone_numbers || [],
44
- role: user.role || '',
96
+ role: detectedRole,
45
97
  permissions: user.permissions || []
46
98
  };
47
99
  } else if (open && !user) {
@@ -49,7 +101,6 @@
49
101
  formData = {
50
102
  first_name: '',
51
103
  last_name: '',
52
- username: '',
53
104
  email_addresses: [{ email_address: '' }],
54
105
  phone_numbers: [],
55
106
  role: '',
@@ -91,7 +142,6 @@
91
142
  id: user?.id || '',
92
143
  first_name: formData.first_name,
93
144
  last_name: formData.last_name,
94
- username: formData.username,
95
145
  email_addresses: formData.email_addresses,
96
146
  phone_numbers: formData.phone_numbers,
97
147
  role: formData.role,
@@ -194,20 +244,6 @@
194
244
  <p class="mt-1 text-xs text-red-500">{formErrors.email}</p>
195
245
  {/if}
196
246
  </div>
197
-
198
- <!-- Username (Optional) -->
199
- <div>
200
- <label for="username" class="mb-1 block text-sm font-medium text-gray-700">
201
- Username
202
- </label>
203
- <input
204
- id="username"
205
- type="text"
206
- bind:value={formData.username}
207
- class="w-full rounded-lg border border-gray-300 px-3 py-2 focus:border-blue-500 focus:ring-2 focus:ring-blue-500"
208
- placeholder="username"
209
- />
210
- </div>
211
247
  </div>
212
248
 
213
249
  <!-- Right Column: Permissions & Role -->
@@ -127,12 +127,6 @@
127
127
  {getUserDisplayName(user)}
128
128
  </p>
129
129
  </div>
130
- {#if user?.username}
131
- <div>
132
- <span class="text-xs text-gray-500">Username</span>
133
- <p class="text-sm">{user.username}</p>
134
- </div>
135
- {/if}
136
130
  <div>
137
131
  <span class="text-xs text-gray-500">User ID</span>
138
132
  <p class="font-mono text-xs break-all">{user?.id || 'N/A'}</p>
@@ -0,0 +1,68 @@
1
+ /**
2
+ * User Management Remote Functions
3
+ *
4
+ * Complete implementation template for user management remote functions.
5
+ * This follows the same pattern as users.remote.ts and is designed to be
6
+ * used as-is in SvelteKit applications like sharkfin-frontend.
7
+ *
8
+ * Configuration:
9
+ * - Set environment variables: CLERK_SECRET_KEY, ADMIN_API_KEY, PRIVATE_BASE_AUTH_URL, ALLOWED_ORG_ID
10
+ * - Configure CLIENT_ID via ADMIN_CONFIG or env variable (CLIENT_ID)
11
+ * - Adjust permission prefix filter via PERMISSION_PREFIX env variable
12
+ *
13
+ * @see https://svelte.dev/docs/kit/remote-functions
14
+ *
15
+ * Usage in your app:
16
+ * ```svelte
17
+ * <script>
18
+ * import { UserManagement } from '@makolabs/ripple';
19
+ * import * as adapter from '../UserManagement.remote';
20
+ * </script>
21
+ *
22
+ * <UserManagement adapter={adapter} />
23
+ * ```
24
+ */
25
+ import type { User } from '../user-management.js';
26
+ import type { GetUsersOptions, GetUsersResult } from './types.js';
27
+ /**
28
+ * Get users with pagination and sorting
29
+ *
30
+ * Transforms adapter options to Clerk API format
31
+ */
32
+ export declare const getUsers: import("@sveltejs/kit").RemoteQueryFunction<GetUsersOptions, GetUsersResult>;
33
+ /**
34
+ * Create a new user
35
+ *
36
+ * Creates user in Clerk, optionally adds to organization, and creates admin key with permissions
37
+ */
38
+ export declare const createUser: import("@sveltejs/kit").RemoteCommand<Partial<User>, Promise<User>>;
39
+ /**
40
+ * Update an existing user
41
+ */
42
+ export declare const updateUser: import("@sveltejs/kit").RemoteCommand<{
43
+ userId: string;
44
+ userData: Partial<User>;
45
+ }, Promise<User>>;
46
+ /**
47
+ * Delete a single user
48
+ */
49
+ export declare const deleteUser: import("@sveltejs/kit").RemoteCommand<string, Promise<void>>;
50
+ /**
51
+ * Delete multiple users
52
+ */
53
+ export declare const deleteUsers: import("@sveltejs/kit").RemoteCommand<string[], Promise<void>>;
54
+ /**
55
+ * Get permissions for a specific user
56
+ *
57
+ * Uses 'sub' (userId) parameter in admin API calls
58
+ */
59
+ export declare const getUserPermissions: import("@sveltejs/kit").RemoteQueryFunction<string, string[]>;
60
+ /**
61
+ * Update permissions for a specific user
62
+ *
63
+ * Uses 'sub' (userId) parameter in admin API calls
64
+ */
65
+ export declare const updateUserPermissions: import("@sveltejs/kit").RemoteCommand<{
66
+ userId: string;
67
+ permissions: string[];
68
+ }, Promise<void>>;