@kyro-cms/admin 0.10.6 → 0.10.10
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 +38 -15122
- package/dist/index.css +1 -1698
- package/dist/index.js +38 -15036
- package/package.json +1 -1
- package/src/components/ApiKeysManager.tsx +33 -126
- package/src/components/AutoForm.tsx +130 -1192
- package/src/components/PluginsManager.tsx +13 -141
- package/src/components/SettingsPage.tsx +0 -1
- package/src/components/UserMenu.tsx +17 -1
- package/src/components/autoform/AutoFormApiView.tsx +96 -0
- package/src/components/autoform/AutoFormEditView.tsx +91 -0
- package/src/components/autoform/AutoFormHeader.tsx +488 -0
- package/src/components/autoform/AutoFormVersionView.tsx +283 -0
- package/src/hooks/useAutoFormState.ts +3 -1
- package/src/integration.ts +0 -1
- package/src/lib/autoform-store.ts +8 -0
- package/dist/index.cjs.map +0 -1
- package/dist/index.css.map +0 -1
- package/dist/index.js.map +0 -1
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Check, X } from "../ui/icons";
|
|
3
|
+
import { useAutoFormStore } from "../../lib/autoform-store";
|
|
4
|
+
|
|
5
|
+
interface AutoFormVersionViewProps {
|
|
6
|
+
handleRestoreVersion: (versionId: string) => void;
|
|
7
|
+
handleCompareVersions: () => Promise<void>;
|
|
8
|
+
toggleCompareSelection: (versionId: string) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function AutoFormVersionView({
|
|
12
|
+
handleRestoreVersion,
|
|
13
|
+
handleCompareVersions,
|
|
14
|
+
toggleCompareSelection,
|
|
15
|
+
}: AutoFormVersionViewProps) {
|
|
16
|
+
const {
|
|
17
|
+
compareMode,
|
|
18
|
+
setCompareMode,
|
|
19
|
+
compareSelected,
|
|
20
|
+
setCompareSelected,
|
|
21
|
+
compareDiffs,
|
|
22
|
+
setCompareDiffs,
|
|
23
|
+
loadingDiffs,
|
|
24
|
+
loadingVersions,
|
|
25
|
+
versions,
|
|
26
|
+
} = useAutoFormStore();
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div className="w-full animate-in fade-in slide-in-from-bottom-4 pb-12">
|
|
30
|
+
<div className="surface-tile p-0 overflow-hidden">
|
|
31
|
+
<div className="px-4 md:px-6 py-3 md:py-4 border-b border-[var(--kyro-border)] flex flex-col md:flex-row md:items-center justify-between gap-2">
|
|
32
|
+
<div>
|
|
33
|
+
<h2 className="text-base md:text-lg font-bold text-[var(--kyro-text-primary)]">
|
|
34
|
+
Version History
|
|
35
|
+
</h2>
|
|
36
|
+
<p className="text-[11px] text-[var(--kyro-text-muted)] mt-0.5">
|
|
37
|
+
{compareMode
|
|
38
|
+
? `Select 2 versions · ${compareSelected.length}/2 chosen`
|
|
39
|
+
: `${versions.length} snapshot${versions.length !== 1 ? "s" : ""} · Auto-saved`}
|
|
40
|
+
</p>
|
|
41
|
+
</div>
|
|
42
|
+
<div className="flex items-center gap-2">
|
|
43
|
+
{compareMode && compareSelected.length === 2 && (
|
|
44
|
+
<button
|
|
45
|
+
type="button"
|
|
46
|
+
onClick={handleCompareVersions}
|
|
47
|
+
disabled={loadingDiffs}
|
|
48
|
+
className="kyro-btn kyro-btn-primary px-3 py-1.5 rounded-lg text-[11px] font-bold tracking-wider hover:opacity-90 disabled:opacity-50"
|
|
49
|
+
>
|
|
50
|
+
{loadingDiffs ? "Comparing..." : "Compare"}
|
|
51
|
+
</button>
|
|
52
|
+
)}
|
|
53
|
+
<button
|
|
54
|
+
type="button"
|
|
55
|
+
onClick={() => {
|
|
56
|
+
setCompareMode(!compareMode);
|
|
57
|
+
setCompareSelected([]);
|
|
58
|
+
setCompareDiffs([]);
|
|
59
|
+
}}
|
|
60
|
+
className={`px-3 py-1.5 rounded-lg text-[11px] font-bold tracking-wider transition-all ${compareMode
|
|
61
|
+
? "bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)]"
|
|
62
|
+
: "border border-[var(--kyro-border)] text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)]"
|
|
63
|
+
}`}
|
|
64
|
+
>
|
|
65
|
+
{compareMode ? "Done" : "Compare"}
|
|
66
|
+
</button>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
{compareDiffs.length > 0 && (
|
|
71
|
+
<div className="border-b border-[var(--kyro-border)]">
|
|
72
|
+
<div className="px-6 py-3 flex items-center justify-between">
|
|
73
|
+
<span className="text-[11px] font-bold text-[var(--kyro-text-primary)] tracking-wider">
|
|
74
|
+
{compareDiffs.length} change
|
|
75
|
+
{compareDiffs.length !== 1 ? "s" : ""}
|
|
76
|
+
</span>
|
|
77
|
+
<button
|
|
78
|
+
type="button"
|
|
79
|
+
onClick={() => setCompareDiffs([])}
|
|
80
|
+
className="p-1 rounded hover:bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-muted)]"
|
|
81
|
+
>
|
|
82
|
+
<X className="w-4 h-4" />
|
|
83
|
+
</button>
|
|
84
|
+
</div>
|
|
85
|
+
<div className="max-h-[400px] overflow-y-auto">
|
|
86
|
+
{compareDiffs.map((d, i) => (
|
|
87
|
+
<div
|
|
88
|
+
key={i}
|
|
89
|
+
className="flex flex-col md:grid md:grid-cols-4 gap-1 md:gap-3 px-4 md:px-6 py-2.5 text-[11px] font-mono border-t border-[var(--kyro-border)] hover:bg-[var(--kyro-bg-secondary)]"
|
|
90
|
+
>
|
|
91
|
+
<div className="text-[var(--kyro-text-muted)] truncate font-semibold md:font-normal">
|
|
92
|
+
{d.field}
|
|
93
|
+
</div>
|
|
94
|
+
<div className="text-[var(--kyro-text-muted)] truncate hidden md:block">
|
|
95
|
+
{typeof d.oldValue === "object"
|
|
96
|
+
? JSON.stringify(d.oldValue)
|
|
97
|
+
: String(d.oldValue ?? "null")}
|
|
98
|
+
</div>
|
|
99
|
+
<div className="md:col-span-2 text-[var(--kyro-text-primary)] truncate">
|
|
100
|
+
<span className="md:hidden text-[var(--kyro-text-muted)]">→ </span>
|
|
101
|
+
{typeof d.newValue === "object"
|
|
102
|
+
? JSON.stringify(d.newValue)
|
|
103
|
+
: String(d.newValue ?? "null")}
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
))}
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
)}
|
|
110
|
+
|
|
111
|
+
{loadingVersions ? (
|
|
112
|
+
<div className="flex justify-center py-16">
|
|
113
|
+
<span className="animate-spin text-[var(--kyro-primary)]">⌛</span>
|
|
114
|
+
</div>
|
|
115
|
+
) : versions.length === 0 ? (
|
|
116
|
+
<div className="text-center py-16 text-[var(--kyro-text-muted)] text-sm italic">
|
|
117
|
+
No versions yet.
|
|
118
|
+
</div>
|
|
119
|
+
) : (
|
|
120
|
+
<div className="divide-y divide-[var(--kyro-border)]">
|
|
121
|
+
{versions.map((v, i) => {
|
|
122
|
+
const isSelected = compareSelected.includes(v.id);
|
|
123
|
+
const isAutoSaved = (v.changeDescription || "")
|
|
124
|
+
.toLowerCase()
|
|
125
|
+
.includes("auto");
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<div
|
|
129
|
+
key={v.id}
|
|
130
|
+
onClick={
|
|
131
|
+
compareMode ? () => toggleCompareSelection(v.id) : undefined
|
|
132
|
+
}
|
|
133
|
+
className={`transition-all ${compareMode
|
|
134
|
+
? isSelected
|
|
135
|
+
? "bg-[var(--kyro-primary)]/5 cursor-pointer"
|
|
136
|
+
: "hover:bg-[var(--kyro-bg-secondary)] cursor-pointer"
|
|
137
|
+
: "hover:bg-[var(--kyro-bg-secondary)]"
|
|
138
|
+
}`}
|
|
139
|
+
>
|
|
140
|
+
{/* Desktop: grid row */}
|
|
141
|
+
<div className="hidden md:grid grid-cols-12 gap-3 px-6 py-3 items-center">
|
|
142
|
+
<div className="col-span-1 flex items-center gap-2">
|
|
143
|
+
{compareMode ? (
|
|
144
|
+
<div
|
|
145
|
+
className={`w-4 h-4 rounded-full border ${isSelected
|
|
146
|
+
? "border-[var(--kyro-primary)] bg-[var(--kyro-primary)]"
|
|
147
|
+
: "border-[var(--kyro-border)]"
|
|
148
|
+
}`}
|
|
149
|
+
>
|
|
150
|
+
{isSelected && (
|
|
151
|
+
<Check className="w-4 h-4" />
|
|
152
|
+
)}
|
|
153
|
+
</div>
|
|
154
|
+
) : (
|
|
155
|
+
<span className="text-[10px] font-bold text-[var(--kyro-text-muted)] w-5">
|
|
156
|
+
{versions.length - i}
|
|
157
|
+
</span>
|
|
158
|
+
)}
|
|
159
|
+
</div>
|
|
160
|
+
<div className="col-span-4 min-w-0">
|
|
161
|
+
<div className="text-[13px] font-medium text-[var(--kyro-text-primary)] truncate flex items-center gap-2">
|
|
162
|
+
{v.changeDescription || "Snapshot"}
|
|
163
|
+
{isAutoSaved && (
|
|
164
|
+
<span className="text-[9px] px-1.5 py-0.5 bg-[var(--kyro-bg-secondary)] text-[var(--kyro-text-secondary)] rounded font-bold tracking-wider">
|
|
165
|
+
Auto
|
|
166
|
+
</span>
|
|
167
|
+
)}
|
|
168
|
+
</div>
|
|
169
|
+
<div className="text-[11px] text-[var(--kyro-text-muted)]">
|
|
170
|
+
{new Date(v.createdAt as string).toLocaleString("en-US", {
|
|
171
|
+
month: "short",
|
|
172
|
+
day: "numeric",
|
|
173
|
+
hour: "2-digit",
|
|
174
|
+
minute: "2-digit",
|
|
175
|
+
})}
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
<div className="col-span-3">
|
|
179
|
+
{v.status && (
|
|
180
|
+
<span
|
|
181
|
+
className={`inline-flex items-center gap-1 px-2 py-0.5 rounded text-[10px] font-bold capitalize tracking-wider ${v.status === "published"
|
|
182
|
+
? " text-[var(--kyro-success)]"
|
|
183
|
+
: " text-[var(--kyro-warning)]"
|
|
184
|
+
}`}
|
|
185
|
+
>
|
|
186
|
+
<span
|
|
187
|
+
className={`w-1.5 h-1.5 rounded-full ${v.status === "published" ? "bg-[var(--kyro-success)]" : "bg-[var(--kyro-warning)]"}`}
|
|
188
|
+
/>
|
|
189
|
+
{v.status}
|
|
190
|
+
</span>
|
|
191
|
+
)}
|
|
192
|
+
</div>
|
|
193
|
+
<div className="col-span-2 text-[11px] text-[var(--kyro-text-muted)]">
|
|
194
|
+
{v.createdBy || "system"}
|
|
195
|
+
</div>
|
|
196
|
+
<div className="col-span-2 flex justify-end">
|
|
197
|
+
{!compareMode && (
|
|
198
|
+
<button
|
|
199
|
+
type="button"
|
|
200
|
+
onClick={() => handleRestoreVersion(v.id)}
|
|
201
|
+
className="px-3 py-1.5 rounded-lg border border-[var(--kyro-border)] text-[11px] font-bold tracking-wider text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)] hover:border-[var(--kyro-primary)] transition-all active:scale-95"
|
|
202
|
+
>
|
|
203
|
+
Restore
|
|
204
|
+
</button>
|
|
205
|
+
)}
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
|
|
209
|
+
{/* Mobile: card layout */}
|
|
210
|
+
<div className="md:hidden flex items-start gap-3 px-4 py-3">
|
|
211
|
+
<div className="pt-0.5 shrink-0">
|
|
212
|
+
{compareMode ? (
|
|
213
|
+
<div
|
|
214
|
+
className={`w-4 h-4 rounded-full border ${isSelected
|
|
215
|
+
? "border-[var(--kyro-primary)] bg-[var(--kyro-primary)]"
|
|
216
|
+
: "border-[var(--kyro-border)]"
|
|
217
|
+
}`}
|
|
218
|
+
>
|
|
219
|
+
{isSelected && <Check className="w-4 h-4" />}
|
|
220
|
+
</div>
|
|
221
|
+
) : (
|
|
222
|
+
<span className="text-[10px] font-bold text-[var(--kyro-text-muted)] w-5 inline-block text-center">
|
|
223
|
+
{versions.length - i}
|
|
224
|
+
</span>
|
|
225
|
+
)}
|
|
226
|
+
</div>
|
|
227
|
+
|
|
228
|
+
<div className="flex-1 min-w-0">
|
|
229
|
+
<div className="text-[13px] font-medium text-[var(--kyro-text-primary)] truncate flex items-center gap-1.5">
|
|
230
|
+
{v.changeDescription || "Snapshot"}
|
|
231
|
+
{isAutoSaved && (
|
|
232
|
+
<span className="text-[9px] px-1 py-0.5 bg-[var(--kyro-bg-secondary)] text-[var(--kyro-text-secondary)] rounded font-bold tracking-wider shrink-0">
|
|
233
|
+
Auto
|
|
234
|
+
</span>
|
|
235
|
+
)}
|
|
236
|
+
</div>
|
|
237
|
+
<div className="flex items-center gap-2 mt-1 flex-wrap">
|
|
238
|
+
<span className="text-[11px] text-[var(--kyro-text-muted)]">
|
|
239
|
+
{new Date(v.createdAt as string).toLocaleString("en-US", {
|
|
240
|
+
month: "short",
|
|
241
|
+
day: "numeric",
|
|
242
|
+
hour: "2-digit",
|
|
243
|
+
minute: "2-digit",
|
|
244
|
+
})}
|
|
245
|
+
</span>
|
|
246
|
+
{v.status && (
|
|
247
|
+
<span
|
|
248
|
+
className={`inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[9px] font-bold capitalize tracking-wider ${v.status === "published"
|
|
249
|
+
? "text-[var(--kyro-success)]"
|
|
250
|
+
: "text-[var(--kyro-warning)]"
|
|
251
|
+
}`}
|
|
252
|
+
>
|
|
253
|
+
<span
|
|
254
|
+
className={`w-1 h-1 rounded-full ${v.status === "published" ? "bg-[var(--kyro-success)]" : "bg-[var(--kyro-warning)]"}`}
|
|
255
|
+
/>
|
|
256
|
+
{v.status}
|
|
257
|
+
</span>
|
|
258
|
+
)}
|
|
259
|
+
<span className="text-[10px] text-[var(--kyro-text-muted)] opacity-60">
|
|
260
|
+
{v.createdBy || "system"}
|
|
261
|
+
</span>
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
|
|
265
|
+
{!compareMode && (
|
|
266
|
+
<button
|
|
267
|
+
type="button"
|
|
268
|
+
onClick={() => handleRestoreVersion(v.id)}
|
|
269
|
+
className="shrink-0 px-2.5 py-1 rounded-lg border border-[var(--kyro-border)] text-[10px] font-bold tracking-wider text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)] hover:border-[var(--kyro-primary)] transition-all active:scale-95"
|
|
270
|
+
>
|
|
271
|
+
Restore
|
|
272
|
+
</button>
|
|
273
|
+
)}
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
);
|
|
277
|
+
})}
|
|
278
|
+
</div>
|
|
279
|
+
)}
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
);
|
|
283
|
+
}
|
|
@@ -13,6 +13,7 @@ interface UseAutoFormStateProps {
|
|
|
13
13
|
initialData: Record<string, unknown>;
|
|
14
14
|
collectionSlug?: string;
|
|
15
15
|
globalSlug?: string;
|
|
16
|
+
documentId?: string;
|
|
16
17
|
onChange?: (data: Record<string, unknown>) => void;
|
|
17
18
|
onActionSuccess?: (msg: string) => void;
|
|
18
19
|
onActionError?: (msg: string) => void;
|
|
@@ -23,6 +24,7 @@ export function useAutoFormState({
|
|
|
23
24
|
initialData,
|
|
24
25
|
collectionSlug,
|
|
25
26
|
globalSlug,
|
|
27
|
+
documentId,
|
|
26
28
|
onChange,
|
|
27
29
|
onActionSuccess,
|
|
28
30
|
onActionError,
|
|
@@ -58,7 +60,7 @@ export function useAutoFormState({
|
|
|
58
60
|
// Globals have no id field and are singleton per slug, so skip this check.
|
|
59
61
|
// Must run in useEffect — calling resetForm() during render triggers
|
|
60
62
|
// "Cannot update a component while rendering" React error.
|
|
61
|
-
const currentContextKey = globalSlug || (initialData?.id as string | undefined) || collectionSlug;
|
|
63
|
+
const currentContextKey = globalSlug || (initialData?.id as string | undefined) || documentId || collectionSlug;
|
|
62
64
|
const needsResetRef = useRef(false);
|
|
63
65
|
if (
|
|
64
66
|
!globalSlug &&
|
package/src/integration.ts
CHANGED
|
@@ -352,6 +352,14 @@ setField: (field: string, value: unknown) => {
|
|
|
352
352
|
data: Record<string, unknown>,
|
|
353
353
|
lastSaved?: Record<string, unknown>,
|
|
354
354
|
) => {
|
|
355
|
+
const state = get();
|
|
356
|
+
if (
|
|
357
|
+
state.formData === data &&
|
|
358
|
+
state.lastSavedData === (lastSaved || data) &&
|
|
359
|
+
!state.hasUnsavedChanges
|
|
360
|
+
) {
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
355
363
|
set({
|
|
356
364
|
formData: data,
|
|
357
365
|
lastSavedData: lastSaved || data,
|