@kyro-cms/admin 0.9.1 → 0.9.3
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 +1196 -1727
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -3
- package/dist/index.d.ts +4 -3
- package/dist/index.js +891 -1422
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/ActionBar.tsx +25 -174
- package/src/components/Admin.tsx +1 -3
- package/src/components/AuditLogsPage.tsx +2 -13
- package/src/components/AutoForm.tsx +160 -265
- package/src/components/DetailView.tsx +38 -66
- package/src/components/FieldRenderer.tsx +1 -1
- package/src/components/ListView.tsx +26 -198
- package/src/components/MediaGallery.tsx +117 -175
- package/src/components/RestPlayground.tsx +54 -47
- package/src/components/fields/BlocksField.tsx +8 -10
- package/src/components/fields/RelationshipBlockField.tsx +2 -3
- package/src/components/fields/RelationshipField.tsx +2 -3
- package/src/components/fix_imports.cjs +23 -0
- package/src/components/fix_imports2.cjs +19 -0
- package/src/components/replace_svgs.cjs +63 -0
- package/src/components/ui/Dropdown.tsx +7 -2
- package/src/components/ui/Modal.tsx +24 -27
- package/src/components/ui/PromptModal.tsx +2 -10
- package/src/components/ui/SlidePanel.tsx +2 -10
- package/src/components/ui/SplitButton.tsx +107 -0
- package/src/components/ui/Toaster.tsx +0 -1
- package/src/components/ui/icons.tsx +1 -0
- package/src/components/users/UsersList.tsx +8 -85
- package/src/hooks/useAutoFormState.ts +89 -161
- package/src/hooks/useQueue.ts +60 -0
- package/src/layouts/AdminLayout.astro +22 -2
- package/src/layouts/AuthLayout.astro +66 -18
- package/src/lib/autoform-store.ts +6 -2
- package/src/lib/globals.ts +5 -3
- package/src/pages/auth/register.astro +5 -1
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { Search, Check, Server } from "./ui/icons";
|
|
1
2
|
import React, { useState, useEffect, useCallback, useRef, useMemo } from "react";
|
|
2
3
|
import { createPortal } from "react-dom";
|
|
3
4
|
import { Spinner } from "./ui/Spinner";
|
|
4
5
|
import { Shimmer } from "./ui/Shimmer";
|
|
5
6
|
import { SlidePanel } from "./ui/SlidePanel";
|
|
7
|
+
import { Modal } from "./ui/Modal";
|
|
8
|
+
import { Pagination } from "./ui/Pagination";
|
|
6
9
|
import { Badge } from "./ui/Badge";
|
|
7
10
|
import { Folder } from "./ui/icons";
|
|
8
11
|
|
|
@@ -455,19 +458,7 @@ export function MediaGallery({
|
|
|
455
458
|
|
|
456
459
|
<div className={`flex items-center gap-3 flex-wrap lg:flex-nowrap ${pickerMode ? "w-full" : ""}`}>
|
|
457
460
|
<div className="relative group flex-1 min-w-[200px]">
|
|
458
|
-
<
|
|
459
|
-
className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--kyro-text-secondary)] opacity-40 group-focus-within:opacity-100 transition-opacity"
|
|
460
|
-
fill="none"
|
|
461
|
-
stroke="currentColor"
|
|
462
|
-
viewBox="0 0 24 24"
|
|
463
|
-
>
|
|
464
|
-
<path
|
|
465
|
-
strokeLinecap="round"
|
|
466
|
-
strokeLinejoin="round"
|
|
467
|
-
strokeWidth="2.5"
|
|
468
|
-
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
|
469
|
-
/>
|
|
470
|
-
</svg>
|
|
461
|
+
<Search className="w-4 h-4" />
|
|
471
462
|
<input
|
|
472
463
|
type="text"
|
|
473
464
|
placeholder="Search assets..."
|
|
@@ -669,19 +660,7 @@ export function MediaGallery({
|
|
|
669
660
|
onClick={(e) => handleSelectOne(item.id, e)}
|
|
670
661
|
className={`kyro-btn-primary p-1.5 rounded-lg transition-all ${selectedIds.has(item.id) ? "" : "bg-white/10 text-white hover:bg-white/20"}`}
|
|
671
662
|
>
|
|
672
|
-
<
|
|
673
|
-
className="w-3 h-3"
|
|
674
|
-
fill="none"
|
|
675
|
-
stroke="currentColor"
|
|
676
|
-
viewBox="0 0 24 24"
|
|
677
|
-
>
|
|
678
|
-
<path
|
|
679
|
-
strokeLinecap="round"
|
|
680
|
-
strokeLinejoin="round"
|
|
681
|
-
strokeWidth="3"
|
|
682
|
-
d="M5 13l4 4L19 7"
|
|
683
|
-
/>
|
|
684
|
-
</svg>
|
|
663
|
+
<Check className="w-4 h-4" />
|
|
685
664
|
</button>
|
|
686
665
|
</div>
|
|
687
666
|
</div>
|
|
@@ -689,19 +668,7 @@ export function MediaGallery({
|
|
|
689
668
|
|
|
690
669
|
{selectedIds.has(item.id) && (
|
|
691
670
|
<div className="absolute top-3 left-3 w-6 h-6 rounded-lg bg-[var(--kyro-primary)] text-white flex items-center justify-center shadow-lg border-2 border-white/20 animate-in zoom-in duration-300">
|
|
692
|
-
<
|
|
693
|
-
className="w-3 h-3"
|
|
694
|
-
fill="none"
|
|
695
|
-
stroke="currentColor"
|
|
696
|
-
viewBox="0 0 24 24"
|
|
697
|
-
>
|
|
698
|
-
<path
|
|
699
|
-
strokeLinecap="round"
|
|
700
|
-
strokeLinejoin="round"
|
|
701
|
-
strokeWidth="3"
|
|
702
|
-
d="M5 13l4 4L19 7"
|
|
703
|
-
/>
|
|
704
|
-
</svg>
|
|
671
|
+
<Check className="w-4 h-4" />
|
|
705
672
|
</div>
|
|
706
673
|
)}
|
|
707
674
|
</div>
|
|
@@ -802,30 +769,11 @@ export function MediaGallery({
|
|
|
802
769
|
)}
|
|
803
770
|
</div>
|
|
804
771
|
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
</span>
|
|
811
|
-
<div className="flex gap-2">
|
|
812
|
-
<button
|
|
813
|
-
disabled={page === 1}
|
|
814
|
-
onClick={() => setPage(page - 1)}
|
|
815
|
-
className="px-4 py-2 border border-[var(--kyro-border)] rounded-xl text-xs font-bold text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)] disabled:opacity-30 transition-all"
|
|
816
|
-
>
|
|
817
|
-
Previous
|
|
818
|
-
</button>
|
|
819
|
-
<button
|
|
820
|
-
disabled={page === totalPages}
|
|
821
|
-
onClick={() => setPage(page + 1)}
|
|
822
|
-
className="px-6 py-2 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-xl text-xs font-bold shadow-lg hover:opacity-90 disabled:opacity-30 transition-all"
|
|
823
|
-
>
|
|
824
|
-
Next
|
|
825
|
-
</button>
|
|
826
|
-
</div>
|
|
827
|
-
</div>
|
|
828
|
-
)}
|
|
772
|
+
<Pagination
|
|
773
|
+
page={page}
|
|
774
|
+
totalPages={totalPages}
|
|
775
|
+
onPageChange={setPage}
|
|
776
|
+
/>
|
|
829
777
|
</div>
|
|
830
778
|
</div>
|
|
831
779
|
|
|
@@ -1088,58 +1036,66 @@ export function MediaGallery({
|
|
|
1088
1036
|
</SlidePanel>
|
|
1089
1037
|
|
|
1090
1038
|
{/* Preview Modal */}
|
|
1091
|
-
{showPreview &&
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
</
|
|
1104
|
-
<
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
>
|
|
1108
|
-
<X className="w-6 h-6" />
|
|
1109
|
-
</button>
|
|
1110
|
-
</div>
|
|
1111
|
-
<div className="flex-1 w-full flex items-center justify-center p-12">
|
|
1112
|
-
{panelItem.type === "image" ? (
|
|
1113
|
-
<img
|
|
1114
|
-
src={getAbsoluteUrl(panelItem.url)}
|
|
1115
|
-
alt=""
|
|
1116
|
-
className="max-h-full max-w-full object-contain shadow-2xl rounded-lg animate-in zoom-in-95 duration-500"
|
|
1117
|
-
/>
|
|
1118
|
-
) : panelItem.type === "video" ? (
|
|
1119
|
-
<video
|
|
1120
|
-
src={getAbsoluteUrl(panelItem.url)}
|
|
1121
|
-
controls
|
|
1122
|
-
autoPlay
|
|
1123
|
-
className="max-h-full max-w-full rounded-lg shadow-2xl"
|
|
1124
|
-
/>
|
|
1125
|
-
) : (
|
|
1126
|
-
<div className="text-white text-center">
|
|
1127
|
-
<FileIcon className="w-24 h-24 mx-auto mb-6 opacity-20" />
|
|
1128
|
-
<p className="text-xl font-bold opacity-50">
|
|
1129
|
-
Preview not available for this file type
|
|
1130
|
-
</p>
|
|
1131
|
-
</div>
|
|
1132
|
-
)}
|
|
1039
|
+
{showPreview && panelItem && (
|
|
1040
|
+
<Modal
|
|
1041
|
+
open={showPreview}
|
|
1042
|
+
onClose={() => setShowPreview(false)}
|
|
1043
|
+
title=""
|
|
1044
|
+
size="full"
|
|
1045
|
+
variant="lightbox"
|
|
1046
|
+
>
|
|
1047
|
+
<div className="flex items-center justify-between p-6">
|
|
1048
|
+
<div className="flex flex-col">
|
|
1049
|
+
<span className="text-white font-bold text-lg tracking-tight">
|
|
1050
|
+
{panelItem.filename}
|
|
1051
|
+
</span>
|
|
1052
|
+
<span className="text-white/40 text-[10px] font-bold tracking-widest mt-1">
|
|
1053
|
+
{formatFileSize(panelItem.fileSize)} · {panelItem.mimeType}
|
|
1054
|
+
</span>
|
|
1133
1055
|
</div>
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1056
|
+
<button
|
|
1057
|
+
onClick={() => setShowPreview(false)}
|
|
1058
|
+
className="p-3 bg-white/10 hover:bg-white/20 text-white rounded-2xl transition-all active:scale-90"
|
|
1059
|
+
>
|
|
1060
|
+
<X className="w-6 h-6" />
|
|
1061
|
+
</button>
|
|
1062
|
+
</div>
|
|
1063
|
+
<div className="flex-1 w-full flex items-center justify-center p-12">
|
|
1064
|
+
{panelItem.type === "image" ? (
|
|
1065
|
+
<img
|
|
1066
|
+
src={getAbsoluteUrl(panelItem.url)}
|
|
1067
|
+
alt=""
|
|
1068
|
+
className="max-h-full max-w-full object-contain shadow-2xl rounded-lg animate-in zoom-in-95 duration-500"
|
|
1069
|
+
/>
|
|
1070
|
+
) : panelItem.type === "video" ? (
|
|
1071
|
+
<video
|
|
1072
|
+
src={getAbsoluteUrl(panelItem.url)}
|
|
1073
|
+
controls
|
|
1074
|
+
autoPlay
|
|
1075
|
+
className="max-h-full max-w-full rounded-lg shadow-2xl"
|
|
1076
|
+
/>
|
|
1077
|
+
) : (
|
|
1078
|
+
<div className="text-white text-center">
|
|
1079
|
+
<FileIcon className="w-24 h-24 mx-auto mb-6 opacity-20" />
|
|
1080
|
+
<p className="text-xl font-bold opacity-50">
|
|
1081
|
+
Preview not available for this file type
|
|
1082
|
+
</p>
|
|
1083
|
+
</div>
|
|
1084
|
+
)}
|
|
1085
|
+
</div>
|
|
1086
|
+
</Modal>
|
|
1087
|
+
)}
|
|
1137
1088
|
|
|
1138
1089
|
{/* Crop Modal */}
|
|
1139
|
-
{!pickerMode && showCrop &&
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1090
|
+
{!pickerMode && showCrop && panelItem && (
|
|
1091
|
+
<Modal
|
|
1092
|
+
open={showCrop}
|
|
1093
|
+
onClose={() => setShowCrop(false)}
|
|
1094
|
+
title=""
|
|
1095
|
+
size="full"
|
|
1096
|
+
variant="lightbox"
|
|
1097
|
+
>
|
|
1098
|
+
<div className="flex flex-col h-full p-8">
|
|
1143
1099
|
<div className="flex items-center justify-between mb-8">
|
|
1144
1100
|
<h3 className="text-white font-bold text-2xl tracking-tighter">
|
|
1145
1101
|
Crop Image
|
|
@@ -1173,9 +1129,9 @@ export function MediaGallery({
|
|
|
1173
1129
|
/>
|
|
1174
1130
|
</ReactCrop>
|
|
1175
1131
|
</div>
|
|
1176
|
-
</div
|
|
1177
|
-
|
|
1178
|
-
|
|
1132
|
+
</div>
|
|
1133
|
+
</Modal>
|
|
1134
|
+
)}
|
|
1179
1135
|
{!pickerMode && (
|
|
1180
1136
|
<PromptModal
|
|
1181
1137
|
open={showNewFolderModal}
|
|
@@ -1185,67 +1141,53 @@ export function MediaGallery({
|
|
|
1185
1141
|
placeholder="Folder name"
|
|
1186
1142
|
/>
|
|
1187
1143
|
)}
|
|
1188
|
-
{!pickerMode &&
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
viewBox="0 0 24 24"
|
|
1199
|
-
>
|
|
1200
|
-
<path
|
|
1201
|
-
strokeLinecap="round"
|
|
1202
|
-
strokeLinejoin="round"
|
|
1203
|
-
strokeWidth={2}
|
|
1204
|
-
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2H5a2 2 0 00-2 2v1m2 2a2 2 0 11-4 0 2 2 0 014 0zm2 2h.008v.008H5v-.008z"
|
|
1205
|
-
/>
|
|
1206
|
-
</svg>
|
|
1207
|
-
</div>
|
|
1208
|
-
<h3 className="text-xl font-bold text-[var(--kyro-text-primary)] mb-2">
|
|
1209
|
-
Storage Not Configured
|
|
1210
|
-
</h3>
|
|
1211
|
-
<p className="text-[var(--kyro-text-secondary)] mb-6 text-sm">
|
|
1212
|
-
Before uploading media, you need to configure your storage
|
|
1213
|
-
settings. Choose where files should be stored and how URLs are
|
|
1214
|
-
generated.
|
|
1215
|
-
</p>
|
|
1216
|
-
<div className="flex gap-3">
|
|
1217
|
-
<a
|
|
1218
|
-
href="/settings/storage-settings"
|
|
1219
|
-
className="flex-1 px-4 py-3 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-xl font-bold text-center hover:opacity-90 transition-colors"
|
|
1220
|
-
>
|
|
1221
|
-
Configure Storage
|
|
1222
|
-
</a>
|
|
1223
|
-
<button
|
|
1224
|
-
type="button"
|
|
1225
|
-
onClick={() => {
|
|
1226
|
-
// Set default storage config programmatically
|
|
1227
|
-
apiPost("/api/globals/storage-settings", {
|
|
1228
|
-
provider: "local",
|
|
1229
|
-
local: {
|
|
1230
|
-
uploadDir: "./public/uploads",
|
|
1231
|
-
baseUrl: "/uploads",
|
|
1232
|
-
},
|
|
1233
|
-
}).then(() => {
|
|
1234
|
-
setShowStorageConfigModal(false);
|
|
1235
|
-
setStorageConfigured(true);
|
|
1236
|
-
window.location.reload();
|
|
1237
|
-
});
|
|
1238
|
-
}}
|
|
1239
|
-
className="flex-1 px-4 py-3 border border-[var(--kyro-border)] text-[var(--kyro-text-secondary)] rounded-xl font-bold hover:bg-[var(--kyro-surface-accent)] transition-colors"
|
|
1240
|
-
>
|
|
1241
|
-
Use Defaults
|
|
1242
|
-
</button>
|
|
1243
|
-
</div>
|
|
1244
|
-
</div>
|
|
1144
|
+
{!pickerMode && (
|
|
1145
|
+
<Modal
|
|
1146
|
+
open={showStorageConfigModal}
|
|
1147
|
+
onClose={() => setShowStorageConfigModal(false)}
|
|
1148
|
+
title="Storage Not Configured"
|
|
1149
|
+
size="md"
|
|
1150
|
+
>
|
|
1151
|
+
<div className="text-center">
|
|
1152
|
+
<div className="w-16 h-16 mx-auto mb-4 rounded-full bg-[var(--kyro-sidebar-active)] flex items-center justify-center">
|
|
1153
|
+
<Server className="w-8 h-8 text-[var(--kyro-sidebar-text-active)]" />
|
|
1245
1154
|
</div>
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1155
|
+
<p className="text-[var(--kyro-text-secondary)] mb-6 text-sm">
|
|
1156
|
+
Before uploading media, you need to configure your storage
|
|
1157
|
+
settings. Choose where files should be stored and how URLs are
|
|
1158
|
+
generated.
|
|
1159
|
+
</p>
|
|
1160
|
+
<div className="flex gap-3">
|
|
1161
|
+
<a
|
|
1162
|
+
href="/settings/storage-settings"
|
|
1163
|
+
className="flex-1 px-4 py-3 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-xl font-bold text-center hover:opacity-90 transition-colors"
|
|
1164
|
+
>
|
|
1165
|
+
Configure Storage
|
|
1166
|
+
</a>
|
|
1167
|
+
<button
|
|
1168
|
+
type="button"
|
|
1169
|
+
onClick={() => {
|
|
1170
|
+
// Set default storage config programmatically
|
|
1171
|
+
apiPost("/api/globals/storage-settings", {
|
|
1172
|
+
provider: "local",
|
|
1173
|
+
local: {
|
|
1174
|
+
uploadDir: "./public/uploads",
|
|
1175
|
+
baseUrl: "/uploads",
|
|
1176
|
+
},
|
|
1177
|
+
}).then(() => {
|
|
1178
|
+
setShowStorageConfigModal(false);
|
|
1179
|
+
setStorageConfigured(true);
|
|
1180
|
+
window.location.reload();
|
|
1181
|
+
});
|
|
1182
|
+
}}
|
|
1183
|
+
className="flex-1 px-4 py-3 border border-[var(--kyro-border)] text-[var(--kyro-text-secondary)] rounded-xl font-bold hover:bg-[var(--kyro-surface-accent)] transition-colors"
|
|
1184
|
+
>
|
|
1185
|
+
Use Defaults
|
|
1186
|
+
</button>
|
|
1187
|
+
</div>
|
|
1188
|
+
</div>
|
|
1189
|
+
</Modal>
|
|
1190
|
+
)}
|
|
1249
1191
|
{!pickerMode && (
|
|
1250
1192
|
<input
|
|
1251
1193
|
type="file"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { useState, useEffect, useCallback, useRef } from "react";
|
|
2
2
|
import { useUIStore, toast } from "../lib/stores";
|
|
3
3
|
import { apiPath } from "../lib/paths";
|
|
4
|
+
import { Modal } from "./ui/Modal";
|
|
4
5
|
|
|
5
6
|
interface EnvVariable {
|
|
6
7
|
key: string;
|
|
@@ -731,59 +732,65 @@ export function RestPlayground({ collections = [] }: RestPlaygroundProps) {
|
|
|
731
732
|
</div>
|
|
732
733
|
|
|
733
734
|
{/* Modals */}
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
735
|
+
<Modal
|
|
736
|
+
open={showFolderModal}
|
|
737
|
+
onClose={() => setShowFolderModal(false)}
|
|
738
|
+
title="Create Folder"
|
|
739
|
+
size="md"
|
|
740
|
+
footer={
|
|
741
|
+
<>
|
|
742
|
+
<button type="button" onClick={() => setShowFolderModal(false)} className="kyro-btn kyro-btn-md kyro-btn-ghost">Cancel</button>
|
|
743
|
+
<button type="button" onClick={createFolder} className="kyro-btn kyro-btn-md bg-pink-500 border-pink-500 text-white hover:bg-pink-600 hover:border-pink-600">Create</button>
|
|
744
|
+
</>
|
|
745
|
+
}
|
|
746
|
+
>
|
|
747
|
+
<div className="py-2">
|
|
748
|
+
<input
|
|
749
|
+
type="text"
|
|
750
|
+
value={newFolderName}
|
|
751
|
+
onChange={(e) => setNewFolderName(e.target.value)}
|
|
752
|
+
placeholder="Folder name..."
|
|
753
|
+
className="w-full bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-lg px-4 py-2"
|
|
754
|
+
/>
|
|
755
|
+
</div>
|
|
756
|
+
</Modal>
|
|
757
|
+
|
|
758
|
+
<Modal
|
|
759
|
+
open={showSaveModal}
|
|
760
|
+
onClose={() => setShowSaveModal(false)}
|
|
761
|
+
title="Save Request"
|
|
762
|
+
size="md"
|
|
763
|
+
footer={
|
|
764
|
+
<>
|
|
765
|
+
<button type="button" onClick={() => setShowSaveModal(false)} className="kyro-btn kyro-btn-md kyro-btn-ghost">Cancel</button>
|
|
766
|
+
<button type="button" onClick={saveRequest} className="kyro-btn kyro-btn-md bg-pink-500 border-pink-500 text-white hover:bg-pink-600 hover:border-pink-600 disabled:opacity-50 disabled:cursor-not-allowed" disabled={!saveRequestName || !saveToFolderId}>Save</button>
|
|
767
|
+
</>
|
|
768
|
+
}
|
|
769
|
+
>
|
|
770
|
+
<div className="space-y-4 py-2">
|
|
771
|
+
<div>
|
|
772
|
+
<label className="text-[10px] font-bold tracking-widest text-[var(--kyro-text-muted)] block mb-1">Request Name</label>
|
|
738
773
|
<input
|
|
739
774
|
type="text"
|
|
740
|
-
value={
|
|
741
|
-
onChange={(e) =>
|
|
742
|
-
placeholder="
|
|
743
|
-
className="w-full bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-lg px-4 py-2
|
|
775
|
+
value={saveRequestName}
|
|
776
|
+
onChange={(e) => setSaveRequestName(e.target.value)}
|
|
777
|
+
placeholder="e.g. List Posts..."
|
|
778
|
+
className="w-full bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-lg px-4 py-2"
|
|
744
779
|
/>
|
|
745
|
-
<div className="p-4 border-t border-[var(--kyro-border)] flex justify-end gap-2 bg-[var(--kyro-surface-accent)]">
|
|
746
|
-
<button type="button" onClick={() => setShowFolderModal(false)} className="kyro-btn kyro-btn-md kyro-btn-ghost">Cancel</button>
|
|
747
|
-
<button type="button" onClick={createFolder} className="kyro-btn kyro-btn-md bg-pink-500 border-pink-500 text-white hover:bg-pink-600 hover:border-pink-600">Create</button>
|
|
748
|
-
</div>
|
|
749
780
|
</div>
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
<
|
|
759
|
-
|
|
760
|
-
<input
|
|
761
|
-
type="text"
|
|
762
|
-
value={saveRequestName}
|
|
763
|
-
onChange={(e) => setSaveRequestName(e.target.value)}
|
|
764
|
-
placeholder="e.g. List Posts..."
|
|
765
|
-
className="w-full bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-lg px-4 py-2"
|
|
766
|
-
/>
|
|
767
|
-
</div>
|
|
768
|
-
<div>
|
|
769
|
-
<label className="text-[10px] font-bold tracking-widest text-[var(--kyro-text-muted)] block mb-1">Folder</label>
|
|
770
|
-
<select
|
|
771
|
-
value={saveToFolderId}
|
|
772
|
-
onChange={(e) => setSaveToFolderId(e.target.value)}
|
|
773
|
-
className="w-full bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-lg px-4 py-2"
|
|
774
|
-
>
|
|
775
|
-
<option value="">Select Folder...</option>
|
|
776
|
-
{folders.map(f => <option key={f.id} value={f.id}>{f.name}</option>)}
|
|
777
|
-
</select>
|
|
778
|
-
</div>
|
|
779
|
-
</div>
|
|
780
|
-
<div className="p-4 border-t border-[var(--kyro-border)] flex justify-end gap-2 bg-[var(--kyro-surface-accent)]">
|
|
781
|
-
<button type="button" onClick={() => setShowSaveModal(false)} className="kyro-btn kyro-btn-md kyro-btn-ghost">Cancel</button>
|
|
782
|
-
<button type="button" onClick={saveRequest} className="kyro-btn kyro-btn-md bg-pink-500 border-pink-500 text-white hover:bg-pink-600 hover:border-pink-600 disabled:opacity-50 disabled:cursor-not-allowed" disabled={!saveRequestName || !saveToFolderId}>Save</button>
|
|
783
|
-
</div>
|
|
781
|
+
<div>
|
|
782
|
+
<label className="text-[10px] font-bold tracking-widest text-[var(--kyro-text-muted)] block mb-1">Folder</label>
|
|
783
|
+
<select
|
|
784
|
+
value={saveToFolderId}
|
|
785
|
+
onChange={(e) => setSaveToFolderId(e.target.value)}
|
|
786
|
+
className="w-full bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-lg px-4 py-2"
|
|
787
|
+
>
|
|
788
|
+
<option value="">Select Folder...</option>
|
|
789
|
+
{folders.map(f => <option key={f.id} value={f.id}>{f.name}</option>)}
|
|
790
|
+
</select>
|
|
784
791
|
</div>
|
|
785
792
|
</div>
|
|
786
|
-
|
|
793
|
+
</Modal>
|
|
787
794
|
</div>
|
|
788
795
|
);
|
|
789
796
|
}
|
|
@@ -394,11 +394,9 @@ export const BlocksField: React.FC<BlocksFieldProps> = ({
|
|
|
394
394
|
useEffect(() => {
|
|
395
395
|
const valueArray = Array.isArray(value) ? value : [];
|
|
396
396
|
const lastValueArray = lastValueRef.current || [];
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
if (valueIds !== lastValueIds) {
|
|
401
|
-
console.log("BlocksField sync: value=", value, "valueIds=", valueIds, "lastValueIds=", lastValueIds);
|
|
397
|
+
|
|
398
|
+
// Deep compare to catch external data changes (e.g. discard draft / auto-save restore)
|
|
399
|
+
if (JSON.stringify(valueArray) !== JSON.stringify(lastValueArray)) {
|
|
402
400
|
const valueArrayCopy = [...valueArray];
|
|
403
401
|
prevBlocksLengthRef.current = valueArrayCopy.length;
|
|
404
402
|
prevBlockIdsRef.current = new Set(valueArrayCopy.map((b: Record<string, unknown>) => b.id));
|
|
@@ -407,6 +405,7 @@ export const BlocksField: React.FC<BlocksFieldProps> = ({
|
|
|
407
405
|
isInitializedRef.current = true;
|
|
408
406
|
} else if (valueArray.length === 0 && !isInitializedRef.current) {
|
|
409
407
|
isInitializedRef.current = true;
|
|
408
|
+
lastValueRef.current = []; // Fix for new pages starting with empty arrays
|
|
410
409
|
}
|
|
411
410
|
}, [value, field.name, store]);
|
|
412
411
|
|
|
@@ -416,12 +415,11 @@ export const BlocksField: React.FC<BlocksFieldProps> = ({
|
|
|
416
415
|
useEffect(() => {
|
|
417
416
|
if (!onChangeRef.current) return;
|
|
418
417
|
const lastValue = lastValueRef.current;
|
|
419
|
-
if (!lastValue) return;
|
|
420
|
-
|
|
421
|
-
const currentIds = blocks.map((b: Record<string, unknown>) => b.id).join(",");
|
|
422
|
-
const lastIds = lastValue.map((b: Record<string, unknown>) => b.id).join(",");
|
|
418
|
+
if (!lastValue) return; // Wait until initialized
|
|
423
419
|
|
|
424
|
-
|
|
420
|
+
// Deep compare blocks vs lastValue to detect content edits, not just ID changes
|
|
421
|
+
if (JSON.stringify(blocks) !== JSON.stringify(lastValue)) {
|
|
422
|
+
lastValueRef.current = [...blocks]; // Update ref BEFORE firing onChange to prevent loops
|
|
425
423
|
onChangeRef.current(blocks);
|
|
426
424
|
}
|
|
427
425
|
}, [blocks]);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { useState, useEffect } from "react";
|
|
2
2
|
import { Search, Loader2, X } from "../ui/icons";
|
|
3
3
|
import { apiGet, buildSearchQuery } from "../../lib/api";
|
|
4
|
+
import { EmptyState } from "../ui/EmptyState";
|
|
4
5
|
|
|
5
6
|
interface RelationshipBlockFieldProps {
|
|
6
7
|
relationTo?: string;
|
|
@@ -171,9 +172,7 @@ export const RelationshipBlockField: React.FC<RelationshipBlockFieldProps> = ({
|
|
|
171
172
|
Loading...
|
|
172
173
|
</div>
|
|
173
174
|
) : options.length === 0 ? (
|
|
174
|
-
<
|
|
175
|
-
No results found
|
|
176
|
-
</div>
|
|
175
|
+
<EmptyState title="No results found" />
|
|
177
176
|
) : (
|
|
178
177
|
<div className="py-1">
|
|
179
178
|
{options.map((opt) => (
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useEffect, useState, useRef, useCallback } from "react";
|
|
2
2
|
import { Search, X, ChevronDown, Loader2 } from "../ui/icons";
|
|
3
3
|
import { apiGet, buildSearchQuery } from "../../lib/api";
|
|
4
|
+
import { EmptyState } from "../ui/EmptyState";
|
|
4
5
|
|
|
5
6
|
interface RelationshipFieldProps {
|
|
6
7
|
field: {
|
|
@@ -314,9 +315,7 @@ export function RelationshipField({
|
|
|
314
315
|
Loading...
|
|
315
316
|
</div>
|
|
316
317
|
) : options.length === 0 ? (
|
|
317
|
-
<
|
|
318
|
-
No results found
|
|
319
|
-
</div>
|
|
318
|
+
<EmptyState title="No results found" />
|
|
320
319
|
) : (
|
|
321
320
|
<div className="py-1">
|
|
322
321
|
{options.map((opt) => (
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
|
|
3
|
+
const addImport = (file, icons, relativePath) => {
|
|
4
|
+
let content = fs.readFileSync(file, 'utf8');
|
|
5
|
+
if (!content.includes('from "' + relativePath + '"')) {
|
|
6
|
+
const importStatement = `import { ${icons.join(', ')} } from "${relativePath}";\n`;
|
|
7
|
+
content = importStatement + content;
|
|
8
|
+
fs.writeFileSync(file, content);
|
|
9
|
+
} else {
|
|
10
|
+
// If it exists, we might need to append to the existing import.
|
|
11
|
+
// To be safe and simple, let's just add a new line. TS merges imports.
|
|
12
|
+
const importStatement = `import { ${icons.join(', ')} } from "${relativePath}";\n`;
|
|
13
|
+
content = importStatement + content;
|
|
14
|
+
fs.writeFileSync(file, content);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
addImport('/Users/macbook/Dev/Web/Astro/kyro-cms/admin/src/components/MediaGallery.tsx', ['Search', 'Check', 'Server'], './ui/icons');
|
|
19
|
+
addImport('/Users/macbook/Dev/Web/Astro/kyro-cms/admin/src/components/ui/Modal.tsx', ['X'], './icons');
|
|
20
|
+
addImport('/Users/macbook/Dev/Web/Astro/kyro-cms/admin/src/components/ui/PromptModal.tsx', ['X'], './icons');
|
|
21
|
+
addImport('/Users/macbook/Dev/Web/Astro/kyro-cms/admin/src/components/ui/SlidePanel.tsx', ['X'], './icons');
|
|
22
|
+
addImport('/Users/macbook/Dev/Web/Astro/kyro-cms/admin/src/components/users/UsersList.tsx', ['Plus', 'Lock', 'CheckCircle2', 'Edit2', 'Trash2', 'XCircle', 'X'], '../ui/icons');
|
|
23
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
|
|
3
|
+
const addImport = (file, icons, relativePath) => {
|
|
4
|
+
let content = fs.readFileSync(file, 'utf8');
|
|
5
|
+
if (!content.includes('from "' + relativePath + '"')) {
|
|
6
|
+
const importStatement = `import { ${icons.join(', ')} } from "${relativePath}";\n`;
|
|
7
|
+
content = importStatement + content;
|
|
8
|
+
fs.writeFileSync(file, content);
|
|
9
|
+
} else {
|
|
10
|
+
const importStatement = `import { ${icons.join(', ')} } from "${relativePath}";\n`;
|
|
11
|
+
content = importStatement + content;
|
|
12
|
+
fs.writeFileSync(file, content);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
addImport('/Users/macbook/Dev/Web/Astro/kyro-cms/admin/src/components/AuditLogsPage.tsx', ['Search'], './ui/icons');
|
|
17
|
+
addImport('/Users/macbook/Dev/Web/Astro/kyro-cms/admin/src/components/AutoForm.tsx', ['ChevronRight', 'Check', 'ExternalLink', 'X'], './ui/icons');
|
|
18
|
+
addImport('/Users/macbook/Dev/Web/Astro/kyro-cms/admin/src/components/ListView.tsx', ['Search', 'Filter', 'Columns3', 'X', 'Trash2', 'Archive', 'ChevronUp', 'Edit2'], './ui/icons');
|
|
19
|
+
|