includio-cms 0.1.0 → 0.1.2
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/CHANGELOG.md +52 -0
- package/ROADMAP.md +17 -15
- package/dist/admin/auth-client.d.ts +1165 -5
- package/dist/admin/auth-client.js +4 -1
- package/dist/admin/client/account/sessions-section.svelte +1 -21
- package/dist/admin/client/index.d.ts +1 -0
- package/dist/admin/client/index.js +1 -0
- package/dist/admin/client/users/accept-invite-page.svelte +118 -0
- package/dist/admin/client/users/accept-invite-page.svelte.d.ts +4 -0
- package/dist/admin/client/users/create-user-dialog.svelte +157 -0
- package/dist/admin/client/users/create-user-dialog.svelte.d.ts +8 -0
- package/dist/admin/client/users/delete-user-dialog.svelte +53 -0
- package/dist/admin/client/users/delete-user-dialog.svelte.d.ts +10 -0
- package/dist/admin/client/users/edit-user-dialog.svelte +127 -0
- package/dist/admin/client/users/edit-user-dialog.svelte.d.ts +16 -0
- package/dist/admin/client/users/invite-user-dialog.svelte +107 -0
- package/dist/admin/client/users/invite-user-dialog.svelte.d.ts +8 -0
- package/dist/admin/client/users/lang.d.ts +57 -0
- package/dist/admin/client/users/lang.js +114 -0
- package/dist/admin/client/users/pending-invitations.svelte +145 -0
- package/dist/admin/client/users/pending-invitations.svelte.d.ts +6 -0
- package/dist/admin/client/users/user-sessions-sheet.svelte +141 -0
- package/dist/admin/client/users/user-sessions-sheet.svelte.d.ts +8 -0
- package/dist/admin/client/users/users-page.svelte +262 -0
- package/dist/admin/client/users/users-page.svelte.d.ts +6 -0
- package/dist/admin/components/fields/array-field.svelte +68 -22
- package/dist/admin/components/fields/field-renderer.svelte +25 -2
- package/dist/admin/components/fields/number-field.svelte +1 -1
- package/dist/admin/components/fields/text-field-wrapper.svelte +56 -1
- package/dist/admin/components/fields/text-field.svelte +2 -2
- package/dist/admin/components/layout/lang.d.ts +1 -0
- package/dist/admin/components/layout/lang.js +4 -2
- package/dist/admin/components/layout/nav-main.svelte +15 -1
- package/dist/admin/remote/invite.d.ts +44 -0
- package/dist/admin/remote/invite.js +44 -0
- package/dist/admin/remote/middleware/auth.d.ts +5 -0
- package/dist/admin/remote/middleware/auth.js +7 -0
- package/dist/admin/utils/parseUserAgent.d.ts +5 -0
- package/dist/admin/utils/parseUserAgent.js +26 -0
- package/dist/components/ui/input-group/input-group-input.svelte.d.ts +1 -1
- package/dist/components/ui/sidebar/sidebar-input.svelte.d.ts +1 -1
- package/dist/core/cms.d.ts +1 -1
- package/dist/core/cms.js +1 -1
- package/dist/core/fields/fieldSchemaToTs.js +18 -4
- package/dist/core/server/forms/submissions/operations/create.js +1 -1
- package/dist/email-nodemailer/index.d.ts +1 -0
- package/dist/server/auth.d.ts +8 -8
- package/dist/server/db/schema/auth-schema.d.ts +143 -0
- package/dist/server/db/schema/auth-schema.js +12 -0
- package/dist/sveltekit/server/handle.js +13 -0
- package/dist/types/cms.d.ts +2 -2
- package/dist/types/roles.d.ts +1 -0
- package/dist/types/roles.js +1 -0
- package/dist/updates/0.1.1/index.d.ts +2 -0
- package/dist/updates/0.1.1/index.js +17 -0
- package/dist/updates/0.1.2/index.d.ts +2 -0
- package/dist/updates/0.1.2/index.js +36 -0
- package/dist/updates/index.js +3 -1
- package/package.json +2 -2
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Search from '@tabler/icons-svelte/icons/search';
|
|
3
|
+
import Plus from '@tabler/icons-svelte/icons/plus';
|
|
4
|
+
import Pencil from '@tabler/icons-svelte/icons/pencil';
|
|
5
|
+
import Trash from '@tabler/icons-svelte/icons/trash';
|
|
6
|
+
import Loader2 from '@tabler/icons-svelte/icons/loader-2';
|
|
7
|
+
import DeviceDesktop from '@tabler/icons-svelte/icons/device-desktop';
|
|
8
|
+
import Mail from '@tabler/icons-svelte/icons/mail';
|
|
9
|
+
import Input from '../../../components/ui/input/input.svelte';
|
|
10
|
+
import Button from '../../../components/ui/button/button.svelte';
|
|
11
|
+
import { Badge } from '../../../components/ui/badge/index.js';
|
|
12
|
+
import * as Table from '../../../components/ui/table/index.js';
|
|
13
|
+
import { authClient } from '../../auth-client.js';
|
|
14
|
+
import { usersLang } from './lang.js';
|
|
15
|
+
import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
|
|
16
|
+
import { toLocaleCode } from '../../utils/formatDate.js';
|
|
17
|
+
import CreateUserDialog from './create-user-dialog.svelte';
|
|
18
|
+
import EditUserDialog from './edit-user-dialog.svelte';
|
|
19
|
+
import DeleteUserDialog from './delete-user-dialog.svelte';
|
|
20
|
+
import UserSessionsSheet from './user-sessions-sheet.svelte';
|
|
21
|
+
import InviteUserDialog from './invite-user-dialog.svelte';
|
|
22
|
+
import PendingInvitations from './pending-invitations.svelte';
|
|
23
|
+
|
|
24
|
+
type Props = {
|
|
25
|
+
emailConfigured?: boolean;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
let { emailConfigured = false }: Props = $props();
|
|
29
|
+
|
|
30
|
+
type User = {
|
|
31
|
+
id: string;
|
|
32
|
+
name: string;
|
|
33
|
+
email: string;
|
|
34
|
+
role: string;
|
|
35
|
+
createdAt: Date;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const interfaceLanguage = useInterfaceLanguage();
|
|
39
|
+
const lang = $derived(usersLang[interfaceLanguage.current]);
|
|
40
|
+
const session = authClient.useSession();
|
|
41
|
+
const currentUserId = $derived(session.value?.data?.user?.id ?? '');
|
|
42
|
+
|
|
43
|
+
let users = $state<User[]>([]);
|
|
44
|
+
let loading = $state(true);
|
|
45
|
+
let searchQuery = $state('');
|
|
46
|
+
|
|
47
|
+
// Dialogs
|
|
48
|
+
let createOpen = $state(false);
|
|
49
|
+
let editOpen = $state(false);
|
|
50
|
+
let deleteOpen = $state(false);
|
|
51
|
+
let inviteOpen = $state(false);
|
|
52
|
+
let editingUser = $state<User | null>(null);
|
|
53
|
+
let deletingUserId = $state<string | null>(null);
|
|
54
|
+
|
|
55
|
+
// Sessions sheet
|
|
56
|
+
let sessionsOpen = $state(false);
|
|
57
|
+
let sessionsUserId = $state<string | null>(null);
|
|
58
|
+
let sessionsUserName = $state('');
|
|
59
|
+
|
|
60
|
+
// Invite refresh
|
|
61
|
+
let inviteRefresh = $state(0);
|
|
62
|
+
|
|
63
|
+
// Pagination
|
|
64
|
+
let pageIndex = $state(0);
|
|
65
|
+
const pageSize = 20;
|
|
66
|
+
|
|
67
|
+
const filtered = $derived(
|
|
68
|
+
searchQuery
|
|
69
|
+
? users.filter(
|
|
70
|
+
(u) =>
|
|
71
|
+
u.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
72
|
+
u.email.toLowerCase().includes(searchQuery.toLowerCase())
|
|
73
|
+
)
|
|
74
|
+
: users
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const totalItems = $derived(filtered.length);
|
|
78
|
+
const pageCount = $derived(Math.ceil(totalItems / pageSize));
|
|
79
|
+
const paged = $derived(filtered.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize));
|
|
80
|
+
|
|
81
|
+
$effect(() => {
|
|
82
|
+
loadUsers();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
async function loadUsers() {
|
|
86
|
+
loading = true;
|
|
87
|
+
const { data, error } = await authClient.admin.listUsers({
|
|
88
|
+
query: { limit: 500 }
|
|
89
|
+
});
|
|
90
|
+
if (data) {
|
|
91
|
+
users = data.users.map((u: any) => ({
|
|
92
|
+
id: u.id,
|
|
93
|
+
name: u.name ?? '',
|
|
94
|
+
email: u.email,
|
|
95
|
+
role: u.role ?? 'user',
|
|
96
|
+
createdAt: new Date(u.createdAt)
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
loading = false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function formatDate(date: Date): string {
|
|
103
|
+
return date.toLocaleString(toLocaleCode(interfaceLanguage.current), {
|
|
104
|
+
year: 'numeric',
|
|
105
|
+
month: 'short',
|
|
106
|
+
day: 'numeric'
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function openEdit(user: User) {
|
|
111
|
+
editingUser = user;
|
|
112
|
+
editOpen = true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function openDelete(userId: string) {
|
|
116
|
+
deletingUserId = userId;
|
|
117
|
+
deleteOpen = true;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function openSessions(user: User) {
|
|
121
|
+
sessionsUserId = user.id;
|
|
122
|
+
sessionsUserName = user.name;
|
|
123
|
+
sessionsOpen = true;
|
|
124
|
+
}
|
|
125
|
+
</script>
|
|
126
|
+
|
|
127
|
+
<div class="container mx-auto max-w-5xl px-4 py-8">
|
|
128
|
+
<div class="mb-6 flex items-center justify-between">
|
|
129
|
+
<h1 class="text-2xl font-bold">{lang.title}</h1>
|
|
130
|
+
<div class="flex items-center gap-2">
|
|
131
|
+
{#if emailConfigured}
|
|
132
|
+
<Button variant="outline" size="sm" onclick={() => (inviteOpen = true)}>
|
|
133
|
+
<Mail class="size-4" />
|
|
134
|
+
{lang.invite.inviteUser}
|
|
135
|
+
</Button>
|
|
136
|
+
{/if}
|
|
137
|
+
<Button variant="gradient" size="sm" onclick={() => (createOpen = true)}>
|
|
138
|
+
<Plus class="size-4" />
|
|
139
|
+
{lang.createUser}
|
|
140
|
+
</Button>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
<div class="relative mb-4">
|
|
145
|
+
<Search class="text-muted-foreground pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2" />
|
|
146
|
+
<Input
|
|
147
|
+
type="text"
|
|
148
|
+
placeholder={lang.search}
|
|
149
|
+
class="pl-9"
|
|
150
|
+
value={searchQuery}
|
|
151
|
+
oninput={(e) => {
|
|
152
|
+
searchQuery = e.currentTarget.value;
|
|
153
|
+
pageIndex = 0;
|
|
154
|
+
}}
|
|
155
|
+
/>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
{#if loading}
|
|
159
|
+
<div class="flex items-center justify-center py-16">
|
|
160
|
+
<Loader2 class="text-muted-foreground size-6 animate-spin" />
|
|
161
|
+
</div>
|
|
162
|
+
{:else if paged.length === 0}
|
|
163
|
+
<p class="text-muted-foreground py-16 text-center">{lang.noResults}</p>
|
|
164
|
+
{:else}
|
|
165
|
+
<div class="overflow-hidden rounded-2xl border">
|
|
166
|
+
<Table.Root>
|
|
167
|
+
<Table.Header>
|
|
168
|
+
<Table.Row>
|
|
169
|
+
<Table.Head>{lang.name}</Table.Head>
|
|
170
|
+
<Table.Head>{lang.email}</Table.Head>
|
|
171
|
+
<Table.Head>{lang.role}</Table.Head>
|
|
172
|
+
<Table.Head>{lang.createdAt}</Table.Head>
|
|
173
|
+
<Table.Head class="text-right">{lang.actions}</Table.Head>
|
|
174
|
+
</Table.Row>
|
|
175
|
+
</Table.Header>
|
|
176
|
+
<Table.Body>
|
|
177
|
+
{#each paged as user (user.id)}
|
|
178
|
+
<Table.Row class="hover:bg-slate-50 dark:hover:bg-slate-800/50">
|
|
179
|
+
<Table.Cell class="font-medium">{user.name}</Table.Cell>
|
|
180
|
+
<Table.Cell>{user.email}</Table.Cell>
|
|
181
|
+
<Table.Cell>
|
|
182
|
+
<Badge variant={user.role === 'admin' ? 'default' : 'secondary'}>
|
|
183
|
+
{user.role === 'admin' ? lang.roleAdmin : lang.roleUser}
|
|
184
|
+
</Badge>
|
|
185
|
+
</Table.Cell>
|
|
186
|
+
<Table.Cell>{formatDate(user.createdAt)}</Table.Cell>
|
|
187
|
+
<Table.Cell class="text-right">
|
|
188
|
+
<div class="flex items-center justify-end gap-1">
|
|
189
|
+
<Button variant="ghost" size="icon" class="h-8 w-8" onclick={() => openSessions(user)}>
|
|
190
|
+
<DeviceDesktop class="size-4" />
|
|
191
|
+
</Button>
|
|
192
|
+
<Button variant="ghost" size="icon" class="h-8 w-8" onclick={() => openEdit(user)}>
|
|
193
|
+
<Pencil class="size-4" />
|
|
194
|
+
</Button>
|
|
195
|
+
{#if user.id !== currentUserId}
|
|
196
|
+
<Button
|
|
197
|
+
variant="ghost"
|
|
198
|
+
size="icon"
|
|
199
|
+
class="text-destructive h-8 w-8"
|
|
200
|
+
onclick={() => openDelete(user.id)}
|
|
201
|
+
>
|
|
202
|
+
<Trash class="size-4" />
|
|
203
|
+
</Button>
|
|
204
|
+
{/if}
|
|
205
|
+
</div>
|
|
206
|
+
</Table.Cell>
|
|
207
|
+
</Table.Row>
|
|
208
|
+
{/each}
|
|
209
|
+
</Table.Body>
|
|
210
|
+
</Table.Root>
|
|
211
|
+
</div>
|
|
212
|
+
|
|
213
|
+
{#if pageCount > 1}
|
|
214
|
+
<div class="mt-4 flex items-center justify-center gap-2">
|
|
215
|
+
<Button variant="outline" size="sm" disabled={pageIndex === 0} onclick={() => (pageIndex -= 1)}>
|
|
216
|
+
←
|
|
217
|
+
</Button>
|
|
218
|
+
<span class="text-muted-foreground text-sm">
|
|
219
|
+
{pageIndex + 1} / {pageCount}
|
|
220
|
+
</span>
|
|
221
|
+
<Button
|
|
222
|
+
variant="outline"
|
|
223
|
+
size="sm"
|
|
224
|
+
disabled={pageIndex >= pageCount - 1}
|
|
225
|
+
onclick={() => (pageIndex += 1)}
|
|
226
|
+
>
|
|
227
|
+
→
|
|
228
|
+
</Button>
|
|
229
|
+
</div>
|
|
230
|
+
{/if}
|
|
231
|
+
{/if}
|
|
232
|
+
|
|
233
|
+
{#if emailConfigured}
|
|
234
|
+
<PendingInvitations refreshTrigger={inviteRefresh} />
|
|
235
|
+
{/if}
|
|
236
|
+
</div>
|
|
237
|
+
|
|
238
|
+
<CreateUserDialog bind:open={createOpen} onOpenChange={(v) => (createOpen = v)} onCreated={loadUsers} />
|
|
239
|
+
|
|
240
|
+
<EditUserDialog
|
|
241
|
+
bind:open={editOpen}
|
|
242
|
+
onOpenChange={(v) => (editOpen = v)}
|
|
243
|
+
onUpdated={loadUsers}
|
|
244
|
+
user={editingUser}
|
|
245
|
+
{currentUserId}
|
|
246
|
+
/>
|
|
247
|
+
|
|
248
|
+
<DeleteUserDialog
|
|
249
|
+
bind:open={deleteOpen}
|
|
250
|
+
onOpenChange={(v) => (deleteOpen = v)}
|
|
251
|
+
onDeleted={loadUsers}
|
|
252
|
+
userId={deletingUserId}
|
|
253
|
+
{currentUserId}
|
|
254
|
+
/>
|
|
255
|
+
|
|
256
|
+
<UserSessionsSheet bind:open={sessionsOpen} userId={sessionsUserId} userName={sessionsUserName} />
|
|
257
|
+
|
|
258
|
+
<InviteUserDialog
|
|
259
|
+
bind:open={inviteOpen}
|
|
260
|
+
onOpenChange={(v) => (inviteOpen = v)}
|
|
261
|
+
onInvited={() => (inviteRefresh += 1)}
|
|
262
|
+
/>
|
|
@@ -71,9 +71,28 @@
|
|
|
71
71
|
acc.push(item);
|
|
72
72
|
return acc;
|
|
73
73
|
}, [] as ObjectFieldData[]);
|
|
74
|
+
|
|
75
|
+
// Fixed-length: pad or trim to exact count
|
|
76
|
+
if (isFixedLength && $value.length !== fixedCount) {
|
|
77
|
+
const current = [...$value];
|
|
78
|
+
if (current.length < fixedCount) {
|
|
79
|
+
const defaults = field.defaultValue;
|
|
80
|
+
for (let i = current.length; i < fixedCount; i++) {
|
|
81
|
+
if (defaults && defaults[i]) {
|
|
82
|
+
current.push({ _id: generateId(), ...JSON.parse(JSON.stringify(defaults[i])) });
|
|
83
|
+
} else {
|
|
84
|
+
current.push({ _id: generateId(), slug: field.of[0].slug, data: {} });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
} else {
|
|
88
|
+
current.length = fixedCount;
|
|
89
|
+
}
|
|
90
|
+
$value = current;
|
|
91
|
+
}
|
|
74
92
|
});
|
|
75
93
|
|
|
76
94
|
async function addItem(field: ObjectFieldType) {
|
|
95
|
+
if (atMax) return;
|
|
77
96
|
$value = [
|
|
78
97
|
...($value ?? []),
|
|
79
98
|
{
|
|
@@ -87,7 +106,7 @@
|
|
|
87
106
|
}
|
|
88
107
|
|
|
89
108
|
function duplicateItem(index: number) {
|
|
90
|
-
if (!$value) return;
|
|
109
|
+
if (!$value || atMax) return;
|
|
91
110
|
const itemToDuplicate = $value[index];
|
|
92
111
|
if (!itemToDuplicate) return;
|
|
93
112
|
|
|
@@ -192,11 +211,28 @@
|
|
|
192
211
|
return normalizePath(flashingPath) === fieldPath;
|
|
193
212
|
}
|
|
194
213
|
|
|
214
|
+
const atMax = $derived(field.maxItems !== undefined && ($value?.length ?? 0) >= field.maxItems);
|
|
215
|
+
|
|
216
|
+
const isFixedLength = $derived(
|
|
217
|
+
field.minItems !== undefined &&
|
|
218
|
+
field.maxItems !== undefined &&
|
|
219
|
+
field.minItems === field.maxItems &&
|
|
220
|
+
field.minItems > 0
|
|
221
|
+
);
|
|
222
|
+
const fixedCount = $derived(isFixedLength ? field.minItems! : 0);
|
|
223
|
+
|
|
195
224
|
let blockPickerOpen = $state(false);
|
|
196
225
|
</script>
|
|
197
226
|
|
|
198
227
|
<div class="flex items-center justify-between gap-4">
|
|
199
|
-
<
|
|
228
|
+
<div class="flex items-center gap-2">
|
|
229
|
+
<RequiredLabel required={field.minItems !== undefined && field.minItems > 0} class="text-lg">{getLocalizedLabel(field.label, interfaceLanguage.current)}</RequiredLabel>
|
|
230
|
+
{#if isFixedLength}
|
|
231
|
+
<span class="text-xs text-muted-foreground">{fixedCount} elementów</span>
|
|
232
|
+
{:else if field.maxItems !== undefined}
|
|
233
|
+
<span class="text-xs {atMax ? 'text-destructive' : 'text-muted-foreground'}">{$value?.length ?? 0} / {field.maxItems}</span>
|
|
234
|
+
{/if}
|
|
235
|
+
</div>
|
|
200
236
|
|
|
201
237
|
<div class="flex items-center gap-2">
|
|
202
238
|
<Button
|
|
@@ -263,6 +299,7 @@
|
|
|
263
299
|
>
|
|
264
300
|
<div class="flex grow items-center justify-between gap-4">
|
|
265
301
|
<div class="flex items-center gap-4">
|
|
302
|
+
{#if !isFixedLength || fixedCount > 1}
|
|
266
303
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
267
304
|
<div
|
|
268
305
|
use:draggable={{ container: index.toString(), dragData: { id: item._id } }}
|
|
@@ -272,11 +309,13 @@
|
|
|
272
309
|
>
|
|
273
310
|
<GripVertical class="h-4 w-4" />
|
|
274
311
|
</div>
|
|
312
|
+
{/if}
|
|
275
313
|
<span>{index < 10 ? '0' : ''}{index + 1}</span>
|
|
276
314
|
<Badge variant="outline">{getLocalizedLabel(objectField.label, interfaceLanguage.current) || objectField.slug}</Badge>
|
|
277
315
|
<span>{getAccordionLabel($value[index])}</span>
|
|
278
316
|
</div>
|
|
279
317
|
|
|
318
|
+
{#if !(isFixedLength && fixedCount <= 1)}
|
|
280
319
|
<DropdownMenu.Root>
|
|
281
320
|
<DropdownMenu.Trigger
|
|
282
321
|
class="data-[state=open]:bg-muted text-muted-foreground flex size-8"
|
|
@@ -289,9 +328,11 @@
|
|
|
289
328
|
{/snippet}
|
|
290
329
|
</DropdownMenu.Trigger>
|
|
291
330
|
<DropdownMenu.Content align="end" class="w-32">
|
|
292
|
-
|
|
331
|
+
{#if !isFixedLength}
|
|
332
|
+
<DropdownMenu.Item onclick={() => duplicateItem(index)} disabled={atMax}>
|
|
293
333
|
Duplicate
|
|
294
334
|
</DropdownMenu.Item>
|
|
335
|
+
{/if}
|
|
295
336
|
<DropdownMenu.Item onclick={() => moveItemUp(index)} disabled={index === 0}>
|
|
296
337
|
Move up
|
|
297
338
|
</DropdownMenu.Item>
|
|
@@ -301,11 +342,14 @@
|
|
|
301
342
|
>
|
|
302
343
|
Move down
|
|
303
344
|
</DropdownMenu.Item>
|
|
345
|
+
{#if !isFixedLength}
|
|
304
346
|
<DropdownMenu.Item variant="destructive" onclick={() => removeItem(index)}>
|
|
305
347
|
Delete
|
|
306
348
|
</DropdownMenu.Item>
|
|
349
|
+
{/if}
|
|
307
350
|
</DropdownMenu.Content>
|
|
308
351
|
</DropdownMenu.Root>
|
|
352
|
+
{/if}
|
|
309
353
|
</div>
|
|
310
354
|
</Accordion.Trigger>
|
|
311
355
|
<Accordion.Content
|
|
@@ -341,25 +385,27 @@
|
|
|
341
385
|
{/if}
|
|
342
386
|
</Accordion.Root>
|
|
343
387
|
|
|
344
|
-
{#if
|
|
345
|
-
|
|
346
|
-
<
|
|
347
|
-
<
|
|
348
|
-
Add Block
|
|
349
|
-
</Button>
|
|
350
|
-
</div>
|
|
351
|
-
<BlockPickerModal
|
|
352
|
-
bind:open={blockPickerOpen}
|
|
353
|
-
options={field.of}
|
|
354
|
-
onSelect={(option) => addItem(option)}
|
|
355
|
-
/>
|
|
356
|
-
{:else}
|
|
357
|
-
<div class="mt-4 flex flex-wrap gap-2">
|
|
358
|
-
{#each field.of as option}
|
|
359
|
-
<Button size="sm" type="button" variant="outline" onclick={() => addItem(option)}>
|
|
388
|
+
{#if !isFixedLength}
|
|
389
|
+
{#if field.displayMode === 'blocks'}
|
|
390
|
+
<div class="mt-4">
|
|
391
|
+
<Button size="sm" type="button" variant="outline" disabled={atMax} onclick={() => (blockPickerOpen = true)}>
|
|
360
392
|
<CirclePlus />
|
|
361
|
-
|
|
393
|
+
Add Block
|
|
362
394
|
</Button>
|
|
363
|
-
|
|
364
|
-
|
|
395
|
+
</div>
|
|
396
|
+
<BlockPickerModal
|
|
397
|
+
bind:open={blockPickerOpen}
|
|
398
|
+
options={field.of}
|
|
399
|
+
onSelect={(option) => addItem(option)}
|
|
400
|
+
/>
|
|
401
|
+
{:else}
|
|
402
|
+
<div class="mt-4 flex flex-wrap gap-2">
|
|
403
|
+
{#each field.of as option}
|
|
404
|
+
<Button size="sm" type="button" variant="outline" disabled={atMax} onclick={() => addItem(option)}>
|
|
405
|
+
<CirclePlus />
|
|
406
|
+
{getLocalizedLabel(option.label, interfaceLanguage.current) || option.slug}
|
|
407
|
+
</Button>
|
|
408
|
+
{/each}
|
|
409
|
+
</div>
|
|
410
|
+
{/if}
|
|
365
411
|
{/if}
|
|
@@ -48,6 +48,23 @@
|
|
|
48
48
|
function isCheckboxesField(field: Field): field is Extract<Field, { type: 'checkboxes' }> {
|
|
49
49
|
return field.type === 'checkboxes';
|
|
50
50
|
}
|
|
51
|
+
|
|
52
|
+
function numberConstraintHint(f: Field): string {
|
|
53
|
+
if (f.type !== 'number') return '';
|
|
54
|
+
const parts: string[] = [];
|
|
55
|
+
if (f.min !== undefined && f.max !== undefined) {
|
|
56
|
+
parts.push(`Zakres: ${f.min}–${f.max}`);
|
|
57
|
+
} else if (f.min !== undefined) {
|
|
58
|
+
parts.push(`Min. ${f.min}`);
|
|
59
|
+
} else if (f.max !== undefined) {
|
|
60
|
+
parts.push(`Maks. ${f.max}`);
|
|
61
|
+
}
|
|
62
|
+
if (f.step !== undefined) {
|
|
63
|
+
if (parts.length > 0) parts.push(` · `);
|
|
64
|
+
parts.push(`Krok: ${f.step}`);
|
|
65
|
+
}
|
|
66
|
+
return parts.join('');
|
|
67
|
+
}
|
|
51
68
|
</script>
|
|
52
69
|
|
|
53
70
|
{#if isRadioField(field)}
|
|
@@ -132,8 +149,14 @@
|
|
|
132
149
|
</div>
|
|
133
150
|
{/snippet}
|
|
134
151
|
</Form.Control>
|
|
135
|
-
{#if !fieldsWithNoDescription.includes(field.type) && !fieldsWithAlternativeDescription.includes(field.type) && field.description}
|
|
136
|
-
<Form.Description>
|
|
152
|
+
{#if !fieldsWithNoDescription.includes(field.type) && !fieldsWithAlternativeDescription.includes(field.type) && (field.description || numberConstraintHint(field))}
|
|
153
|
+
<Form.Description>
|
|
154
|
+
{#if field.description}{getLocalizedLabel(field.description, interfaceLanguage.current)}{/if}
|
|
155
|
+
{#if numberConstraintHint(field)}
|
|
156
|
+
{#if field.description}<br />{/if}
|
|
157
|
+
{numberConstraintHint(field)}
|
|
158
|
+
{/if}
|
|
159
|
+
</Form.Description>
|
|
137
160
|
{/if}
|
|
138
161
|
<Form.FieldErrors />
|
|
139
162
|
</Form.Field>
|
|
@@ -27,6 +27,30 @@
|
|
|
27
27
|
};
|
|
28
28
|
|
|
29
29
|
let { field, form, path, ...props }: Props = $props();
|
|
30
|
+
|
|
31
|
+
const formData = form.form;
|
|
32
|
+
|
|
33
|
+
const isText = $derived(field.type === 'text');
|
|
34
|
+
const hasConstraints = $derived(
|
|
35
|
+
isText && (field.minLength !== undefined || field.maxLength !== undefined || field.pattern !== undefined)
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
function resolvePathValue(data: Record<string, unknown>, dotPath: string): unknown {
|
|
39
|
+
return dotPath.split('.').reduce<unknown>((obj, key) => (obj as Record<string, unknown>)?.[key], data);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function constraintHint(): string {
|
|
43
|
+
if (field.type !== 'text') return '';
|
|
44
|
+
const parts: string[] = [];
|
|
45
|
+
if (field.minLength !== undefined && field.maxLength !== undefined) {
|
|
46
|
+
parts.push(`${field.minLength}–${field.maxLength} znaków`);
|
|
47
|
+
} else if (field.minLength !== undefined) {
|
|
48
|
+
parts.push(`Min. ${field.minLength} znaków`);
|
|
49
|
+
} else if (field.maxLength !== undefined) {
|
|
50
|
+
parts.push(`Maks. ${field.maxLength} znaków`);
|
|
51
|
+
}
|
|
52
|
+
return parts.join('');
|
|
53
|
+
}
|
|
30
54
|
</script>
|
|
31
55
|
|
|
32
56
|
<Tabs.Root
|
|
@@ -66,7 +90,38 @@
|
|
|
66
90
|
{/snippet}
|
|
67
91
|
</Form.Control>
|
|
68
92
|
|
|
69
|
-
|
|
93
|
+
{#if isText}
|
|
94
|
+
{@const val = resolvePathValue($formData, joinPath(path, lang))}
|
|
95
|
+
{@const charCount = typeof val === 'string' ? val.length : 0}
|
|
96
|
+
{@const atLimit = field.type === 'text' && field.maxLength !== undefined && charCount >= field.maxLength}
|
|
97
|
+
<div class="flex items-start justify-between gap-4">
|
|
98
|
+
{#if field.description || hasConstraints}
|
|
99
|
+
<Form.Description class="flex-1">
|
|
100
|
+
{#if field.description}{getLocalizedLabel(field.description, interfaceLanguage.current)}{/if}
|
|
101
|
+
{#if hasConstraints}
|
|
102
|
+
{#if field.description && constraintHint()}<br />{/if}
|
|
103
|
+
{#if constraintHint()}{constraintHint()}{/if}
|
|
104
|
+
{#if field.type === 'text' && field.pattern}
|
|
105
|
+
{#if constraintHint()} · {/if}Format: <code class="text-xs bg-muted px-1 py-0.5 rounded">{field.pattern}</code>
|
|
106
|
+
{/if}
|
|
107
|
+
{/if}
|
|
108
|
+
</Form.Description>
|
|
109
|
+
{:else}
|
|
110
|
+
<div></div>
|
|
111
|
+
{/if}
|
|
112
|
+
<span class="shrink-0 text-xs {atLimit ? 'text-destructive' : 'text-muted-foreground'}" aria-live="polite">
|
|
113
|
+
{#if field.type === 'text' && field.maxLength !== undefined}
|
|
114
|
+
{charCount} / {field.maxLength}
|
|
115
|
+
{:else if field.type === 'text' && field.minLength !== undefined}
|
|
116
|
+
{charCount} (min. {field.minLength})
|
|
117
|
+
{:else}
|
|
118
|
+
{charCount}
|
|
119
|
+
{/if}
|
|
120
|
+
</span>
|
|
121
|
+
</div>
|
|
122
|
+
{:else if field.description}
|
|
123
|
+
<Form.Description>{getLocalizedLabel(field.description, interfaceLanguage.current)}</Form.Description>
|
|
124
|
+
{/if}
|
|
70
125
|
|
|
71
126
|
<Form.FieldErrors />
|
|
72
127
|
</Form.Field>
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
</script>
|
|
36
36
|
|
|
37
37
|
{#if field.multiline}
|
|
38
|
-
<Textarea {...props} bind:value={$value} placeholder={getLocalizedLabel(field.placeholder, interfaceLanguage.current)} />
|
|
38
|
+
<Textarea {...props} bind:value={$value} placeholder={getLocalizedLabel(field.placeholder, interfaceLanguage.current)} minlength={field.minLength} maxlength={field.maxLength} />
|
|
39
39
|
{:else}
|
|
40
|
-
<Input {...props} bind:value={$value} type="text" placeholder={getLocalizedLabel(field.placeholder, interfaceLanguage.current)} />
|
|
40
|
+
<Input {...props} bind:value={$value} type="text" placeholder={getLocalizedLabel(field.placeholder, interfaceLanguage.current)} minlength={field.minLength} maxlength={field.maxLength} />
|
|
41
41
|
{/if}
|
|
@@ -7,7 +7,8 @@ export const sidebarLang = {
|
|
|
7
7
|
main: {
|
|
8
8
|
platform: 'Platforma',
|
|
9
9
|
media: 'Biblioteka mediów',
|
|
10
|
-
dashboard: 'Kokpit'
|
|
10
|
+
dashboard: 'Kokpit',
|
|
11
|
+
users: 'Użytkownicy'
|
|
11
12
|
},
|
|
12
13
|
collections: {
|
|
13
14
|
title: 'Kolekcje'
|
|
@@ -32,7 +33,8 @@ export const sidebarLang = {
|
|
|
32
33
|
main: {
|
|
33
34
|
platform: 'Platform',
|
|
34
35
|
media: 'Media Library',
|
|
35
|
-
dashboard: 'Dashboard'
|
|
36
|
+
dashboard: 'Dashboard',
|
|
37
|
+
users: 'Users'
|
|
36
38
|
},
|
|
37
39
|
collections: {
|
|
38
40
|
title: 'Collections'
|
|
@@ -5,14 +5,19 @@
|
|
|
5
5
|
import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
|
|
6
6
|
import CameraIcon from '@tabler/icons-svelte/icons/camera';
|
|
7
7
|
import DashboardIcon from '@tabler/icons-svelte/icons/dashboard';
|
|
8
|
+
import UsersIcon from '@tabler/icons-svelte/icons/users';
|
|
8
9
|
import SettingsIcon from '@tabler/icons-svelte/icons/settings';
|
|
9
10
|
import { getRemotes } from '../../../sveltekit/index.js';
|
|
10
11
|
import Skeleton from '../../../components/ui/skeleton/skeleton.svelte';
|
|
11
12
|
import { getLocalizedLabel } from '../../utils/collectionLabel.js';
|
|
12
13
|
import { page } from '$app/state';
|
|
14
|
+
import { authClient } from '../../auth-client.js';
|
|
13
15
|
|
|
14
16
|
const interfaceLanguage = useInterfaceLanguage();
|
|
15
17
|
const remotes = getRemotes();
|
|
18
|
+
let session = authClient.useSession();
|
|
19
|
+
|
|
20
|
+
const isAdmin = $derived(($session.data?.user as any)?.role === 'admin');
|
|
16
21
|
|
|
17
22
|
let items: { title: string; url: string; icon?: Icon }[] = $derived([
|
|
18
23
|
{
|
|
@@ -24,7 +29,16 @@
|
|
|
24
29
|
title: sidebarLang[interfaceLanguage.current].main.media,
|
|
25
30
|
url: '/admin/media',
|
|
26
31
|
icon: CameraIcon
|
|
27
|
-
}
|
|
32
|
+
},
|
|
33
|
+
...(isAdmin
|
|
34
|
+
? [
|
|
35
|
+
{
|
|
36
|
+
title: sidebarLang[interfaceLanguage.current].main.users,
|
|
37
|
+
url: '/admin/users',
|
|
38
|
+
icon: UsersIcon
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
: [])
|
|
28
42
|
]);
|
|
29
43
|
|
|
30
44
|
function isActive(url: string) {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { UserRole } from '../../types/roles.js';
|
|
2
|
+
export declare function createInvitation(email: string, role: UserRole, createdBy: string): Promise<{
|
|
3
|
+
createdAt: Date;
|
|
4
|
+
email: string;
|
|
5
|
+
id: string;
|
|
6
|
+
expiresAt: Date;
|
|
7
|
+
token: string;
|
|
8
|
+
role: string;
|
|
9
|
+
createdBy: string;
|
|
10
|
+
usedAt: Date | null;
|
|
11
|
+
}>;
|
|
12
|
+
export declare function getInvitationByToken(token: string): Promise<{
|
|
13
|
+
id: string;
|
|
14
|
+
email: string;
|
|
15
|
+
role: string;
|
|
16
|
+
token: string;
|
|
17
|
+
expiresAt: Date;
|
|
18
|
+
createdAt: Date;
|
|
19
|
+
createdBy: string;
|
|
20
|
+
usedAt: Date | null;
|
|
21
|
+
}>;
|
|
22
|
+
export declare function markInvitationUsed(id: string): Promise<void>;
|
|
23
|
+
export declare function getPendingInvitations(): Promise<{
|
|
24
|
+
id: string;
|
|
25
|
+
email: string;
|
|
26
|
+
role: string;
|
|
27
|
+
token: string;
|
|
28
|
+
expiresAt: Date;
|
|
29
|
+
createdAt: Date;
|
|
30
|
+
createdBy: string;
|
|
31
|
+
usedAt: Date | null;
|
|
32
|
+
}[]>;
|
|
33
|
+
export declare function getInvitationById(id: string): Promise<{
|
|
34
|
+
id: string;
|
|
35
|
+
email: string;
|
|
36
|
+
role: string;
|
|
37
|
+
token: string;
|
|
38
|
+
expiresAt: Date;
|
|
39
|
+
createdAt: Date;
|
|
40
|
+
createdBy: string;
|
|
41
|
+
usedAt: Date | null;
|
|
42
|
+
}>;
|
|
43
|
+
export declare function deleteInvitation(id: string): Promise<void>;
|
|
44
|
+
export declare function checkEmailExists(email: string): Promise<boolean>;
|