@makolabs/ripple 3.0.10 → 3.0.11

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.
@@ -1,10 +1,11 @@
1
1
  <script lang="ts">
2
2
  import { onMount } from 'svelte';
3
3
  import { toast } from 'svelte-sonner';
4
- import { PageHeader, Button, cn } from '../index.js';
4
+ import { PageHeader, Button, TabGroup, cn } from '../index.js';
5
5
  import UserTable from './UserTable.svelte';
6
6
  import UserModal from './UserModal.svelte';
7
7
  import UserViewModal from './UserViewModal.svelte';
8
+ import UserApproveModal from './UserApproveModal.svelte';
8
9
  import type {
9
10
  User,
10
11
  UserManagementProps,
@@ -42,6 +43,23 @@
42
43
  let selectedUsers = new SvelteSet<string>();
43
44
  let bulkAction = $state<'delete' | ''>('');
44
45
 
46
+ // Pending approval state — gated on the FULL workflow (list + approve + reject).
47
+ // If an adapter exposes only listing without both mutations, the tab stays hidden
48
+ // to avoid action buttons that throw or silently no-op.
49
+ const pendingEnabled = $derived(
50
+ typeof adapter.getPendingUsers === 'function' &&
51
+ typeof adapter.approveUser === 'function' &&
52
+ typeof adapter.rejectUser === 'function'
53
+ );
54
+ let activeTab = $state<'active' | 'pending'>('active');
55
+ let pendingUsers = $state<User[]>([]);
56
+ let totalPending = $state(0);
57
+ let pendingPage = $state(1);
58
+ let pendingPageSize = $state(10);
59
+ let pendingLoading = $state(false);
60
+ let showApproveModal = $state(false);
61
+ let userToApprove = $state<User | null>(null);
62
+
45
63
  // Derived states
46
64
  const hasSelectedUsers = $derived(selectedUsers.size > 0);
47
65
 
@@ -267,9 +285,110 @@
267
285
  }
268
286
  }
269
287
 
288
+ // Pending users: load + refresh + approve + reject
289
+ async function loadPendingUsers() {
290
+ if (!adapter.getPendingUsers) return;
291
+ try {
292
+ pendingLoading = true;
293
+ const result = await adapter.getPendingUsers({
294
+ page: pendingPage,
295
+ pageSize: pendingPageSize
296
+ });
297
+ pendingUsers = result.users.map((u) => ({ ...u }));
298
+ totalPending = result.totalUsers;
299
+ } catch (error) {
300
+ console.error('Error loading pending users:', error);
301
+ toast.error('Failed to load pending users');
302
+ } finally {
303
+ pendingLoading = false;
304
+ }
305
+ }
306
+
307
+ async function refreshPendingQuery() {
308
+ if (!adapter.getPendingUsers) return;
309
+ const fn = adapter.getPendingUsers as typeof adapter.getPendingUsers & {
310
+ refresh?: () => Promise<unknown>;
311
+ };
312
+ if (typeof fn.refresh === 'function') {
313
+ await fn.refresh();
314
+ }
315
+ await loadPendingUsers();
316
+ }
317
+
318
+ function openApproveModal(user: User) {
319
+ userToApprove = user;
320
+ showApproveModal = true;
321
+ }
322
+
323
+ async function handleApproveUser({ userId, role }: { userId: string; role: string }) {
324
+ if (!adapter.approveUser) {
325
+ throw new Error('approveUser is not configured on the adapter');
326
+ }
327
+ const result = await adapter.approveUser({ userId, role });
328
+ // The user is approved AND we hold their one-time API key. A refresh failure
329
+ // here must NOT lose that key — surface a soft warning instead of throwing.
330
+ try {
331
+ await Promise.all([refreshPendingQuery(), refreshUsersQuery()]);
332
+ } catch (error) {
333
+ console.error('Error refreshing users after approval:', error);
334
+ toast.warning(
335
+ 'User approved, but the lists failed to refresh. Reload the page to sync the latest state.'
336
+ );
337
+ }
338
+ return result;
339
+ }
340
+
341
+ async function handleRejectUser(user: User) {
342
+ if (!adapter.rejectUser) return;
343
+ if (
344
+ !confirm(
345
+ `Reject ${user.email_addresses?.[0]?.email_address ?? user.id}? This permanently deletes them from your identity provider.`
346
+ )
347
+ ) {
348
+ return;
349
+ }
350
+ // Optimistic removal — capture both previous list AND total so a paginated
351
+ // failure rollback restores the true count, not just the visible page length.
352
+ const previous = pendingUsers;
353
+ const previousTotal = totalPending;
354
+ pendingUsers = pendingUsers.filter((u) => u.id !== user.id);
355
+ totalPending = Math.max(0, totalPending - 1);
356
+ try {
357
+ await adapter.rejectUser(user.id);
358
+ await refreshPendingQuery();
359
+ } catch (error) {
360
+ console.error('Error rejecting user:', error);
361
+ toast.error('Failed to reject user');
362
+ pendingUsers = previous;
363
+ totalPending = previousTotal;
364
+ throw error;
365
+ }
366
+ }
367
+
368
+ function handlePendingPageChange(page: number) {
369
+ pendingPage = page;
370
+ loadPendingUsers();
371
+ }
372
+
373
+ function handlePendingPageSizeChange(size: number) {
374
+ pendingPageSize = size;
375
+ pendingPage = 1;
376
+ loadPendingUsers();
377
+ }
378
+
379
+ function handleTabChange(value: string) {
380
+ activeTab = value === 'pending' ? 'pending' : 'active';
381
+ if (activeTab === 'pending') {
382
+ loadPendingUsers();
383
+ }
384
+ }
385
+
270
386
  // Initialize on mount
271
387
  onMount(async () => {
272
388
  await loadUsers();
389
+ if (pendingEnabled) {
390
+ await loadPendingUsers();
391
+ }
273
392
  });
274
393
  </script>
275
394
 
@@ -287,14 +406,29 @@
287
406
  layout="horizontal"
288
407
  class="mb-6"
289
408
  >
290
- <Button onclick={openCreateModal} color="primary">
291
- {@render PlusIcon()}
292
- Add User
293
- </Button>
409
+ {#if activeTab === 'active'}
410
+ <Button onclick={openCreateModal} color="primary">
411
+ {@render PlusIcon()}
412
+ Add User
413
+ </Button>
414
+ {/if}
294
415
  </PageHeader>
295
416
 
417
+ {#if pendingEnabled}
418
+ <TabGroup
419
+ tabs={[
420
+ { value: 'active', label: `Active (${totalUsers})` },
421
+ { value: 'pending', label: `Pending (${totalPending})` }
422
+ ]}
423
+ selected={activeTab}
424
+ onchange={handleTabChange}
425
+ class="mb-4"
426
+ testId="user-management-tabs"
427
+ />
428
+ {/if}
429
+
296
430
  <!-- Bulk Actions Bar -->
297
- {#if hasSelectedUsers}
431
+ {#if activeTab === 'active' && hasSelectedUsers}
298
432
  <div
299
433
  class="mb-4 flex flex-wrap items-center justify-between gap-3 rounded-lg border border-amber-200 bg-amber-50 p-4"
300
434
  >
@@ -319,19 +453,33 @@
319
453
  {/if}
320
454
 
321
455
  <!-- Users Table Component -->
322
- <UserTable
323
- {users}
324
- {loading}
325
- {currentPage}
326
- {pageSize}
327
- {totalUsers}
328
- onpagechange={handlePageChange}
329
- onpagesizechange={handlePageSizeChange}
330
- onsort={handleSort}
331
- onview={openViewModal}
332
- onedit={openEditModal}
333
- ondelete={handleDeleteUser}
334
- />
456
+ {#if activeTab === 'active'}
457
+ <UserTable
458
+ {users}
459
+ {loading}
460
+ {currentPage}
461
+ {pageSize}
462
+ {totalUsers}
463
+ onpagechange={handlePageChange}
464
+ onpagesizechange={handlePageSizeChange}
465
+ onsort={handleSort}
466
+ onview={openViewModal}
467
+ onedit={openEditModal}
468
+ ondelete={handleDeleteUser}
469
+ />
470
+ {:else}
471
+ <UserTable
472
+ users={pendingUsers}
473
+ loading={pendingLoading}
474
+ currentPage={pendingPage}
475
+ pageSize={pendingPageSize}
476
+ totalUsers={totalPending}
477
+ onpagechange={handlePendingPageChange}
478
+ onpagesizechange={handlePendingPageSizeChange}
479
+ onapprove={openApproveModal}
480
+ onreject={handleRejectUser}
481
+ />
482
+ {/if}
335
483
 
336
484
  <!-- User View Modal -->
337
485
  <UserViewModal
@@ -352,4 +500,15 @@
352
500
  onsave={handleUserSave}
353
501
  onclose={handleModalClose}
354
502
  />
503
+
504
+ <!-- Approve Pending User Modal -->
505
+ {#if pendingEnabled}
506
+ <UserApproveModal
507
+ bind:open={showApproveModal}
508
+ user={userToApprove}
509
+ {roles}
510
+ onapprove={handleApproveUser}
511
+ onclose={() => (userToApprove = null)}
512
+ />
513
+ {/if}
355
514
  </div>