@kyro-cms/admin 0.12.3 → 0.12.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/dist/index.cjs +22 -22
- package/dist/index.js +17 -17
- package/package.json +2 -2
- package/src/components/DetailView.tsx +88 -16
- package/src/components/WebhookManager.tsx +134 -7
- package/src/components/autoform/AutoFormEditView.tsx +2 -2
- package/src/components/ui/PageHeader.tsx +5 -6
- package/src/lib/autoform-store.ts +4 -0
package/package.json
CHANGED
|
@@ -10,6 +10,7 @@ import { ActionBar, type DocumentStatus, type SaveStatus } from "./ActionBar";
|
|
|
10
10
|
import { Spinner } from "./ui/Spinner";
|
|
11
11
|
import { Shimmer } from "./ui/Shimmer";
|
|
12
12
|
import { useUIStore, toast } from "../lib/stores";
|
|
13
|
+
import { useAutoFormStore } from "../lib/autoform-store";
|
|
13
14
|
import { PageHeader } from "./ui/PageHeader";
|
|
14
15
|
import { Badge } from "./ui/Badge";
|
|
15
16
|
import { SplitButton } from "./ui/SplitButton";
|
|
@@ -53,6 +54,10 @@ export function DetailView({
|
|
|
53
54
|
const [updatedAt, setUpdatedAt] = useState<string | null>(null);
|
|
54
55
|
const [publishedAt, setPublishedAt] = useState<string | null>(null);
|
|
55
56
|
const [justSaved, setJustSaved] = useState(false);
|
|
57
|
+
const showPreview = useAutoFormStore((state) => state.showPreview);
|
|
58
|
+
const setShowPreview = useAutoFormStore((state) => state.setShowPreview);
|
|
59
|
+
const previewUrl = useAutoFormStore((state) => state.previewUrl);
|
|
60
|
+
const setPreviewUrl = useAutoFormStore((state) => state.setPreviewUrl);
|
|
56
61
|
|
|
57
62
|
const fields = global?.fields || collection?.fields || [];
|
|
58
63
|
const label = global?.label || collection?.label || "Document";
|
|
@@ -162,7 +167,12 @@ export function DetailView({
|
|
|
162
167
|
setTimeout(() => {
|
|
163
168
|
setSaveStatus("idle");
|
|
164
169
|
}, 2000);
|
|
165
|
-
|
|
170
|
+
|
|
171
|
+
if (showPreview) {
|
|
172
|
+
refreshPreviewUrl(savedData);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
} catch (e: any) {
|
|
166
176
|
setSaveStatus("error");
|
|
167
177
|
if (!isAutosave) {
|
|
168
178
|
onError("Failed to save changes");
|
|
@@ -284,7 +294,37 @@ export function DetailView({
|
|
|
284
294
|
}
|
|
285
295
|
});
|
|
286
296
|
};
|
|
297
|
+
const refreshPreviewUrl = async (docData: any = data) => {
|
|
298
|
+
try {
|
|
299
|
+
const endpoint = mode === "global" ? `/api/globals/${slug}/preview-url` : `/api/${slug}/preview-url`;
|
|
300
|
+
console.log("[Kyro Preview] Calling endpoint:", endpoint, "with data keys:", Object.keys(docData || {}), "documentId:", documentId);
|
|
301
|
+
const res = await apiPost(endpoint, { ...docData, id: documentId }, { autoToast: false });
|
|
302
|
+
console.log("[Kyro Preview] Response:", JSON.stringify(res));
|
|
303
|
+
if (res && (res as any).url) {
|
|
304
|
+
console.log("[Kyro Preview] Setting previewUrl:", (res as any).url);
|
|
305
|
+
setPreviewUrl((res as any).url);
|
|
306
|
+
} else {
|
|
307
|
+
console.warn("[Kyro Preview] No url in response:", res);
|
|
308
|
+
}
|
|
309
|
+
} catch (e: any) {
|
|
310
|
+
console.error("[Kyro Preview] Error:", e.message, e);
|
|
311
|
+
toast.error(e.message || "Failed to generate preview URL");
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
useEffect(() => {
|
|
316
|
+
if (showPreview) {
|
|
317
|
+
refreshPreviewUrl(data);
|
|
318
|
+
}
|
|
319
|
+
}, [showPreview]);
|
|
287
320
|
|
|
321
|
+
const togglePreview = () => {
|
|
322
|
+
const nextState = !showPreview;
|
|
323
|
+
setShowPreview(nextState);
|
|
324
|
+
if (nextState) {
|
|
325
|
+
refreshPreviewUrl(data);
|
|
326
|
+
}
|
|
327
|
+
};
|
|
288
328
|
if (loading) {
|
|
289
329
|
return (
|
|
290
330
|
<div className="kyro-detail">
|
|
@@ -344,9 +384,7 @@ export function DetailView({
|
|
|
344
384
|
onViewHistory={() => {
|
|
345
385
|
window.dispatchEvent(new CustomEvent('kyro:show-version-history'));
|
|
346
386
|
}}
|
|
347
|
-
onPreview={
|
|
348
|
-
window.open(`/preview/${slug}/${documentId}`, "_blank")
|
|
349
|
-
}
|
|
387
|
+
onPreview={togglePreview}
|
|
350
388
|
onDelete={handleDeleteTrigger}
|
|
351
389
|
onBack={onBack}
|
|
352
390
|
onToggleSidebar={() => window.dispatchEvent(new CustomEvent("toggle-sidebar"))}
|
|
@@ -356,12 +394,14 @@ export function DetailView({
|
|
|
356
394
|
|
|
357
395
|
<div
|
|
358
396
|
className={
|
|
359
|
-
|
|
360
|
-
? "w-full pt-4 md:pt-
|
|
361
|
-
:
|
|
397
|
+
showPreview
|
|
398
|
+
? "w-full mx-auto grid grid-cols-1 lg:grid-cols-2 gap-4 md:gap-8 pt-4 md:pt-0 h-[calc(100vh-140px)]"
|
|
399
|
+
: isSingleLayout
|
|
400
|
+
? "w-full pt-4 md:pt-8"
|
|
401
|
+
: "w-full mx-auto grid grid-cols-1 lg:grid-cols-[1fr_360px] gap-4 md:gap-8 pt-4 md:pt-0"
|
|
362
402
|
}
|
|
363
403
|
>
|
|
364
|
-
<div className=
|
|
404
|
+
<div className={`space-y-4 md:space-y-8 min-w-0 ${showPreview ? "overflow-y-auto pr-2 pb-20" : ""}`}>
|
|
365
405
|
<div className="surface-tile p-4 md:p-8">
|
|
366
406
|
<div className="flex items-center justify-between mb-8 px-1">
|
|
367
407
|
<h2 className="text-[10px] font-bold tracking-[0.2em] opacity-40">
|
|
@@ -409,7 +449,37 @@ export function DetailView({
|
|
|
409
449
|
</div>
|
|
410
450
|
</div>
|
|
411
451
|
|
|
412
|
-
{
|
|
452
|
+
{showPreview && (
|
|
453
|
+
<div className="surface-tile flex flex-col overflow-hidden h-full border border-[var(--kyro-border)] rounded-xl animate-in fade-in slide-in-from-right-4 duration-500 shadow-xl bg-white dark:bg-black">
|
|
454
|
+
<div className="bg-[var(--kyro-surface-accent)] border-b border-[var(--kyro-border)] p-2 flex items-center justify-between shrink-0">
|
|
455
|
+
<div className="flex items-center gap-2 pl-2">
|
|
456
|
+
<div className="w-3 h-3 rounded-full bg-red-400/80"></div>
|
|
457
|
+
<div className="w-3 h-3 rounded-full bg-amber-400/80"></div>
|
|
458
|
+
<div className="w-3 h-3 rounded-full bg-green-400/80"></div>
|
|
459
|
+
</div>
|
|
460
|
+
<div className="text-[10px] font-mono opacity-50 truncate max-w-[60%] bg-[var(--kyro-bg)] px-3 py-1 rounded">
|
|
461
|
+
{previewUrl || "Generating preview URL..."}
|
|
462
|
+
</div>
|
|
463
|
+
<div className="w-16"></div>
|
|
464
|
+
</div>
|
|
465
|
+
<div className="flex-1 bg-white relative">
|
|
466
|
+
{!previewUrl ? (
|
|
467
|
+
<div className="absolute inset-0 flex items-center justify-center">
|
|
468
|
+
<Spinner className="w-6 h-6 text-[var(--kyro-primary)]" />
|
|
469
|
+
</div>
|
|
470
|
+
) : (
|
|
471
|
+
<iframe
|
|
472
|
+
src={previewUrl}
|
|
473
|
+
className="w-full h-full border-0 bg-white"
|
|
474
|
+
title="Preview"
|
|
475
|
+
sandbox="allow-scripts allow-same-origin allow-popups allow-forms"
|
|
476
|
+
/>
|
|
477
|
+
)}
|
|
478
|
+
</div>
|
|
479
|
+
</div>
|
|
480
|
+
)}
|
|
481
|
+
|
|
482
|
+
{!isSingleLayout && !showPreview && (
|
|
413
483
|
<div className="space-y-4 md:space-y-6 animate-in fade-in slide-in-from-right-4 duration-500">
|
|
414
484
|
<div className="surface-tile p-4 md:p-8">
|
|
415
485
|
<h3 className="text-[10px] font-bold tracking-[0.2em] opacity-40 mb-4 md:mb-6">
|
|
@@ -483,13 +553,15 @@ export function DetailView({
|
|
|
483
553
|
>
|
|
484
554
|
{isDuplicating ? "Duplicating..." : "Duplicate Document"}
|
|
485
555
|
</button>
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
556
|
+
{previewUrl && (
|
|
557
|
+
<button
|
|
558
|
+
type="button"
|
|
559
|
+
onClick={() => window.open(previewUrl, "_blank")}
|
|
560
|
+
className="kyro-btn kyro-btn-sm kyro-btn-ghost w-full justify-start"
|
|
561
|
+
>
|
|
562
|
+
Open Preview in New Tab
|
|
563
|
+
</button>
|
|
564
|
+
)}
|
|
493
565
|
<button
|
|
494
566
|
type="button"
|
|
495
567
|
onClick={handleDeleteTrigger}
|
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
ExternalLink,
|
|
18
18
|
Zap,
|
|
19
19
|
Shield,
|
|
20
|
+
Activity,
|
|
21
|
+
XCircle,
|
|
20
22
|
} from "./ui/icons";
|
|
21
23
|
import { useUIStore, toast } from "../lib/stores";
|
|
22
24
|
import { Modal, ModalContent, ModalActions } from "./ui/Modal";
|
|
@@ -52,14 +54,25 @@ interface WebhookItem {
|
|
|
52
54
|
action?: string;
|
|
53
55
|
config?: Record<string, string>;
|
|
54
56
|
lastTriggered?: string;
|
|
57
|
+
lastError?: string;
|
|
58
|
+
createdAt: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface WebhookDelivery {
|
|
62
|
+
id: string;
|
|
63
|
+
webhookId: string;
|
|
64
|
+
event: string;
|
|
65
|
+
status: "pending" | "success" | "failed" | "retrying";
|
|
66
|
+
responseStatus?: number;
|
|
67
|
+
responseBody?: string;
|
|
68
|
+
error?: string;
|
|
69
|
+
duration?: number;
|
|
55
70
|
createdAt: string;
|
|
56
71
|
}
|
|
57
72
|
|
|
58
73
|
const ACTION_ICONS: Record<string, string> = {
|
|
59
74
|
generic: "🔗",
|
|
60
|
-
"github-
|
|
61
|
-
"dokploy-deploy": "🚀",
|
|
62
|
-
"coolify-deploy": "🎯",
|
|
75
|
+
"github-push": "⚙️",
|
|
63
76
|
};
|
|
64
77
|
|
|
65
78
|
export function WebhookManager() {
|
|
@@ -97,6 +110,11 @@ export function WebhookManager() {
|
|
|
97
110
|
});
|
|
98
111
|
const [createError, setCreateError] = useState("");
|
|
99
112
|
|
|
113
|
+
const [showHistoryModal, setShowHistoryModal] = useState(false);
|
|
114
|
+
const [selectedWebhookId, setSelectedWebhookId] = useState<string | null>(null);
|
|
115
|
+
const [deliveryHistory, setDeliveryHistory] = useState<WebhookDelivery[]>([]);
|
|
116
|
+
const [loadingHistory, setLoadingHistory] = useState(false);
|
|
117
|
+
|
|
100
118
|
useEffect(() => {
|
|
101
119
|
loadActions();
|
|
102
120
|
}, []);
|
|
@@ -168,9 +186,10 @@ export function WebhookManager() {
|
|
|
168
186
|
await create(formData);
|
|
169
187
|
resetCreateModal();
|
|
170
188
|
toast.success(`Webhook created: ${formData.name}`);
|
|
171
|
-
} catch (e) {
|
|
172
|
-
|
|
173
|
-
|
|
189
|
+
} catch (e: any) {
|
|
190
|
+
const errorMsg = e.message || "Failed to create webhook";
|
|
191
|
+
setCreateError(errorMsg);
|
|
192
|
+
toast.error(errorMsg);
|
|
174
193
|
}
|
|
175
194
|
};
|
|
176
195
|
|
|
@@ -204,6 +223,21 @@ export function WebhookManager() {
|
|
|
204
223
|
}
|
|
205
224
|
};
|
|
206
225
|
|
|
226
|
+
const handleViewHistory = async (id: string) => {
|
|
227
|
+
setSelectedWebhookId(id);
|
|
228
|
+
setShowHistoryModal(true);
|
|
229
|
+
setLoadingHistory(true);
|
|
230
|
+
try {
|
|
231
|
+
const data = await apiGet<{ docs: WebhookDelivery[] }>(`/api/webhooks/${id}/history`);
|
|
232
|
+
setDeliveryHistory(data.docs || []);
|
|
233
|
+
} catch (e) {
|
|
234
|
+
console.error("Failed to load history:", e);
|
|
235
|
+
toast.error("Failed to load delivery history");
|
|
236
|
+
} finally {
|
|
237
|
+
setLoadingHistory(false);
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
|
|
207
241
|
const eventOptions = [
|
|
208
242
|
{
|
|
209
243
|
label: "Create",
|
|
@@ -359,11 +393,29 @@ export function WebhookManager() {
|
|
|
359
393
|
? `Last triggered ${new Date(webhook.lastTriggered).toLocaleDateString()}`
|
|
360
394
|
: "Never triggered"}
|
|
361
395
|
</div>
|
|
396
|
+
{webhook.lastError && (
|
|
397
|
+
<div className="text-[10px] text-red-500/80 flex items-center gap-1.5 mt-1">
|
|
398
|
+
<XCircle className="w-3 h-3" />
|
|
399
|
+
<span className="truncate max-w-[150px]" title={webhook.lastError}>
|
|
400
|
+
{webhook.lastError}
|
|
401
|
+
</span>
|
|
402
|
+
</div>
|
|
403
|
+
)}
|
|
362
404
|
</div>
|
|
363
405
|
</div>
|
|
364
406
|
</div>
|
|
365
407
|
|
|
366
408
|
<div className="flex items-center gap-1.5 lg:opacity-0 group-hover:opacity-100 transition-opacity duration-200">
|
|
409
|
+
<button
|
|
410
|
+
type="button"
|
|
411
|
+
onClick={() => handleViewHistory(webhook.id)}
|
|
412
|
+
className="p-2 rounded-lg border border-[var(--kyro-border)] hover:border-[var(--kyro-primary)] transition-colors flex items-center gap-1.5"
|
|
413
|
+
title="View delivery history"
|
|
414
|
+
>
|
|
415
|
+
<Activity className="w-3.5 h-3.5" />
|
|
416
|
+
<span className="text-[11px] font-medium hidden sm:inline">History</span>
|
|
417
|
+
</button>
|
|
418
|
+
|
|
367
419
|
<button
|
|
368
420
|
type="button"
|
|
369
421
|
onClick={() => handleTest(webhook.id)}
|
|
@@ -418,7 +470,7 @@ export function WebhookManager() {
|
|
|
418
470
|
type="button"
|
|
419
471
|
key={key}
|
|
420
472
|
onClick={() => handleActionSelect(key)}
|
|
421
|
-
className="flex flex-col items-start p-4 rounded-
|
|
473
|
+
className="flex flex-col items-start p-4 rounded-md border border-[var(--kyro-border)] bg-[var(--kyro-surface)] hover:border-[var(--kyro-primary)]/40 transition-all text-left group"
|
|
422
474
|
>
|
|
423
475
|
<div className="text-lg mb-2">{ACTION_ICONS[key] || "🔗"}</div>
|
|
424
476
|
<h3 className="text-sm font-medium mb-0.5 group-hover:text-[var(--kyro-primary)] transition-colors">
|
|
@@ -680,6 +732,81 @@ export function WebhookManager() {
|
|
|
680
732
|
</button>
|
|
681
733
|
</ModalActions>
|
|
682
734
|
</Modal>
|
|
735
|
+
|
|
736
|
+
{/* History Modal */}
|
|
737
|
+
<Modal
|
|
738
|
+
open={showHistoryModal}
|
|
739
|
+
onClose={() => setShowHistoryModal(false)}
|
|
740
|
+
title="Delivery History"
|
|
741
|
+
>
|
|
742
|
+
<ModalContent>
|
|
743
|
+
<div className="space-y-4">
|
|
744
|
+
{loadingHistory ? (
|
|
745
|
+
<div className="flex items-center justify-center p-12 opacity-50">
|
|
746
|
+
<RefreshCw className="w-5 h-5 animate-spin text-[var(--kyro-primary)]" />
|
|
747
|
+
</div>
|
|
748
|
+
) : deliveryHistory.length === 0 ? (
|
|
749
|
+
<div className="text-center p-8 text-[var(--kyro-text-secondary)] opacity-60 text-sm">
|
|
750
|
+
No delivery history available for this webhook.
|
|
751
|
+
</div>
|
|
752
|
+
) : (
|
|
753
|
+
<div className="space-y-3 max-h-[60vh] overflow-y-auto pr-1">
|
|
754
|
+
{deliveryHistory.map((delivery) => (
|
|
755
|
+
<div key={delivery.id} className="p-4 rounded-md border border-[var(--kyro-border)] bg-[var(--kyro-surface)] text-sm">
|
|
756
|
+
<div className="flex items-center justify-between mb-2">
|
|
757
|
+
<div className="flex items-center gap-2">
|
|
758
|
+
{delivery.status === "success" ? (
|
|
759
|
+
<CheckCircle2 className="w-4 h-4 text-green-500" />
|
|
760
|
+
) : (
|
|
761
|
+
<XCircle className="w-4 h-4 text-red-500" />
|
|
762
|
+
)}
|
|
763
|
+
<span className="font-medium">{delivery.event}</span>
|
|
764
|
+
</div>
|
|
765
|
+
<span className="text-[10px] text-[var(--kyro-text-secondary)] opacity-50">
|
|
766
|
+
{new Date(delivery.createdAt).toLocaleString()}
|
|
767
|
+
</span>
|
|
768
|
+
</div>
|
|
769
|
+
|
|
770
|
+
<div className="grid grid-cols-2 gap-4 text-xs text-[var(--kyro-text-secondary)] mb-3">
|
|
771
|
+
<div>
|
|
772
|
+
<span className="opacity-50">Status: </span>
|
|
773
|
+
<span className={`font-medium ${delivery.status === "success" ? "text-green-500" : "text-red-500"}`}>
|
|
774
|
+
{delivery.responseStatus || delivery.status}
|
|
775
|
+
</span>
|
|
776
|
+
</div>
|
|
777
|
+
<div>
|
|
778
|
+
<span className="opacity-50">Duration: </span>
|
|
779
|
+
{delivery.duration ? `${delivery.duration}ms` : "-"}
|
|
780
|
+
</div>
|
|
781
|
+
</div>
|
|
782
|
+
|
|
783
|
+
{delivery.error && (
|
|
784
|
+
<div className="text-xs text-red-500 bg-red-500/5 p-2 rounded border border-red-500/10 font-mono break-all mb-2">
|
|
785
|
+
{delivery.error}
|
|
786
|
+
</div>
|
|
787
|
+
)}
|
|
788
|
+
|
|
789
|
+
{delivery.responseBody && (
|
|
790
|
+
<div className="text-[10px] font-mono bg-[var(--kyro-bg)] p-2 rounded border border-[var(--kyro-border)] text-[var(--kyro-text-secondary)] opacity-70 max-h-24 overflow-y-auto break-all">
|
|
791
|
+
{delivery.responseBody}
|
|
792
|
+
</div>
|
|
793
|
+
)}
|
|
794
|
+
</div>
|
|
795
|
+
))}
|
|
796
|
+
</div>
|
|
797
|
+
)}
|
|
798
|
+
</div>
|
|
799
|
+
</ModalContent>
|
|
800
|
+
<ModalActions>
|
|
801
|
+
<button
|
|
802
|
+
type="button"
|
|
803
|
+
onClick={() => setShowHistoryModal(false)}
|
|
804
|
+
className="w-full py-2.5 rounded-lg text-sm font-medium bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] hover:bg-[var(--kyro-surface)] transition-colors"
|
|
805
|
+
>
|
|
806
|
+
Close
|
|
807
|
+
</button>
|
|
808
|
+
</ModalActions>
|
|
809
|
+
</Modal>
|
|
683
810
|
</div>
|
|
684
811
|
);
|
|
685
812
|
}
|
|
@@ -15,7 +15,7 @@ export function AutoFormEditView({
|
|
|
15
15
|
collectionSlug,
|
|
16
16
|
renderField,
|
|
17
17
|
}: AutoFormEditViewProps) {
|
|
18
|
-
const { showPreview, sidebarCollapsed, formData } = useAutoFormStore();
|
|
18
|
+
const { showPreview, sidebarCollapsed, formData, previewUrl } = useAutoFormStore();
|
|
19
19
|
|
|
20
20
|
if (layout === "single") {
|
|
21
21
|
return (
|
|
@@ -65,7 +65,7 @@ export function AutoFormEditView({
|
|
|
65
65
|
</span>
|
|
66
66
|
</div>
|
|
67
67
|
<iframe
|
|
68
|
-
src={
|
|
68
|
+
src={previewUrl || ""}
|
|
69
69
|
className="w-full h-full border-none"
|
|
70
70
|
title="Live Preview"
|
|
71
71
|
/>
|
|
@@ -96,13 +96,12 @@ function ActionsSlot({ actions }: { actions: NonNullable<PageHeaderProps["action
|
|
|
96
96
|
type="button"
|
|
97
97
|
onClick={act.onClick}
|
|
98
98
|
disabled={act.disabled}
|
|
99
|
-
className={`flex items-center gap-2 px-6 py-2.5 rounded-xl font-bold text-sm transition-all
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
: act.variant === "ghost"
|
|
99
|
+
className={`flex items-center gap-2 px-6 py-2.5 rounded-xl font-bold text-sm transition-all ${act.variant === "outline"
|
|
100
|
+
? "border border-[var(--kyro-border)] text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)]"
|
|
101
|
+
: act.variant === "ghost"
|
|
103
102
|
? "text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)] shadow-none"
|
|
104
103
|
: "kyro-btn-primary hover:opacity-90"
|
|
105
|
-
|
|
104
|
+
} ${act.disabled ? "opacity-50 cursor-wait pointer-events-none" : ""} ${act.className || ""}`}
|
|
106
105
|
>
|
|
107
106
|
{act.icon && <act.icon className="w-4 h-4" />}
|
|
108
107
|
{act.label}
|
|
@@ -120,7 +119,7 @@ function SingleAction({ action }: { action: NonNullable<PageHeaderProps["action"
|
|
|
120
119
|
type="button"
|
|
121
120
|
onClick={action.onClick}
|
|
122
121
|
disabled={action.disabled}
|
|
123
|
-
className={`kyro-btn kyro-btn-primary flex items-center gap-2 px-6 py-2.5 rounded-xl font-bold text-sm hover:opacity-90 transition-all
|
|
122
|
+
className={`kyro-btn kyro-btn-primary flex items-center gap-2 px-6 py-2.5 rounded-xl font-bold text-sm hover:opacity-90 transition-all w-full lg:w-auto justify-center ${action.disabled ? "opacity-50 cursor-wait pointer-events-none" : ""} ${action.className || ""}`}
|
|
124
123
|
>
|
|
125
124
|
{action.icon && <action.icon className="w-4 h-4" />}
|
|
126
125
|
{action.label}
|
|
@@ -87,6 +87,7 @@ interface AutoFormStore {
|
|
|
87
87
|
versions: Version[];
|
|
88
88
|
loadingVersions: boolean;
|
|
89
89
|
showPreview: boolean;
|
|
90
|
+
previewUrl: string | null;
|
|
90
91
|
isMenuOpen: boolean;
|
|
91
92
|
hasUnsavedChanges: boolean;
|
|
92
93
|
loadingFields: Record<string, boolean>;
|
|
@@ -118,6 +119,7 @@ interface AutoFormStore {
|
|
|
118
119
|
setVersions: (versions: Version[]) => void;
|
|
119
120
|
setLoadingVersions: (loading: boolean) => void;
|
|
120
121
|
setShowPreview: (show: boolean | ((prev: boolean) => boolean)) => void;
|
|
122
|
+
setPreviewUrl: (url: string | null) => void;
|
|
121
123
|
setIsMenuOpen: (open: boolean | ((prev: boolean) => boolean)) => void;
|
|
122
124
|
setHasUnsavedChanges: (hasChanges: boolean) => void;
|
|
123
125
|
setLoadingFields: (fields: Record<string, boolean> | ((prev: Record<string, boolean>) => Record<string, boolean>)) => void;
|
|
@@ -187,6 +189,7 @@ export const useAutoFormStore = create<AutoFormStore>()(
|
|
|
187
189
|
versions: [],
|
|
188
190
|
loadingVersions: false,
|
|
189
191
|
showPreview: false,
|
|
192
|
+
previewUrl: null,
|
|
190
193
|
isMenuOpen: false,
|
|
191
194
|
hasUnsavedChanges: false,
|
|
192
195
|
loadingFields: {},
|
|
@@ -275,6 +278,7 @@ export const useAutoFormStore = create<AutoFormStore>()(
|
|
|
275
278
|
showPreview:
|
|
276
279
|
typeof show === "function" ? show(state.showPreview) : show,
|
|
277
280
|
})),
|
|
281
|
+
setPreviewUrl: (previewUrl) => set({ previewUrl }),
|
|
278
282
|
setIsMenuOpen: (open) =>
|
|
279
283
|
set((state) => ({
|
|
280
284
|
isMenuOpen: typeof open === "function" ? open(state.isMenuOpen) : open,
|