@makolabs/ripple 3.7.1 → 3.8.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.
- package/dist/elements/accordion/Accordion.svelte +5 -1
- package/dist/elements/dropdown/Select.svelte +29 -0
- package/dist/elements/file-upload/FileUpload.svelte +11 -3
- package/dist/elements/file-upload/FilesPreview.svelte +8 -2
- package/dist/file-browser/FileBrowser.svelte +8 -0
- package/dist/filters/CompactFilters.svelte +14 -3
- package/dist/funcs/clerk-requests.d.ts +34 -0
- package/dist/funcs/clerk-requests.js +38 -0
- package/dist/funcs/user-management.remote.js +34 -43
- package/dist/header/PageHeader.svelte +10 -4
- package/dist/layout/sidebar/NavGroup.svelte +4 -2
- package/dist/layout/sidebar/NavItem.svelte +7 -2
- package/dist/layout/tabs/Tab.svelte +3 -2
- package/dist/layout/tabs/Tab.svelte.d.ts +1 -0
- package/dist/layout/tabs/TabGroup.svelte +3 -0
- package/package.json +3 -2
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
header,
|
|
27
27
|
children,
|
|
28
28
|
icon: Icon,
|
|
29
|
+
iconPosition = 'start',
|
|
29
30
|
testId
|
|
30
31
|
}: AccordionProps = $props();
|
|
31
32
|
|
|
@@ -109,7 +110,7 @@
|
|
|
109
110
|
|
|
110
111
|
{#snippet accordionHeaderContent()}
|
|
111
112
|
<div class="flex items-start justify-start gap-3">
|
|
112
|
-
{#if Icon}
|
|
113
|
+
{#if Icon && iconPosition === 'start'}
|
|
113
114
|
<Icon class="text-default-500 size-5" />
|
|
114
115
|
{/if}
|
|
115
116
|
<div class="flex flex-col items-start text-left">
|
|
@@ -120,6 +121,9 @@
|
|
|
120
121
|
<p class="text-default-500 text-xs font-normal">{description}</p>
|
|
121
122
|
{/if}
|
|
122
123
|
</div>
|
|
124
|
+
{#if Icon && iconPosition === 'end'}
|
|
125
|
+
<Icon class="text-default-500 size-5" />
|
|
126
|
+
{/if}
|
|
123
127
|
</div>
|
|
124
128
|
{@render summary?.()}
|
|
125
129
|
{/snippet}
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
multiple = false,
|
|
27
27
|
placeholder = 'Select an option',
|
|
28
28
|
searchable = false,
|
|
29
|
+
clearable = false,
|
|
29
30
|
disabled = false,
|
|
30
31
|
size = Size.MD,
|
|
31
32
|
class: className = '',
|
|
@@ -95,6 +96,7 @@
|
|
|
95
96
|
items.find((i) => i.value === value)
|
|
96
97
|
);
|
|
97
98
|
const selectedItems = $derived(items.filter((i) => valueArray.includes(i.value)));
|
|
99
|
+
const hasSelection = $derived(valueArray.some((v) => v !== ''));
|
|
98
100
|
|
|
99
101
|
const filteredItems = $derived.by<SelectItem[]>(() => {
|
|
100
102
|
if (isAsync) return asyncResults;
|
|
@@ -215,6 +217,15 @@
|
|
|
215
217
|
}
|
|
216
218
|
}
|
|
217
219
|
|
|
220
|
+
function handleClear(event: MouseEvent) {
|
|
221
|
+
// Don't let the click bubble to the trigger and open the dropdown.
|
|
222
|
+
event.stopPropagation();
|
|
223
|
+
event.preventDefault();
|
|
224
|
+
if (disabled) return;
|
|
225
|
+
value = multiple ? [] : '';
|
|
226
|
+
onselect({ value });
|
|
227
|
+
}
|
|
228
|
+
|
|
218
229
|
function handleClickOutside(event: MouseEvent) {
|
|
219
230
|
if (!open) return;
|
|
220
231
|
// On mobile the sheet handles its own close via backdrop.
|
|
@@ -385,6 +396,24 @@
|
|
|
385
396
|
</span>
|
|
386
397
|
|
|
387
398
|
<span class="ml-auto flex flex-shrink-0 items-center pl-2">
|
|
399
|
+
{#if clearable && hasSelection && !disabled}
|
|
400
|
+
<button
|
|
401
|
+
type="button"
|
|
402
|
+
onclick={handleClear}
|
|
403
|
+
aria-label="Clear selection"
|
|
404
|
+
class="text-default-400 hover:text-default-700 relative z-10 mr-1 flex size-5 cursor-pointer items-center justify-center rounded"
|
|
405
|
+
data-testid={buildTestId('select', 'clear', testId)}
|
|
406
|
+
>
|
|
407
|
+
<svg class="size-3" viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
|
408
|
+
<path
|
|
409
|
+
d="M3 3l6 6M9 3l-6 6"
|
|
410
|
+
stroke="currentColor"
|
|
411
|
+
stroke-width="1.5"
|
|
412
|
+
stroke-linecap="round"
|
|
413
|
+
/>
|
|
414
|
+
</svg>
|
|
415
|
+
</button>
|
|
416
|
+
{/if}
|
|
388
417
|
{#if Icon}
|
|
389
418
|
<Icon class={triggerIconClass} />
|
|
390
419
|
{:else}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { cn } from '../../helper/cls.js';
|
|
3
|
+
import { buildTestId } from '../../helper/testid.js';
|
|
3
4
|
import { fileUpload } from './file-upload.js';
|
|
4
5
|
import Button from '../../button/Button.svelte';
|
|
5
6
|
import Spinner from '../spinner/Spinner.svelte';
|
|
@@ -25,7 +26,8 @@
|
|
|
25
26
|
clearButtonLabel = 'Clear All',
|
|
26
27
|
filesListLabel = 'Selected files',
|
|
27
28
|
listMaxHeight = 'max-h-64',
|
|
28
|
-
header
|
|
29
|
+
header,
|
|
30
|
+
testId
|
|
29
31
|
}: FileUploadProps = $props();
|
|
30
32
|
|
|
31
33
|
const slots = $derived(fileUpload({ size }));
|
|
@@ -169,7 +171,10 @@
|
|
|
169
171
|
}
|
|
170
172
|
</script>
|
|
171
173
|
|
|
172
|
-
<div
|
|
174
|
+
<div
|
|
175
|
+
class={cn('flex w-full flex-col gap-4', className)}
|
|
176
|
+
data-testid={testId ? buildTestId('fileupload', undefined, testId) : undefined}
|
|
177
|
+
>
|
|
173
178
|
{#if header}
|
|
174
179
|
{@render header()}
|
|
175
180
|
{/if}
|
|
@@ -191,6 +196,7 @@
|
|
|
191
196
|
ondrop={handleDrop}
|
|
192
197
|
for={id}
|
|
193
198
|
data-dropzone-full={dropzoneFull ? 'true' : undefined}
|
|
199
|
+
data-testid={testId ? buildTestId('fileupload', 'dropzone', testId) : undefined}
|
|
194
200
|
>
|
|
195
201
|
<input
|
|
196
202
|
type="file"
|
|
@@ -200,6 +206,7 @@
|
|
|
200
206
|
disabled={!dropzoneEnabled}
|
|
201
207
|
class="hidden"
|
|
202
208
|
onchange={handleInputChange}
|
|
209
|
+
data-testid={testId ? buildTestId('fileupload', 'input', testId) : undefined}
|
|
203
210
|
{id}
|
|
204
211
|
/>
|
|
205
212
|
|
|
@@ -327,12 +334,13 @@
|
|
|
327
334
|
listMaxHeight
|
|
328
335
|
)}
|
|
329
336
|
>
|
|
330
|
-
{#each files as stagedFile (stagedFile.id)}
|
|
337
|
+
{#each files as stagedFile, rowIndex (stagedFile.id)}
|
|
331
338
|
{@const status = stagedFile.status ?? 'ready'}
|
|
332
339
|
<li
|
|
333
340
|
class={cn('flex items-center gap-3 px-3 py-2', status === 'success' && 'opacity-60')}
|
|
334
341
|
data-fileupload-row=""
|
|
335
342
|
data-status={status}
|
|
343
|
+
data-testid={testId ? buildTestId('fileupload', 'row', testId, rowIndex) : undefined}
|
|
336
344
|
transition:fade={{ duration: 300, easing: quintOut }}
|
|
337
345
|
animate:flip={{ duration: 350, easing: quintOut }}
|
|
338
346
|
>
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { cn } from '../../helper/cls.js';
|
|
3
|
+
import { buildTestId } from '../../helper/testid.js';
|
|
3
4
|
import type { FilePreviewProps } from '../../index.js';
|
|
4
5
|
|
|
5
|
-
let { files = [], ondelete, class: className = '' }: FilePreviewProps = $props();
|
|
6
|
+
let { files = [], ondelete, class: className = '', testId }: FilePreviewProps = $props();
|
|
6
7
|
|
|
7
8
|
function formatFileSize(bytes: number): string {
|
|
8
9
|
if (bytes === 0) return '0 B';
|
|
@@ -14,9 +15,13 @@
|
|
|
14
15
|
</script>
|
|
15
16
|
|
|
16
17
|
{#if files.length > 0}
|
|
17
|
-
<ul
|
|
18
|
+
<ul
|
|
19
|
+
class={cn('mt-6 space-y-3', className)}
|
|
20
|
+
data-testid={buildTestId('filespreview', undefined, testId)}
|
|
21
|
+
>
|
|
18
22
|
{#each files as file, i (i)}
|
|
19
23
|
<li
|
|
24
|
+
data-testid={buildTestId('filespreview', 'item', testId, i)}
|
|
20
25
|
class={cn('flex items-center justify-between rounded-xl p-4 ring-4', {
|
|
21
26
|
'bg-danger-50 ring-danger-100': file.status === 'error',
|
|
22
27
|
'bg-success-50 ring-success-100': file.status === 'success',
|
|
@@ -71,6 +76,7 @@
|
|
|
71
76
|
|
|
72
77
|
<button
|
|
73
78
|
type="button"
|
|
79
|
+
data-testid={buildTestId('filespreview', 'delete', testId, i)}
|
|
74
80
|
aria-label="Delete file"
|
|
75
81
|
class={cn('ml-4 flex size-6 cursor-pointer items-center justify-center rounded-lg', {
|
|
76
82
|
'bg-danger-100 text-danger-500': file.status === 'error',
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { onMount } from 'svelte';
|
|
3
3
|
import { cn } from '../helper/cls.js';
|
|
4
|
+
import { buildTestId } from '../helper/testid.js';
|
|
4
5
|
import { Button, Table, Color, Size, Spinner, Skeleton } from '../index.js';
|
|
5
6
|
import type { TableColumn, FileBrowserProps } from '../index.js';
|
|
6
7
|
import { formatDate } from '../utils/dateUtils.js';
|
|
@@ -22,10 +23,15 @@
|
|
|
22
23
|
isSelectable,
|
|
23
24
|
height = 'h-[500px]',
|
|
24
25
|
class: className = '',
|
|
26
|
+
testId,
|
|
25
27
|
selectedItems = $bindable<FileItem[]>([]),
|
|
26
28
|
selectedFiles = $bindable<string[]>([])
|
|
27
29
|
}: FileBrowserProps = $props();
|
|
28
30
|
|
|
31
|
+
// Namespaced testId forwarded to the inner Table so its data-testids are
|
|
32
|
+
// prefixed under the FileBrowser's own id (e.g. `{testId}-filebrowser-table-row-0`).
|
|
33
|
+
const tableTestId = $derived(testId ? buildTestId('filebrowser', undefined, testId) : undefined);
|
|
34
|
+
|
|
29
35
|
let files = $state<FileItem[]>([]);
|
|
30
36
|
let displayFiles = $state<FileItem[]>([]);
|
|
31
37
|
let currentPath = $state(startPath || '');
|
|
@@ -767,6 +773,7 @@
|
|
|
767
773
|
height,
|
|
768
774
|
className
|
|
769
775
|
)}
|
|
776
|
+
data-testid={testId ? buildTestId('filebrowser', undefined, testId) : undefined}
|
|
770
777
|
>
|
|
771
778
|
<div class="flex min-w-0 flex-1 flex-col">
|
|
772
779
|
{#if !isAuthenticated && adapter.authenticate}
|
|
@@ -960,6 +967,7 @@
|
|
|
960
967
|
{selectAllScope}
|
|
961
968
|
{onselect}
|
|
962
969
|
{selected}
|
|
970
|
+
testId={tableTestId}
|
|
963
971
|
/>
|
|
964
972
|
{/if}
|
|
965
973
|
</div>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { cn } from '../helper/cls.js';
|
|
3
|
+
import { buildTestId } from '../helper/testid.js';
|
|
3
4
|
import type {
|
|
4
5
|
FilterTab,
|
|
5
6
|
FilterGroup,
|
|
@@ -24,7 +25,8 @@
|
|
|
24
25
|
class: className,
|
|
25
26
|
summaryClass,
|
|
26
27
|
expandedClass,
|
|
27
|
-
FilterIcon
|
|
28
|
+
FilterIcon,
|
|
29
|
+
testId
|
|
28
30
|
}: CompactFiltersProps = $props();
|
|
29
31
|
|
|
30
32
|
// Search input only renders when the consumer bound to searchQuery.
|
|
@@ -187,7 +189,10 @@
|
|
|
187
189
|
</svg>
|
|
188
190
|
{/snippet}
|
|
189
191
|
|
|
190
|
-
<div
|
|
192
|
+
<div
|
|
193
|
+
class={cn(card && 'border-default-200 rounded-lg border bg-white p-3 shadow-sm', className)}
|
|
194
|
+
data-testid={testId ? buildTestId('compact-filters', undefined, testId) : undefined}
|
|
195
|
+
>
|
|
191
196
|
{#if card || expandable}
|
|
192
197
|
<!-- Header row: title | search | clear | chevron -->
|
|
193
198
|
<div class="mb-2 flex flex-wrap items-center gap-2">
|
|
@@ -214,6 +219,7 @@
|
|
|
214
219
|
placeholder={searchPlaceholder}
|
|
215
220
|
bind:value={searchQuery}
|
|
216
221
|
data-filters-search=""
|
|
222
|
+
data-testid={testId ? buildTestId('compact-filters', 'search', testId) : undefined}
|
|
217
223
|
/>
|
|
218
224
|
{#if searchQuery}
|
|
219
225
|
<button
|
|
@@ -222,6 +228,9 @@
|
|
|
222
228
|
class="text-default-400 hover:text-default-700 absolute top-1/2 right-1 flex size-5 -translate-y-1/2 cursor-pointer items-center justify-center rounded"
|
|
223
229
|
aria-label="Clear search"
|
|
224
230
|
data-filters-search-clear=""
|
|
231
|
+
data-testid={testId
|
|
232
|
+
? buildTestId('compact-filters', 'search-clear', testId)
|
|
233
|
+
: undefined}
|
|
225
234
|
>
|
|
226
235
|
<svg class="size-3" viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
|
227
236
|
<path
|
|
@@ -242,6 +251,7 @@
|
|
|
242
251
|
onclick={clearAll}
|
|
243
252
|
class="text-default-600 hover:bg-default-100 cursor-pointer rounded-md px-2 py-1 text-xs font-medium"
|
|
244
253
|
data-filters-clear-all=""
|
|
254
|
+
data-testid={testId ? buildTestId('compact-filters', 'clear-all', testId) : undefined}
|
|
245
255
|
>
|
|
246
256
|
{clearAllLabel}
|
|
247
257
|
</button>
|
|
@@ -269,10 +279,11 @@
|
|
|
269
279
|
{#if !isExpanded}
|
|
270
280
|
<div class={cn('flex flex-wrap gap-2', summaryClass)}>
|
|
271
281
|
{#if chipSummary}
|
|
272
|
-
{#each chips as chip (chip.key + '::' + chip.value)}
|
|
282
|
+
{#each chips as chip, index (chip.key + '::' + chip.value)}
|
|
273
283
|
<span
|
|
274
284
|
class="bg-primary-50 text-primary-700 border-primary-200 flex items-center gap-1 rounded-full border px-2 py-1 text-xs"
|
|
275
285
|
data-filters-chip=""
|
|
286
|
+
data-testid={testId ? buildTestId('compact-filters', 'chip', testId, index) : undefined}
|
|
276
287
|
>
|
|
277
288
|
<span class="font-medium">{chip.groupLabel}:</span>
|
|
278
289
|
<span>{chip.label}</span>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure builders for Clerk user-API requests, extracted from
|
|
3
|
+
* `user-management.remote.ts` so the request shape is unit-testable — a
|
|
4
|
+
* `*.remote.ts` module cannot be imported in vitest (SvelteKit's plugin rejects
|
|
5
|
+
* any non-remote export).
|
|
6
|
+
*
|
|
7
|
+
* They also document the rule that caused minted API keys to silently never
|
|
8
|
+
* persist: Clerk deprecated sending `private_metadata` in the body of
|
|
9
|
+
* `PATCH /users/{id}` (it now returns 422 `form_param_deprecated`). User
|
|
10
|
+
* metadata must be written through the dedicated `PATCH /users/{id}/metadata`
|
|
11
|
+
* endpoint, which merges into the existing metadata.
|
|
12
|
+
*/
|
|
13
|
+
export interface ClerkRequest {
|
|
14
|
+
endpoint: string;
|
|
15
|
+
init: {
|
|
16
|
+
method: string;
|
|
17
|
+
body: string;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Build a request that persists `privateMetadata` on a Clerk user via the
|
|
22
|
+
* dedicated metadata endpoint. Clerk merges the provided keys into the user's
|
|
23
|
+
* existing private metadata.
|
|
24
|
+
*/
|
|
25
|
+
export declare function clerkMetadataPatch(userId: string, privateMetadata: Record<string, unknown>): ClerkRequest;
|
|
26
|
+
/**
|
|
27
|
+
* Build the body for a profile update (`PATCH /users/{id}`). Metadata is
|
|
28
|
+
* intentionally excluded — it must go through {@link clerkMetadataPatch}.
|
|
29
|
+
*/
|
|
30
|
+
export declare function clerkProfilePatchBody(userData: {
|
|
31
|
+
first_name?: string;
|
|
32
|
+
last_name?: string;
|
|
33
|
+
username?: string;
|
|
34
|
+
}): Record<string, string>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure builders for Clerk user-API requests, extracted from
|
|
3
|
+
* `user-management.remote.ts` so the request shape is unit-testable — a
|
|
4
|
+
* `*.remote.ts` module cannot be imported in vitest (SvelteKit's plugin rejects
|
|
5
|
+
* any non-remote export).
|
|
6
|
+
*
|
|
7
|
+
* They also document the rule that caused minted API keys to silently never
|
|
8
|
+
* persist: Clerk deprecated sending `private_metadata` in the body of
|
|
9
|
+
* `PATCH /users/{id}` (it now returns 422 `form_param_deprecated`). User
|
|
10
|
+
* metadata must be written through the dedicated `PATCH /users/{id}/metadata`
|
|
11
|
+
* endpoint, which merges into the existing metadata.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Build a request that persists `privateMetadata` on a Clerk user via the
|
|
15
|
+
* dedicated metadata endpoint. Clerk merges the provided keys into the user's
|
|
16
|
+
* existing private metadata.
|
|
17
|
+
*/
|
|
18
|
+
export function clerkMetadataPatch(userId, privateMetadata) {
|
|
19
|
+
return {
|
|
20
|
+
endpoint: `/users/${userId}/metadata`,
|
|
21
|
+
init: { method: 'PATCH', body: JSON.stringify({ private_metadata: privateMetadata }) }
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Build the body for a profile update (`PATCH /users/{id}`). Metadata is
|
|
26
|
+
* intentionally excluded — it must go through {@link clerkMetadataPatch}.
|
|
27
|
+
*/
|
|
28
|
+
export function clerkProfilePatchBody(userData) {
|
|
29
|
+
const body = {};
|
|
30
|
+
if (userData.first_name !== undefined)
|
|
31
|
+
body.first_name = userData.first_name;
|
|
32
|
+
if (userData.last_name !== undefined)
|
|
33
|
+
body.last_name = userData.last_name;
|
|
34
|
+
if (userData.username !== undefined && userData.username !== '') {
|
|
35
|
+
body.username = userData.username;
|
|
36
|
+
}
|
|
37
|
+
return body;
|
|
38
|
+
}
|
|
@@ -2,6 +2,7 @@ import { command } from '$app/server';
|
|
|
2
2
|
import { getRequestEvent } from '$app/server';
|
|
3
3
|
import { env } from '$env/dynamic/private';
|
|
4
4
|
import { building } from '$app/environment';
|
|
5
|
+
import { clerkMetadataPatch, clerkProfilePatchBody } from './clerk-requests.js';
|
|
5
6
|
const CLIENT_ID = env.CLIENT_ID;
|
|
6
7
|
const ORGANIZATION_ID = env.ALLOWED_ORG_ID;
|
|
7
8
|
if (!CLIENT_ID && !building) {
|
|
@@ -248,6 +249,16 @@ async function createUserPermissions(email, permissions, clientId = CLIENT_ID) {
|
|
|
248
249
|
log.trace('createUserPermissions', 'Result:', result);
|
|
249
250
|
return result;
|
|
250
251
|
}
|
|
252
|
+
/**
|
|
253
|
+
* Persist private metadata on a Clerk user via the dedicated metadata endpoint.
|
|
254
|
+
* Clerk deprecated `private_metadata` in the body of `PATCH /users/{id}` (it
|
|
255
|
+
* now returns 422 form_param_deprecated), which silently dropped minted API
|
|
256
|
+
* keys. Routes through `PATCH /users/{id}/metadata` (which merges) instead.
|
|
257
|
+
*/
|
|
258
|
+
async function writeClerkMetadata(userId, privateMetadata) {
|
|
259
|
+
const { endpoint, init } = clerkMetadataPatch(userId, privateMetadata);
|
|
260
|
+
return makeClerkRequest(endpoint, init);
|
|
261
|
+
}
|
|
251
262
|
export const getUsers = command('unchecked', async (options) => {
|
|
252
263
|
log.trace('getUsers', 'Called with options:', options);
|
|
253
264
|
try {
|
|
@@ -347,14 +358,9 @@ export const createUser = command('unchecked', async (userData) => {
|
|
|
347
358
|
const apiKey = adminKeyResult?.data?.key;
|
|
348
359
|
log.trace('createUser', 'Admin key created, has key:', !!apiKey);
|
|
349
360
|
if (adminKeyResult && apiKey) {
|
|
350
|
-
const updatedUser = await
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
private_metadata: {
|
|
354
|
-
...result.private_metadata,
|
|
355
|
-
mako_api_key: apiKey
|
|
356
|
-
}
|
|
357
|
-
})
|
|
361
|
+
const updatedUser = await writeClerkMetadata(result.id, {
|
|
362
|
+
...result.private_metadata,
|
|
363
|
+
mako_api_key: apiKey
|
|
358
364
|
});
|
|
359
365
|
// Ensure updatedUser is serializable
|
|
360
366
|
result = JSON.parse(JSON.stringify(updatedUser));
|
|
@@ -385,22 +391,19 @@ export const updateUser = command('unchecked', async (options) => {
|
|
|
385
391
|
const { userId, userData } = options;
|
|
386
392
|
log.trace('updateUser', 'Called for userId:', userId, 'fields:', Object.keys(userData));
|
|
387
393
|
try {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
394
|
+
// Profile fields and metadata live behind different Clerk endpoints —
|
|
395
|
+
// `private_metadata` is no longer accepted on PATCH /users/{id}.
|
|
396
|
+
const profileBody = clerkProfilePatchBody(userData);
|
|
397
|
+
log.trace('updateUser', 'Profile payload:', profileBody);
|
|
398
|
+
let result = Object.keys(profileBody).length > 0
|
|
399
|
+
? await makeClerkRequest(`/users/${userId}`, {
|
|
400
|
+
method: 'PATCH',
|
|
401
|
+
body: JSON.stringify(profileBody)
|
|
402
|
+
})
|
|
403
|
+
: await makeClerkRequest(`/users/${userId}`);
|
|
396
404
|
if (userData.private_metadata !== undefined) {
|
|
397
|
-
|
|
405
|
+
result = await writeClerkMetadata(userId, userData.private_metadata);
|
|
398
406
|
}
|
|
399
|
-
log.trace('updateUser', 'Update payload:', updateData);
|
|
400
|
-
let result = await makeClerkRequest(`/users/${userId}`, {
|
|
401
|
-
method: 'PATCH',
|
|
402
|
-
body: JSON.stringify(updateData)
|
|
403
|
-
});
|
|
404
407
|
// Ensure result is serializable
|
|
405
408
|
result = JSON.parse(JSON.stringify(result));
|
|
406
409
|
if (userData.permissions !== undefined) {
|
|
@@ -572,14 +575,9 @@ export const updateUserPermissions = command('unchecked', async (options) => {
|
|
|
572
575
|
const newKeyResult = await createUserPermissions(email, permissions);
|
|
573
576
|
const newApiKey = newKeyResult?.data?.key;
|
|
574
577
|
if (newApiKey) {
|
|
575
|
-
await
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
private_metadata: {
|
|
579
|
-
...(user.private_metadata || {}),
|
|
580
|
-
mako_api_key: newApiKey
|
|
581
|
-
}
|
|
582
|
-
})
|
|
578
|
+
await writeClerkMetadata(userId, {
|
|
579
|
+
...(user.private_metadata || {}),
|
|
580
|
+
mako_api_key: newApiKey
|
|
583
581
|
});
|
|
584
582
|
log.trace('updateUserPermissions', 'New key stored in Clerk metadata');
|
|
585
583
|
}
|
|
@@ -742,14 +740,9 @@ export const generateApiKey = command('unchecked', async (options) => {
|
|
|
742
740
|
currentUser = await makeClerkRequest(`/users/${options.userId}`);
|
|
743
741
|
}
|
|
744
742
|
if (currentUser) {
|
|
745
|
-
await
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
private_metadata: {
|
|
749
|
-
...(currentUser.private_metadata || {}),
|
|
750
|
-
mako_api_key: newApiKey
|
|
751
|
-
}
|
|
752
|
-
})
|
|
743
|
+
await writeClerkMetadata(options.userId, {
|
|
744
|
+
...(currentUser.private_metadata || {}),
|
|
745
|
+
mako_api_key: newApiKey
|
|
753
746
|
});
|
|
754
747
|
log.trace('generateApiKey', 'Clerk metadata updated with new key');
|
|
755
748
|
}
|
|
@@ -874,11 +867,9 @@ export const approveUser = command('unchecked', async (input) => {
|
|
|
874
867
|
if (!apiKey) {
|
|
875
868
|
throw new Error('Failed to mint API key during approval');
|
|
876
869
|
}
|
|
877
|
-
await
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
private_metadata: { ...(user.private_metadata || {}), mako_api_key: apiKey }
|
|
881
|
-
})
|
|
870
|
+
await writeClerkMetadata(input.userId, {
|
|
871
|
+
...(user.private_metadata || {}),
|
|
872
|
+
mako_api_key: apiKey
|
|
882
873
|
});
|
|
883
874
|
log.trace('approveUser', 'Approved and key issued');
|
|
884
875
|
return JSON.parse(JSON.stringify({ apiKey }));
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { cn } from '../helper/cls.js';
|
|
3
|
+
import { buildTestId } from '../helper/testid.js';
|
|
3
4
|
import type { PageHeaderProps } from '../index.js';
|
|
4
5
|
import Breadcrumbs from './Breadcrumbs.svelte';
|
|
5
6
|
|
|
@@ -10,7 +11,8 @@
|
|
|
10
11
|
children,
|
|
11
12
|
class: className = '',
|
|
12
13
|
titleClass = '',
|
|
13
|
-
layout = 'vertical'
|
|
14
|
+
layout = 'vertical',
|
|
15
|
+
testId
|
|
14
16
|
}: PageHeaderProps = $props();
|
|
15
17
|
|
|
16
18
|
const defaultTitleClasses =
|
|
@@ -19,9 +21,13 @@
|
|
|
19
21
|
const hasBreadcrumbs = $derived(breadcrumbs && breadcrumbs.length > 0);
|
|
20
22
|
const titleClasses = $derived(cn(defaultTitleClasses, titleClass));
|
|
21
23
|
const containerClass = $derived(cn('space-y-4', className as string));
|
|
24
|
+
|
|
25
|
+
// Root data-testid for the whole header; the Breadcrumbs child receives a
|
|
26
|
+
// prefixed testId so its selectors nest under this header's namespace.
|
|
27
|
+
const rootTestId = $derived(testId ? buildTestId('pageheader', undefined, testId) : undefined);
|
|
22
28
|
</script>
|
|
23
29
|
|
|
24
|
-
<div class="pb-4">
|
|
30
|
+
<div class="pb-4" data-testid={rootTestId}>
|
|
25
31
|
{#if layout === 'horizontal'}
|
|
26
32
|
<div
|
|
27
33
|
class="flex flex-col space-y-3 md:flex-row md:items-start md:justify-between md:space-y-0 {className}"
|
|
@@ -29,7 +35,7 @@
|
|
|
29
35
|
<!-- Title and subtitle grouped together as one unit -->
|
|
30
36
|
<div class="min-w-0 flex-1 gap-4">
|
|
31
37
|
{#if hasBreadcrumbs}
|
|
32
|
-
<Breadcrumbs items={breadcrumbs} />
|
|
38
|
+
<Breadcrumbs items={breadcrumbs} testId={rootTestId} />
|
|
33
39
|
{/if}
|
|
34
40
|
<h2 class={titleClasses}>{title}</h2>
|
|
35
41
|
{#if subtitle}
|
|
@@ -52,7 +58,7 @@
|
|
|
52
58
|
<!-- Title and subtitle grouped together -->
|
|
53
59
|
<div class="min-w-0 gap-4">
|
|
54
60
|
{#if hasBreadcrumbs}
|
|
55
|
-
<Breadcrumbs items={breadcrumbs} />
|
|
61
|
+
<Breadcrumbs items={breadcrumbs} testId={rootTestId} />
|
|
56
62
|
{/if}
|
|
57
63
|
<h2 class={titleClasses}>{title}</h2>
|
|
58
64
|
{#if subtitle}
|
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
import { getContext } from 'svelte';
|
|
3
3
|
import type { MenuBar, NavGroupProps } from '../../index.js';
|
|
4
4
|
import { cn } from '../../helper/cls.js';
|
|
5
|
+
import { buildTestId } from '../../helper/testid.js';
|
|
5
6
|
|
|
6
7
|
let {
|
|
7
8
|
labelArea,
|
|
8
9
|
children,
|
|
9
10
|
class: className = '',
|
|
10
|
-
active: isActive = false
|
|
11
|
+
active: isActive = false,
|
|
12
|
+
testId
|
|
11
13
|
}: NavGroupProps = $props();
|
|
12
14
|
|
|
13
15
|
function toggle() {
|
|
@@ -58,7 +60,7 @@
|
|
|
58
60
|
);
|
|
59
61
|
</script>
|
|
60
62
|
|
|
61
|
-
<div>
|
|
63
|
+
<div data-testid={buildTestId('navgroup', undefined, testId)}>
|
|
62
64
|
<button type="button" class={navGroupClasses} onclick={toggle}>
|
|
63
65
|
{@render labelArea(labelClasses, iconClasses)}
|
|
64
66
|
{@render ChevronIcon(chevronIconClasses, 0.7)}
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
import { getContext } from 'svelte';
|
|
4
4
|
import type { MenuBar, NavItemProps } from '../../index.js';
|
|
5
5
|
import { cn } from '../../helper/cls.js';
|
|
6
|
+
import { buildTestId } from '../../helper/testid.js';
|
|
6
7
|
import { resolve } from '$app/paths';
|
|
7
8
|
|
|
8
|
-
let { href, children, active, class: className = '' }: NavItemProps = $props();
|
|
9
|
+
let { href, children, active, class: className = '', testId }: NavItemProps = $props();
|
|
9
10
|
|
|
10
11
|
const menubar: MenuBar = getContext('menubar');
|
|
11
12
|
|
|
@@ -25,6 +26,10 @@
|
|
|
25
26
|
);
|
|
26
27
|
</script>
|
|
27
28
|
|
|
28
|
-
<a
|
|
29
|
+
<a
|
|
30
|
+
href={resolve(href as `/`)}
|
|
31
|
+
class={navItemClasses}
|
|
32
|
+
data-testid={buildTestId('navitem', undefined, testId)}
|
|
33
|
+
>
|
|
29
34
|
{@render children?.(navChildrenClasses)}
|
|
30
35
|
</a>
|
|
@@ -16,8 +16,9 @@
|
|
|
16
16
|
onclick = () => {},
|
|
17
17
|
variant = 'line',
|
|
18
18
|
index = 0,
|
|
19
|
+
class: className = '',
|
|
19
20
|
testId
|
|
20
|
-
}: TabProps & { index?: number } = $props();
|
|
21
|
+
}: TabProps & { index?: number; class?: string } = $props();
|
|
21
22
|
|
|
22
23
|
function handleClick(event: Event & { currentTarget: EventTarget & HTMLButtonElement }) {
|
|
23
24
|
event.preventDefault();
|
|
@@ -35,7 +36,7 @@
|
|
|
35
36
|
})
|
|
36
37
|
);
|
|
37
38
|
|
|
38
|
-
const triggerClass = $derived(cn(trigger()));
|
|
39
|
+
const triggerClass = $derived(cn(trigger(), className));
|
|
39
40
|
</script>
|
|
40
41
|
|
|
41
42
|
<button
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
variant = 'line',
|
|
18
18
|
class: className = '',
|
|
19
19
|
listClass = '',
|
|
20
|
+
triggerClass = '',
|
|
20
21
|
panelClass = '',
|
|
21
22
|
onchange = () => {},
|
|
22
23
|
children,
|
|
@@ -33,6 +34,7 @@
|
|
|
33
34
|
|
|
34
35
|
const baseClass = $derived(cn(base(), className));
|
|
35
36
|
const listClass_ = $derived(cn(list(), listClass));
|
|
37
|
+
const triggerClass_ = $derived(cn(triggerClass));
|
|
36
38
|
const panelClass_ = $derived(cn(panel(), panelClass));
|
|
37
39
|
|
|
38
40
|
function handleTabClick(value: string) {
|
|
@@ -56,6 +58,7 @@
|
|
|
56
58
|
{size}
|
|
57
59
|
{variant}
|
|
58
60
|
{index}
|
|
61
|
+
class={triggerClass_}
|
|
59
62
|
testId={buildTestId('tabgroup', 'tab', testId, index)}
|
|
60
63
|
onclick={() => handleTabClick(tab.value)}
|
|
61
64
|
/>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@makolabs/ripple",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.8.1",
|
|
4
4
|
"description": "Simple Svelte 5 powered component library ✨",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"repository": {
|
|
@@ -139,7 +139,8 @@
|
|
|
139
139
|
"uuid": ">=11.1.1",
|
|
140
140
|
"devalue": ">=5.8.1",
|
|
141
141
|
"fast-xml-builder": ">=1.1.7",
|
|
142
|
-
"esbuild": ">=0.28.1"
|
|
142
|
+
"esbuild": ">=0.28.1",
|
|
143
|
+
"cookie": ">=0.7.0 <1.0.0"
|
|
143
144
|
},
|
|
144
145
|
"dependencies": {
|
|
145
146
|
"@aws-sdk/client-s3": "^3.1029.0",
|