@makolabs/ripple 1.6.8 → 1.7.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.
|
@@ -2,7 +2,6 @@ import { query, command } from '$app/server';
|
|
|
2
2
|
import { getRequestEvent } from '$app/server';
|
|
3
3
|
import { env } from '$env/dynamic/private';
|
|
4
4
|
const CLIENT_ID = env.CLIENT_ID || 'sharkfin';
|
|
5
|
-
const PERMISSION_PREFIX = env.PERMISSION_PREFIX || 'sharkfin:';
|
|
6
5
|
const ORGANIZATION_ID = env.ALLOWED_ORG_ID;
|
|
7
6
|
function handleClerkError(error, defaultMessage) {
|
|
8
7
|
if (error && typeof error === 'object' && 'status' in error && 'details' in error) {
|
|
@@ -93,7 +92,7 @@ async function makeAuthRequest(endpoint, options = {}) {
|
|
|
93
92
|
try {
|
|
94
93
|
data = JSON.parse(text);
|
|
95
94
|
}
|
|
96
|
-
catch
|
|
95
|
+
catch {
|
|
97
96
|
// Not JSON, treat as plain text error (e.g., "404 page not found")
|
|
98
97
|
data = { error: text, message: text };
|
|
99
98
|
}
|
|
@@ -116,7 +115,7 @@ async function verifyApiKeyToken(apiKey) {
|
|
|
116
115
|
const verifyResult = await makeAuthRequest('/auth/verify', {
|
|
117
116
|
method: 'GET',
|
|
118
117
|
headers: {
|
|
119
|
-
|
|
118
|
+
Authorization: `Bearer ${token}`
|
|
120
119
|
}
|
|
121
120
|
});
|
|
122
121
|
if (verifyResult.ok && verifyResult.data?.data) {
|
|
@@ -129,7 +128,9 @@ async function verifyApiKeyToken(apiKey) {
|
|
|
129
128
|
};
|
|
130
129
|
}
|
|
131
130
|
}
|
|
132
|
-
const errorMsg = result.data?.message ||
|
|
131
|
+
const errorMsg = result.data?.message ||
|
|
132
|
+
result.data?.error ||
|
|
133
|
+
`API key verification failed with status ${result.status}`;
|
|
133
134
|
console.warn('[verifyApiKeyToken] Verification failed:', errorMsg);
|
|
134
135
|
return {
|
|
135
136
|
valid: false,
|
|
@@ -145,10 +146,7 @@ async function verifyApiKeyToken(apiKey) {
|
|
|
145
146
|
}
|
|
146
147
|
}
|
|
147
148
|
async function createUserPermissions(userId, permissions, clientId = CLIENT_ID) {
|
|
148
|
-
|
|
149
|
-
? permissions.filter((scope) => scope.startsWith(PERMISSION_PREFIX))
|
|
150
|
-
: permissions;
|
|
151
|
-
if (filteredPermissions.length === 0) {
|
|
149
|
+
if (permissions.length === 0) {
|
|
152
150
|
return null;
|
|
153
151
|
}
|
|
154
152
|
return await makeAdminRequest('/admin/keys', {
|
|
@@ -156,7 +154,7 @@ async function createUserPermissions(userId, permissions, clientId = CLIENT_ID)
|
|
|
156
154
|
body: JSON.stringify({
|
|
157
155
|
client_id: clientId,
|
|
158
156
|
sub: userId,
|
|
159
|
-
scopes:
|
|
157
|
+
scopes: permissions
|
|
160
158
|
})
|
|
161
159
|
});
|
|
162
160
|
}
|
|
@@ -421,9 +419,6 @@ export const updateUserPermissions = command('unchecked', async (options) => {
|
|
|
421
419
|
// Fetch user's active keys
|
|
422
420
|
const allKeysData = await makeAdminRequest(`/admin/keys?client_id=${CLIENT_ID}&sub=${userId}`);
|
|
423
421
|
const userKeys = (allKeysData?.data?.data || []).filter((key) => key.status === 'active');
|
|
424
|
-
const filteredPermissions = PERMISSION_PREFIX
|
|
425
|
-
? permissions.filter((scope) => scope.startsWith(PERMISSION_PREFIX))
|
|
426
|
-
: permissions;
|
|
427
422
|
if (userKeys.length === 0) {
|
|
428
423
|
// No active key exists, create new one
|
|
429
424
|
const newKeyResult = await createUserPermissions(userId, permissions);
|
|
@@ -450,7 +445,7 @@ export const updateUserPermissions = command('unchecked', async (options) => {
|
|
|
450
445
|
await makeAdminRequest(`/admin/keys/${keyId}`, {
|
|
451
446
|
method: 'PUT',
|
|
452
447
|
body: JSON.stringify({
|
|
453
|
-
scopes:
|
|
448
|
+
scopes: permissions
|
|
454
449
|
})
|
|
455
450
|
});
|
|
456
451
|
// Verify the token has updated scopes
|
|
@@ -460,9 +455,9 @@ export const updateUserPermissions = command('unchecked', async (options) => {
|
|
|
460
455
|
console.log('[updateUserPermissions] Key verification:', verification);
|
|
461
456
|
if (verification.valid) {
|
|
462
457
|
// Check if the scopes match what we expect
|
|
463
|
-
const scopesMatch =
|
|
458
|
+
const scopesMatch = permissions.every((perm) => verification.scopes?.includes(perm));
|
|
464
459
|
if (!scopesMatch) {
|
|
465
|
-
console.warn('[updateUserPermissions] Scopes mismatch. Expected:',
|
|
460
|
+
console.warn('[updateUserPermissions] Scopes mismatch. Expected:', permissions, 'Got:', verification.scopes);
|
|
466
461
|
}
|
|
467
462
|
}
|
|
468
463
|
else {
|
|
@@ -491,12 +486,9 @@ export const updateUserPermissions = command('unchecked', async (options) => {
|
|
|
491
486
|
});
|
|
492
487
|
export const generateApiKey = command('unchecked', async (options) => {
|
|
493
488
|
try {
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
// Default to readonly permission if none provided (first-time key generation)
|
|
498
|
-
if (filteredPermissions.length === 0 && PERMISSION_PREFIX) {
|
|
499
|
-
filteredPermissions = [`${PERMISSION_PREFIX}readonly`];
|
|
489
|
+
// No default permissions - require explicit permissions to be passed
|
|
490
|
+
if (options.permissions.length === 0) {
|
|
491
|
+
throw new Error('At least one permission scope is required');
|
|
500
492
|
}
|
|
501
493
|
// Check if user has existing active key
|
|
502
494
|
const allKeysData = await makeAdminRequest(`/admin/keys?client_id=${CLIENT_ID}&sub=${options.userId}`);
|
|
@@ -505,16 +497,17 @@ export const generateApiKey = command('unchecked', async (options) => {
|
|
|
505
497
|
let wasRotated = false;
|
|
506
498
|
let oldApiKey;
|
|
507
499
|
let currentUser = null;
|
|
500
|
+
let verificationWarning;
|
|
508
501
|
if (userKeys.length > 0 && options.revokeOld) {
|
|
509
502
|
// Use rotate endpoint (per Mako Auth API spec)
|
|
510
503
|
const keyId = userKeys[0].id;
|
|
511
504
|
// Get the old API key from Clerk's private_metadata
|
|
512
505
|
currentUser = await makeClerkRequest(`/users/${options.userId}`);
|
|
513
|
-
oldApiKey = currentUser
|
|
506
|
+
oldApiKey = currentUser?.private_metadata?.mako_api_key;
|
|
514
507
|
const rotateResult = await makeAdminRequest(`/admin/keys/${keyId}/rotate`, {
|
|
515
508
|
method: 'POST',
|
|
516
509
|
body: JSON.stringify({
|
|
517
|
-
scopes:
|
|
510
|
+
scopes: options.permissions
|
|
518
511
|
})
|
|
519
512
|
});
|
|
520
513
|
// Rotate endpoint returns key in data.key field
|
|
@@ -542,22 +535,25 @@ export const generateApiKey = command('unchecked', async (options) => {
|
|
|
542
535
|
console.log('[generateApiKey] New key verification:', newKeyVerification);
|
|
543
536
|
if (newKeyVerification.valid) {
|
|
544
537
|
// Check if the scopes match what we expect
|
|
545
|
-
const scopesMatch =
|
|
538
|
+
const scopesMatch = options.permissions.every((perm) => newKeyVerification.scopes?.includes(perm));
|
|
546
539
|
if (!scopesMatch) {
|
|
547
|
-
console.warn('[generateApiKey] Scopes mismatch. Expected:',
|
|
540
|
+
console.warn('[generateApiKey] Scopes mismatch. Expected:', options.permissions, 'Got:', newKeyVerification.scopes);
|
|
541
|
+
verificationWarning = `New API key scopes do not match expected permissions. Expected: ${options.permissions.join(', ')}, Got: ${newKeyVerification.scopes?.join(', ') || 'none'}`;
|
|
548
542
|
}
|
|
549
543
|
}
|
|
550
544
|
else {
|
|
551
545
|
console.warn('[generateApiKey] New key verification failed:', newKeyVerification.error);
|
|
546
|
+
verificationWarning = `New API key failed verification - ${newKeyVerification.error || 'Unknown error'}`;
|
|
552
547
|
}
|
|
553
548
|
}
|
|
554
549
|
catch (verifyError) {
|
|
555
550
|
console.warn('[generateApiKey] Could not verify new key:', verifyError);
|
|
551
|
+
verificationWarning = `Could not verify new API key - ${verifyError instanceof Error ? verifyError.message : 'Unknown error'}`;
|
|
556
552
|
}
|
|
557
553
|
}
|
|
558
554
|
else {
|
|
559
555
|
// Create new key if none exists or revokeOld is false
|
|
560
|
-
const createData = await createUserPermissions(options.userId,
|
|
556
|
+
const createData = await createUserPermissions(options.userId, options.permissions);
|
|
561
557
|
if (!createData) {
|
|
562
558
|
throw new Error('Failed to create admin key');
|
|
563
559
|
}
|
|
@@ -572,15 +568,17 @@ export const generateApiKey = command('unchecked', async (options) => {
|
|
|
572
568
|
if (!currentUser) {
|
|
573
569
|
currentUser = await makeClerkRequest(`/users/${options.userId}`);
|
|
574
570
|
}
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
571
|
+
if (currentUser) {
|
|
572
|
+
await makeClerkRequest(`/users/${options.userId}`, {
|
|
573
|
+
method: 'PATCH',
|
|
574
|
+
body: JSON.stringify({
|
|
575
|
+
private_metadata: {
|
|
576
|
+
...(currentUser.private_metadata || {}),
|
|
577
|
+
mako_api_key: newApiKey
|
|
578
|
+
}
|
|
579
|
+
})
|
|
580
|
+
});
|
|
581
|
+
}
|
|
584
582
|
}
|
|
585
583
|
catch (clerkError) {
|
|
586
584
|
console.error('[generateApiKey] Failed to update Clerk profile:', clerkError);
|
|
@@ -589,9 +587,8 @@ export const generateApiKey = command('unchecked', async (options) => {
|
|
|
589
587
|
const result = {
|
|
590
588
|
success: true,
|
|
591
589
|
apiKey: newApiKey,
|
|
592
|
-
message: wasRotated
|
|
593
|
-
|
|
594
|
-
: 'API key generated successfully'
|
|
590
|
+
message: wasRotated ? 'API key rotated successfully' : 'API key generated successfully',
|
|
591
|
+
verificationWarning
|
|
595
592
|
};
|
|
596
593
|
return JSON.parse(JSON.stringify(result));
|
|
597
594
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -4,9 +4,8 @@
|
|
|
4
4
|
import UserTable from './UserTable.svelte';
|
|
5
5
|
import UserModal from './UserModal.svelte';
|
|
6
6
|
import UserViewModal from './UserViewModal.svelte';
|
|
7
|
-
import type { User, UserManagementProps, Role, Permission
|
|
7
|
+
import type { User, UserManagementProps, Role, Permission } from '../index.js';
|
|
8
8
|
import { SvelteSet } from 'svelte/reactivity';
|
|
9
|
-
import type { RemoteQuery } from '@sveltejs/kit';
|
|
10
9
|
|
|
11
10
|
let {
|
|
12
11
|
adapter,
|
|
@@ -60,13 +59,7 @@
|
|
|
60
59
|
|
|
61
60
|
// Refresh the query cache to get fresh data
|
|
62
61
|
async function refreshUsersQuery() {
|
|
63
|
-
|
|
64
|
-
page: currentPage,
|
|
65
|
-
pageSize,
|
|
66
|
-
sortBy: sortBy || undefined,
|
|
67
|
-
sortOrder: sortOrder || 'desc'
|
|
68
|
-
}) as RemoteQuery<GetUsersResult>;
|
|
69
|
-
await query.refresh();
|
|
62
|
+
await loadUsers();
|
|
70
63
|
}
|
|
71
64
|
|
|
72
65
|
// Handlers
|
|
@@ -146,10 +139,8 @@
|
|
|
146
139
|
} else {
|
|
147
140
|
await adapter.updateUser({ userId: user.id, userData: user });
|
|
148
141
|
}
|
|
149
|
-
//
|
|
142
|
+
// Refresh the users list
|
|
150
143
|
await refreshUsersQuery();
|
|
151
|
-
// Force refresh by reassigning state to trigger reactivity
|
|
152
|
-
await loadUsers();
|
|
153
144
|
} catch (error) {
|
|
154
145
|
console.error('Error saving user:', error);
|
|
155
146
|
// Reload on error to restore correct state
|
|
@@ -180,11 +171,8 @@
|
|
|
180
171
|
// Perform the actual delete operation
|
|
181
172
|
await adapter.deleteUser(userId);
|
|
182
173
|
|
|
183
|
-
// Invalidate the query cache by calling refresh() before loading
|
|
184
|
-
await refreshUsersQuery();
|
|
185
|
-
|
|
186
174
|
// Refresh to ensure we have the latest data
|
|
187
|
-
await
|
|
175
|
+
await refreshUsersQuery();
|
|
188
176
|
} catch (error) {
|
|
189
177
|
console.error('Error deleting user:', error);
|
|
190
178
|
// Reload on error to restore correct state
|
|
@@ -225,11 +213,8 @@
|
|
|
225
213
|
// Perform the actual delete operation
|
|
226
214
|
await adapter.deleteUsers(userIds);
|
|
227
215
|
|
|
228
|
-
// Invalidate the query cache by calling refresh() before loading
|
|
229
|
-
await refreshUsersQuery();
|
|
230
|
-
|
|
231
216
|
// Refresh to ensure we have the latest data
|
|
232
|
-
await
|
|
217
|
+
await refreshUsersQuery();
|
|
233
218
|
} catch (error) {
|
|
234
219
|
console.error('Error deleting users:', error);
|
|
235
220
|
// Reload on error to restore correct state
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
type Role,
|
|
10
10
|
getUserDisplayName
|
|
11
11
|
} from '../index.js';
|
|
12
|
+
import { toast } from 'svelte-sonner';
|
|
12
13
|
|
|
13
14
|
// Icons as simple SVGs
|
|
14
15
|
let {
|
|
@@ -31,7 +32,9 @@
|
|
|
31
32
|
let showApiKey = $state(false);
|
|
32
33
|
let regeneratingApiKey = $state(false);
|
|
33
34
|
let verifyingToken = $state(false);
|
|
34
|
-
let tokenVerification = $state<{ valid?: boolean; scopes?: string[]; error?: string } | null>(
|
|
35
|
+
let tokenVerification = $state<{ valid?: boolean; scopes?: string[]; error?: string } | null>(
|
|
36
|
+
null
|
|
37
|
+
);
|
|
35
38
|
let initialRole = $state<string>('');
|
|
36
39
|
|
|
37
40
|
// Form data
|
|
@@ -187,7 +190,7 @@
|
|
|
187
190
|
role: roleValue,
|
|
188
191
|
permissions: role ? [...role.permissions] : []
|
|
189
192
|
};
|
|
190
|
-
|
|
193
|
+
|
|
191
194
|
// Clear token verification when permissions change
|
|
192
195
|
tokenVerification = null;
|
|
193
196
|
}
|
|
@@ -220,6 +223,14 @@
|
|
|
220
223
|
}
|
|
221
224
|
};
|
|
222
225
|
}
|
|
226
|
+
|
|
227
|
+
// Show warning toast if new key verification failed
|
|
228
|
+
if (result.verificationWarning) {
|
|
229
|
+
toast.warning('API Key Verification Warning', {
|
|
230
|
+
description: result.verificationWarning,
|
|
231
|
+
duration: 8000 // 8 seconds for important warnings
|
|
232
|
+
});
|
|
233
|
+
}
|
|
223
234
|
} catch (error) {
|
|
224
235
|
console.error('Error regenerating API key:', error);
|
|
225
236
|
formErrors.apiKey = error instanceof Error ? error.message : 'Failed to regenerate API key';
|
|
@@ -233,7 +244,7 @@
|
|
|
233
244
|
|
|
234
245
|
try {
|
|
235
246
|
verifyingToken = true;
|
|
236
|
-
formErrors.apiKey
|
|
247
|
+
delete formErrors.apiKey;
|
|
237
248
|
const result = await adapter.verifyToken({ apiKey });
|
|
238
249
|
tokenVerification = result;
|
|
239
250
|
|
|
@@ -274,7 +285,7 @@
|
|
|
274
285
|
contentclass="max-w-4xl"
|
|
275
286
|
class={cn(className)}
|
|
276
287
|
>
|
|
277
|
-
<form bind:this={formElement} onsubmit={handleSubmit} class="flex gap-6">
|
|
288
|
+
<form bind:this={formElement} onsubmit={handleSubmit} class="flex gap-6" data-testid="user-form">
|
|
278
289
|
<!-- Left Column: Profile Information -->
|
|
279
290
|
<div class="min-w-0 flex-1 space-y-4">
|
|
280
291
|
<div class="border-default-200 border-b pb-3">
|
|
@@ -294,6 +305,7 @@
|
|
|
294
305
|
? 'border-danger-300'
|
|
295
306
|
: 'border-default-300'}"
|
|
296
307
|
placeholder="First name"
|
|
308
|
+
data-testid="first-name-input"
|
|
297
309
|
required
|
|
298
310
|
/>
|
|
299
311
|
{#if formErrors.first_name}
|
|
@@ -314,6 +326,7 @@
|
|
|
314
326
|
? 'border-danger-300'
|
|
315
327
|
: 'border-default-300'}"
|
|
316
328
|
placeholder="Last name"
|
|
329
|
+
data-testid="last-name-input"
|
|
317
330
|
required
|
|
318
331
|
/>
|
|
319
332
|
{#if formErrors.last_name}
|
|
@@ -334,6 +347,7 @@
|
|
|
334
347
|
? 'border-danger-300'
|
|
335
348
|
: 'border-default-300'}"
|
|
336
349
|
placeholder="user@example.com"
|
|
350
|
+
data-testid="email-input"
|
|
337
351
|
required
|
|
338
352
|
/>
|
|
339
353
|
{#if formErrors.email}
|
|
@@ -356,6 +370,7 @@
|
|
|
356
370
|
disabled={verifyingToken}
|
|
357
371
|
class="disabled:text-default-400 inline-flex items-center gap-1 text-sm text-green-600 transition-colors hover:text-green-700 hover:underline disabled:cursor-not-allowed"
|
|
358
372
|
aria-label="Verify token"
|
|
373
|
+
data-testid="verify-token-button"
|
|
359
374
|
>
|
|
360
375
|
<svg
|
|
361
376
|
class="h-4 w-4 {verifyingToken ? 'animate-spin' : ''}"
|
|
@@ -382,6 +397,7 @@
|
|
|
382
397
|
formData.permissions.length === 0}
|
|
383
398
|
class="disabled:text-default-400 inline-flex items-center gap-1 text-sm text-blue-600 transition-colors hover:text-blue-700 hover:underline disabled:cursor-not-allowed"
|
|
384
399
|
aria-label="Regenerate API key"
|
|
400
|
+
data-testid="regenerate-api-key-button"
|
|
385
401
|
>
|
|
386
402
|
<svg
|
|
387
403
|
class="h-4 w-4 {regeneratingApiKey ? 'animate-spin' : ''}"
|
|
@@ -409,12 +425,14 @@
|
|
|
409
425
|
readonly
|
|
410
426
|
class="border-default-300 bg-default-50 w-full rounded-lg border px-3 py-2 pr-10 font-mono text-sm"
|
|
411
427
|
placeholder="No API key generated"
|
|
428
|
+
data-testid="api-key-input"
|
|
412
429
|
/>
|
|
413
430
|
<button
|
|
414
431
|
type="button"
|
|
415
432
|
onclick={() => (showApiKey = !showApiKey)}
|
|
416
433
|
class="text-default-500 hover:text-default-700 absolute top-1/2 right-2 -translate-y-1/2"
|
|
417
434
|
aria-label={showApiKey ? 'Hide API key' : 'Show API key'}
|
|
435
|
+
data-testid="toggle-api-key-visibility"
|
|
418
436
|
>
|
|
419
437
|
{#if showApiKey}
|
|
420
438
|
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
@@ -449,8 +467,18 @@
|
|
|
449
467
|
{#if tokenVerification.valid}
|
|
450
468
|
<div class="bg-success-50 border-success-200 mt-2 rounded-lg border p-3">
|
|
451
469
|
<div class="flex items-start gap-2">
|
|
452
|
-
<svg
|
|
453
|
-
|
|
470
|
+
<svg
|
|
471
|
+
class="text-success-600 mt-0.5 h-4 w-4 shrink-0"
|
|
472
|
+
fill="none"
|
|
473
|
+
stroke="currentColor"
|
|
474
|
+
viewBox="0 0 24 24"
|
|
475
|
+
>
|
|
476
|
+
<path
|
|
477
|
+
stroke-linecap="round"
|
|
478
|
+
stroke-linejoin="round"
|
|
479
|
+
stroke-width="2"
|
|
480
|
+
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
481
|
+
></path>
|
|
454
482
|
</svg>
|
|
455
483
|
<div class="min-w-0 flex-1">
|
|
456
484
|
<p class="text-success-800 text-xs font-medium">Token verified successfully</p>
|
|
@@ -465,8 +493,18 @@
|
|
|
465
493
|
{:else}
|
|
466
494
|
<div class="bg-danger-50 border-danger-200 mt-2 rounded-lg border p-3">
|
|
467
495
|
<div class="flex items-start gap-2">
|
|
468
|
-
<svg
|
|
469
|
-
|
|
496
|
+
<svg
|
|
497
|
+
class="text-danger-600 mt-0.5 h-4 w-4 shrink-0"
|
|
498
|
+
fill="none"
|
|
499
|
+
stroke="currentColor"
|
|
500
|
+
viewBox="0 0 24 24"
|
|
501
|
+
>
|
|
502
|
+
<path
|
|
503
|
+
stroke-linecap="round"
|
|
504
|
+
stroke-linejoin="round"
|
|
505
|
+
stroke-width="2"
|
|
506
|
+
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
507
|
+
></path>
|
|
470
508
|
</svg>
|
|
471
509
|
<div class="min-w-0 flex-1">
|
|
472
510
|
<p class="text-danger-800 text-xs font-medium">Token verification failed</p>
|
|
@@ -511,6 +549,7 @@
|
|
|
511
549
|
? 'border-blue-500 bg-blue-50 opacity-75'
|
|
512
550
|
: 'border-blue-500 bg-blue-50'
|
|
513
551
|
: 'border-default-200 hover:border-default-300 bg-white'}"
|
|
552
|
+
data-testid="role-{role.value}"
|
|
514
553
|
>
|
|
515
554
|
<div class="flex items-center justify-between gap-2">
|
|
516
555
|
<div class="min-w-0 flex-1">
|
|
@@ -565,7 +604,13 @@
|
|
|
565
604
|
<div></div>
|
|
566
605
|
{/if}
|
|
567
606
|
<div class="flex gap-3">
|
|
568
|
-
<Button
|
|
607
|
+
<Button
|
|
608
|
+
variant="outline"
|
|
609
|
+
onclick={handleClose}
|
|
610
|
+
disabled={saving}
|
|
611
|
+
type="button"
|
|
612
|
+
data-testid="cancel-button"
|
|
613
|
+
>
|
|
569
614
|
Cancel
|
|
570
615
|
</Button>
|
|
571
616
|
<Button
|
|
@@ -574,6 +619,7 @@
|
|
|
574
619
|
onclick={() => formElement?.requestSubmit()}
|
|
575
620
|
disabled={saving}
|
|
576
621
|
isLoading={saving}
|
|
622
|
+
data-testid="save-user-button"
|
|
577
623
|
>
|
|
578
624
|
{mode === 'create' ? 'Create User' : 'Save Changes'}
|
|
579
625
|
</Button>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@makolabs/ripple",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.1",
|
|
4
4
|
"description": "Simple Svelte 5 powered component library ✨",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"repository": {
|
|
@@ -18,6 +18,10 @@
|
|
|
18
18
|
"format": "prettier --write .",
|
|
19
19
|
"lint": "prettier --check . && eslint .",
|
|
20
20
|
"test:unit": "vitest",
|
|
21
|
+
"test:e2e": "playwright test",
|
|
22
|
+
"test:e2e:ui": "playwright test --ui",
|
|
23
|
+
"test:e2e:debug": "playwright test --debug",
|
|
24
|
+
"test:e2e:report": "playwright show-report",
|
|
21
25
|
"test": "npm run test:unit -- --run",
|
|
22
26
|
"pub:minor": "git add . && git commit -m \"chore: prepare for publish minor\" && npm version minor && git push --follow-tags && npm publish",
|
|
23
27
|
"pub:patch": "git add . && git commit -m \"chore: prepare for publish patch\" && npm version patch && git push --follow-tags && npm publish",
|