@kyro-cms/admin 0.12.10 → 0.12.11
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 +23 -23
- package/dist/index.js +16 -16
- package/package.json +2 -1
- package/src/components/ApiKeysManager.tsx +1 -50
- package/src/components/AutoForm.tsx +1 -1
- package/src/components/DashboardMetrics.tsx +17 -81
- package/src/components/DetailView.tsx +4 -4
- package/src/components/ListView.tsx +6 -14
- package/src/components/PluginsManager.tsx +2 -2
- package/src/components/UserMenu.tsx +1 -1
- package/src/components/fields/BlocksField.tsx +91 -11
- package/src/components/fields/RichTextField.tsx +1 -1
- package/src/components/users/UserDetail.tsx +2 -2
- package/src/components/users/UserForm.tsx +2 -2
- package/src/components/users/UsersList.tsx +1 -1
- package/src/hooks/useAutoFormState.ts +3 -3
- package/src/index.node.ts +2 -0
- package/src/integration.ts +4 -16
- package/src/layouts/AdminLayout.astro +25 -15
- package/src/layouts/AuthLayout.astro +11 -6
- package/src/lib/navigate.ts +15 -0
- package/src/lib/virtual-kyro-plugins.ts +2 -0
- package/src/plugins/seo-admin.tsx +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kyro-cms/admin",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.11",
|
|
4
4
|
"engines": {
|
|
5
5
|
"node": ">=22"
|
|
6
6
|
},
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"types": "./src/index.ts",
|
|
16
16
|
"exports": {
|
|
17
17
|
".": {
|
|
18
|
+
"node": "./src/index.node.ts",
|
|
18
19
|
"import": "./src/index.ts",
|
|
19
20
|
"default": "./src/index.ts"
|
|
20
21
|
},
|
|
@@ -23,28 +23,7 @@ interface ApiKeyItem {
|
|
|
23
23
|
expiresAt?: string;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
interface PermissionsOption {
|
|
27
|
-
label: string;
|
|
28
|
-
value: string;
|
|
29
|
-
}
|
|
30
26
|
|
|
31
|
-
const ALL_PERMISSIONS: PermissionsOption[] = [
|
|
32
|
-
{ label: "All (*)", value: "*" },
|
|
33
|
-
{ label: "Posts — Read", value: "posts:read" },
|
|
34
|
-
{ label: "Posts — Create", value: "posts:create" },
|
|
35
|
-
{ label: "Posts — Update", value: "posts:update" },
|
|
36
|
-
{ label: "Posts — Delete", value: "posts:delete" },
|
|
37
|
-
{ label: "Pages — Read", value: "pages:read" },
|
|
38
|
-
{ label: "Pages — Create", value: "pages:create" },
|
|
39
|
-
{ label: "Pages — Update", value: "pages:update" },
|
|
40
|
-
{ label: "Pages — Delete", value: "pages:delete" },
|
|
41
|
-
{ label: "Users — Read", value: "users:read" },
|
|
42
|
-
{ label: "Users — Admin", value: "users:admin" },
|
|
43
|
-
{ label: "Media — Read", value: "media:read" },
|
|
44
|
-
{ label: "Media — Upload", value: "media:upload" },
|
|
45
|
-
{ label: "API Keys — Read", value: "apikeys:read" },
|
|
46
|
-
{ label: "API Keys — Admin", value: "apikeys:admin" },
|
|
47
|
-
];
|
|
48
27
|
|
|
49
28
|
export function ApiKeysManager() {
|
|
50
29
|
const { t } = useTranslation();
|
|
@@ -360,35 +339,7 @@ export function ApiKeysManager() {
|
|
|
360
339
|
{createError && <p className="mt-1.5 text-xs text-red-500">{createError}</p>}
|
|
361
340
|
</div>
|
|
362
341
|
|
|
363
|
-
|
|
364
|
-
<label className="block text-xs font-bold mb-1.5 text-[var(--kyro-text-secondary)]">Permissions</label>
|
|
365
|
-
<div className="space-y-1 max-h-48 overflow-y-auto">
|
|
366
|
-
{ALL_PERMISSIONS.map((opt) => (
|
|
367
|
-
<label key={opt.value} className="flex items-center gap-3 p-2 rounded-lg hover:bg-[var(--kyro-surface-accent)] cursor-pointer">
|
|
368
|
-
<input
|
|
369
|
-
type="checkbox"
|
|
370
|
-
checked={newKeyPermissions.includes(opt.value)}
|
|
371
|
-
onChange={(e) => {
|
|
372
|
-
if (opt.value === "*") {
|
|
373
|
-
setNewKeyPermissions(e.target.checked ? ["*"] : []);
|
|
374
|
-
} else {
|
|
375
|
-
setNewKeyPermissions(
|
|
376
|
-
e.target.checked
|
|
377
|
-
? [...newKeyPermissions.filter((p) => p !== "*"), opt.value]
|
|
378
|
-
: newKeyPermissions.filter((p) => p !== opt.value)
|
|
379
|
-
);
|
|
380
|
-
}
|
|
381
|
-
}}
|
|
382
|
-
className="accent-[var(--kyro-primary)]"
|
|
383
|
-
/>
|
|
384
|
-
<span className="text-sm font-mono">{opt.label}</span>
|
|
385
|
-
</label>
|
|
386
|
-
))}
|
|
387
|
-
</div>
|
|
388
|
-
<p className="mt-1 text-[10px] text-[var(--kyro-text-muted)]">
|
|
389
|
-
Select specific permissions or choose "*" for full access.
|
|
390
|
-
</p>
|
|
391
|
-
</div>
|
|
342
|
+
|
|
392
343
|
|
|
393
344
|
<div>
|
|
394
345
|
<label className="block text-xs font-bold mb-1.5 text-[var(--kyro-text-secondary)]">Expires (optional)</label>
|
|
@@ -11,7 +11,7 @@ type View = "edit" | "version" | "api";
|
|
|
11
11
|
import { globals, collections } from "../lib/config";
|
|
12
12
|
import { resolveUrl, apiGet, apiDelete, fetchWithAuth } from "../lib/api";
|
|
13
13
|
import { EmptyState } from "./ui/EmptyState";
|
|
14
|
-
import { navigate } from "
|
|
14
|
+
import { navigate } from "../lib/navigate";
|
|
15
15
|
import { Shimmer } from "./ui/Shimmer";
|
|
16
16
|
import { normalizeUploadFields } from "../lib/normalize-upload-fields";
|
|
17
17
|
import { useAutoFormStore } from "../lib/autoform-store";
|
|
@@ -54,72 +54,12 @@ function SkeletonCard() {
|
|
|
54
54
|
|
|
55
55
|
function MetricCard({
|
|
56
56
|
icon,
|
|
57
|
-
gradient,
|
|
58
57
|
value,
|
|
59
58
|
label,
|
|
60
59
|
subtext,
|
|
61
60
|
trend,
|
|
62
61
|
}: {
|
|
63
62
|
icon: React.ReactNode;
|
|
64
|
-
gradient: string;
|
|
65
|
-
value: string | number;
|
|
66
|
-
label: string;
|
|
67
|
-
subtext?: string;
|
|
68
|
-
trend?: { value: string; up: boolean };
|
|
69
|
-
}) {
|
|
70
|
-
// Extract the first color from the gradient string (e.g. "#6366f1")
|
|
71
|
-
const firstColor = gradient.match(/#[0-9a-fA-F]{6}/)?.[0] ?? "#6366f1";
|
|
72
|
-
|
|
73
|
-
return (
|
|
74
|
-
<div
|
|
75
|
-
className="kyro-metric-card group p-5 rounded-2xl transition-all duration-300 relative overflow-hidden"
|
|
76
|
-
style={{
|
|
77
|
-
background: "var(--kyro-surface-accent)",
|
|
78
|
-
border: "1px solid var(--kyro-border-strong, rgba(255,255,255,0.14))",
|
|
79
|
-
}}
|
|
80
|
-
>
|
|
81
|
-
{/* Glow blob — keep the tile gradients */}
|
|
82
|
-
<div className="absolute -top-6 -right-6 w-24 h-24 rounded-full opacity-20 blur-2xl pointer-events-none" style={{ background: gradient }} />
|
|
83
|
-
|
|
84
|
-
{/* Icon with colored stroke on subtle background */}
|
|
85
|
-
<div
|
|
86
|
-
className="w-11 h-11 rounded-xl flex items-center justify-center mb-4 relative z-10"
|
|
87
|
-
style={{ background: `${firstColor}18` }}
|
|
88
|
-
>
|
|
89
|
-
<span style={{ color: firstColor }}>{icon}</span>
|
|
90
|
-
</div>
|
|
91
|
-
|
|
92
|
-
<h4 className="text-2xl font-bold tracking-tight mb-0.5 relative z-10" style={{ color: "var(--kyro-text-primary)" }}>
|
|
93
|
-
{typeof value === "number" ? formatNumber(value) : value}
|
|
94
|
-
</h4>
|
|
95
|
-
<p className="text-xs font-semibold relative z-10" style={{ color: "var(--kyro-text-secondary)" }}>
|
|
96
|
-
{label}
|
|
97
|
-
</p>
|
|
98
|
-
{subtext && (
|
|
99
|
-
<p className="text-[10px] mt-1 relative z-10" style={{ color: "var(--kyro-text-muted)" }}>
|
|
100
|
-
{subtext}
|
|
101
|
-
</p>
|
|
102
|
-
)}
|
|
103
|
-
{trend && (
|
|
104
|
-
<div className={`inline-flex items-center gap-1 mt-2 text-[10px] font-bold px-2 py-0.5 rounded-full relative z-10 ${trend.up ? "bg-emerald-500/15 text-emerald-400" : "bg-red-500/15 text-red-400"}`}>
|
|
105
|
-
{trend.up ? "↑" : "↓"} {trend.value}
|
|
106
|
-
</div>
|
|
107
|
-
)}
|
|
108
|
-
</div>
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// ─── Ecommerce Metric Card (Commerce Overview) ───────────────────────────────
|
|
113
|
-
|
|
114
|
-
function EcommerceMetricCard({
|
|
115
|
-
icon,
|
|
116
|
-
value,
|
|
117
|
-
label,
|
|
118
|
-
subtext,
|
|
119
|
-
trend,
|
|
120
|
-
}: {
|
|
121
|
-
icon: React.ReactNode;
|
|
122
|
-
gradient: string;
|
|
123
63
|
value: string | number;
|
|
124
64
|
label: string;
|
|
125
65
|
subtext?: string;
|
|
@@ -199,8 +139,8 @@ function CustomTooltip({ active, payload, label, currency }: any) {
|
|
|
199
139
|
{p.name === "revenue"
|
|
200
140
|
? formatCurrency(p.value, currency)
|
|
201
141
|
: p.name === "orders"
|
|
202
|
-
|
|
203
|
-
|
|
142
|
+
? `${p.value} orders`
|
|
143
|
+
: p.value}
|
|
204
144
|
</span>
|
|
205
145
|
</div>
|
|
206
146
|
))}
|
|
@@ -237,9 +177,9 @@ export const RevenueChart: React.FC = () => {
|
|
|
237
177
|
// Build order-count per month from chartData if present
|
|
238
178
|
const enrichedData = hasChartData
|
|
239
179
|
? data.chartData.map((d: any) => ({
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
180
|
+
...d,
|
|
181
|
+
orders: d.orders ?? Math.round(d.revenue / Math.max(avgOrderValue, 1)),
|
|
182
|
+
}))
|
|
243
183
|
: [];
|
|
244
184
|
|
|
245
185
|
// Donut data: order status breakdown
|
|
@@ -252,16 +192,16 @@ export const RevenueChart: React.FC = () => {
|
|
|
252
192
|
cancelled: "#f87171", // Red
|
|
253
193
|
};
|
|
254
194
|
const fallbackColors = ["#818cf8", "#94a3b8", "#fbbf24", "#f87171", "#c084fc", "#38bdf8"];
|
|
255
|
-
|
|
195
|
+
|
|
256
196
|
const statusData = data?.ordersByStatus
|
|
257
197
|
? Object.entries(data.ordersByStatus).map(([name, value]: any, i) => {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
198
|
+
const normalized = name.toLowerCase().trim();
|
|
199
|
+
return {
|
|
200
|
+
name: name.charAt(0).toUpperCase() + name.slice(1),
|
|
201
|
+
value,
|
|
202
|
+
color: STATUS_COLORS[normalized] || fallbackColors[i % fallbackColors.length],
|
|
203
|
+
};
|
|
204
|
+
})
|
|
265
205
|
: [];
|
|
266
206
|
|
|
267
207
|
return (
|
|
@@ -605,14 +545,10 @@ export const DashboardMetrics: React.FC<DashboardMetricsProps> = ({ isEcommerce
|
|
|
605
545
|
{loading
|
|
606
546
|
? Array.from({ length: 5 }).map((_, i) => <SkeletonCard key={i} />)
|
|
607
547
|
: data
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
<MetricCard key={i} {...card} />
|
|
613
|
-
)
|
|
614
|
-
)
|
|
615
|
-
: null}
|
|
548
|
+
? cards.map((card, i) => (
|
|
549
|
+
<MetricCard key={i} {...card} />
|
|
550
|
+
))
|
|
551
|
+
: null}
|
|
616
552
|
</div>
|
|
617
553
|
</div>
|
|
618
554
|
);
|
|
@@ -15,7 +15,7 @@ import { PageHeader } from "./ui/PageHeader";
|
|
|
15
15
|
import { Badge } from "./ui/Badge";
|
|
16
16
|
import { SplitButton } from "./ui/SplitButton";
|
|
17
17
|
import { Lock, FileText, ChevronLeft, Save } from "lucide-react";
|
|
18
|
-
import { navigate } from "
|
|
18
|
+
import { navigate } from "../lib/navigate";
|
|
19
19
|
import { useTranslation } from "react-i18next";
|
|
20
20
|
import "../lib/i18n";
|
|
21
21
|
import { adminPath } from "../lib/paths";
|
|
@@ -172,7 +172,7 @@ export function DetailView({
|
|
|
172
172
|
setTimeout(() => {
|
|
173
173
|
setSaveStatus("idle");
|
|
174
174
|
}, 2000);
|
|
175
|
-
|
|
175
|
+
|
|
176
176
|
if (showPreview) {
|
|
177
177
|
refreshPreviewUrl(savedData);
|
|
178
178
|
}
|
|
@@ -252,7 +252,7 @@ export function DetailView({
|
|
|
252
252
|
delete copyPayload.createdAt;
|
|
253
253
|
delete copyPayload.updatedAt;
|
|
254
254
|
delete (copyPayload as any).status;
|
|
255
|
-
|
|
255
|
+
|
|
256
256
|
await navigator.clipboard.writeText(JSON.stringify(copyPayload));
|
|
257
257
|
toast.success(t("toast.dataCopied", { defaultValue: "Document data copied to clipboard" }));
|
|
258
258
|
} catch (e) {
|
|
@@ -267,7 +267,7 @@ export function DetailView({
|
|
|
267
267
|
if (typeof pastedData !== 'object' || pastedData === null) {
|
|
268
268
|
throw new Error("Invalid format");
|
|
269
269
|
}
|
|
270
|
-
|
|
270
|
+
|
|
271
271
|
setData((prev) => ({
|
|
272
272
|
...prev,
|
|
273
273
|
...pastedData,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Search, Filter, Columns3, X, Trash2, Archive, ChevronUp, Edit2 } from "./ui/icons";
|
|
2
2
|
import { useState, useEffect, useMemo, useCallback, useRef } from "react";
|
|
3
3
|
import { Spinner } from "./ui/Spinner";
|
|
4
|
-
import { navigate } from "
|
|
4
|
+
import { navigate } from "../lib/navigate";
|
|
5
5
|
import { Shimmer } from "./ui/Shimmer";
|
|
6
6
|
import { Plus } from "./ui/icons";
|
|
7
7
|
import { apiGet, apiDelete, withCacheBust } from "../lib/api";
|
|
@@ -75,12 +75,7 @@ export function ListView({
|
|
|
75
75
|
providedOnCreate();
|
|
76
76
|
} else {
|
|
77
77
|
const href = `${ADMIN_BASE}/${collectionSlug}/new`;
|
|
78
|
-
|
|
79
|
-
import("astro:transitions/client").then(({ navigate }) => {
|
|
80
|
-
navigate(href);
|
|
81
|
-
}).catch(() => {
|
|
82
|
-
navigate(href);
|
|
83
|
-
});
|
|
78
|
+
navigate(href);
|
|
84
79
|
}
|
|
85
80
|
};
|
|
86
81
|
|
|
@@ -89,12 +84,7 @@ export function ListView({
|
|
|
89
84
|
providedOnEdit(id);
|
|
90
85
|
} else {
|
|
91
86
|
const href = `${ADMIN_BASE}/${collectionSlug}/${id}`;
|
|
92
|
-
|
|
93
|
-
import("astro:transitions/client").then(({ navigate }) => {
|
|
94
|
-
navigate(href);
|
|
95
|
-
}).catch(() => {
|
|
96
|
-
navigate(href);
|
|
97
|
-
});
|
|
87
|
+
navigate(href);
|
|
98
88
|
}
|
|
99
89
|
};
|
|
100
90
|
|
|
@@ -141,7 +131,8 @@ export function ListView({
|
|
|
141
131
|
function flattenFields(fields: FieldConfig[]): FieldConfig[] {
|
|
142
132
|
const result: FieldConfig[] = [];
|
|
143
133
|
for (const field of fields || []) {
|
|
144
|
-
if (
|
|
134
|
+
if (field.hidden === true || field.admin?.hidden || field.name === "id") continue;
|
|
135
|
+
|
|
145
136
|
if (field.type === "tabs" && field.tabs) {
|
|
146
137
|
for (const tab of field.tabs) {
|
|
147
138
|
if (tab.fields) {
|
|
@@ -154,6 +145,7 @@ export function ListView({
|
|
|
154
145
|
) {
|
|
155
146
|
result.push(...flattenFields(field.fields));
|
|
156
147
|
} else {
|
|
148
|
+
if (!field.name) continue;
|
|
157
149
|
result.push(field);
|
|
158
150
|
}
|
|
159
151
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { pluginViews } from "virtual
|
|
1
|
+
import { pluginViews } from "../lib/virtual-kyro-plugins";
|
|
2
2
|
import React, { useState, useEffect, Suspense } from "react";
|
|
3
3
|
import {
|
|
4
4
|
Blocks,
|
|
@@ -16,7 +16,7 @@ import { Modal, ModalContent, ModalActions } from "./ui/Modal";
|
|
|
16
16
|
import { PageHeader } from "./ui/PageHeader";
|
|
17
17
|
import { Badge } from "./ui/Badge";
|
|
18
18
|
import { useTranslation } from "react-i18next";
|
|
19
|
-
import { navigate } from "
|
|
19
|
+
import { navigate } from "../lib/navigate";
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
interface Plugin {
|
|
@@ -4,7 +4,7 @@ import { User, Shield, Key, Webhook, Clock, FileText, ExternalLink, HelpCircle,
|
|
|
4
4
|
import { useAuthStore } from "../lib/stores";
|
|
5
5
|
import { apiGet } from "../lib/api";
|
|
6
6
|
import { useTranslation } from "react-i18next";
|
|
7
|
-
import { navigate } from '
|
|
7
|
+
import { navigate } from '../lib/navigate';
|
|
8
8
|
|
|
9
9
|
interface UserMenuProps {
|
|
10
10
|
adminPath: string;
|
|
@@ -83,7 +83,7 @@ const SortableBlockComponent = ({
|
|
|
83
83
|
onDuplicate: (id: string) => void;
|
|
84
84
|
compact?: boolean;
|
|
85
85
|
}) => {
|
|
86
|
-
|
|
86
|
+
const { t } = useTranslation();
|
|
87
87
|
const {
|
|
88
88
|
attributes,
|
|
89
89
|
listeners,
|
|
@@ -116,6 +116,23 @@ const SortableBlockComponent = ({
|
|
|
116
116
|
}
|
|
117
117
|
}, [nameDraft, block.name, block.id, updateBlock]);
|
|
118
118
|
|
|
119
|
+
const copyToClipboard = useCallback((e: React.MouseEvent) => {
|
|
120
|
+
e.stopPropagation();
|
|
121
|
+
try {
|
|
122
|
+
const exportData = {
|
|
123
|
+
__kyro_block: true,
|
|
124
|
+
type: block.type,
|
|
125
|
+
blockProps: { ...block, id: undefined, type: undefined },
|
|
126
|
+
};
|
|
127
|
+
navigator.clipboard.writeText(JSON.stringify(exportData, null, 2));
|
|
128
|
+
setCopied(true);
|
|
129
|
+
toast.success(t("notifications.blockCopied", { defaultValue: "Block copied to clipboard" }));
|
|
130
|
+
setTimeout(() => setCopied(false), 2000);
|
|
131
|
+
} catch (err) {
|
|
132
|
+
toast.error(t("notifications.blockCopyFailed", { defaultValue: "Failed to copy block" }));
|
|
133
|
+
}
|
|
134
|
+
}, [block, t]);
|
|
135
|
+
|
|
119
136
|
const style = {
|
|
120
137
|
transform: CSS.Transform.toString(transform),
|
|
121
138
|
transition,
|
|
@@ -133,8 +150,8 @@ const SortableBlockComponent = ({
|
|
|
133
150
|
<div
|
|
134
151
|
onClick={() => setEditingBlockId(block.id as string)}
|
|
135
152
|
className={`flex items-center gap-2 pl-7 pr-2 py-2 w-full bg-[var(--kyro-bg-secondary)] rounded-md border transition-colors cursor-pointer text-sm ${isEditing
|
|
136
|
-
|
|
137
|
-
|
|
153
|
+
? `${(blockTheme[block.type as string] || blockTheme.default).border} bg-[var(--kyro-primary)]/5`
|
|
154
|
+
: "border-[var(--kyro-border)] hover:border-[var(--kyro-primary)]/50 hover:bg-[var(--kyro-primary)]/5"
|
|
138
155
|
}`}
|
|
139
156
|
>
|
|
140
157
|
<div
|
|
@@ -202,7 +219,7 @@ const SortableBlockComponent = ({
|
|
|
202
219
|
className="p-0.5 hover:bg-[var(--kyro-surface-accent)] rounded text-[var(--kyro-text-secondary)] transition-colors"
|
|
203
220
|
title={t("tooltips.rename", { defaultValue: "Rename" })}
|
|
204
221
|
>
|
|
205
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/><path d="m15 5 4 4"/></svg>
|
|
222
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z" /><path d="m15 5 4 4" /></svg>
|
|
206
223
|
</button>
|
|
207
224
|
<button
|
|
208
225
|
type="button"
|
|
@@ -215,6 +232,14 @@ const SortableBlockComponent = ({
|
|
|
215
232
|
>
|
|
216
233
|
<Copy className="w-3 h-3" />
|
|
217
234
|
</button>
|
|
235
|
+
<button
|
|
236
|
+
type="button"
|
|
237
|
+
onClick={copyToClipboard}
|
|
238
|
+
className="p-0.5 hover:bg-[var(--kyro-surface-accent)] rounded text-[var(--kyro-text-secondary)] transition-colors"
|
|
239
|
+
title={t("tooltips.copyBlock", { defaultValue: "Copy Block to Clipboard" })}
|
|
240
|
+
>
|
|
241
|
+
{copied ? <Check className="w-3 h-3 text-green-500" /> : <ClipboardCopy className="w-3 h-3" />}
|
|
242
|
+
</button>
|
|
218
243
|
|
|
219
244
|
<button
|
|
220
245
|
type="button"
|
|
@@ -247,8 +272,8 @@ const SortableBlockComponent = ({
|
|
|
247
272
|
<div
|
|
248
273
|
onClick={() => setEditingBlockId(block.id as string)}
|
|
249
274
|
className={`flex items-center gap-3 p-3 bg-[var(--kyro-bg-secondary)] rounded-lg border transition-colors cursor-pointer ${isEditing
|
|
250
|
-
|
|
251
|
-
|
|
275
|
+
? `${(blockTheme[block.type as string] || blockTheme.default).border} bg-[var(--kyro-primary)]/5`
|
|
276
|
+
: "border-[var(--kyro-border)] hover:border-[var(--kyro-primary)]/50 hover:bg-[var(--kyro-primary)]/5"
|
|
252
277
|
}`}
|
|
253
278
|
>
|
|
254
279
|
<div
|
|
@@ -338,7 +363,7 @@ const SortableBlockComponent = ({
|
|
|
338
363
|
className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded text-[var(--kyro-text-secondary)] transition-colors"
|
|
339
364
|
title={t("tooltips.renameBlock", { defaultValue: "Rename Block" })}
|
|
340
365
|
>
|
|
341
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/><path d="m15 5 4 4"/></svg>
|
|
366
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z" /><path d="m15 5 4 4" /></svg>
|
|
342
367
|
</button>
|
|
343
368
|
<button
|
|
344
369
|
type="button"
|
|
@@ -351,6 +376,14 @@ const SortableBlockComponent = ({
|
|
|
351
376
|
>
|
|
352
377
|
<Copy className="w-3.5 h-3.5" />
|
|
353
378
|
</button>
|
|
379
|
+
<button
|
|
380
|
+
type="button"
|
|
381
|
+
onClick={copyToClipboard}
|
|
382
|
+
className="p-1 hover:bg-[var(--kyro-surface-accent)] rounded text-[var(--kyro-text-secondary)] transition-colors"
|
|
383
|
+
title={t("tooltips.copyBlock", { defaultValue: "Copy Block to Clipboard" })}
|
|
384
|
+
>
|
|
385
|
+
{copied ? <Check className="w-3.5 h-3.5 text-green-500" /> : <ClipboardCopy className="w-3.5 h-3.5" />}
|
|
386
|
+
</button>
|
|
354
387
|
|
|
355
388
|
<button
|
|
356
389
|
type="button"
|
|
@@ -526,10 +559,31 @@ export const BlocksField: React.FC<BlocksFieldProps> = ({
|
|
|
526
559
|
[store],
|
|
527
560
|
);
|
|
528
561
|
|
|
562
|
+
// const handlePasteBlock = useCallback((parsedData: any) => {
|
|
563
|
+
// const allowedBlocks = (field.blocks as Array<{ slug: string }>) || [];
|
|
564
|
+
// const isAllowed = allowedBlocks.some((b) => b.slug === parsedData.type);
|
|
565
|
+
|
|
566
|
+
// if (!isAllowed) {
|
|
567
|
+
// alert(`The block type "${parsedData.type}" is not allowed in this collection.`);
|
|
568
|
+
// return;
|
|
569
|
+
// }
|
|
570
|
+
|
|
571
|
+
// const newId = Math.random().toString(36).substring(2, 11);
|
|
572
|
+
// const newBlock = {
|
|
573
|
+
// ...(parsedData.blockProps || {}),
|
|
574
|
+
// id: newId,
|
|
575
|
+
// type: parsedData.type,
|
|
576
|
+
// };
|
|
577
|
+
|
|
578
|
+
// store.getState().setBlocks([...blocks, newBlock]);
|
|
579
|
+
// setIsDrawerOpen(false);
|
|
580
|
+
// }, [field.blocks, blocks, store]);
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
|
|
529
584
|
const handlePasteBlock = useCallback((parsedData: any) => {
|
|
530
|
-
const allowedBlocks = (field.blocks as Array<{ slug: string }>) || [];
|
|
531
585
|
const isAllowed = allowedBlocks.some((b) => b.slug === parsedData.type);
|
|
532
|
-
|
|
586
|
+
|
|
533
587
|
if (!isAllowed) {
|
|
534
588
|
alert(`The block type "${parsedData.type}" is not allowed in this collection.`);
|
|
535
589
|
return;
|
|
@@ -544,9 +598,34 @@ export const BlocksField: React.FC<BlocksFieldProps> = ({
|
|
|
544
598
|
|
|
545
599
|
store.getState().setBlocks([...blocks, newBlock]);
|
|
546
600
|
setIsDrawerOpen(false);
|
|
547
|
-
}, [
|
|
601
|
+
}, [allowedBlocks, blocks, store]);
|
|
548
602
|
|
|
603
|
+
const handleContainerPaste = useCallback((e: React.ClipboardEvent<HTMLDivElement>) => {
|
|
604
|
+
const activeEl = document.activeElement;
|
|
605
|
+
if (
|
|
606
|
+
activeEl &&
|
|
607
|
+
(activeEl.tagName === "INPUT" ||
|
|
608
|
+
activeEl.tagName === "TEXTAREA" ||
|
|
609
|
+
activeEl.getAttribute("contenteditable") === "true")
|
|
610
|
+
) {
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
549
613
|
|
|
614
|
+
const text = e.clipboardData?.getData("text");
|
|
615
|
+
if (!text) return;
|
|
616
|
+
|
|
617
|
+
try {
|
|
618
|
+
const parsed = JSON.parse(text);
|
|
619
|
+
if (parsed && parsed.__kyro_block) {
|
|
620
|
+
e.preventDefault();
|
|
621
|
+
e.stopPropagation();
|
|
622
|
+
handlePasteBlock(parsed);
|
|
623
|
+
toast.success(`Block pasted: ${parsed.type}`);
|
|
624
|
+
}
|
|
625
|
+
} catch {
|
|
626
|
+
// Ignore
|
|
627
|
+
}
|
|
628
|
+
}, [handlePasteBlock]);
|
|
550
629
|
|
|
551
630
|
const duplicateBlock = useCallback(
|
|
552
631
|
(blockId: string) => {
|
|
@@ -668,8 +747,9 @@ export const BlocksField: React.FC<BlocksFieldProps> = ({
|
|
|
668
747
|
|
|
669
748
|
return (
|
|
670
749
|
<BlocksContext.Provider value={storeRef.current}>
|
|
671
|
-
<div
|
|
750
|
+
<div
|
|
672
751
|
className="kyro-blocks-field"
|
|
752
|
+
onPaste={handleContainerPaste}
|
|
673
753
|
tabIndex={0}
|
|
674
754
|
style={{ outline: "none" }}
|
|
675
755
|
>
|
|
@@ -3,7 +3,7 @@ import { apiGet } from "../../lib/api";
|
|
|
3
3
|
import { useUIStore, toast } from "../../lib/stores";
|
|
4
4
|
import { UploadField } from "../fields/UploadField";
|
|
5
5
|
import { useTranslation } from "react-i18next";
|
|
6
|
-
import { navigate } from '
|
|
6
|
+
import { navigate } from '../../lib/navigate';
|
|
7
7
|
|
|
8
8
|
interface User {
|
|
9
9
|
id: string;
|
|
@@ -36,7 +36,7 @@ const roleOptions = [
|
|
|
36
36
|
];
|
|
37
37
|
|
|
38
38
|
export function UserDetail({ user, apiPath, adminPath }: UserDetailProps) {
|
|
39
|
-
|
|
39
|
+
const { t } = useTranslation();
|
|
40
40
|
const [name, setName] = useState(user.name || "");
|
|
41
41
|
const [role, setRole] = useState(user.role);
|
|
42
42
|
const [avatar, setAvatar] = useState<string | undefined>(user.avatar);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { useState } from "react";
|
|
2
2
|
import { useTranslation } from "react-i18next";
|
|
3
|
-
import { navigate } from '
|
|
3
|
+
import { navigate } from '../../lib/navigate';
|
|
4
4
|
|
|
5
5
|
interface UserFormProps {
|
|
6
6
|
mode: "create" | "edit";
|
|
@@ -27,7 +27,7 @@ const roleOptions = [
|
|
|
27
27
|
];
|
|
28
28
|
|
|
29
29
|
export function UserForm({ mode, apiPath, adminPath, user }: UserFormProps) {
|
|
30
|
-
|
|
30
|
+
const { t } = useTranslation();
|
|
31
31
|
const [email, setEmail] = useState(user?.email || "");
|
|
32
32
|
const [name, setName] = useState(user?.name || "");
|
|
33
33
|
const [password, setPassword] = useState("");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Plus, Lock, CheckCircle2, Edit2, Trash2, XCircle, X } from "../ui/icons";
|
|
2
2
|
import { useState } from "react";
|
|
3
3
|
import { useUIStore } from "../../lib/stores";
|
|
4
|
-
import { navigate } from '
|
|
4
|
+
import { navigate } from '../../lib/navigate';
|
|
5
5
|
|
|
6
6
|
interface User {
|
|
7
7
|
id: string;
|
|
@@ -186,7 +186,7 @@ const persistBrowserDraft = useCallback(
|
|
|
186
186
|
},
|
|
187
187
|
keepalive: options?.keepalive,
|
|
188
188
|
body: JSON.stringify({
|
|
189
|
-
...(normalizeUploadFields(latestFormData) as Record<string, unknown>),
|
|
189
|
+
...(normalizeUploadFields(latestFormData, true) as Record<string, unknown>),
|
|
190
190
|
baseUpdatedAt: currentLastSaved.updatedAt ?? null,
|
|
191
191
|
}),
|
|
192
192
|
});
|
|
@@ -279,7 +279,7 @@ const persistBrowserDraft = useCallback(
|
|
|
279
279
|
"X-Draft": String(isDraft),
|
|
280
280
|
},
|
|
281
281
|
body: JSON.stringify({
|
|
282
|
-
...normalizeUploadFields(payload) as Record<string, unknown>,
|
|
282
|
+
...normalizeUploadFields(payload, true) as Record<string, unknown>,
|
|
283
283
|
baseUpdatedAt: state.lastSavedData.updatedAt ?? null,
|
|
284
284
|
}),
|
|
285
285
|
},
|
|
@@ -313,7 +313,7 @@ const persistBrowserDraft = useCallback(
|
|
|
313
313
|
"X-Draft": String(isDraft),
|
|
314
314
|
},
|
|
315
315
|
body: JSON.stringify({
|
|
316
|
-
...normalizeUploadFields(payload) as Record<string, unknown>,
|
|
316
|
+
...normalizeUploadFields(payload, true) as Record<string, unknown>,
|
|
317
317
|
}),
|
|
318
318
|
},
|
|
319
319
|
);
|