@makolabs/ripple 3.0.9 → 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.
- package/dist/drawer/Drawer.svelte +33 -12
- package/dist/drawer/drawer-types.d.ts +13 -5
- package/dist/drawer/drawer.d.ts +45 -0
- package/dist/drawer/drawer.js +14 -5
- package/dist/funcs/mock-user-management.d.ts +40 -0
- package/dist/funcs/mock-user-management.js +85 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.js +4 -1
- package/dist/modal/ConfirmDialog.svelte +6 -9
- package/dist/modal/Modal.svelte +18 -14
- package/dist/modal/modal-types.d.ts +9 -5
- package/dist/modal/modal.d.ts +42 -0
- package/dist/modal/modal.js +18 -8
- package/dist/user-management/ApiKeyField.svelte +165 -0
- package/dist/user-management/ApiKeyField.svelte.d.ts +32 -0
- package/dist/user-management/RoleCard.svelte +73 -0
- package/dist/user-management/RoleCard.svelte.d.ts +16 -0
- package/dist/user-management/UserApproveModal.svelte +115 -0
- package/dist/user-management/UserApproveModal.svelte.d.ts +4 -0
- package/dist/user-management/UserIdentityCard.svelte +53 -0
- package/dist/user-management/UserIdentityCard.svelte.d.ts +11 -0
- package/dist/user-management/UserManagement.svelte +191 -20
- package/dist/user-management/UserModal.svelte +203 -439
- package/dist/user-management/UserTable.svelte +48 -55
- package/dist/user-management/UserViewModal.svelte +87 -221
- package/dist/user-management/UserViewModal.svelte.d.ts +1 -1
- package/dist/user-management/user-management-types.d.ts +52 -3
- package/package.json +1 -1
- package/dist/modal/ModalFooter.svelte +0 -35
- package/dist/modal/ModalFooter.svelte.d.ts +0 -11
|
@@ -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
|
|
|
@@ -66,8 +84,20 @@
|
|
|
66
84
|
}
|
|
67
85
|
}
|
|
68
86
|
|
|
69
|
-
// Refresh the query cache
|
|
87
|
+
// Refresh the query cache and re-fetch.
|
|
88
|
+
//
|
|
89
|
+
// SvelteKit `query()` results are cached per-arg-combo. After a mutating
|
|
90
|
+
// `command()`, the cache is stale — calling the query again returns the
|
|
91
|
+
// old data until `.refresh()` is invoked. We detect a query by the
|
|
92
|
+
// presence of a `.refresh` method and invalidate before re-fetching.
|
|
93
|
+
// Plain async-function adapters (no caching) skip this branch.
|
|
70
94
|
async function refreshUsersQuery() {
|
|
95
|
+
const fn = adapter.getUsers as typeof adapter.getUsers & {
|
|
96
|
+
refresh?: () => Promise<unknown>;
|
|
97
|
+
};
|
|
98
|
+
if (typeof fn.refresh === 'function') {
|
|
99
|
+
await fn.refresh();
|
|
100
|
+
}
|
|
71
101
|
await loadUsers();
|
|
72
102
|
}
|
|
73
103
|
|
|
@@ -255,9 +285,110 @@
|
|
|
255
285
|
}
|
|
256
286
|
}
|
|
257
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
|
+
|
|
258
386
|
// Initialize on mount
|
|
259
387
|
onMount(async () => {
|
|
260
388
|
await loadUsers();
|
|
389
|
+
if (pendingEnabled) {
|
|
390
|
+
await loadPendingUsers();
|
|
391
|
+
}
|
|
261
392
|
});
|
|
262
393
|
</script>
|
|
263
394
|
|
|
@@ -275,14 +406,29 @@
|
|
|
275
406
|
layout="horizontal"
|
|
276
407
|
class="mb-6"
|
|
277
408
|
>
|
|
278
|
-
|
|
279
|
-
{
|
|
280
|
-
|
|
281
|
-
|
|
409
|
+
{#if activeTab === 'active'}
|
|
410
|
+
<Button onclick={openCreateModal} color="primary">
|
|
411
|
+
{@render PlusIcon()}
|
|
412
|
+
Add User
|
|
413
|
+
</Button>
|
|
414
|
+
{/if}
|
|
282
415
|
</PageHeader>
|
|
283
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
|
+
|
|
284
430
|
<!-- Bulk Actions Bar -->
|
|
285
|
-
{#if hasSelectedUsers}
|
|
431
|
+
{#if activeTab === 'active' && hasSelectedUsers}
|
|
286
432
|
<div
|
|
287
433
|
class="mb-4 flex flex-wrap items-center justify-between gap-3 rounded-lg border border-amber-200 bg-amber-50 p-4"
|
|
288
434
|
>
|
|
@@ -307,19 +453,33 @@
|
|
|
307
453
|
{/if}
|
|
308
454
|
|
|
309
455
|
<!-- Users Table Component -->
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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}
|
|
323
483
|
|
|
324
484
|
<!-- User View Modal -->
|
|
325
485
|
<UserViewModal
|
|
@@ -340,4 +500,15 @@
|
|
|
340
500
|
onsave={handleUserSave}
|
|
341
501
|
onclose={handleModalClose}
|
|
342
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}
|
|
343
514
|
</div>
|