@kyro-cms/admin 0.1.9 → 0.2.0

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.
@@ -1,4 +1,6 @@
1
1
  import React, { useState, useEffect } from "react";
2
+ import { apiGet, apiPost, apiPatch, apiDelete } from "@kyro-cms/utils/lib/api";
3
+ import { formatDate } from "@kyro-cms/utils/lib/date-utils";
2
4
  import {
3
5
  Webhook,
4
6
  Plus,
@@ -52,11 +54,8 @@ export function WebhookManager() {
52
54
  const loadWebhooks = async () => {
53
55
  setLoading(true);
54
56
  try {
55
- const res = await fetch("/api/webhooks");
56
- if (res.ok) {
57
- const data = await res.json();
58
- setWebhooks(data);
59
- }
57
+ const data = await apiGet("/api/webhooks");
58
+ setWebhooks(data);
60
59
  } catch (e) {
61
60
  console.error(e);
62
61
  } finally {
@@ -75,20 +74,10 @@ export function WebhookManager() {
75
74
  }
76
75
 
77
76
  try {
78
- const res = await fetch("/api/webhooks", {
79
- method: "POST",
80
- headers: { "Content-Type": "application/json" },
81
- body: JSON.stringify(formData),
82
- });
83
-
84
- if (res.ok) {
85
- setShowCreateModal(false);
86
- setFormData({ name: "", url: "", events: [], secret: "" });
87
- loadWebhooks();
88
- } else {
89
- const error = await res.json();
90
- setCreateError(error.error || "Failed to create webhook");
91
- }
77
+ await apiPost("/api/webhooks", formData);
78
+ setShowCreateModal(false);
79
+ setFormData({ name: "", url: "", events: [], secret: "" });
80
+ loadWebhooks();
92
81
  } catch (e) {
93
82
  console.error(e);
94
83
  setCreateError("Failed to create webhook");
@@ -104,12 +93,8 @@ export function WebhookManager() {
104
93
  if (!deleteId) return;
105
94
 
106
95
  try {
107
- const res = await fetch(`/api/webhooks/${deleteId}`, {
108
- method: "DELETE",
109
- });
110
- if (res.ok) {
111
- loadWebhooks();
112
- }
96
+ await apiDelete(`/api/webhooks/${deleteId}`);
97
+ loadWebhooks();
113
98
  } catch (e) {
114
99
  console.error(e);
115
100
  }
@@ -123,33 +108,22 @@ export function WebhookManager() {
123
108
  setShowTestModal(true);
124
109
 
125
110
  try {
126
- const res = await fetch(`/api/webhooks/${id}/test`, { method: "POST" });
127
- const data = await res.json();
111
+ const data = await apiPost(`/api/webhooks/${id}/test`);
128
112
  setTestResult({
129
- success: res.ok,
130
- message:
131
- data.message ||
132
- (res.ok
133
- ? "Webhook triggered successfully"
134
- : "Failed to trigger webhook"),
113
+ success: true,
114
+ message: data.message || "Webhook triggered successfully",
135
115
  });
136
116
  } catch (e) {
137
- setTestResult({ success: false, message: "Error testing webhook" });
117
+ setTestResult({ success: false, message: "Failed to trigger webhook" });
138
118
  }
139
119
  };
140
120
 
141
121
  const toggleStatus = async (id: string, currentStatus: string) => {
142
122
  try {
143
- const res = await fetch(`/api/webhooks/${id}`, {
144
- method: "PATCH",
145
- headers: { "Content-Type": "application/json" },
146
- body: JSON.stringify({
147
- status: currentStatus === "active" ? "paused" : "active",
148
- }),
123
+ await apiPatch(`/api/webhooks/${id}`, {
124
+ status: currentStatus === "active" ? "paused" : "active",
149
125
  });
150
- if (res.ok) {
151
- loadWebhooks();
152
- }
126
+ loadWebhooks();
153
127
  } catch (e) {
154
128
  console.error(e);
155
129
  }
@@ -183,7 +157,8 @@ export function WebhookManager() {
183
157
  <h1 className="text-4xl font-black tracking-tighter text-[var(--kyro-text-primary)]">
184
158
  Web<span className="text-[var(--kyro-primary)]">hooks</span>
185
159
  </h1>
186
- <button type="button"
160
+ <button
161
+ type="button"
187
162
  onClick={() => setShowHelpModal(true)}
188
163
  className="p-2 rounded-lg text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-primary)] hover:bg-[var(--kyro-surface-accent)] transition-all"
189
164
  title="Learn how webhooks work"
@@ -195,7 +170,8 @@ export function WebhookManager() {
195
170
  Receive real-time notifications when events occur in your CMS.
196
171
  </p>
197
172
  </div>
198
- <button type="button"
173
+ <button
174
+ type="button"
199
175
  onClick={() => {
200
176
  setFormData({ name: "", url: "", events: [], secret: "" });
201
177
  setCreateError("");
@@ -253,7 +229,8 @@ export function WebhookManager() {
253
229
  <p className="text-sm text-[var(--kyro-text-secondary)] opacity-60 mb-6">
254
230
  Add your first webhook to start receiving event notifications.
255
231
  </p>
256
- <button type="button"
232
+ <button
233
+ type="button"
257
234
  onClick={() => setShowCreateModal(true)}
258
235
  className="px-6 py-3 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-full font-black text-sm hover:opacity-90 transition-all"
259
236
  >
@@ -310,7 +287,8 @@ export function WebhookManager() {
310
287
  </div>
311
288
  </div>
312
289
  <div className="flex items-center gap-2 flex-shrink-0">
313
- <button type="button"
290
+ <button
291
+ type="button"
314
292
  onClick={() => handleTest(webhook.id)}
315
293
  className="p-3 bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-xl hover:bg-[var(--kyro-surface)] flex items-center gap-2"
316
294
  title="Send test request"
@@ -320,7 +298,8 @@ export function WebhookManager() {
320
298
  Test
321
299
  </span>
322
300
  </button>
323
- <button type="button"
301
+ <button
302
+ type="button"
324
303
  onClick={() => toggleStatus(webhook.id, webhook.status)}
325
304
  className={`p-3 rounded-xl flex items-center gap-2 ${
326
305
  webhook.status === "active"
@@ -349,7 +328,8 @@ export function WebhookManager() {
349
328
  </>
350
329
  )}
351
330
  </button>
352
- <button type="button"
331
+ <button
332
+ type="button"
353
333
  onClick={() => handleDelete(webhook.id)}
354
334
  className="p-3 text-red-500 bg-red-500/10 rounded-xl hover:bg-red-500/20"
355
335
  title="Delete webhook"
@@ -409,7 +389,8 @@ export function WebhookManager() {
409
389
  </label>
410
390
  <div className="grid grid-cols-2 gap-3">
411
391
  {eventOptions.map((opt) => (
412
- <button type="button"
392
+ <button
393
+ type="button"
413
394
  key={opt.value}
414
395
  onClick={() => {
415
396
  const events = formData.events.includes(opt.value)
@@ -454,13 +435,15 @@ export function WebhookManager() {
454
435
  </div>
455
436
  </ModalContent>
456
437
  <ModalActions>
457
- <button type="button"
438
+ <button
439
+ type="button"
458
440
  onClick={() => setShowCreateModal(false)}
459
441
  className="px-4 py-2 rounded-lg font-medium text-sm border border-[var(--kyro-border)] text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)]"
460
442
  >
461
443
  Cancel
462
444
  </button>
463
- <button type="button"
445
+ <button
446
+ type="button"
464
447
  onClick={handleCreate}
465
448
  className="px-4 py-2 rounded-lg font-medium text-sm bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] hover:opacity-90"
466
449
  >
@@ -488,13 +471,15 @@ export function WebhookManager() {
488
471
  </div>
489
472
  </ModalContent>
490
473
  <ModalActions>
491
- <button type="button"
474
+ <button
475
+ type="button"
492
476
  onClick={() => setShowDeleteModal(false)}
493
477
  className="px-4 py-2 rounded-lg font-medium text-sm border border-[var(--kyro-border)] text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)]"
494
478
  >
495
479
  Keep Webhook
496
480
  </button>
497
- <button type="button"
481
+ <button
482
+ type="button"
498
483
  onClick={confirmDelete}
499
484
  className="px-4 py-2 rounded-lg font-medium text-sm bg-red-500 text-white hover:bg-red-600"
500
485
  >
@@ -536,7 +521,8 @@ export function WebhookManager() {
536
521
  )}
537
522
  </ModalContent>
538
523
  <ModalActions>
539
- <button type="button"
524
+ <button
525
+ type="button"
540
526
  onClick={() => setShowTestModal(false)}
541
527
  className="px-4 py-2 rounded-lg font-medium text-sm bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] hover:opacity-90"
542
528
  >
@@ -595,7 +581,8 @@ export function WebhookManager() {
595
581
  </div>
596
582
  </ModalContent>
597
583
  <ModalActions>
598
- <button type="button"
584
+ <button
585
+ type="button"
599
586
  onClick={() => setShowHelpModal(false)}
600
587
  className="px-4 py-2 rounded-lg font-medium text-sm bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] hover:opacity-90"
601
588
  >
@@ -1,5 +1,6 @@
1
1
  import React, { useState, useEffect } from "react";
2
2
  import { Search, Loader2, X } from "lucide-react";
3
+ import { apiGet, buildSearchQuery } from "@kyro-cms/utils/lib/api";
3
4
 
4
5
  interface RelationshipBlockFieldProps {
5
6
  relationTo?: string;
@@ -28,8 +29,7 @@ export const RelationshipBlockField: React.FC<RelationshipBlockFieldProps> = ({
28
29
  const [loadingCollections, setLoadingCollections] = useState(true);
29
30
 
30
31
  useEffect(() => {
31
- fetch("/api/collections", { credentials: "include" })
32
- .then((res) => res.json())
32
+ apiGet("/api/collections")
33
33
  .then((data) => {
34
34
  setCollections(
35
35
  (data.collections || []).map((c: any) => c.slug || c.name || c),
@@ -41,12 +41,9 @@ export const RelationshipBlockField: React.FC<RelationshipBlockFieldProps> = ({
41
41
 
42
42
  const fetchOptions = (query: string = "") => {
43
43
  setLoading(true);
44
- const url = query
45
- ? `/api/${relationTo}?where[${labelField}][contains]=${encodeURIComponent(query)}&limit=20`
46
- : `/api/${relationTo}?limit=20`;
44
+ const url = `/api/${relationTo}?${buildSearchQuery(query, [labelField], 20)}`;
47
45
 
48
- fetch(url, { credentials: "include" })
49
- .then((res) => res.json())
46
+ apiGet(url)
50
47
  .then((data) => {
51
48
  setOptions(data.docs || []);
52
49
  setLoading(false);
@@ -1,5 +1,6 @@
1
1
  import { useEffect, useState, useRef } from "react";
2
2
  import { Search, X, ChevronDown, Loader2 } from "lucide-react";
3
+ import { apiGet, buildSearchQuery } from "@kyro-cms/utils/lib/api";
3
4
 
4
5
  interface RelationshipFieldProps {
5
6
  field: {
@@ -42,15 +43,9 @@ export function RelationshipField({
42
43
  const fetchOptions = (query: string = "") => {
43
44
  setLoading(true);
44
45
  const searchFields = ["title", "name", "label", "email"];
45
- const searchQuery = searchFields
46
- .map((f) => `where[${f}][contains]=${encodeURIComponent(query)}`)
47
- .join("&");
48
- const url = query
49
- ? `/api/${targetCollection}?${searchQuery}&limit=50`
50
- : `/api/${targetCollection}?limit=50`;
46
+ const url = `/api/${targetCollection}?${buildSearchQuery(query, searchFields)}`;
51
47
 
52
- fetch(url, { credentials: "include" })
53
- .then((res) => res.json())
48
+ apiGet(url)
54
49
  .then((data) => {
55
50
  setOptions((prev) => {
56
51
  const existingIds = new Set(prev.map((o) => o.id));
@@ -77,8 +72,7 @@ export function RelationshipField({
77
72
  items.forEach((itemId) => {
78
73
  const id = typeof itemId === "object" ? itemId?.id : itemId;
79
74
  if (id && !options.some((o) => o.id === id)) {
80
- fetch(`/api/${targetCollection}/${id}`, { credentials: "include" })
81
- .then((res) => res.json())
75
+ apiGet(`/api/${targetCollection}/${id}`)
82
76
  .then((doc) => {
83
77
  setOptions((prev) => [...prev, doc]);
84
78
  })
@@ -1,6 +1,12 @@
1
1
  import React, { useState, useEffect, useRef, useMemo } from "react";
2
2
  import { createPortal } from "react-dom";
3
3
  import { Image, Film, FileText, Music, File, X, Loader2 } from "lucide-react";
4
+ import {
5
+ apiGet,
6
+ withCacheBust,
7
+ apiPost,
8
+ apiUpload,
9
+ } from "@kyro-cms/utils/lib/api";
4
10
 
5
11
  interface UploadFieldProps {
6
12
  field: any;
@@ -102,10 +108,7 @@ export function UploadField({
102
108
 
103
109
  const loadFolders = async () => {
104
110
  try {
105
- const resp = await fetch("/api/media/folders?t=" + Date.now(), {
106
- credentials: "include",
107
- });
108
- const result = await resp.json();
111
+ const result = await apiGet(withCacheBust("/api/media/folders"));
109
112
  setFolders(result.folders || []);
110
113
  } catch {
111
114
  setFolders([]);
@@ -115,12 +118,13 @@ export function UploadField({
115
118
  const loadMedia = async () => {
116
119
  setMediaLoading(true);
117
120
  try {
118
- let url = `/api/media?limit=60&sortBy=createdAt&sortDir=desc&t=${Date.now()}`;
121
+ let url = withCacheBust(
122
+ `/api/media?limit=60&sortBy=createdAt&sortDir=desc`,
123
+ );
119
124
  if (selectedFolder) {
120
125
  url += "&folder=" + encodeURIComponent(selectedFolder);
121
126
  }
122
- const resp = await fetch(url, { credentials: "include" });
123
- const result = await resp.json();
127
+ const result = await apiGet(url);
124
128
  setMediaItems(result.docs || []);
125
129
  } catch {
126
130
  setMediaItems([]);
@@ -137,17 +141,11 @@ export function UploadField({
137
141
  if (selectedFolder) {
138
142
  formData.append("folder", selectedFolder);
139
143
  }
140
- const resp = await fetch("/api/upload", {
141
- method: "POST",
142
- body: formData,
143
- credentials: "include",
144
- });
145
- if (!resp.ok) throw new Error("Upload failed");
146
- const result = await resp.json();
144
+ const result = await apiUpload("/api/upload", formData);
147
145
  const newImage = {
148
146
  id: result.id,
149
147
  filename: result.filename,
150
- originalName: result.originalName ?? file.name,
148
+ originalName: (result as any).originalName ?? file.name,
151
149
  url: result.url,
152
150
  mimeType: file.type,
153
151
  };
@@ -169,17 +167,7 @@ export function UploadField({
169
167
 
170
168
  setUrlError("");
171
169
  try {
172
- const resp = await fetch("/api/upload", {
173
- method: "POST",
174
- headers: { "Content-Type": "application/json" },
175
- body: JSON.stringify({ url }),
176
- credentials: "include",
177
- });
178
- if (!resp.ok) {
179
- const data = await resp.json();
180
- throw new Error(data.error || "Failed to add URL");
181
- }
182
- const result = await resp.json();
170
+ const result = await apiPost("/api/upload", { url });
183
171
  const originalName = (() => {
184
172
  try {
185
173
  return (
@@ -465,10 +453,11 @@ function MediaPickerContent({
465
453
  }) {
466
454
  return (
467
455
  <div
468
- className={`${isFullscreen
469
- ? "fixed inset-0 z-[9999]"
470
- : "absolute z-50 w-[360px] max-h-[400px] mt-1 rounded-lg shadow-lg"
471
- } overflow-hidden bg-[var(--kyro-surface)] border border-[var(--kyro-border)] flex flex-col`}
456
+ className={`${
457
+ isFullscreen
458
+ ? "fixed inset-0 z-[9999]"
459
+ : "absolute z-50 w-[360px] max-h-[400px] mt-1 rounded-lg shadow-lg"
460
+ } overflow-hidden bg-[var(--kyro-surface)] border border-[var(--kyro-border)] flex flex-col`}
472
461
  >
473
462
  <div className="p-2 border-b border-[var(--kyro-border)] flex flex-col gap-2">
474
463
  <input
@@ -483,10 +472,11 @@ function MediaPickerContent({
483
472
  <button
484
473
  type="button"
485
474
  onClick={() => setSelectedFolder("")}
486
- className={`px-2 py-1 text-xs rounded transition-colors ${selectedFolder === ""
487
- ? "bg-[var(--kyro-primary)] text-white"
488
- : "bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-border)]"
489
- }`}
475
+ className={`px-2 py-1 text-xs rounded transition-colors ${
476
+ selectedFolder === ""
477
+ ? "bg-[var(--kyro-primary)] text-white"
478
+ : "bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-border)]"
479
+ }`}
490
480
  >
491
481
  All
492
482
  </button>
@@ -495,10 +485,11 @@ function MediaPickerContent({
495
485
  key={folder.path}
496
486
  type="button"
497
487
  onClick={() => setSelectedFolder(folder.path)}
498
- className={`px-2 py-1 text-xs rounded transition-colors ${selectedFolder === folder.path
499
- ? "bg-[var(--kyro-primary)] text-white"
500
- : "bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-border)]"
501
- }`}
488
+ className={`px-2 py-1 text-xs rounded transition-colors ${
489
+ selectedFolder === folder.path
490
+ ? "bg-[var(--kyro-primary)] text-white"
491
+ : "bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-border)]"
492
+ }`}
502
493
  >
503
494
  {folder.name}
504
495
  </button>
@@ -519,10 +510,11 @@ function MediaPickerContent({
519
510
  </div>
520
511
  ) : (
521
512
  <div
522
- className={`grid gap-1 ${isFullscreen
523
- ? "grid-cols-[repeat(auto-fill,minmax(140px,1fr))]"
524
- : "grid-cols-3"
525
- }`}
513
+ className={`grid gap-1 ${
514
+ isFullscreen
515
+ ? "grid-cols-[repeat(auto-fill,minmax(140px,1fr))]"
516
+ : "grid-cols-3"
517
+ }`}
526
518
  >
527
519
  {filteredMedia.map((item) => (
528
520
  <button
@@ -532,8 +524,9 @@ function MediaPickerContent({
532
524
  className="border border-[var(--kyro-border)] rounded-md overflow-hidden cursor-pointer p-0 bg-[var(--kyro-surface)] hover:border-[var(--kyro-primary)] transition-all relative group"
533
525
  >
534
526
  <div
535
- className={`w-full flex items-center justify-center bg-[var(--kyro-surface-accent)] ${isFullscreen ? "h-[120px]" : "h-[80px]"
536
- }`}
527
+ className={`w-full flex items-center justify-center bg-[var(--kyro-surface-accent)] ${
528
+ isFullscreen ? "h-[120px]" : "h-[80px]"
529
+ }`}
537
530
  >
538
531
  {getFileType(item.mimeType, item.filename) === "image" ? (
539
532
  <img
@@ -1,4 +1,5 @@
1
1
  import React, { useState, useEffect, useRef, useCallback } from "react";
2
+ import { apiGet } from "@kyro-cms/utils/lib/api";
2
3
  import {
3
4
  Search,
4
5
  FileText,