@makolabs/ripple 1.2.3 → 1.2.4
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/README.md +77 -0
- package/dist/adapters/ai/OpenAIAdapter.js +16 -11
- package/dist/adapters/ai/types.d.ts +3 -3
- package/dist/adapters/storage/BaseAdapter.d.ts +1 -1
- package/dist/adapters/storage/BaseAdapter.js +1 -1
- package/dist/adapters/storage/S3Adapter.js +2 -2
- package/dist/ai/AIChatInterface.svelte +32 -34
- package/dist/ai/AIChatInterface.svelte.d.ts +0 -1
- package/dist/ai/AIChatInterfaceTestWrapper.svelte +26 -0
- package/dist/ai/AIChatInterfaceTestWrapper.svelte.d.ts +17 -0
- package/dist/ai/ChatInput.svelte +7 -15
- package/dist/ai/ChatInput.svelte.d.ts +0 -2
- package/dist/ai/CodeRenderer.svelte +25 -12
- package/dist/ai/ComposeDropdown.svelte +17 -14
- package/dist/ai/MermaidRenderer.svelte +21 -17
- package/dist/ai/MermaidRenderer.svelte.d.ts +0 -1
- package/dist/ai/MessageBox.svelte +10 -7
- package/dist/ai/ThinkingDisplay.svelte +67 -43
- package/dist/ai/ai-chat-interface.d.ts +22 -21
- package/dist/ai/ai-chat-interface.js +8 -7
- package/dist/ai/content-detector.js +2 -2
- package/dist/button/ButtonTestWrapper.svelte +10 -0
- package/dist/button/ButtonTestWrapper.svelte.d.ts +7 -0
- package/dist/charts/Chart.svelte +6 -1
- package/dist/config/ai.js +1 -0
- package/dist/drawer/DrawerTestWrapper.svelte +19 -0
- package/dist/drawer/DrawerTestWrapper.svelte.d.ts +9 -0
- package/dist/drawer/drawer.d.ts +19 -18
- package/dist/drawer/drawer.js +7 -6
- package/dist/elements/accordion/Accordion.svelte +1 -1
- package/dist/elements/accordion/Accordion.svelte.d.ts +1 -1
- package/dist/elements/accordion/AccordionTestWrapper.svelte +21 -0
- package/dist/elements/accordion/AccordionTestWrapper.svelte.d.ts +10 -0
- package/dist/elements/badge/Badge.svelte +5 -4
- package/dist/elements/badge/BadgeTestWrapper.svelte +14 -0
- package/dist/elements/badge/BadgeTestWrapper.svelte.d.ts +9 -0
- package/dist/elements/badge/badge.d.ts +40 -39
- package/dist/elements/badge/badge.js +14 -13
- package/dist/elements/dropdown/Dropdown.svelte +0 -1
- package/dist/elements/pagination/Pagination.svelte +20 -26
- package/dist/elements/progress/Progress.svelte +3 -3
- package/dist/elements/timeline/Timeline.svelte +1 -1
- package/dist/file-browser/FileBrowser.svelte +7 -10
- package/dist/filters/CompactFilters.svelte +3 -3
- package/dist/forms/Checkbox.svelte +0 -1
- package/dist/forms/CheckboxTestWrapper.svelte +8 -0
- package/dist/forms/CheckboxTestWrapper.svelte.d.ts +4 -0
- package/dist/forms/DateRange.svelte +186 -198
- package/dist/forms/Form.svelte +1 -0
- package/dist/forms/Input.svelte +14 -5
- package/dist/forms/InputTestWrapper.svelte +8 -0
- package/dist/forms/InputTestWrapper.svelte.d.ts +4 -0
- package/dist/forms/NumberInput.svelte +2 -2
- package/dist/forms/RadioInputs.svelte +1 -1
- package/dist/forms/RadioPill.svelte +1 -1
- package/dist/forms/Slider.svelte +2 -2
- package/dist/forms/Tags.svelte +3 -3
- package/dist/forms/ToggleTestWrapper.svelte +8 -0
- package/dist/forms/ToggleTestWrapper.svelte.d.ts +7 -0
- package/dist/forms/slider.js +1 -1
- package/dist/header/PageHeader.svelte +2 -1
- package/dist/header/breadcrumbs.d.ts +47 -33
- package/dist/header/breadcrumbs.js +12 -11
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -0
- package/dist/layout/activity-list/ActivityList.svelte +9 -11
- package/dist/layout/card/CardTestWrapper.svelte +15 -0
- package/dist/layout/card/CardTestWrapper.svelte.d.ts +7 -0
- package/dist/layout/card/RankedCard.svelte +2 -3
- package/dist/layout/navbar/navbar.d.ts +19 -18
- package/dist/layout/navbar/navbar.js +7 -6
- package/dist/layout/sidebar/NavGroup.svelte +1 -0
- package/dist/layout/table/Cells.svelte +5 -5
- package/dist/layout/table/Table.svelte +8 -8
- package/dist/layout/table/table.d.ts +28 -24
- package/dist/layout/table/table.js +14 -13
- package/dist/modal/Modal.svelte +1 -1
- package/dist/modal/ModalTestWrapper.svelte +20 -0
- package/dist/modal/ModalTestWrapper.svelte.d.ts +8 -0
- package/dist/modal/modal.d.ts +1 -20
- package/dist/pipeline/Pipeline.svelte +29 -17
- package/dist/user-management/README.md +417 -0
- package/dist/user-management/UserManagement.svelte +184 -0
- package/dist/user-management/UserManagement.svelte.d.ts +4 -0
- package/dist/user-management/UserManagementTestWrapper.svelte +47 -0
- package/dist/user-management/UserManagementTestWrapper.svelte.d.ts +7 -0
- package/dist/user-management/UserModal.svelte +303 -0
- package/dist/user-management/UserModal.svelte.d.ts +4 -0
- package/dist/user-management/UserModalTestWrapper.svelte +22 -0
- package/dist/user-management/UserModalTestWrapper.svelte.d.ts +7 -0
- package/dist/user-management/UserTable.svelte +219 -0
- package/dist/user-management/UserTable.svelte.d.ts +4 -0
- package/dist/user-management/UserTableTestWrapper.svelte +41 -0
- package/dist/user-management/UserTableTestWrapper.svelte.d.ts +7 -0
- package/dist/user-management/UserViewModal.svelte +282 -0
- package/dist/user-management/UserViewModal.svelte.d.ts +4 -0
- package/dist/user-management/UserViewModalTestWrapper.svelte +22 -0
- package/dist/user-management/UserViewModalTestWrapper.svelte.d.ts +7 -0
- package/dist/user-management/index.d.ts +10 -0
- package/dist/user-management/index.js +11 -0
- package/dist/user-management/user-management.d.ts +99 -0
- package/dist/user-management/user-management.js +42 -0
- package/package.json +3 -1
- package/dist/types/markdown.d.ts +0 -14
- package/dist/types/variants.d.ts +0 -1
- package/dist/types/variants.js +0 -1
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Modal, Button, cn } from '../index.js';
|
|
3
|
+
import type { User, UserModalProps, FormErrors } from './user-management.js';
|
|
4
|
+
import { getUserDisplayName } from './user-management.js';
|
|
5
|
+
|
|
6
|
+
// Icons as simple SVGs
|
|
7
|
+
let {
|
|
8
|
+
open = $bindable(),
|
|
9
|
+
user = $bindable(),
|
|
10
|
+
roles = [],
|
|
11
|
+
onSave,
|
|
12
|
+
onClose,
|
|
13
|
+
class: className
|
|
14
|
+
}: UserModalProps = $props();
|
|
15
|
+
|
|
16
|
+
// Mode determination
|
|
17
|
+
const mode = $derived(user ? 'edit' : 'create');
|
|
18
|
+
|
|
19
|
+
// Local state
|
|
20
|
+
let formErrors = $state<FormErrors>({});
|
|
21
|
+
let saving = $state(false);
|
|
22
|
+
let formElement = $state<HTMLFormElement | null>(null);
|
|
23
|
+
|
|
24
|
+
// Form data
|
|
25
|
+
let formData = $state<Partial<User>>({
|
|
26
|
+
first_name: '',
|
|
27
|
+
last_name: '',
|
|
28
|
+
username: '',
|
|
29
|
+
email_addresses: [{ email_address: '' }],
|
|
30
|
+
phone_numbers: [],
|
|
31
|
+
role: '',
|
|
32
|
+
permissions: []
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Initialize form data when user changes
|
|
36
|
+
$effect(() => {
|
|
37
|
+
if (open && user) {
|
|
38
|
+
formData = {
|
|
39
|
+
first_name: user.first_name || '',
|
|
40
|
+
last_name: user.last_name || '',
|
|
41
|
+
username: user.username || '',
|
|
42
|
+
email_addresses: user.email_addresses || [{ email_address: '' }],
|
|
43
|
+
phone_numbers: user.phone_numbers || [],
|
|
44
|
+
role: user.role || '',
|
|
45
|
+
permissions: user.permissions || []
|
|
46
|
+
};
|
|
47
|
+
} else if (open && !user) {
|
|
48
|
+
// Reset for create mode
|
|
49
|
+
formData = {
|
|
50
|
+
first_name: '',
|
|
51
|
+
last_name: '',
|
|
52
|
+
username: '',
|
|
53
|
+
email_addresses: [{ email_address: '' }],
|
|
54
|
+
phone_numbers: [],
|
|
55
|
+
role: '',
|
|
56
|
+
permissions: []
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
formErrors = {};
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
function handleClose() {
|
|
63
|
+
open = false;
|
|
64
|
+
formErrors = {};
|
|
65
|
+
saving = false;
|
|
66
|
+
if (onClose) onClose();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function handleSubmit(event: SubmitEvent) {
|
|
70
|
+
event.preventDefault();
|
|
71
|
+
formErrors = {};
|
|
72
|
+
|
|
73
|
+
// Basic validation
|
|
74
|
+
if (!formData.first_name?.trim()) {
|
|
75
|
+
formErrors.first_name = 'First name is required';
|
|
76
|
+
}
|
|
77
|
+
if (!formData.last_name?.trim()) {
|
|
78
|
+
formErrors.last_name = 'Last name is required';
|
|
79
|
+
}
|
|
80
|
+
if (!formData.email_addresses?.[0]?.email_address?.trim()) {
|
|
81
|
+
formErrors.email = 'Email address is required';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (Object.keys(formErrors).length > 0) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
saving = true;
|
|
90
|
+
const userData: User = {
|
|
91
|
+
id: user?.id || '',
|
|
92
|
+
first_name: formData.first_name,
|
|
93
|
+
last_name: formData.last_name,
|
|
94
|
+
username: formData.username,
|
|
95
|
+
email_addresses: formData.email_addresses,
|
|
96
|
+
phone_numbers: formData.phone_numbers,
|
|
97
|
+
role: formData.role,
|
|
98
|
+
permissions: formData.permissions
|
|
99
|
+
};
|
|
100
|
+
await onSave(userData, mode);
|
|
101
|
+
handleClose();
|
|
102
|
+
} catch (error) {
|
|
103
|
+
console.error('Error saving user:', error);
|
|
104
|
+
formErrors.submit = error instanceof Error ? error.message : 'Failed to save user';
|
|
105
|
+
} finally {
|
|
106
|
+
saving = false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function handleRoleChange(roleValue: string) {
|
|
111
|
+
formData.role = roleValue;
|
|
112
|
+
const role = roles?.find((r) => r.value === roleValue);
|
|
113
|
+
if (role) {
|
|
114
|
+
formData.permissions = [...role.permissions];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function getModalTitle() {
|
|
119
|
+
if (mode === 'create') return 'Create New User';
|
|
120
|
+
return `Edit ${getUserDisplayName(user)}`;
|
|
121
|
+
}
|
|
122
|
+
</script>
|
|
123
|
+
|
|
124
|
+
<Modal
|
|
125
|
+
{open}
|
|
126
|
+
onclose={handleClose}
|
|
127
|
+
title={getModalTitle()}
|
|
128
|
+
contentclass="max-w-4xl"
|
|
129
|
+
class={cn(className)}
|
|
130
|
+
>
|
|
131
|
+
<form bind:this={formElement} onsubmit={handleSubmit} class="flex gap-6">
|
|
132
|
+
<!-- Left Column: Profile Information -->
|
|
133
|
+
<div class="min-w-0 flex-1 space-y-4">
|
|
134
|
+
<div class="border-b border-gray-200 pb-3">
|
|
135
|
+
<h3 class="text-lg font-semibold text-gray-900">Profile Information</h3>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<!-- First Name -->
|
|
139
|
+
<div>
|
|
140
|
+
<label for="first-name" class="mb-1 block text-sm font-medium text-gray-700">
|
|
141
|
+
First Name <span class="text-red-500">*</span>
|
|
142
|
+
</label>
|
|
143
|
+
<input
|
|
144
|
+
id="first-name"
|
|
145
|
+
type="text"
|
|
146
|
+
bind:value={formData.first_name}
|
|
147
|
+
class="w-full rounded-lg border px-3 py-2 focus:border-blue-500 focus:ring-2 focus:ring-blue-500 {formErrors.first_name
|
|
148
|
+
? 'border-red-300'
|
|
149
|
+
: 'border-gray-300'}"
|
|
150
|
+
placeholder="First name"
|
|
151
|
+
required
|
|
152
|
+
/>
|
|
153
|
+
{#if formErrors.first_name}
|
|
154
|
+
<p class="mt-1 text-xs text-red-500">{formErrors.first_name}</p>
|
|
155
|
+
{/if}
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
<!-- Last Name -->
|
|
159
|
+
<div>
|
|
160
|
+
<label for="last-name" class="mb-1 block text-sm font-medium text-gray-700">
|
|
161
|
+
Last Name <span class="text-red-500">*</span>
|
|
162
|
+
</label>
|
|
163
|
+
<input
|
|
164
|
+
id="last-name"
|
|
165
|
+
type="text"
|
|
166
|
+
bind:value={formData.last_name}
|
|
167
|
+
class="w-full rounded-lg border px-3 py-2 focus:border-blue-500 focus:ring-2 focus:ring-blue-500 {formErrors.last_name
|
|
168
|
+
? 'border-red-300'
|
|
169
|
+
: 'border-gray-300'}"
|
|
170
|
+
placeholder="Last name"
|
|
171
|
+
required
|
|
172
|
+
/>
|
|
173
|
+
{#if formErrors.last_name}
|
|
174
|
+
<p class="mt-1 text-xs text-red-500">{formErrors.last_name}</p>
|
|
175
|
+
{/if}
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
<!-- Email Address -->
|
|
179
|
+
<div>
|
|
180
|
+
<label for="email" class="mb-1 block text-sm font-medium text-gray-700">
|
|
181
|
+
Email Address <span class="text-red-500">*</span>
|
|
182
|
+
</label>
|
|
183
|
+
<input
|
|
184
|
+
id="email"
|
|
185
|
+
type="email"
|
|
186
|
+
bind:value={formData.email_addresses![0].email_address}
|
|
187
|
+
class="w-full rounded-lg border px-3 py-2 focus:border-blue-500 focus:ring-2 focus:ring-blue-500 {formErrors.email
|
|
188
|
+
? 'border-red-300'
|
|
189
|
+
: 'border-gray-300'}"
|
|
190
|
+
placeholder="user@example.com"
|
|
191
|
+
required
|
|
192
|
+
/>
|
|
193
|
+
{#if formErrors.email}
|
|
194
|
+
<p class="mt-1 text-xs text-red-500">{formErrors.email}</p>
|
|
195
|
+
{/if}
|
|
196
|
+
</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
|
+
</div>
|
|
212
|
+
|
|
213
|
+
<!-- Right Column: Permissions & Role -->
|
|
214
|
+
<div class="min-w-0 flex-1 space-y-4">
|
|
215
|
+
<div class="border-b border-gray-200 pb-3">
|
|
216
|
+
<h3 class="text-lg font-semibold text-gray-900">Role & Permissions</h3>
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
<!-- Role Selection -->
|
|
220
|
+
{#if roles && roles.length > 0}
|
|
221
|
+
<div>
|
|
222
|
+
<span class="mb-3 block text-sm font-medium text-gray-700">
|
|
223
|
+
Select User Role {#if mode === 'create'}<span class="text-red-500">*</span>{/if}
|
|
224
|
+
</span>
|
|
225
|
+
<div class="grid grid-cols-1 gap-2">
|
|
226
|
+
{#each roles as role (role.value)}
|
|
227
|
+
{@const isSelected = formData.role === role.value}
|
|
228
|
+
<button
|
|
229
|
+
type="button"
|
|
230
|
+
onclick={() => handleRoleChange(role.value)}
|
|
231
|
+
class="cursor-pointer rounded-lg border-2 p-3 text-left transition-all {isSelected
|
|
232
|
+
? 'border-blue-500 bg-blue-50'
|
|
233
|
+
: 'border-gray-200 bg-white hover:border-gray-300'}"
|
|
234
|
+
>
|
|
235
|
+
<div class="flex items-center justify-between gap-2">
|
|
236
|
+
<div class="min-w-0 flex-1">
|
|
237
|
+
<h4 class="text-sm font-semibold text-gray-900">{role.label}</h4>
|
|
238
|
+
{#if role.description}
|
|
239
|
+
<p class="mt-1 line-clamp-2 text-xs text-gray-600">{role.description}</p>
|
|
240
|
+
{/if}
|
|
241
|
+
</div>
|
|
242
|
+
<div
|
|
243
|
+
class="flex h-5 w-5 shrink-0 items-center justify-center rounded-full border-2 {isSelected
|
|
244
|
+
? 'border-blue-500 bg-blue-500'
|
|
245
|
+
: 'border-gray-300 bg-white'}"
|
|
246
|
+
>
|
|
247
|
+
{#if isSelected}
|
|
248
|
+
<div class="h-2 w-2 rounded-full bg-white"></div>
|
|
249
|
+
{/if}
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
252
|
+
</button>
|
|
253
|
+
{/each}
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
256
|
+
{/if}
|
|
257
|
+
|
|
258
|
+
<!-- Permission Preview -->
|
|
259
|
+
{#if formData.permissions && formData.permissions.length > 0}
|
|
260
|
+
<div>
|
|
261
|
+
<h4 class="mb-2 text-sm font-medium text-gray-700">
|
|
262
|
+
Permissions ({formData.permissions.length})
|
|
263
|
+
</h4>
|
|
264
|
+
<div class="max-h-60 overflow-y-auto rounded-lg bg-gray-50 p-3">
|
|
265
|
+
<div class="space-y-2">
|
|
266
|
+
{#each formData.permissions as permission (permission)}
|
|
267
|
+
<div class="flex items-start gap-2 text-xs">
|
|
268
|
+
<div class="mt-1 h-1 w-1 shrink-0 rounded-full bg-blue-500"></div>
|
|
269
|
+
<div class="font-mono text-gray-700">{permission}</div>
|
|
270
|
+
</div>
|
|
271
|
+
{/each}
|
|
272
|
+
</div>
|
|
273
|
+
</div>
|
|
274
|
+
</div>
|
|
275
|
+
{/if}
|
|
276
|
+
</div>
|
|
277
|
+
</form>
|
|
278
|
+
|
|
279
|
+
<!-- Form Actions -->
|
|
280
|
+
{#snippet footer()}
|
|
281
|
+
<div class="flex items-center justify-between gap-4">
|
|
282
|
+
{#if formErrors.submit}
|
|
283
|
+
<p class="text-sm text-red-500">{formErrors.submit}</p>
|
|
284
|
+
{:else}
|
|
285
|
+
<div></div>
|
|
286
|
+
{/if}
|
|
287
|
+
<div class="flex gap-3">
|
|
288
|
+
<Button variant="outline" onclick={handleClose} disabled={saving} type="button">
|
|
289
|
+
Cancel
|
|
290
|
+
</Button>
|
|
291
|
+
<Button
|
|
292
|
+
type="button"
|
|
293
|
+
color="primary"
|
|
294
|
+
onclick={() => formElement?.requestSubmit()}
|
|
295
|
+
disabled={saving}
|
|
296
|
+
isLoading={saving}
|
|
297
|
+
>
|
|
298
|
+
{mode === 'create' ? 'Create User' : 'Save Changes'}
|
|
299
|
+
</Button>
|
|
300
|
+
</div>
|
|
301
|
+
</div>
|
|
302
|
+
{/snippet}
|
|
303
|
+
</Modal>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import UserModal from './UserModal.svelte';
|
|
3
|
+
import type { UserModalProps } from './user-management.js';
|
|
4
|
+
|
|
5
|
+
interface Props extends UserModalProps {
|
|
6
|
+
testId?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
let {
|
|
10
|
+
open = $bindable(),
|
|
11
|
+
user = $bindable(),
|
|
12
|
+
roles = [],
|
|
13
|
+
onSave = async () => {},
|
|
14
|
+
onClose = () => {},
|
|
15
|
+
testId,
|
|
16
|
+
...rest
|
|
17
|
+
}: Props = $props();
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<div data-testid={testId}>
|
|
21
|
+
<UserModal bind:open bind:user {roles} {onSave} {onClose} {...rest} />
|
|
22
|
+
</div>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { UserModalProps } from './user-management.js';
|
|
2
|
+
interface Props extends UserModalProps {
|
|
3
|
+
testId?: string;
|
|
4
|
+
}
|
|
5
|
+
declare const UserModalTestWrapper: import("svelte").Component<Props, {}, "open" | "user">;
|
|
6
|
+
type UserModalTestWrapper = ReturnType<typeof UserModalTestWrapper>;
|
|
7
|
+
export default UserModalTestWrapper;
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Table, type TableColumn } from '../index.js';
|
|
3
|
+
import type { User, UserTableProps } from './user-management.js';
|
|
4
|
+
import { getUserDisplayName, getUserInitials } from './user-management.js';
|
|
5
|
+
import { SvelteDate } from 'svelte/reactivity';
|
|
6
|
+
|
|
7
|
+
let {
|
|
8
|
+
users = [],
|
|
9
|
+
loading = false,
|
|
10
|
+
currentPage = 1,
|
|
11
|
+
pageSize = 10,
|
|
12
|
+
totalUsers = 0,
|
|
13
|
+
onPageChange,
|
|
14
|
+
onPageSizeChange,
|
|
15
|
+
onSort,
|
|
16
|
+
onView,
|
|
17
|
+
onEdit,
|
|
18
|
+
onDelete,
|
|
19
|
+
class: className = ''
|
|
20
|
+
}: UserTableProps = $props();
|
|
21
|
+
|
|
22
|
+
// Format date helper
|
|
23
|
+
function formatDate(timestamp?: number) {
|
|
24
|
+
if (!timestamp) return 'Never';
|
|
25
|
+
return new SvelteDate(timestamp).toLocaleDateString('en-US', {
|
|
26
|
+
year: 'numeric',
|
|
27
|
+
month: 'short',
|
|
28
|
+
day: 'numeric',
|
|
29
|
+
hour: '2-digit',
|
|
30
|
+
minute: '2-digit'
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Handle table sort
|
|
35
|
+
function handleTableSort(state: { column: string | null; direction: 'asc' | 'desc' | null }) {
|
|
36
|
+
if (onSort && state.column && state.direction) {
|
|
37
|
+
onSort(state);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Table column definitions with cell snippets
|
|
42
|
+
const columns: TableColumn<User>[] = [
|
|
43
|
+
{
|
|
44
|
+
key: 'user',
|
|
45
|
+
header: 'User',
|
|
46
|
+
sortable: true,
|
|
47
|
+
sortKey: 'first_name',
|
|
48
|
+
cell: UserCell
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
key: 'email_addresses',
|
|
52
|
+
header: 'Contact',
|
|
53
|
+
sortable: false,
|
|
54
|
+
cell: ContactCell
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
key: 'last_sign_in_at',
|
|
58
|
+
header: 'Last Sign In',
|
|
59
|
+
sortable: true,
|
|
60
|
+
class: 'hidden lg:table-cell',
|
|
61
|
+
cell: LastSignInCell
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
key: 'created_at',
|
|
65
|
+
header: 'Created',
|
|
66
|
+
sortable: true,
|
|
67
|
+
class: 'hidden xl:table-cell',
|
|
68
|
+
cell: CreatedCell
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
key: 'actions',
|
|
72
|
+
header: 'Actions',
|
|
73
|
+
sortable: false,
|
|
74
|
+
align: 'right',
|
|
75
|
+
cell: ActionsCell
|
|
76
|
+
}
|
|
77
|
+
];
|
|
78
|
+
</script>
|
|
79
|
+
|
|
80
|
+
{#snippet EyeIcon()}
|
|
81
|
+
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
82
|
+
<path
|
|
83
|
+
stroke-linecap="round"
|
|
84
|
+
stroke-linejoin="round"
|
|
85
|
+
stroke-width="2"
|
|
86
|
+
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
|
87
|
+
></path>
|
|
88
|
+
<path
|
|
89
|
+
stroke-linecap="round"
|
|
90
|
+
stroke-linejoin="round"
|
|
91
|
+
stroke-width="2"
|
|
92
|
+
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
|
|
93
|
+
></path>
|
|
94
|
+
</svg>
|
|
95
|
+
{/snippet}
|
|
96
|
+
|
|
97
|
+
{#snippet EditIcon()}
|
|
98
|
+
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
99
|
+
<path
|
|
100
|
+
stroke-linecap="round"
|
|
101
|
+
stroke-linejoin="round"
|
|
102
|
+
stroke-width="2"
|
|
103
|
+
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
|
|
104
|
+
></path>
|
|
105
|
+
</svg>
|
|
106
|
+
{/snippet}
|
|
107
|
+
|
|
108
|
+
{#snippet TrashIcon()}
|
|
109
|
+
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
110
|
+
<path
|
|
111
|
+
stroke-linecap="round"
|
|
112
|
+
stroke-linejoin="round"
|
|
113
|
+
stroke-width="2"
|
|
114
|
+
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
|
115
|
+
></path>
|
|
116
|
+
</svg>
|
|
117
|
+
{/snippet}
|
|
118
|
+
|
|
119
|
+
{#snippet UserCell(user: User)}
|
|
120
|
+
<div class="flex items-center">
|
|
121
|
+
<div class="h-8 w-8 shrink-0">
|
|
122
|
+
{#if user.image_url}
|
|
123
|
+
<img class="h-8 w-8 rounded-full" src={user.image_url} alt="" />
|
|
124
|
+
{:else}
|
|
125
|
+
<div class="flex h-8 w-8 items-center justify-center rounded-full bg-gray-300">
|
|
126
|
+
<span class="text-xs font-medium text-gray-700">
|
|
127
|
+
{getUserInitials(user)}
|
|
128
|
+
</span>
|
|
129
|
+
</div>
|
|
130
|
+
{/if}
|
|
131
|
+
</div>
|
|
132
|
+
<div class="ml-3">
|
|
133
|
+
<div class="text-sm font-medium text-gray-900">
|
|
134
|
+
{getUserDisplayName(user)}
|
|
135
|
+
</div>
|
|
136
|
+
<div class="text-xs text-gray-500">
|
|
137
|
+
{user.username || user.id.slice(0, 8) + '...'}
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
{/snippet}
|
|
142
|
+
|
|
143
|
+
{#snippet ContactCell(user: User)}
|
|
144
|
+
<div class="text-sm text-gray-900">
|
|
145
|
+
{#if user.email_addresses?.[0]}
|
|
146
|
+
<div class="max-w-[180px] truncate text-xs" title={user.email_addresses[0].email_address}>
|
|
147
|
+
{user.email_addresses[0].email_address}
|
|
148
|
+
</div>
|
|
149
|
+
{/if}
|
|
150
|
+
{#if user.phone_numbers?.[0]}
|
|
151
|
+
<div class="mt-1 text-xs text-gray-500">
|
|
152
|
+
{user.phone_numbers[0].phone_number}
|
|
153
|
+
</div>
|
|
154
|
+
{/if}
|
|
155
|
+
</div>
|
|
156
|
+
{/snippet}
|
|
157
|
+
|
|
158
|
+
{#snippet LastSignInCell(user: User)}
|
|
159
|
+
<span class="text-xs whitespace-nowrap text-gray-500">
|
|
160
|
+
{formatDate(user.last_sign_in_at)}
|
|
161
|
+
</span>
|
|
162
|
+
{/snippet}
|
|
163
|
+
|
|
164
|
+
{#snippet CreatedCell(user: User)}
|
|
165
|
+
<span class="text-xs whitespace-nowrap text-gray-500">
|
|
166
|
+
{formatDate(user.created_at)}
|
|
167
|
+
</span>
|
|
168
|
+
{/snippet}
|
|
169
|
+
|
|
170
|
+
{#snippet ActionsCell(user: User)}
|
|
171
|
+
<div class="flex items-center justify-end space-x-1">
|
|
172
|
+
<button
|
|
173
|
+
onclick={() => onView(user)}
|
|
174
|
+
class="inline-flex rounded p-1 text-blue-600 transition-colors hover:bg-blue-50 hover:text-blue-900"
|
|
175
|
+
title="View User"
|
|
176
|
+
aria-label="View User"
|
|
177
|
+
>
|
|
178
|
+
{@render EyeIcon()}
|
|
179
|
+
</button>
|
|
180
|
+
<button
|
|
181
|
+
onclick={() => onEdit(user)}
|
|
182
|
+
class="inline-flex rounded p-1 text-green-600 transition-colors hover:bg-green-50 hover:text-green-900"
|
|
183
|
+
title="Edit User"
|
|
184
|
+
aria-label="Edit User"
|
|
185
|
+
>
|
|
186
|
+
{@render EditIcon()}
|
|
187
|
+
</button>
|
|
188
|
+
<button
|
|
189
|
+
onclick={() => onDelete(user.id)}
|
|
190
|
+
class="inline-flex rounded p-1 text-red-600 transition-colors hover:bg-red-50 hover:text-red-900"
|
|
191
|
+
title="Delete User"
|
|
192
|
+
aria-label="Delete User"
|
|
193
|
+
>
|
|
194
|
+
{@render TrashIcon()}
|
|
195
|
+
</button>
|
|
196
|
+
</div>
|
|
197
|
+
{/snippet}
|
|
198
|
+
|
|
199
|
+
<Table
|
|
200
|
+
data={users}
|
|
201
|
+
{columns}
|
|
202
|
+
{currentPage}
|
|
203
|
+
{pageSize}
|
|
204
|
+
totalItems={totalUsers}
|
|
205
|
+
{loading}
|
|
206
|
+
pagination={true}
|
|
207
|
+
showPagination={true}
|
|
208
|
+
showPageSize={true}
|
|
209
|
+
pageSizeOptions={[10, 25, 50, 100]}
|
|
210
|
+
paginationTemplate="full"
|
|
211
|
+
onpagechange={onPageChange}
|
|
212
|
+
onpagesizechange={onPageSizeChange}
|
|
213
|
+
onsort={handleTableSort}
|
|
214
|
+
paginationPosition="bottom"
|
|
215
|
+
paginationclass="whitespace-nowrap flex-wrap gap-3 justify-between items-center"
|
|
216
|
+
wrapperclass="overflow-x-auto"
|
|
217
|
+
tableclass="min-w-full w-full"
|
|
218
|
+
class={className}
|
|
219
|
+
/>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import UserTable from './UserTable.svelte';
|
|
3
|
+
import type { UserTableProps } from './user-management.js';
|
|
4
|
+
|
|
5
|
+
interface Props extends UserTableProps {
|
|
6
|
+
testId?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
let {
|
|
10
|
+
users = [],
|
|
11
|
+
loading = false,
|
|
12
|
+
currentPage = 1,
|
|
13
|
+
pageSize = 10,
|
|
14
|
+
totalUsers = 0,
|
|
15
|
+
onPageChange = () => {},
|
|
16
|
+
onPageSizeChange = () => {},
|
|
17
|
+
onSort,
|
|
18
|
+
onView = () => {},
|
|
19
|
+
onEdit = () => {},
|
|
20
|
+
onDelete = () => {},
|
|
21
|
+
testId,
|
|
22
|
+
...rest
|
|
23
|
+
}: Props = $props();
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<div data-testid={testId}>
|
|
27
|
+
<UserTable
|
|
28
|
+
{users}
|
|
29
|
+
{loading}
|
|
30
|
+
{currentPage}
|
|
31
|
+
{pageSize}
|
|
32
|
+
{totalUsers}
|
|
33
|
+
{onPageChange}
|
|
34
|
+
{onPageSizeChange}
|
|
35
|
+
{onSort}
|
|
36
|
+
{onView}
|
|
37
|
+
{onEdit}
|
|
38
|
+
{onDelete}
|
|
39
|
+
{...rest}
|
|
40
|
+
/>
|
|
41
|
+
</div>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { UserTableProps } from './user-management.js';
|
|
2
|
+
interface Props extends UserTableProps {
|
|
3
|
+
testId?: string;
|
|
4
|
+
}
|
|
5
|
+
declare const UserTableTestWrapper: import("svelte").Component<Props, {}, "">;
|
|
6
|
+
type UserTableTestWrapper = ReturnType<typeof UserTableTestWrapper>;
|
|
7
|
+
export default UserTableTestWrapper;
|