@kyro-cms/admin 0.9.0 → 0.9.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/index.cjs +11960 -11006
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +67 -65
- package/dist/index.css.map +1 -1
- package/dist/index.d.cts +563 -0
- package/dist/index.d.ts +7 -7
- package/dist/index.js +12183 -11238
- package/dist/index.js.map +1 -1
- package/package.json +15 -11
- package/src/components/ActionBar.tsx +27 -14
- package/src/components/Admin.tsx +1 -1
- package/src/components/ApiKeysManager.tsx +5 -5
- package/src/components/AutoForm.tsx +585 -369
- package/src/components/BrandingHub.tsx +7 -4
- package/src/components/CreateView.tsx +2 -0
- package/src/components/DetailView.tsx +71 -56
- package/src/components/DeveloperCenter.tsx +8 -6
- package/src/components/FieldRenderer.tsx +94 -19
- package/src/components/ListView.tsx +33 -20
- package/src/components/MediaGallery.tsx +219 -194
- package/src/components/PluginsManager.tsx +197 -70
- package/src/components/RestPlayground.tsx +7 -7
- package/src/components/SessionsManager.tsx +1 -1
- package/src/components/SettingsPage.tsx +22 -0
- package/src/components/Sidebar.astro +13 -41
- package/src/components/UserManagement.tsx +153 -15
- package/src/components/UserMenu.tsx +30 -4
- package/src/components/VersionHistoryPanel.tsx +112 -119
- package/src/components/WebhookManager.tsx +6 -4
- package/src/components/blocks/ArrayBlock.tsx +6 -23
- package/src/components/blocks/BlockEditModal.tsx +82 -309
- package/src/components/blocks/CardBlock.tsx +35 -0
- package/src/components/blocks/ChildBlocksTree.tsx +57 -31
- package/src/components/blocks/GenericBlock.tsx +44 -0
- package/src/components/blocks/HeadingSubheadingBlock.tsx +32 -0
- package/src/components/blocks/HeroBlock.tsx +5 -14
- package/src/components/blocks/RichTextBlock.tsx +5 -5
- package/src/components/blocks/index.ts +5 -3
- package/src/components/fields/AccordionField.tsx +2 -2
- package/src/components/fields/ArrayField.tsx +1 -1
- package/src/components/fields/ArrayLayout.tsx +120 -29
- package/src/components/fields/BlocksField.tsx +430 -50
- package/src/components/fields/CardField.tsx +73 -0
- package/src/components/fields/CheckboxField.tsx +7 -3
- package/src/components/fields/DateField.tsx +4 -1
- package/src/components/fields/GroupLayout.tsx +2 -2
- package/src/components/fields/HeadingSubheadingField.tsx +43 -0
- package/src/components/fields/ListField.tsx +2 -2
- package/src/components/fields/NumberField.tsx +4 -1
- package/src/components/fields/RelationshipField.tsx +153 -87
- package/src/components/fields/RichTextField.tsx +781 -0
- package/src/components/fields/SecretField.tsx +102 -0
- package/src/components/fields/SelectField.tsx +19 -6
- package/src/components/fields/TabsLayout.tsx +19 -9
- package/src/components/fields/TextField.tsx +4 -1
- package/src/components/fields/UploadField.tsx +122 -56
- package/src/components/fields/extensions/blockComponents.tsx +103 -174
- package/src/components/fields/extensions/blocksStore.ts +8 -1
- package/src/components/fields/index.ts +4 -2
- package/src/components/ui/PageHeader.tsx +5 -5
- package/src/components/ui/SlidePanel.tsx +8 -3
- package/src/components/ui/icons.tsx +109 -109
- package/src/components/users/UserDetail.tsx +79 -16
- package/src/hooks/useAutoFormState.ts +125 -62
- package/src/integration.ts +148 -46
- package/src/kyro-cms.d.ts +7 -2
- package/src/layouts/AuthLayout.astro +14 -2
- package/src/lib/autoform-store.ts +85 -52
- package/src/lib/change-source.ts +9 -0
- package/src/lib/config.ts +104 -8
- package/src/lib/globals.ts +44 -9
- package/src/lib/normalize-upload-fields.ts +41 -0
- package/src/lib/paths.ts +2 -2
- package/src/lib/resolve-field-value.ts +110 -0
- package/src/lib/shim/use-sync-external-store-with-selector.js +30 -0
- package/src/lib/shim/use-sync-external-store.js +1 -0
- package/src/lib/stores/index.ts +1 -0
- package/src/lib/useResourceManager.ts +4 -4
- package/src/lib/vite-shim-plugin.ts +100 -0
- package/src/pages/[collection]/[id].astro +1 -1
- package/src/pages/preview/[collection]/[id].astro +4 -4
- package/src/pages/settings/[slug].astro +2 -2
- package/src/styles/main.css +60 -54
- package/README.md +0 -46
- package/dist/EditorClient-Q23UXR37.cjs +0 -468
- package/dist/EditorClient-Q23UXR37.cjs.map +0 -1
- package/dist/EditorClient-T5PASFNR.js +0 -466
- package/dist/EditorClient-T5PASFNR.js.map +0 -1
- package/dist/chunk-3BGDYKTD.cjs +0 -348
- package/dist/chunk-3BGDYKTD.cjs.map +0 -1
- package/dist/chunk-EEFXLQVT.js +0 -3
- package/dist/chunk-EEFXLQVT.js.map +0 -1
- package/src/components/blocks/ButtonBlock.tsx +0 -64
- package/src/components/blocks/ColumnsBlock.tsx +0 -55
- package/src/components/blocks/DividerBlock.tsx +0 -43
- package/src/components/blocks/LinkBlock.tsx +0 -65
- package/src/components/blocks/VStackBlock.tsx +0 -29
- package/src/components/fields/EditorClient.tsx +0 -535
- package/src/components/fields/PortableTextField.tsx +0 -155
- package/src/components/fields/PortableTextRenderer.tsx +0 -68
|
@@ -1,23 +1,18 @@
|
|
|
1
1
|
import React, { useState, useEffect } from "react";
|
|
2
|
-
import { apiGet, apiPatch, apiDelete } from "../lib/api";
|
|
2
|
+
import { apiGet, apiPost, apiPatch, apiDelete } from "../lib/api";
|
|
3
3
|
import {
|
|
4
4
|
Users,
|
|
5
5
|
UserPlus,
|
|
6
6
|
Shield,
|
|
7
7
|
Lock,
|
|
8
8
|
Unlock,
|
|
9
|
-
MoreVertical,
|
|
10
|
-
Mail,
|
|
11
9
|
Clock,
|
|
12
10
|
Search,
|
|
13
|
-
Filter,
|
|
14
11
|
Trash2,
|
|
15
|
-
|
|
16
|
-
ChevronRight,
|
|
17
|
-
ShieldCheck,
|
|
18
|
-
ShieldAlert
|
|
12
|
+
AlertTriangle,
|
|
19
13
|
} from "./ui/icons";
|
|
20
14
|
import { useUIStore, toast } from "../lib/stores";
|
|
15
|
+
import { Modal, ModalContent, ModalActions } from "./ui/Modal";
|
|
21
16
|
import { Badge } from "./ui/Badge";
|
|
22
17
|
import { PageHeader } from "./ui/PageHeader";
|
|
23
18
|
|
|
@@ -26,6 +21,7 @@ interface User {
|
|
|
26
21
|
email: string;
|
|
27
22
|
name?: string;
|
|
28
23
|
role: string;
|
|
24
|
+
avatar?: string;
|
|
29
25
|
locked?: boolean;
|
|
30
26
|
lastLogin?: string;
|
|
31
27
|
tenantId?: string;
|
|
@@ -36,6 +32,10 @@ export function UserManagement() {
|
|
|
36
32
|
const [users, setUsers] = useState<User[]>([]);
|
|
37
33
|
const [loading, setLoading] = useState(true);
|
|
38
34
|
const [searchQuery, setSearchQuery] = useState("");
|
|
35
|
+
const [showCreateModal, setShowCreateModal] = useState(false);
|
|
36
|
+
const [createForm, setCreateForm] = useState({ name: "", email: "", password: "", role: "customer" });
|
|
37
|
+
const [createError, setCreateError] = useState("");
|
|
38
|
+
const [creating, setCreating] = useState(false);
|
|
39
39
|
const { confirm, alert } = useUIStore();
|
|
40
40
|
|
|
41
41
|
useEffect(() => {
|
|
@@ -71,6 +71,7 @@ export function UserManagement() {
|
|
|
71
71
|
toast.success(isLocking ? `Account locked: ${user.email}` : `Account restored: ${user.email}`);
|
|
72
72
|
} catch (error) {
|
|
73
73
|
console.error("Failed to toggle user lock:", error);
|
|
74
|
+
toast.error("Failed to update account status");
|
|
74
75
|
}
|
|
75
76
|
},
|
|
76
77
|
});
|
|
@@ -89,11 +90,41 @@ export function UserManagement() {
|
|
|
89
90
|
toast.success(`Identity purged: ${user.email}`);
|
|
90
91
|
} catch (error) {
|
|
91
92
|
console.error("Failed to delete user:", error);
|
|
93
|
+
toast.error("Failed to delete user");
|
|
92
94
|
}
|
|
93
95
|
},
|
|
94
96
|
});
|
|
95
97
|
};
|
|
96
98
|
|
|
99
|
+
const handleCreateUser = async () => {
|
|
100
|
+
if (!createForm.email.trim() || !createForm.password.trim()) {
|
|
101
|
+
setCreateError("Email and password are required");
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
setCreating(true);
|
|
105
|
+
setCreateError("");
|
|
106
|
+
try {
|
|
107
|
+
await apiPost("/api/users", {
|
|
108
|
+
name: createForm.name.trim() || undefined,
|
|
109
|
+
email: createForm.email.trim(),
|
|
110
|
+
password: createForm.password,
|
|
111
|
+
role: createForm.role,
|
|
112
|
+
});
|
|
113
|
+
setShowCreateModal(false);
|
|
114
|
+
setCreateForm({ name: "", email: "", password: "", role: "customer" });
|
|
115
|
+
toast.success("User created successfully");
|
|
116
|
+
loadUsers();
|
|
117
|
+
} catch (err) {
|
|
118
|
+
const message = err instanceof Error ? err.message : "Failed to create user";
|
|
119
|
+
setCreateError(message);
|
|
120
|
+
toast.error(message);
|
|
121
|
+
} finally {
|
|
122
|
+
setCreating(false);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const roleOptions = ["super_admin", "admin", "editor", "author", "customer", "guest"];
|
|
127
|
+
|
|
97
128
|
const filteredUsers = users.filter(
|
|
98
129
|
(u) =>
|
|
99
130
|
u.email.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
@@ -110,7 +141,9 @@ export function UserManagement() {
|
|
|
110
141
|
action={{
|
|
111
142
|
label: "New User",
|
|
112
143
|
onClick: () => {
|
|
113
|
-
|
|
144
|
+
setCreateForm({ name: "", email: "", password: "", role: "customer" });
|
|
145
|
+
setCreateError("");
|
|
146
|
+
setShowCreateModal(true);
|
|
114
147
|
},
|
|
115
148
|
icon: UserPlus,
|
|
116
149
|
}}
|
|
@@ -165,9 +198,7 @@ export function UserManagement() {
|
|
|
165
198
|
<tr key={user.id} className={`hover:bg-[var(--kyro-surface-accent)]/50 transition-colors group ${user.locked ? "opacity-50 grayscale" : ""}`}>
|
|
166
199
|
<td className="px-6 py-3.5">
|
|
167
200
|
<div className="flex items-center gap-3">
|
|
168
|
-
<
|
|
169
|
-
{user.name ? user.name[0] : user.email[0].toUpperCase()}
|
|
170
|
-
</div>
|
|
201
|
+
<AvatarCell user={user} />
|
|
171
202
|
<div className="min-w-0">
|
|
172
203
|
<div className="flex items-center gap-2">
|
|
173
204
|
<div className="text-xs font-bold text-[var(--kyro-text-primary)] truncate">{user.name || user.email.split("@")[0]}</div>
|
|
@@ -214,9 +245,6 @@ export function UserManagement() {
|
|
|
214
245
|
>
|
|
215
246
|
<Trash2 className="w-3.5 h-3.5" />
|
|
216
247
|
</button>
|
|
217
|
-
<button className="p-1.5 rounded-lg border border-[var(--kyro-border)] bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface)] transition-all">
|
|
218
|
-
<MoreVertical className="w-3.5 h-3.5" />
|
|
219
|
-
</button>
|
|
220
248
|
</div>
|
|
221
249
|
</td>
|
|
222
250
|
</tr>
|
|
@@ -225,6 +253,116 @@ export function UserManagement() {
|
|
|
225
253
|
</tbody>
|
|
226
254
|
</table>
|
|
227
255
|
</div>
|
|
256
|
+
|
|
257
|
+
{/* Create User Modal */}
|
|
258
|
+
<Modal
|
|
259
|
+
open={showCreateModal}
|
|
260
|
+
onClose={() => setShowCreateModal(false)}
|
|
261
|
+
title="Create User"
|
|
262
|
+
size="lg"
|
|
263
|
+
>
|
|
264
|
+
<ModalContent>
|
|
265
|
+
<div className="space-y-6">
|
|
266
|
+
<div>
|
|
267
|
+
<label className="block text-xs font-bold mb-1.5 text-[var(--kyro-text-secondary)] uppercase tracking-wider">Name (optional)</label>
|
|
268
|
+
<input
|
|
269
|
+
type="text"
|
|
270
|
+
value={createForm.name}
|
|
271
|
+
onChange={(e) => setCreateForm({ ...createForm, name: e.target.value })}
|
|
272
|
+
placeholder="John Doe"
|
|
273
|
+
className="w-full px-4 py-3 bg-[var(--kyro-bg)] border border-[var(--kyro-border)] rounded-xl text-[var(--kyro-text-primary)] focus:outline-none focus:border-[var(--kyro-primary)]"
|
|
274
|
+
/>
|
|
275
|
+
</div>
|
|
276
|
+
<div>
|
|
277
|
+
<label className="block text-xs font-bold mb-1.5 text-[var(--kyro-text-secondary)] uppercase tracking-wider">Email Address</label>
|
|
278
|
+
<input
|
|
279
|
+
type="email"
|
|
280
|
+
value={createForm.email}
|
|
281
|
+
onChange={(e) => setCreateForm({ ...createForm, email: e.target.value })}
|
|
282
|
+
placeholder="user@example.com"
|
|
283
|
+
required
|
|
284
|
+
className="w-full px-4 py-3 bg-[var(--kyro-bg)] border border-[var(--kyro-border)] rounded-xl text-[var(--kyro-text-primary)] focus:outline-none focus:border-[var(--kyro-primary)]"
|
|
285
|
+
/>
|
|
286
|
+
</div>
|
|
287
|
+
<div>
|
|
288
|
+
<label className="block text-xs font-bold mb-1.5 text-[var(--kyro-text-secondary)] uppercase tracking-wider">Password</label>
|
|
289
|
+
<input
|
|
290
|
+
type="password"
|
|
291
|
+
value={createForm.password}
|
|
292
|
+
onChange={(e) => setCreateForm({ ...createForm, password: e.target.value })}
|
|
293
|
+
placeholder="Minimum 12 characters"
|
|
294
|
+
required
|
|
295
|
+
minLength={12}
|
|
296
|
+
className="w-full px-4 py-3 bg-[var(--kyro-bg)] border border-[var(--kyro-border)] rounded-xl text-[var(--kyro-text-primary)] focus:outline-none focus:border-[var(--kyro-primary)]"
|
|
297
|
+
/>
|
|
298
|
+
</div>
|
|
299
|
+
<div>
|
|
300
|
+
<label className="block text-xs font-bold mb-1.5 text-[var(--kyro-text-secondary)] uppercase tracking-wider">Role</label>
|
|
301
|
+
<select
|
|
302
|
+
value={createForm.role}
|
|
303
|
+
onChange={(e) => setCreateForm({ ...createForm, role: e.target.value })}
|
|
304
|
+
className="w-full px-4 py-3 bg-[var(--kyro-bg)] border border-[var(--kyro-border)] rounded-xl text-[var(--kyro-text-primary)] focus:outline-none focus:border-[var(--kyro-primary)]"
|
|
305
|
+
>
|
|
306
|
+
{roleOptions.map((r) => (
|
|
307
|
+
<option key={r} value={r}>{r}</option>
|
|
308
|
+
))}
|
|
309
|
+
</select>
|
|
310
|
+
</div>
|
|
311
|
+
{createError && (
|
|
312
|
+
<div className="p-3 bg-red-500/10 border border-red-500/20 rounded-xl flex items-center gap-2 text-red-500 text-xs font-bold">
|
|
313
|
+
<AlertTriangle className="w-4 h-4" />
|
|
314
|
+
{createError}
|
|
315
|
+
</div>
|
|
316
|
+
)}
|
|
317
|
+
</div>
|
|
318
|
+
</ModalContent>
|
|
319
|
+
<ModalActions>
|
|
320
|
+
<button
|
|
321
|
+
type="button"
|
|
322
|
+
onClick={() => setShowCreateModal(false)}
|
|
323
|
+
className="px-6 py-2.5 rounded-xl font-bold text-sm border border-[var(--kyro-border)] text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)] transition-colors"
|
|
324
|
+
>
|
|
325
|
+
Cancel
|
|
326
|
+
</button>
|
|
327
|
+
<button
|
|
328
|
+
type="button"
|
|
329
|
+
onClick={handleCreateUser}
|
|
330
|
+
disabled={creating}
|
|
331
|
+
className="kyro-btn kyro-btn-primary px-6 py-2.5 rounded-xl font-bold text-sm hover:opacity-90 transition-all shadow-lg shadow-[var(--kyro-primary)]/10 disabled:opacity-50"
|
|
332
|
+
>
|
|
333
|
+
{creating ? "Creating..." : "Create User"}
|
|
334
|
+
</button>
|
|
335
|
+
</ModalActions>
|
|
336
|
+
</Modal>
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
</div>
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function AvatarCell({ user }: { user: User }) {
|
|
344
|
+
const [url, setUrl] = useState<string | null>(null);
|
|
345
|
+
|
|
346
|
+
useEffect(() => {
|
|
347
|
+
const avatar = user.avatar;
|
|
348
|
+
if (typeof avatar === "string" && /^[0-9a-f-]+$/i.test(avatar)) {
|
|
349
|
+
apiGet<any>(`/api/media/${avatar}`)
|
|
350
|
+
.then((media) => setUrl(media?.thumbnailUrl || media?.url || null))
|
|
351
|
+
.catch(() => setUrl(null));
|
|
352
|
+
}
|
|
353
|
+
}, [user.avatar]);
|
|
354
|
+
|
|
355
|
+
if (url) {
|
|
356
|
+
return (
|
|
357
|
+
<div className="w-8 h-8 rounded-lg overflow-hidden border border-[var(--kyro-border)] flex-shrink-0">
|
|
358
|
+
<img src={url} alt="" className="w-full h-full object-cover" />
|
|
359
|
+
</div>
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return (
|
|
364
|
+
<div className="w-8 h-8 rounded-lg bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] flex items-center justify-center text-xs font-bold text-[var(--kyro-primary)] flex-shrink-0">
|
|
365
|
+
{user.name ? user.name[0] : user.email[0].toUpperCase()}
|
|
228
366
|
</div>
|
|
229
367
|
);
|
|
230
368
|
}
|
|
@@ -1,22 +1,41 @@
|
|
|
1
|
-
import React from "react";
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
2
|
import { Dropdown, DropdownItem, DropdownSeparator } from "./ui/Dropdown";
|
|
3
3
|
import { User, Shield, Key, Webhook, Clock, FileText, ExternalLink, HelpCircle, LogOut } from "./ui/icons";
|
|
4
|
+
import { useAuthStore } from "../lib/stores";
|
|
5
|
+
import { apiGet } from "../lib/api";
|
|
4
6
|
|
|
5
7
|
interface UserMenuProps {
|
|
6
8
|
adminPath: string;
|
|
7
9
|
}
|
|
8
10
|
|
|
9
11
|
export function UserMenu({ adminPath }: UserMenuProps) {
|
|
12
|
+
const currentUser = useAuthStore((s) => s.user);
|
|
13
|
+
const [avatarUrl, setAvatarUrl] = useState<string | null>(null);
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const avatar = currentUser?.avatar;
|
|
17
|
+
if (typeof avatar === "string" && /^[0-9a-f-]+$/i.test(avatar)) {
|
|
18
|
+
apiGet<any>(`/api/media/${avatar}`)
|
|
19
|
+
.then((media) => setAvatarUrl(media?.thumbnailUrl || media?.url || null))
|
|
20
|
+
.catch(() => setAvatarUrl(null));
|
|
21
|
+
} else {
|
|
22
|
+
setAvatarUrl(null);
|
|
23
|
+
}
|
|
24
|
+
}, [currentUser?.avatar]);
|
|
10
25
|
|
|
11
26
|
return (
|
|
12
27
|
<Dropdown
|
|
13
28
|
align="right"
|
|
14
29
|
trigger={
|
|
15
30
|
<div
|
|
16
|
-
className="flex justify-center p
|
|
31
|
+
className="flex justify-center p-.5 text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface)] rounded-xl transition-all shadow-sm active:scale-95"
|
|
17
32
|
title="Account"
|
|
18
33
|
>
|
|
19
|
-
|
|
34
|
+
{avatarUrl ? (
|
|
35
|
+
<img src={avatarUrl} alt="" className="w-8 h-8 rounded-full object-cover" />
|
|
36
|
+
) : (
|
|
37
|
+
<User className="w-4 h-4" strokeWidth={2.5} />
|
|
38
|
+
)}
|
|
20
39
|
</div>
|
|
21
40
|
}
|
|
22
41
|
>
|
|
@@ -28,7 +47,14 @@ export function UserMenu({ adminPath }: UserMenuProps) {
|
|
|
28
47
|
|
|
29
48
|
<DropdownItem
|
|
30
49
|
icon={<User className="w-4 h-4" />}
|
|
31
|
-
onClick={() =>
|
|
50
|
+
onClick={() => {
|
|
51
|
+
const id = currentUser?.id;
|
|
52
|
+
if (id) {
|
|
53
|
+
window.location.href = `${adminPath}/users/${id}`;
|
|
54
|
+
} else {
|
|
55
|
+
window.location.href = `${adminPath}/users`;
|
|
56
|
+
}
|
|
57
|
+
}}
|
|
32
58
|
>
|
|
33
59
|
Profile Settings
|
|
34
60
|
</DropdownItem>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { SlidePanel } from "./ui/SlidePanel";
|
|
3
|
-
import { Button } from "./ui/Button";
|
|
4
3
|
import { Spinner } from "./ui/Spinner";
|
|
4
|
+
import { History, Eye, GitCompare, Undo2, CheckCircle2, Clock, User } from "lucide-react";
|
|
5
5
|
|
|
6
6
|
interface Version {
|
|
7
7
|
id: string;
|
|
@@ -38,8 +38,15 @@ export function VersionHistoryPanel({
|
|
|
38
38
|
onCompare,
|
|
39
39
|
loading = false,
|
|
40
40
|
}: VersionHistoryPanelProps) {
|
|
41
|
-
const formatDate = (
|
|
42
|
-
|
|
41
|
+
const formatDate = (dateValue: any) => {
|
|
42
|
+
if (!dateValue) return "Unknown date";
|
|
43
|
+
const date = new Date(dateValue);
|
|
44
|
+
if (isNaN(date.getTime())) {
|
|
45
|
+
// Sometimes strings from DB like SQLite might need parsing
|
|
46
|
+
const parsed = Date.parse(dateValue);
|
|
47
|
+
if (!isNaN(parsed)) return new Date(parsed).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric", hour: "2-digit", minute: "2-digit" });
|
|
48
|
+
return "Invalid date";
|
|
49
|
+
}
|
|
43
50
|
return date.toLocaleDateString("en-US", {
|
|
44
51
|
month: "short",
|
|
45
52
|
day: "numeric",
|
|
@@ -49,10 +56,17 @@ export function VersionHistoryPanel({
|
|
|
49
56
|
});
|
|
50
57
|
};
|
|
51
58
|
|
|
52
|
-
const formatTimeAgo = (
|
|
53
|
-
|
|
59
|
+
const formatTimeAgo = (dateValue: any) => {
|
|
60
|
+
if (!dateValue) return "Unknown date";
|
|
61
|
+
const date = new Date(dateValue);
|
|
62
|
+
if (isNaN(date.getTime())) return formatDate(dateValue);
|
|
63
|
+
|
|
54
64
|
const now = new Date();
|
|
55
65
|
const diffMs = now.getTime() - date.getTime();
|
|
66
|
+
|
|
67
|
+
// If it's in the future (e.g. server clock skew), just say "Just now"
|
|
68
|
+
if (diffMs < 0) return "Just now";
|
|
69
|
+
|
|
56
70
|
const diffMins = Math.floor(diffMs / 60000);
|
|
57
71
|
const diffHours = Math.floor(diffMs / 3600000);
|
|
58
72
|
const diffDays = Math.floor(diffMs / 86400000);
|
|
@@ -61,7 +75,7 @@ export function VersionHistoryPanel({
|
|
|
61
75
|
if (diffMins < 60) return `${diffMins}m ago`;
|
|
62
76
|
if (diffHours < 24) return `${diffHours}h ago`;
|
|
63
77
|
if (diffDays < 7) return `${diffDays}d ago`;
|
|
64
|
-
return formatDate(
|
|
78
|
+
return formatDate(dateValue);
|
|
65
79
|
};
|
|
66
80
|
|
|
67
81
|
return (
|
|
@@ -76,128 +90,107 @@ export function VersionHistoryPanel({
|
|
|
76
90
|
<Spinner />
|
|
77
91
|
</div>
|
|
78
92
|
) : versions.length === 0 ? (
|
|
79
|
-
<div className="text-center py-
|
|
80
|
-
<
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
fill="none"
|
|
84
|
-
stroke="currentColor"
|
|
85
|
-
strokeWidth="1.5"
|
|
86
|
-
>
|
|
87
|
-
<circle cx="12" cy="12" r="10" />
|
|
88
|
-
<polyline points="12,6 12,12 16,14" />
|
|
89
|
-
</svg>
|
|
90
|
-
<p>No version history yet</p>
|
|
91
|
-
<p className="text-sm text-gray-400 mt-1">
|
|
92
|
-
Versions are created when you save changes
|
|
93
|
-
</p>
|
|
93
|
+
<div className="text-center flex flex-col items-center justify-center py-16 text-[var(--kyro-text-muted)]">
|
|
94
|
+
<History className="w-12 h-12 mb-4 opacity-20" />
|
|
95
|
+
<p className="font-medium text-[var(--kyro-text)]">No version history yet</p>
|
|
96
|
+
<p className="text-sm mt-1">Versions are automatically saved as you work.</p>
|
|
94
97
|
</div>
|
|
95
98
|
) : (
|
|
96
|
-
<div className="space-y-1">
|
|
97
|
-
{versions.map((version) =>
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
version.id
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
99
|
+
<div className="space-y-3 px-1 pb-4 pt-1">
|
|
100
|
+
{versions.map((version) => {
|
|
101
|
+
const isCurrent = version.id === currentVersionId;
|
|
102
|
+
return (
|
|
103
|
+
<div
|
|
104
|
+
key={version.id}
|
|
105
|
+
className={`p-4 rounded-xl border transition-all duration-200 group relative overflow-hidden ${
|
|
106
|
+
isCurrent
|
|
107
|
+
? "border-[var(--kyro-primary)] bg-[var(--kyro-primary)]/5 shadow-sm"
|
|
108
|
+
: "border-[var(--kyro-border)] hover:border-[var(--kyro-primary)]/30 hover:bg-[var(--kyro-surface-accent)] hover:shadow-sm bg-[var(--kyro-surface)]"
|
|
109
|
+
}`}
|
|
110
|
+
>
|
|
111
|
+
{isCurrent && (
|
|
112
|
+
<div className="absolute top-0 left-0 w-1 h-full bg-[var(--kyro-primary)] shadow-[0_0_8px_var(--kyro-primary)]" />
|
|
113
|
+
)}
|
|
114
|
+
|
|
115
|
+
<div className="flex items-start justify-between gap-4">
|
|
116
|
+
<div className="flex-1 min-w-0">
|
|
117
|
+
<div className="flex items-center gap-2 mb-2">
|
|
118
|
+
<span
|
|
119
|
+
className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[10px] font-bold tracking-wide uppercase ${
|
|
120
|
+
version.status === "published"
|
|
121
|
+
? "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400"
|
|
122
|
+
: "bg-zinc-500/10 text-zinc-600 dark:text-zinc-400"
|
|
123
|
+
}`}
|
|
124
|
+
>
|
|
125
|
+
{version.status === "published" && <CheckCircle2 className="w-3 h-3" />}
|
|
126
|
+
{version.status === "published" ? "Published" : "Draft"}
|
|
127
|
+
</span>
|
|
128
|
+
<span className="text-xs font-semibold text-[var(--kyro-text)] px-2 py-0.5 rounded-md bg-[var(--kyro-surface-accent)]">
|
|
129
|
+
v{version.version}
|
|
130
|
+
</span>
|
|
131
|
+
{isCurrent && (
|
|
132
|
+
<span className="text-[10px] font-medium text-[var(--kyro-primary)] flex items-center gap-1">
|
|
133
|
+
<span className="w-1.5 h-1.5 rounded-full bg-[var(--kyro-primary)] animate-pulse" />
|
|
134
|
+
Current
|
|
135
|
+
</span>
|
|
136
|
+
)}
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<div className="flex items-center gap-1.5 text-sm font-medium text-[var(--kyro-text)] truncate mb-1">
|
|
140
|
+
<Clock className="w-3.5 h-3.5 text-[var(--kyro-text-muted)]" />
|
|
141
|
+
{formatTimeAgo(version.createdAt)}
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
{version.createdBy && (
|
|
145
|
+
<div className="flex items-center gap-1.5 text-xs text-[var(--kyro-text-muted)] mt-1.5">
|
|
146
|
+
<User className="w-3.5 h-3.5" />
|
|
147
|
+
<span>{version.createdBy.name || version.createdBy.email}</span>
|
|
148
|
+
</div>
|
|
149
|
+
)}
|
|
150
|
+
|
|
151
|
+
{version.changelog && (
|
|
152
|
+
<p className="text-xs text-[var(--kyro-text-secondary)] mt-2 italic border-l-2 border-[var(--kyro-border)] pl-2">
|
|
153
|
+
"{version.changelog}"
|
|
154
|
+
</p>
|
|
155
|
+
)}
|
|
121
156
|
</div>
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
</p>
|
|
125
|
-
{version.createdBy && (
|
|
126
|
-
<p className="text-xs text-gray-400 mt-0.5">
|
|
127
|
-
by {version.createdBy.name || version.createdBy.email}
|
|
128
|
-
</p>
|
|
129
|
-
)}
|
|
130
|
-
{version.changelog && (
|
|
131
|
-
<p className="text-xs text-gray-500 mt-1 truncate">
|
|
132
|
-
{version.changelog}
|
|
133
|
-
</p>
|
|
134
|
-
)}
|
|
135
|
-
</div>
|
|
136
|
-
<div className="flex items-center gap-1 ml-2">
|
|
137
|
-
<button type="button"
|
|
138
|
-
onClick={() => onPreview(version)}
|
|
139
|
-
className="p-1.5 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded transition-colors"
|
|
140
|
-
title="Preview this version"
|
|
141
|
-
>
|
|
142
|
-
<svg
|
|
143
|
-
width="14"
|
|
144
|
-
height="14"
|
|
145
|
-
viewBox="0 0 24 24"
|
|
146
|
-
fill="none"
|
|
147
|
-
stroke="currentColor"
|
|
148
|
-
strokeWidth="2"
|
|
149
|
-
>
|
|
150
|
-
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
|
|
151
|
-
<circle cx="12" cy="12" r="3" />
|
|
152
|
-
</svg>
|
|
153
|
-
</button>
|
|
154
|
-
{onCompare && (
|
|
157
|
+
|
|
158
|
+
<div className="flex flex-col sm:flex-row items-center gap-1.5 opacity-100 sm:opacity-0 sm:group-hover:opacity-100 transition-opacity">
|
|
155
159
|
<button type="button"
|
|
156
|
-
onClick={() =>
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
versions.find((v) => v.id === currentVersionId) ||
|
|
160
|
-
version,
|
|
161
|
-
)
|
|
162
|
-
}
|
|
163
|
-
className="p-1.5 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded transition-colors"
|
|
164
|
-
title="Compare with current"
|
|
160
|
+
onClick={() => onPreview(version)}
|
|
161
|
+
className="p-2 text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-primary)] hover:bg-[var(--kyro-primary)]/10 rounded-md transition-colors"
|
|
162
|
+
title="Preview this version"
|
|
165
163
|
>
|
|
166
|
-
<
|
|
167
|
-
width="14"
|
|
168
|
-
height="14"
|
|
169
|
-
viewBox="0 0 24 24"
|
|
170
|
-
fill="none"
|
|
171
|
-
stroke="currentColor"
|
|
172
|
-
strokeWidth="2"
|
|
173
|
-
>
|
|
174
|
-
<path d="M16 3h5v5M8 3H3v5M3 16v5h5M21 16v5h-5M3 12h18" />
|
|
175
|
-
</svg>
|
|
164
|
+
<Eye className="w-4 h-4" />
|
|
176
165
|
</button>
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
viewBox="0 0 24 24"
|
|
188
|
-
fill="none"
|
|
189
|
-
stroke="currentColor"
|
|
190
|
-
strokeWidth="2"
|
|
166
|
+
{onCompare && (
|
|
167
|
+
<button type="button"
|
|
168
|
+
onClick={() =>
|
|
169
|
+
onCompare(
|
|
170
|
+
version,
|
|
171
|
+
versions.find((v) => v.id === currentVersionId) || version,
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
className="p-2 text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-primary)] hover:bg-[var(--kyro-primary)]/10 rounded-md transition-colors"
|
|
175
|
+
title="Compare with current"
|
|
191
176
|
>
|
|
192
|
-
<
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
177
|
+
<GitCompare className="w-4 h-4" />
|
|
178
|
+
</button>
|
|
179
|
+
)}
|
|
180
|
+
{!isCurrent && (
|
|
181
|
+
<button type="button"
|
|
182
|
+
onClick={() => onRestore(version)}
|
|
183
|
+
className="p-2 text-amber-600 hover:bg-amber-500/10 rounded-md transition-colors"
|
|
184
|
+
title="Restore this version"
|
|
185
|
+
>
|
|
186
|
+
<Undo2 className="w-4 h-4" />
|
|
187
|
+
</button>
|
|
188
|
+
)}
|
|
189
|
+
</div>
|
|
197
190
|
</div>
|
|
198
191
|
</div>
|
|
199
|
-
|
|
200
|
-
)
|
|
192
|
+
);
|
|
193
|
+
})}
|
|
201
194
|
</div>
|
|
202
195
|
)}
|
|
203
196
|
</SlidePanel>
|
|
@@ -48,7 +48,7 @@ export function WebhookManager() {
|
|
|
48
48
|
endpoint: "/api/webhooks",
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
-
const {
|
|
51
|
+
const { confirm } = useUIStore();
|
|
52
52
|
const [showTestModal, setShowTestModal] = useState(false);
|
|
53
53
|
const [showHelpModal, setShowHelpModal] = useState(false);
|
|
54
54
|
const [testResult, setTestResult] = useState<{
|
|
@@ -76,6 +76,7 @@ export function WebhookManager() {
|
|
|
76
76
|
toast.success(`Webhook established: ${formData.name}`);
|
|
77
77
|
} catch (e) {
|
|
78
78
|
setCreateError("Failed to create webhook");
|
|
79
|
+
toast.error("Failed to create webhook");
|
|
79
80
|
}
|
|
80
81
|
};
|
|
81
82
|
|
|
@@ -105,6 +106,7 @@ export function WebhookManager() {
|
|
|
105
106
|
toast.success(newStatus === "active" ? "Signals resumed" : "Dispatcher paused");
|
|
106
107
|
} catch (e) {
|
|
107
108
|
console.error(e);
|
|
109
|
+
toast.error("Failed to toggle webhook status");
|
|
108
110
|
}
|
|
109
111
|
};
|
|
110
112
|
|
|
@@ -214,7 +216,7 @@ export function WebhookManager() {
|
|
|
214
216
|
<button
|
|
215
217
|
type="button"
|
|
216
218
|
onClick={() => setShowCreateModal(true)}
|
|
217
|
-
className="inline-flex items-center gap-3 px-8 py-4
|
|
219
|
+
className="kyro-btn kyro-btn-primary inline-flex items-center gap-3 px-8 py-4 rounded-2xl font-bold hover:scale-[1.05] transition-all shadow-xl shadow-[var(--kyro-primary)]/10"
|
|
218
220
|
>
|
|
219
221
|
<Plus className="w-5 h-5" />
|
|
220
222
|
Configure Webhook
|
|
@@ -413,7 +415,7 @@ export function WebhookManager() {
|
|
|
413
415
|
<button
|
|
414
416
|
type="button"
|
|
415
417
|
onClick={handleCreate}
|
|
416
|
-
className="px-6 py-2.5 rounded-xl font-bold text-sm
|
|
418
|
+
className="kyro-btn kyro-btn-primary px-6 py-2.5 rounded-xl font-bold text-sm hover:opacity-90 transition-all shadow-lg shadow-[var(--kyro-primary)]/10"
|
|
417
419
|
>
|
|
418
420
|
Create Webhook
|
|
419
421
|
</button>
|
|
@@ -522,7 +524,7 @@ export function WebhookManager() {
|
|
|
522
524
|
<button
|
|
523
525
|
type="button"
|
|
524
526
|
onClick={() => setShowHelpModal(false)}
|
|
525
|
-
className="w-full py-3 rounded-xl font-bold text-sm
|
|
527
|
+
className="kyro-btn kyro-btn-primary w-full py-3 rounded-xl font-bold text-sm hover:opacity-90 transition-all shadow-lg shadow-[var(--kyro-primary)]/20"
|
|
526
528
|
>
|
|
527
529
|
I Understand
|
|
528
530
|
</button>
|