@kyro-cms/admin 0.3.2 → 0.3.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/EditorClient-XEUOVAAC.js +466 -0
- package/dist/EditorClient-XEUOVAAC.js.map +1 -0
- package/dist/EditorClient-YLCGVDXY.cjs +468 -0
- package/dist/EditorClient-YLCGVDXY.cjs.map +1 -0
- package/dist/chunk-7KPIUCGT.js +384 -0
- package/dist/chunk-7KPIUCGT.js.map +1 -0
- package/dist/chunk-GOACG6R7.cjs +473 -0
- package/dist/chunk-GOACG6R7.cjs.map +1 -0
- package/dist/index.cjs +14861 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.css +1661 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.ts +563 -0
- package/dist/index.js +14784 -0
- package/dist/index.js.map +1 -0
- package/package.json +19 -19
- package/src/components/ActionBar.tsx +7 -43
- package/src/components/Admin.tsx +138 -277
- package/src/components/ApiKeysManager.tsx +428 -419
- package/src/components/AuditLogsPage.tsx +35 -39
- package/src/components/AuthBridge.tsx +51 -0
- package/src/components/AutoForm.tsx +495 -1230
- package/src/components/BrandingHub.tsx +18 -19
- package/src/components/BulkActionsBar.tsx +1 -1
- package/src/components/CreateView.tsx +22 -36
- package/src/components/Dashboard.tsx +60 -84
- package/src/components/DetailView.tsx +113 -91
- package/src/components/DeveloperCenter.tsx +200 -198
- package/src/components/FieldRenderer.tsx +206 -0
- package/src/components/GraphQLPlayground.tsx +340 -480
- package/src/components/ListView.tsx +828 -254
- package/src/components/LoginPage.tsx +3 -4
- package/src/components/MarketplaceManager.tsx +254 -0
- package/src/components/MediaGallery.tsx +856 -1192
- package/src/components/PluginsManager.tsx +277 -0
- package/src/components/RestPlayground.tsx +398 -560
- package/src/components/SessionsManager.tsx +211 -0
- package/src/components/Sidebar.astro +179 -151
- package/src/components/ThemeProvider.tsx +7 -161
- package/src/components/UserManagement.tsx +162 -146
- package/src/components/UserMenu.tsx +110 -0
- package/src/components/WebhookManager.tsx +305 -367
- package/src/components/blocks/AccordionBlock.tsx +4 -4
- package/src/components/blocks/ArrayBlock.tsx +3 -3
- package/src/components/blocks/BlockEditModal.tsx +8 -8
- package/src/components/blocks/BlockWrapper.tsx +61 -0
- package/src/components/blocks/ButtonBlock.tsx +4 -4
- package/src/components/blocks/ChildBlocksTree.tsx +23 -25
- package/src/components/blocks/CodeBlock.tsx +15 -15
- package/src/components/blocks/ColumnsBlock.tsx +6 -44
- package/src/components/blocks/DividerBlock.tsx +3 -3
- package/src/components/blocks/FileBlock.tsx +4 -4
- package/src/components/blocks/HeadingBlock.tsx +6 -38
- package/src/components/blocks/HeroBlock.tsx +4 -4
- package/src/components/blocks/ImageBlock.tsx +4 -4
- package/src/components/blocks/LinkBlock.tsx +4 -4
- package/src/components/blocks/ListBlock.tsx +3 -3
- package/src/components/blocks/ParagraphBlock.tsx +12 -42
- package/src/components/blocks/RelationshipBlock.tsx +4 -4
- package/src/components/blocks/RichTextBlock.tsx +4 -4
- package/src/components/blocks/VStackBlock.tsx +5 -37
- package/src/components/blocks/VideoBlock.tsx +4 -4
- package/src/components/blocks/types.ts +11 -0
- package/src/components/fields/AccordionField.tsx +1 -1
- package/src/components/fields/ArrayField.tsx +2 -2
- package/src/components/fields/ArrayLayout.tsx +93 -0
- package/src/components/fields/BlocksField.tsx +122 -111
- package/src/components/fields/ButtonField.tsx +1 -1
- package/src/components/fields/CheckboxField.tsx +14 -15
- package/src/components/fields/ChildrenField.tsx +2 -2
- package/src/components/fields/CodeField.tsx +3 -3
- package/src/components/fields/ColumnsField.tsx +2 -2
- package/src/components/fields/DateField.tsx +13 -26
- package/src/components/fields/EditorClient.tsx +26 -28
- package/src/components/fields/FieldLayout.tsx +52 -0
- package/src/components/fields/GroupLayout.tsx +35 -0
- package/src/components/fields/JSONField.tsx +7 -7
- package/src/components/fields/LinkField.tsx +1 -1
- package/src/components/fields/MarkdownField.tsx +1 -1
- package/src/components/fields/NumberField.tsx +13 -26
- package/src/components/fields/PortableTextField.tsx +4 -4
- package/src/components/fields/PortableTextRenderer.tsx +1 -1
- package/src/components/fields/RelationshipBlockField.tsx +31 -23
- package/src/components/fields/RelationshipField.tsx +14 -14
- package/src/components/fields/SelectField.tsx +17 -26
- package/src/components/fields/TabsLayout.tsx +69 -0
- package/src/components/fields/TextField.tsx +85 -38
- package/src/components/fields/UploadField.tsx +71 -41
- package/src/components/fields/VideoField.tsx +1 -1
- package/src/components/fields/extensions/blockComponents.tsx +2 -2
- package/src/components/fields/extensions/blocksStore.ts +207 -193
- package/src/components/fields/types.ts +22 -0
- package/src/components/layout/Layout.tsx +1 -1
- package/src/components/ui/ActionMenu.tsx +63 -0
- package/src/components/ui/Badge.tsx +59 -5
- package/src/components/ui/BlockDrawer.tsx +4 -5
- package/src/components/ui/CommandPalette.tsx +58 -36
- package/src/components/ui/CommandPaletteWrapper.tsx +18 -17
- package/src/components/ui/Dropdown.tsx +18 -16
- package/src/components/ui/EmptyState.tsx +25 -0
- package/src/components/ui/GlobalModal.tsx +49 -0
- package/src/components/ui/IconButton.tsx +44 -0
- package/src/components/ui/Modal.tsx +19 -20
- package/src/components/ui/PageHeader.tsx +158 -0
- package/src/components/ui/Pagination.tsx +61 -0
- package/src/components/ui/PromptModal.tsx +1 -1
- package/src/components/ui/SearchInput.tsx +57 -0
- package/src/components/ui/SeoPreview.tsx +31 -0
- package/src/components/ui/SessionModal.tsx +0 -0
- package/src/components/ui/SlidePanel.tsx +2 -0
- package/src/components/ui/Toast.tsx +65 -122
- package/src/components/ui/Toaster.tsx +18 -0
- package/src/components/ui/icons.tsx +112 -0
- package/src/components/users/UserDetail.tsx +290 -0
- package/src/components/users/UserForm.tsx +242 -0
- package/src/components/users/UsersList.tsx +338 -0
- package/src/env.d.ts +13 -13
- package/src/fields/index.ts +2 -1
- package/src/global.d.ts +7 -0
- package/src/hooks/data.ts +2 -9
- package/src/hooks/useAsyncData.ts +36 -0
- package/src/hooks/useAutoFormState.ts +527 -0
- package/src/hooks/useSelection.ts +49 -0
- package/src/hooks/useSession.ts +0 -0
- package/src/index.ts +11 -1
- package/src/integration.ts +86 -11
- package/src/kyro-cms.d.ts +209 -0
- package/src/layouts/AdminLayout.astro +128 -11
- package/src/layouts/AuthLayout.astro +21 -5
- package/src/lib/api.ts +175 -55
- package/src/lib/autoform-store.ts +435 -0
- package/src/lib/config.ts +82 -34
- package/src/lib/createRegistry.ts +29 -0
- package/src/lib/default-kyro-config.ts +4 -0
- package/src/lib/globals.ts +50 -0
- package/src/lib/media-utils.ts +18 -0
- package/src/lib/object-utils.ts +77 -0
- package/src/lib/paths.ts +61 -0
- package/src/lib/stores/index.ts +370 -0
- package/src/lib/types.ts +43 -0
- package/src/lib/useResourceManager.ts +105 -0
- package/src/pages/403.astro +67 -0
- package/src/pages/[collection]/[id].astro +14 -180
- package/src/pages/[collection]/index.astro +11 -6
- package/src/pages/api-explorer.astro +173 -0
- package/src/pages/audit/index.astro +2 -0
- package/src/pages/auth/login.astro +122 -0
- package/src/pages/auth/register.astro +167 -0
- package/src/pages/graphql-explorer.astro +59 -0
- package/src/pages/{admin/graphql.astro → graphql.astro} +51 -17
- package/src/pages/index.astro +577 -0
- package/src/pages/index_ALT.astro +3 -0
- package/src/pages/keys.astro +11 -0
- package/src/pages/marketplace.astro +11 -0
- package/src/pages/media.astro +3 -0
- package/src/pages/plugins.astro +8 -0
- package/src/pages/preview/[collection]/[id].astro +188 -123
- package/src/pages/rest-playground.astro +62 -0
- package/src/pages/roles/index.astro +183 -76
- package/src/pages/sessions.astro +8 -0
- package/src/pages/settings/[slug].astro +92 -114
- package/src/pages/settings/index.astro +5 -3
- package/src/pages/users/[id].astro +25 -154
- package/src/pages/users/index.astro +19 -130
- package/src/pages/users/new.astro +9 -86
- package/src/pages/webhooks.astro +11 -0
- package/src/routes.ts +80 -0
- package/src/styles/main.css +119 -79
- package/src/theme/tokens.ts +1 -0
- package/src/vite-env.d.ts +14 -0
- package/src/collections/auth/index.ts +0 -155
- package/src/collections/portfolio/index.ts +0 -343
- package/src/components/ApiExplorer.tsx +0 -325
- package/src/components/EnhancedListView.tsx +0 -889
- package/src/components/GraphQLExplorer.tsx +0 -675
- package/src/components/Icons.tsx +0 -23
- package/src/components/StatusBadge.tsx +0 -76
- package/src/lib/MediaService.ts +0 -541
- package/src/lib/auth/sqlite-adapter.ts +0 -319
- package/src/lib/dataStore.ts +0 -226
- package/src/lib/db/adapter.ts +0 -54
- package/src/lib/db/drizzle-mysql-adapter.ts +0 -194
- package/src/lib/db/drizzle-mysql-auth-adapter.ts +0 -327
- package/src/lib/db/drizzle-postgres-adapter.ts +0 -202
- package/src/lib/db/drizzle-postgres-auth-adapter.ts +0 -304
- package/src/lib/db/drizzle-sqlite-adapter.ts +0 -227
- package/src/lib/db/drizzle-sqlite-auth-adapter.ts +0 -548
- package/src/lib/db/index.ts +0 -449
- package/src/lib/db/mongodb-adapter.ts +0 -207
- package/src/lib/db/mongodb-auth-adapter.ts +0 -305
- package/src/lib/db/schema/mysql-auth.ts +0 -113
- package/src/lib/db/schema/mysql-content.ts +0 -20
- package/src/lib/db/schema/postgres-auth.ts +0 -116
- package/src/lib/db/schema/postgres-content.ts +0 -35
- package/src/lib/db/schema/postgres-media.ts +0 -52
- package/src/lib/db/schema/postgres-settings.ts +0 -11
- package/src/lib/db/schema/sqlite-auth.ts +0 -112
- package/src/lib/db/schema/sqlite-content.ts +0 -20
- package/src/lib/db/version-adapter.ts +0 -248
- package/src/lib/graphql/index.ts +0 -1
- package/src/lib/graphql/schema.ts +0 -443
- package/src/lib/rate-limit.ts +0 -267
- package/src/lib/storage.ts +0 -374
- package/src/lib/store.ts +0 -85
- package/src/middleware.ts +0 -177
- package/src/pages/admin/api-explorer.astro +0 -98
- package/src/pages/admin/graphql-explorer.astro +0 -40
- package/src/pages/admin/index.astro +0 -286
- package/src/pages/admin/keys.astro +0 -8
- package/src/pages/admin/rest-playground.astro +0 -44
- package/src/pages/admin/webhooks.astro +0 -8
- package/src/pages/api/[collection]/[id]/publish.ts +0 -52
- package/src/pages/api/[collection]/[id]/unpublish.ts +0 -42
- package/src/pages/api/[collection]/[id]/versions.ts +0 -66
- package/src/pages/api/[collection]/[id].ts +0 -213
- package/src/pages/api/[collection]/index.ts +0 -209
- package/src/pages/api/auth/[id].ts +0 -121
- package/src/pages/api/auth/audit-logs.ts +0 -57
- package/src/pages/api/auth/login.ts +0 -211
- package/src/pages/api/auth/logout.ts +0 -66
- package/src/pages/api/auth/me.ts +0 -36
- package/src/pages/api/auth/refresh.ts +0 -119
- package/src/pages/api/auth/register.ts +0 -188
- package/src/pages/api/auth/users.ts +0 -97
- package/src/pages/api/collections.ts +0 -59
- package/src/pages/api/globals/[slug].ts +0 -42
- package/src/pages/api/graphql.ts +0 -90
- package/src/pages/api/health.ts +0 -426
- package/src/pages/api/keys/[id].ts +0 -26
- package/src/pages/api/keys/index.ts +0 -75
- package/src/pages/api/media/[id].ts +0 -309
- package/src/pages/api/media/folders.ts +0 -609
- package/src/pages/api/media/index.ts +0 -146
- package/src/pages/api/media/resize.ts +0 -267
- package/src/pages/api/search.ts +0 -82
- package/src/pages/api/slug-availability.ts +0 -70
- package/src/pages/api/storage-config.ts +0 -20
- package/src/pages/api/storage-status.ts +0 -206
- package/src/pages/api/upload.ts +0 -334
- package/src/pages/api/webhooks/index.ts +0 -71
- package/src/pages/login.astro +0 -82
- package/src/pages/register.astro +0 -102
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import React, { useState, useEffect, useCallback, useRef } from "react";
|
|
2
|
+
import { useUIStore } from "../lib/stores";
|
|
3
|
+
import { apiPath } from "../lib/paths";
|
|
2
4
|
|
|
3
5
|
interface EnvVariable {
|
|
4
6
|
key: string;
|
|
@@ -49,41 +51,38 @@ const STORAGE_KEYS = {
|
|
|
49
51
|
folders: "kyro-rest-folders",
|
|
50
52
|
history: "kyro-rest-history",
|
|
51
53
|
env: "kyro-rest-env",
|
|
52
|
-
activeEnv: "kyro-rest-active-env",
|
|
53
54
|
};
|
|
54
55
|
|
|
55
56
|
export function RestPlayground({ collections = [] }: RestPlaygroundProps) {
|
|
57
|
+
const [sidebarTab, setSidebarTab] = useState<
|
|
58
|
+
"collections" | "saved" | "history" | "env"
|
|
59
|
+
>("collections");
|
|
56
60
|
const [folders, setFolders] = useState<RequestFolder[]>([]);
|
|
57
61
|
const [history, setHistory] = useState<HistoryItem[]>([]);
|
|
58
62
|
const [envVars, setEnvVars] = useState<EnvVariable[]>([]);
|
|
59
|
-
const [activeEnv, setActiveEnv] = useState<string>("default");
|
|
60
63
|
const [selectedRequest, setSelectedRequest] = useState<SavedRequest | null>(
|
|
61
64
|
null,
|
|
62
65
|
);
|
|
63
66
|
const [currentRequest, setCurrentRequest] = useState<SavedRequest>({
|
|
64
|
-
id: "",
|
|
65
|
-
name: "
|
|
67
|
+
id: "new",
|
|
68
|
+
name: "Untitled Request",
|
|
66
69
|
method: "GET",
|
|
67
70
|
url: "",
|
|
68
|
-
headers: {
|
|
71
|
+
headers: {},
|
|
69
72
|
body: "",
|
|
70
73
|
});
|
|
71
|
-
const [response, setResponse] = useState<string>(
|
|
72
|
-
const [responseStatus, setResponseStatus] = useState<number | null>(null);
|
|
73
|
-
const [responseTime, setResponseTime] = useState<number | null>(null);
|
|
74
|
+
const [response, setResponse] = useState<Record<string, unknown> | null>(null);
|
|
74
75
|
const [loading, setLoading] = useState(false);
|
|
75
|
-
const [
|
|
76
|
-
|
|
76
|
+
const [error, setError] = useState<string | null>(null);
|
|
77
|
+
const [activeTab, setActiveTab] = useState<"params" | "headers" | "body">(
|
|
78
|
+
"params",
|
|
77
79
|
);
|
|
80
|
+
const [showFolderModal, setShowFolderModal] = useState(false);
|
|
78
81
|
const [showSaveModal, setShowSaveModal] = useState(false);
|
|
79
|
-
const [
|
|
80
|
-
const [
|
|
81
|
-
const [
|
|
82
|
-
const
|
|
83
|
-
const [sidebarTab, setSidebarTab] = useState<
|
|
84
|
-
"collections" | "saved" | "history" | "env"
|
|
85
|
-
>("collections");
|
|
86
|
-
const fileInputRef = React.useRef<HTMLInputElement>(null);
|
|
82
|
+
const [newFolderName, setNewFolderName] = useState("");
|
|
83
|
+
const [saveToFolderId, setSaveToFolderId] = useState("");
|
|
84
|
+
const [saveRequestName, setSaveRequestName] = useState("");
|
|
85
|
+
const { confirm, alert } = useUIStore();
|
|
87
86
|
|
|
88
87
|
// Load from localStorage
|
|
89
88
|
useEffect(() => {
|
|
@@ -97,184 +96,160 @@ export function RestPlayground({ collections = [] }: RestPlaygroundProps) {
|
|
|
97
96
|
if (savedEnv) setEnvVars(JSON.parse(savedEnv));
|
|
98
97
|
else
|
|
99
98
|
setEnvVars([
|
|
100
|
-
{ key: "baseUrl", value:
|
|
99
|
+
{ key: "baseUrl", value: apiPath, enabled: true },
|
|
101
100
|
{ key: "token", value: "", enabled: true },
|
|
102
101
|
]);
|
|
103
102
|
|
|
104
|
-
|
|
105
|
-
if (savedActiveEnv) setActiveEnv(savedActiveEnv);
|
|
103
|
+
setIsMounted(true);
|
|
106
104
|
}, []);
|
|
107
105
|
|
|
106
|
+
const [isMounted, setIsMounted] = useState(false);
|
|
107
|
+
|
|
108
108
|
// Save to localStorage
|
|
109
109
|
useEffect(() => {
|
|
110
|
-
|
|
111
|
-
|
|
110
|
+
if (isMounted) {
|
|
111
|
+
localStorage.setItem(STORAGE_KEYS.folders, JSON.stringify(folders));
|
|
112
|
+
}
|
|
113
|
+
}, [folders, isMounted]);
|
|
112
114
|
|
|
113
115
|
useEffect(() => {
|
|
114
|
-
|
|
115
|
-
STORAGE_KEYS.history,
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}, [history]);
|
|
116
|
+
if (isMounted) {
|
|
117
|
+
localStorage.setItem(STORAGE_KEYS.history, JSON.stringify(history));
|
|
118
|
+
}
|
|
119
|
+
}, [history, isMounted]);
|
|
119
120
|
|
|
120
121
|
useEffect(() => {
|
|
121
|
-
|
|
122
|
-
|
|
122
|
+
if (isMounted) {
|
|
123
|
+
localStorage.setItem(STORAGE_KEYS.env, JSON.stringify(envVars));
|
|
124
|
+
}
|
|
125
|
+
}, [envVars, isMounted]);
|
|
123
126
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
(text: string): string => {
|
|
130
|
-
if (!text) return text;
|
|
131
|
-
let result = text;
|
|
132
|
-
for (const env of envVars.filter((e) => e.enabled)) {
|
|
133
|
-
const pattern = new RegExp(`\\{\\{${env.key}\\}\\}`, "g");
|
|
134
|
-
result = result.replace(pattern, env.value);
|
|
127
|
+
const resolveUrl = (url: string) => {
|
|
128
|
+
let resolved = url;
|
|
129
|
+
envVars.forEach((v) => {
|
|
130
|
+
if (v.enabled) {
|
|
131
|
+
resolved = resolved.replace(`{{${v.key}}}`, v.value);
|
|
135
132
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const executeRequest = async () => {
|
|
142
|
-
const url = replaceEnvVars(currentRequest.url);
|
|
143
|
-
if (!url) return;
|
|
144
|
-
|
|
145
|
-
setLoading(true);
|
|
146
|
-
setResponse("");
|
|
147
|
-
setResponseStatus(null);
|
|
148
|
-
setResponseTime(null);
|
|
149
|
-
|
|
150
|
-
const startTime = performance.now();
|
|
151
|
-
const headers: Record<string, string> = {};
|
|
152
|
-
for (const [key, value] of Object.entries(currentRequest.headers)) {
|
|
153
|
-
headers[key] = replaceEnvVars(value);
|
|
133
|
+
});
|
|
134
|
+
// Default base URL if relative
|
|
135
|
+
if (resolved.startsWith("/")) {
|
|
136
|
+
const baseUrl = envVars.find((v) => v.key === "baseUrl" && v.enabled)?.value || apiPath;
|
|
137
|
+
resolved = `${baseUrl}${resolved}`;
|
|
154
138
|
}
|
|
139
|
+
return resolved;
|
|
140
|
+
};
|
|
155
141
|
|
|
142
|
+
const handleSend = async () => {
|
|
143
|
+
setLoading(true);
|
|
144
|
+
setError(null);
|
|
145
|
+
const start = Date.now();
|
|
156
146
|
try {
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
headers,
|
|
160
|
-
};
|
|
147
|
+
const url = resolveUrl(currentRequest.url);
|
|
148
|
+
const headers: Record<string, string> = { ...currentRequest.headers };
|
|
161
149
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
currentRequest.body
|
|
165
|
-
) {
|
|
166
|
-
options.body = replaceEnvVars(currentRequest.body);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const res = await fetch(url, options);
|
|
170
|
-
const endTime = performance.now();
|
|
171
|
-
|
|
172
|
-
setResponseStatus(res.status);
|
|
173
|
-
setResponseTime(Math.round(endTime - startTime));
|
|
150
|
+
const token = envVars.find(v => v.key === 'token' && v.enabled)?.value;
|
|
151
|
+
if (token) headers['Authorization'] = `Bearer ${token}`;
|
|
174
152
|
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
153
|
+
const res = await fetch(url, {
|
|
154
|
+
method: currentRequest.method,
|
|
155
|
+
headers: {
|
|
156
|
+
"Content-Type": "application/json",
|
|
157
|
+
...headers,
|
|
158
|
+
},
|
|
159
|
+
body:
|
|
160
|
+
currentRequest.method !== "GET" && currentRequest.body
|
|
161
|
+
? currentRequest.body
|
|
162
|
+
: undefined,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const duration = Date.now() - start;
|
|
166
|
+
const status = res.status;
|
|
167
|
+
const data = await res.json().catch(() => ({}));
|
|
168
|
+
|
|
169
|
+
setResponse({
|
|
170
|
+
status,
|
|
171
|
+
duration,
|
|
172
|
+
size: JSON.stringify(data).length,
|
|
173
|
+
data,
|
|
174
|
+
});
|
|
182
175
|
|
|
183
176
|
// Add to history
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
);
|
|
197
|
-
} catch (error) {
|
|
198
|
-
setResponseStatus(0);
|
|
199
|
-
setResponse(error instanceof Error ? error.message : "Request failed");
|
|
177
|
+
const historyItem: HistoryItem = {
|
|
178
|
+
id: Date.now().toString(),
|
|
179
|
+
timestamp: Date.now(),
|
|
180
|
+
method: currentRequest.method,
|
|
181
|
+
url: currentRequest.url,
|
|
182
|
+
status,
|
|
183
|
+
duration,
|
|
184
|
+
};
|
|
185
|
+
setHistory((prev) => [historyItem, ...prev].slice(0, 50));
|
|
186
|
+
} catch (err: unknown) {
|
|
187
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
188
|
+
setError(message);
|
|
200
189
|
} finally {
|
|
201
190
|
setLoading(false);
|
|
202
191
|
}
|
|
203
192
|
};
|
|
204
193
|
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
folderId: saveFolder || undefined,
|
|
211
|
-
};
|
|
212
|
-
|
|
213
|
-
if (saveFolder) {
|
|
214
|
-
setFolders((prev) =>
|
|
215
|
-
prev
|
|
216
|
-
.map((f) =>
|
|
217
|
-
f.id === saveFolder
|
|
218
|
-
? { ...f, requests: [...f.requests, request] }
|
|
219
|
-
: f,
|
|
220
|
-
)
|
|
221
|
-
.concat(
|
|
222
|
-
prev.find((f) => f.id === saveFolder)
|
|
223
|
-
? []
|
|
224
|
-
: [{ id: saveFolder, name: saveFolder, requests: [request] }],
|
|
225
|
-
),
|
|
226
|
-
);
|
|
227
|
-
} else {
|
|
228
|
-
setFolders((prev) => [
|
|
229
|
-
...prev,
|
|
230
|
-
{ id: "default", name: "Uncategorized", requests: [request] },
|
|
231
|
-
]);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
setCurrentRequest(request);
|
|
235
|
-
setShowSaveModal(false);
|
|
236
|
-
setSaveName("");
|
|
194
|
+
const loadRequest = (req: SavedRequest) => {
|
|
195
|
+
setCurrentRequest(req);
|
|
196
|
+
setSelectedRequest(req);
|
|
197
|
+
setResponse(null);
|
|
198
|
+
setError(null);
|
|
237
199
|
};
|
|
238
200
|
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
201
|
+
const createFolder = () => {
|
|
202
|
+
if (!newFolderName.trim()) return;
|
|
203
|
+
const newFolder: RequestFolder = {
|
|
204
|
+
id: Date.now().toString(),
|
|
205
|
+
name: newFolderName,
|
|
206
|
+
requests: [],
|
|
207
|
+
};
|
|
208
|
+
setFolders((prev) => [...prev, newFolder]);
|
|
209
|
+
setNewFolderName("");
|
|
210
|
+
setShowFolderModal(false);
|
|
244
211
|
};
|
|
245
212
|
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
);
|
|
255
|
-
if (selectedRequest?.id === requestId) {
|
|
256
|
-
setSelectedRequest(null);
|
|
257
|
-
}
|
|
213
|
+
const deleteFolder = (id: string) => {
|
|
214
|
+
confirm({
|
|
215
|
+
title: "Delete Folder",
|
|
216
|
+
message: "Are you sure? All requests inside will be deleted.",
|
|
217
|
+
variant: "danger",
|
|
218
|
+
onConfirm: () => {
|
|
219
|
+
setFolders((prev) => prev.filter((f) => f.id !== id));
|
|
220
|
+
}
|
|
221
|
+
});
|
|
258
222
|
};
|
|
259
223
|
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
...prev,
|
|
263
|
-
headers: { ...prev.headers, [key]: value },
|
|
264
|
-
}));
|
|
265
|
-
};
|
|
224
|
+
const saveRequest = () => {
|
|
225
|
+
if (!saveRequestName.trim() || !saveToFolderId) return;
|
|
266
226
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
227
|
+
const newSavedRequest: SavedRequest = {
|
|
228
|
+
...currentRequest,
|
|
229
|
+
id: Date.now().toString(),
|
|
230
|
+
name: saveRequestName,
|
|
231
|
+
folderId: saveToFolderId,
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
setFolders((prev) =>
|
|
235
|
+
prev.map((f) =>
|
|
236
|
+
f.id === saveToFolderId
|
|
237
|
+
? { ...f, requests: [...f.requests, newSavedRequest] }
|
|
238
|
+
: f,
|
|
239
|
+
),
|
|
240
|
+
);
|
|
241
|
+
setSelectedRequest(newSavedRequest);
|
|
242
|
+
setShowSaveModal(false);
|
|
271
243
|
};
|
|
272
244
|
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
245
|
+
const deleteRequest = (id: string) => {
|
|
246
|
+
setFolders((prev) =>
|
|
247
|
+
prev.map((f) => ({
|
|
248
|
+
...f,
|
|
249
|
+
requests: f.requests.filter((r) => r.id !== id),
|
|
250
|
+
})),
|
|
251
|
+
);
|
|
252
|
+
if (selectedRequest?.id === id) setSelectedRequest(null);
|
|
278
253
|
};
|
|
279
254
|
|
|
280
255
|
const getMethodColor = (method: string) => {
|
|
@@ -283,8 +258,6 @@ export function RestPlayground({ collections = [] }: RestPlaygroundProps) {
|
|
|
283
258
|
return "bg-green-500";
|
|
284
259
|
case "POST":
|
|
285
260
|
return "bg-blue-500";
|
|
286
|
-
case "PUT":
|
|
287
|
-
return "bg-blue-500";
|
|
288
261
|
case "PATCH":
|
|
289
262
|
return "bg-yellow-500";
|
|
290
263
|
case "DELETE":
|
|
@@ -352,9 +325,9 @@ export function RestPlayground({ collections = [] }: RestPlaygroundProps) {
|
|
|
352
325
|
setEnvVars((prev) => [...prev, ...data.envVars]);
|
|
353
326
|
}
|
|
354
327
|
|
|
355
|
-
alert("Import
|
|
328
|
+
alert({ title: "Import Successful", message: "Your playground data has been imported." });
|
|
356
329
|
} catch (error) {
|
|
357
|
-
alert("Failed
|
|
330
|
+
alert({ title: "Import Failed", message: "Invalid JSON file structure." });
|
|
358
331
|
}
|
|
359
332
|
};
|
|
360
333
|
reader.readAsText(file);
|
|
@@ -362,21 +335,22 @@ export function RestPlayground({ collections = [] }: RestPlaygroundProps) {
|
|
|
362
335
|
|
|
363
336
|
// Clear all data
|
|
364
337
|
const clearAllData = () => {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
338
|
+
confirm({
|
|
339
|
+
title: "Clear All Data?",
|
|
340
|
+
message: "Are you sure you want to clear all saved requests, history, and environment variables? This action cannot be undone.",
|
|
341
|
+
variant: "danger",
|
|
342
|
+
onConfirm: () => {
|
|
343
|
+
setFolders([]);
|
|
344
|
+
setHistory([]);
|
|
345
|
+
setEnvVars([
|
|
346
|
+
{ key: "baseUrl", value: apiPath, enabled: true },
|
|
347
|
+
{ key: "token", value: "", enabled: true },
|
|
348
|
+
]);
|
|
349
|
+
localStorage.removeItem(STORAGE_KEYS.folders);
|
|
350
|
+
localStorage.removeItem(STORAGE_KEYS.history);
|
|
351
|
+
localStorage.removeItem(STORAGE_KEYS.env);
|
|
352
|
+
}
|
|
353
|
+
});
|
|
380
354
|
};
|
|
381
355
|
|
|
382
356
|
return (
|
|
@@ -389,11 +363,10 @@ export function RestPlayground({ collections = [] }: RestPlaygroundProps) {
|
|
|
389
363
|
<button type="button"
|
|
390
364
|
key={tab}
|
|
391
365
|
onClick={() => setSidebarTab(tab)}
|
|
392
|
-
className={`flex-1 px-2 py-2 text-xs font-bold transition-colors ${
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
}`}
|
|
366
|
+
className={`flex-1 px-2 py-2 text-xs font-bold transition-colors ${sidebarTab === tab
|
|
367
|
+
? "bg-[var(--kyro-surface)] text-[var(--kyro-text-primary)] border-b-2 border-pink-500"
|
|
368
|
+
: "text-[var(--kyro-text-secondary)] hover:bg-[var(--kyro-surface-accent)]"
|
|
369
|
+
}`}
|
|
397
370
|
>
|
|
398
371
|
{tab.charAt(0).toUpperCase() + tab.slice(1)}
|
|
399
372
|
</button>
|
|
@@ -407,7 +380,7 @@ export function RestPlayground({ collections = [] }: RestPlaygroundProps) {
|
|
|
407
380
|
<div className="space-y-3">
|
|
408
381
|
{collections.map((col) => (
|
|
409
382
|
<div key={col.slug}>
|
|
410
|
-
<h3 className="text-xs font-bold text-[var(--kyro-text-muted)]
|
|
383
|
+
<h3 className="text-xs font-bold text-[var(--kyro-text-muted)] mb-2 px-2">
|
|
411
384
|
{col.name}
|
|
412
385
|
</h3>
|
|
413
386
|
{[
|
|
@@ -438,7 +411,7 @@ export function RestPlayground({ collections = [] }: RestPlaygroundProps) {
|
|
|
438
411
|
onClick={() => {
|
|
439
412
|
setCurrentRequest((prev) => ({
|
|
440
413
|
...prev,
|
|
441
|
-
method: endpoint.method as
|
|
414
|
+
method: endpoint.method as SavedRequest["method"],
|
|
442
415
|
url: endpoint.path,
|
|
443
416
|
name: `${col.name} - ${endpoint.name}`,
|
|
444
417
|
}));
|
|
@@ -464,6 +437,23 @@ export function RestPlayground({ collections = [] }: RestPlaygroundProps) {
|
|
|
464
437
|
{/* Saved */}
|
|
465
438
|
{sidebarTab === "saved" && (
|
|
466
439
|
<div className="space-y-3">
|
|
440
|
+
<div className="flex items-center justify-between mb-2">
|
|
441
|
+
<button type="button"
|
|
442
|
+
onClick={() => setShowFolderModal(true)}
|
|
443
|
+
className="text-xs font-bold text-pink-500 hover:underline"
|
|
444
|
+
>
|
|
445
|
+
+ New Folder
|
|
446
|
+
</button>
|
|
447
|
+
<div className="flex gap-2">
|
|
448
|
+
<button type="button" onClick={exportData} className="text-xs text-[var(--kyro-text-muted)] hover:text-pink-500" title="Export">
|
|
449
|
+
Export
|
|
450
|
+
</button>
|
|
451
|
+
<label className="text-xs text-[var(--kyro-text-muted)] hover:text-pink-500 cursor-pointer">
|
|
452
|
+
Import
|
|
453
|
+
<input type="file" className="hidden" accept=".json" onChange={(e) => e.target.files?.[0] && importData(e.target.files[0])} />
|
|
454
|
+
</label>
|
|
455
|
+
</div>
|
|
456
|
+
</div>
|
|
467
457
|
{folders.length === 0 && (
|
|
468
458
|
<p className="text-xs text-[var(--kyro-text-muted)] text-center py-4">
|
|
469
459
|
No saved requests yet
|
|
@@ -471,17 +461,21 @@ export function RestPlayground({ collections = [] }: RestPlaygroundProps) {
|
|
|
471
461
|
)}
|
|
472
462
|
{folders.map((folder) => (
|
|
473
463
|
<div key={folder.id}>
|
|
474
|
-
<
|
|
475
|
-
|
|
476
|
-
|
|
464
|
+
<div className="flex items-center justify-between mb-2 px-2">
|
|
465
|
+
<h3 className="text-xs font-bold text-[var(--kyro-text-muted)] ">
|
|
466
|
+
{folder.name}
|
|
467
|
+
</h3>
|
|
468
|
+
<button type="button" onClick={() => deleteFolder(folder.id)} className="text-[var(--kyro-text-muted)] hover:text-red-500">
|
|
469
|
+
×
|
|
470
|
+
</button>
|
|
471
|
+
</div>
|
|
477
472
|
{folder.requests.map((req) => (
|
|
478
473
|
<div
|
|
479
474
|
key={req.id}
|
|
480
|
-
className={`group flex items-center gap-2 px-2 py-1.5 rounded cursor-pointer transition-colors ${
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
}`}
|
|
475
|
+
className={`group flex items-center gap-2 px-2 py-1.5 rounded cursor-pointer transition-colors ${selectedRequest?.id === req.id
|
|
476
|
+
? "bg-[var(--kyro-sidebar-active)]"
|
|
477
|
+
: "hover:bg-[var(--kyro-surface-accent)]"
|
|
478
|
+
}`}
|
|
485
479
|
onClick={() => loadRequest(req)}
|
|
486
480
|
>
|
|
487
481
|
<span
|
|
@@ -489,7 +483,7 @@ export function RestPlayground({ collections = [] }: RestPlaygroundProps) {
|
|
|
489
483
|
>
|
|
490
484
|
{req.method}
|
|
491
485
|
</span>
|
|
492
|
-
<span className=
|
|
486
|
+
<span className={`flex-1 text-xs truncate ${selectedRequest?.id === req.id ? "text-[var(--kyro-sidebar-text-active)]" : "text-[var(--kyro-text-secondary)]"}`}>
|
|
493
487
|
{req.name}
|
|
494
488
|
</span>
|
|
495
489
|
<button type="button"
|
|
@@ -497,451 +491,295 @@ export function RestPlayground({ collections = [] }: RestPlaygroundProps) {
|
|
|
497
491
|
e.stopPropagation();
|
|
498
492
|
deleteRequest(req.id);
|
|
499
493
|
}}
|
|
500
|
-
className="opacity-0 group-hover:opacity-100 text-
|
|
494
|
+
className="opacity-0 group-hover:opacity-100 text-[var(--kyro-text-muted)] hover:text-red-500"
|
|
501
495
|
>
|
|
502
|
-
|
|
503
|
-
className="w-3 h-3"
|
|
504
|
-
fill="none"
|
|
505
|
-
stroke="currentColor"
|
|
506
|
-
viewBox="0 0 24 24"
|
|
507
|
-
>
|
|
508
|
-
<path
|
|
509
|
-
strokeLinecap="round"
|
|
510
|
-
strokeLinejoin="round"
|
|
511
|
-
strokeWidth="2"
|
|
512
|
-
d="M6 18L18 6M6 6l12 12"
|
|
513
|
-
/>
|
|
514
|
-
</svg>
|
|
496
|
+
×
|
|
515
497
|
</button>
|
|
516
498
|
</div>
|
|
517
499
|
))}
|
|
518
500
|
</div>
|
|
519
501
|
))}
|
|
520
|
-
|
|
521
|
-
{/* Export/Import/Clear Actions */}
|
|
522
|
-
<div className="flex gap-2 mt-4 pt-4 border-t border-[var(--kyro-border]">
|
|
523
|
-
<button type="button"
|
|
524
|
-
onClick={exportData}
|
|
525
|
-
className="flex-1 px-3 py-2 text-xs font-bold bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] rounded-lg hover:bg-[var(--kyro-surface)] transition-colors"
|
|
526
|
-
title="Export all data"
|
|
527
|
-
>
|
|
528
|
-
Export
|
|
529
|
-
</button>
|
|
530
|
-
<button type="button"
|
|
531
|
-
onClick={() => fileInputRef.current?.click()}
|
|
532
|
-
className="flex-1 px-3 py-2 text-xs font-bold bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] rounded-lg hover:bg-[var(--kyro-surface)] transition-colors"
|
|
533
|
-
title="Import data"
|
|
534
|
-
>
|
|
535
|
-
Import
|
|
536
|
-
</button>
|
|
537
|
-
<button type="button"
|
|
538
|
-
onClick={clearAllData}
|
|
539
|
-
className="px-3 py-2 text-xs font-bold bg-red-500/10 text-red-600 rounded-lg hover:bg-red-500/20 transition-colors"
|
|
540
|
-
title="Clear all data"
|
|
541
|
-
>
|
|
542
|
-
<svg
|
|
543
|
-
className="w-4 h-4"
|
|
544
|
-
fill="none"
|
|
545
|
-
stroke="currentColor"
|
|
546
|
-
viewBox="0 0 24 24"
|
|
547
|
-
>
|
|
548
|
-
<path
|
|
549
|
-
strokeLinecap="round"
|
|
550
|
-
strokeLinejoin="round"
|
|
551
|
-
strokeWidth="2"
|
|
552
|
-
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
|
553
|
-
/>
|
|
554
|
-
</svg>
|
|
555
|
-
</button>
|
|
556
|
-
</div>
|
|
557
|
-
<input
|
|
558
|
-
ref={fileInputRef}
|
|
559
|
-
type="file"
|
|
560
|
-
accept=".json"
|
|
561
|
-
onChange={(e) => {
|
|
562
|
-
const file = e.target.files?.[0];
|
|
563
|
-
if (file) importData(file);
|
|
564
|
-
e.target.value = "";
|
|
565
|
-
}}
|
|
566
|
-
className="hidden"
|
|
567
|
-
/>
|
|
568
502
|
</div>
|
|
569
503
|
)}
|
|
570
504
|
|
|
571
505
|
{/* History */}
|
|
572
506
|
{sidebarTab === "history" && (
|
|
573
507
|
<div className="space-y-1">
|
|
574
|
-
|
|
575
|
-
<
|
|
576
|
-
|
|
577
|
-
</
|
|
578
|
-
|
|
508
|
+
<div className="flex justify-end mb-2">
|
|
509
|
+
<button type="button" onClick={() => setHistory([])} className="text-[10px] font-bold text-red-500 hover:underline">
|
|
510
|
+
Clear History
|
|
511
|
+
</button>
|
|
512
|
+
</div>
|
|
579
513
|
{history.map((item) => (
|
|
580
|
-
<
|
|
514
|
+
<div
|
|
581
515
|
key={item.id}
|
|
516
|
+
className="p-2 rounded hover:bg-[var(--kyro-surface-accent)] cursor-pointer transition-colors border-b border-[var(--kyro-border)] last:border-0"
|
|
582
517
|
onClick={() => {
|
|
583
518
|
setCurrentRequest((prev) => ({
|
|
584
519
|
...prev,
|
|
585
|
-
method: item.method as
|
|
520
|
+
method: item.method as SavedRequest["method"],
|
|
586
521
|
url: item.url,
|
|
587
522
|
}));
|
|
588
|
-
setResponse(
|
|
523
|
+
setResponse(null);
|
|
589
524
|
}}
|
|
590
|
-
className="w-full flex items-center gap-2 px-2 py-2 rounded hover:bg-[var(--kyro-surface-accent)] transition-colors"
|
|
591
525
|
>
|
|
592
|
-
<
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
<
|
|
599
|
-
{item.
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
{item.duration}ms
|
|
604
|
-
</p>
|
|
526
|
+
<div className="flex items-center justify-between mb-1">
|
|
527
|
+
<span
|
|
528
|
+
className={`${getMethodColor(item.method)} text-white px-1 py-0.5 rounded text-[10px] font-bold`}
|
|
529
|
+
>
|
|
530
|
+
{item.method}
|
|
531
|
+
</span>
|
|
532
|
+
<span
|
|
533
|
+
className={`${getStatusColor(item.status)} text-white px-1 py-0.5 rounded text-[10px] font-bold`}
|
|
534
|
+
>
|
|
535
|
+
{item.status}
|
|
536
|
+
</span>
|
|
605
537
|
</div>
|
|
606
|
-
<
|
|
607
|
-
|
|
608
|
-
>
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
538
|
+
<div className="text-[10px] text-[var(--kyro-text-secondary)] truncate mb-1">
|
|
539
|
+
{item.url}
|
|
540
|
+
</div>
|
|
541
|
+
<div className="text-[9px] text-[var(--kyro-text-muted)] flex justify-between">
|
|
542
|
+
<span>{new Date(item.timestamp).toLocaleTimeString()}</span>
|
|
543
|
+
<span>{item.duration}ms</span>
|
|
544
|
+
</div>
|
|
545
|
+
</div>
|
|
612
546
|
))}
|
|
613
547
|
</div>
|
|
614
548
|
)}
|
|
615
549
|
|
|
616
550
|
{/* Environment */}
|
|
617
551
|
{sidebarTab === "env" && (
|
|
618
|
-
<div className="space-y-
|
|
619
|
-
<div className="flex
|
|
620
|
-
<
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
552
|
+
<div className="space-y-4">
|
|
553
|
+
<div className="flex justify-between items-center">
|
|
554
|
+
<h3 className="text-xs font-bold text-[var(--kyro-text-muted)] ">Environment</h3>
|
|
555
|
+
<button type="button" onClick={clearAllData} className="text-[10px] text-red-500 font-bold hover:underline">Reset All</button>
|
|
556
|
+
</div>
|
|
557
|
+
<div className="space-y-3">
|
|
558
|
+
{envVars.map((env, i) => (
|
|
559
|
+
<div key={i} className="p-3 bg-[var(--kyro-surface-accent)] rounded-lg border border-[var(--kyro-border)] space-y-2">
|
|
560
|
+
<div className="flex items-center justify-between">
|
|
561
|
+
<span className="text-[10px] font-bold tracking-widest text-[var(--kyro-text-muted)]">{env.key}</span>
|
|
562
|
+
<input type="checkbox" checked={env.enabled} onChange={() => {
|
|
563
|
+
const next = [...envVars];
|
|
564
|
+
next[i].enabled = !next[i].enabled;
|
|
565
|
+
setEnvVars(next);
|
|
566
|
+
}} />
|
|
567
|
+
</div>
|
|
568
|
+
<input
|
|
569
|
+
type="text"
|
|
570
|
+
value={env.value}
|
|
571
|
+
onChange={(e) => {
|
|
572
|
+
const next = [...envVars];
|
|
573
|
+
next[i].value = e.target.value;
|
|
574
|
+
setEnvVars(next);
|
|
575
|
+
}}
|
|
576
|
+
className="w-full bg-[var(--kyro-bg)] border border-[var(--kyro-border)] rounded px-2 py-1 text-xs"
|
|
577
|
+
/>
|
|
578
|
+
</div>
|
|
579
|
+
))}
|
|
580
|
+
<button type="button" onClick={() => setEnvVars([...envVars, { key: "", value: "", enabled: true }])} className="w-full py-2 border-2 border-dashed border-[var(--kyro-border)] rounded-lg text-xs text-[var(--kyro-text-muted)] hover:border-pink-500 hover:text-pink-500 transition-all">
|
|
581
|
+
+ Add Variable
|
|
633
582
|
</button>
|
|
634
583
|
</div>
|
|
635
|
-
{envVars.map((env, i) => (
|
|
636
|
-
<div key={i} className="flex items-center gap-2">
|
|
637
|
-
<input
|
|
638
|
-
type="checkbox"
|
|
639
|
-
checked={env.enabled}
|
|
640
|
-
onChange={(e) => {
|
|
641
|
-
const newVars = [...envVars];
|
|
642
|
-
newVars[i].enabled = e.target.checked;
|
|
643
|
-
setEnvVars(newVars);
|
|
644
|
-
}}
|
|
645
|
-
className="rounded"
|
|
646
|
-
/>
|
|
647
|
-
<input
|
|
648
|
-
type="text"
|
|
649
|
-
value={env.key}
|
|
650
|
-
onChange={(e) => {
|
|
651
|
-
const newVars = [...envVars];
|
|
652
|
-
newVars[i].key = e.target.value;
|
|
653
|
-
setEnvVars(newVars);
|
|
654
|
-
}}
|
|
655
|
-
placeholder="key"
|
|
656
|
-
className="flex-1 px-2 py-1 text-xs bg-[var(--kyro-surface)] border border-[var(--kyro-border)] rounded"
|
|
657
|
-
/>
|
|
658
|
-
<input
|
|
659
|
-
type="text"
|
|
660
|
-
value={env.value}
|
|
661
|
-
onChange={(e) => {
|
|
662
|
-
const newVars = [...envVars];
|
|
663
|
-
newVars[i].value = e.target.value;
|
|
664
|
-
setEnvVars(newVars);
|
|
665
|
-
}}
|
|
666
|
-
placeholder="value"
|
|
667
|
-
className="flex-1 px-2 py-1 text-xs bg-[var(--kyro-surface)] border border-[var(--kyro-border)] rounded"
|
|
668
|
-
/>
|
|
669
|
-
<button type="button"
|
|
670
|
-
onClick={() =>
|
|
671
|
-
setEnvVars((prev) => prev.filter((_, j) => j !== i))
|
|
672
|
-
}
|
|
673
|
-
className="text-red-500 hover:text-red-600"
|
|
674
|
-
>
|
|
675
|
-
<svg
|
|
676
|
-
className="w-4 h-4"
|
|
677
|
-
fill="none"
|
|
678
|
-
stroke="currentColor"
|
|
679
|
-
viewBox="0 0 24 24"
|
|
680
|
-
>
|
|
681
|
-
<path
|
|
682
|
-
strokeLinecap="round"
|
|
683
|
-
strokeLinejoin="round"
|
|
684
|
-
strokeWidth="2"
|
|
685
|
-
d="M6 18L18 6M6 6l12 12"
|
|
686
|
-
/>
|
|
687
|
-
</svg>
|
|
688
|
-
</button>
|
|
689
|
-
</div>
|
|
690
|
-
))}
|
|
691
|
-
<p className="text-xs text-[var(--kyro-text-muted)]">
|
|
692
|
-
Use {"{{variableName}}"} in URLs, headers, and body
|
|
693
|
-
</p>
|
|
694
584
|
</div>
|
|
695
585
|
)}
|
|
696
586
|
</div>
|
|
697
587
|
</div>
|
|
698
588
|
|
|
699
|
-
{/* Main
|
|
700
|
-
<div className="flex-1 flex flex-col
|
|
589
|
+
{/* Main Area */}
|
|
590
|
+
<div className="flex-1 flex flex-col bg-[var(--kyro-bg)]">
|
|
701
591
|
{/* URL Bar */}
|
|
702
|
-
<div className="
|
|
592
|
+
<div className="p-4 border-b border-[var(--kyro-border)] flex gap-2">
|
|
703
593
|
<select
|
|
704
594
|
value={currentRequest.method}
|
|
705
595
|
onChange={(e) =>
|
|
706
|
-
setCurrentRequest(
|
|
596
|
+
setCurrentRequest({ ...currentRequest, method: e.target.value })
|
|
707
597
|
}
|
|
708
|
-
className={`px-3 py-2 rounded-lg font-bold text-
|
|
598
|
+
className={`px-3 py-2 rounded-lg font-bold text-white ${getMethodColor(currentRequest.method)}`}
|
|
709
599
|
>
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
))}
|
|
600
|
+
<option value="GET">GET</option>
|
|
601
|
+
<option value="POST">POST</option>
|
|
602
|
+
<option value="PATCH">PATCH</option>
|
|
603
|
+
<option value="DELETE">DELETE</option>
|
|
715
604
|
</select>
|
|
716
|
-
<
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
605
|
+
<div className="flex-1 relative">
|
|
606
|
+
<input
|
|
607
|
+
type="text"
|
|
608
|
+
value={currentRequest.url}
|
|
609
|
+
onChange={(e) =>
|
|
610
|
+
setCurrentRequest({ ...currentRequest, url: e.target.value })
|
|
611
|
+
}
|
|
612
|
+
placeholder="Enter request URL..."
|
|
613
|
+
className="w-full bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-lg px-4 py-2 text-sm focus:outline-none focus:border-pink-500"
|
|
614
|
+
/>
|
|
615
|
+
</div>
|
|
725
616
|
<button type="button"
|
|
726
|
-
onClick={
|
|
727
|
-
disabled={loading
|
|
728
|
-
className="px-6 py-2 bg-pink-500 text-white rounded-lg font-bold
|
|
617
|
+
onClick={handleSend}
|
|
618
|
+
disabled={loading}
|
|
619
|
+
className="px-6 py-2 bg-pink-500 text-white rounded-lg font-bold hover:bg-pink-600 transition-colors disabled:opacity-50"
|
|
729
620
|
>
|
|
730
|
-
{loading ? "
|
|
621
|
+
{loading ? "..." : "Send"}
|
|
731
622
|
</button>
|
|
732
623
|
<button type="button"
|
|
733
624
|
onClick={() => setShowSaveModal(true)}
|
|
734
|
-
className="px-4 py-2 bg-[var(--kyro-surface-accent)]
|
|
735
|
-
title="Save request"
|
|
736
|
-
>
|
|
737
|
-
<svg
|
|
738
|
-
className="w-4 h-4"
|
|
739
|
-
fill="none"
|
|
740
|
-
stroke="currentColor"
|
|
741
|
-
viewBox="0 0 24 24"
|
|
742
|
-
>
|
|
743
|
-
<path
|
|
744
|
-
strokeLinecap="round"
|
|
745
|
-
strokeLinejoin="round"
|
|
746
|
-
strokeWidth="2"
|
|
747
|
-
d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4"
|
|
748
|
-
/>
|
|
749
|
-
</svg>
|
|
750
|
-
</button>
|
|
751
|
-
<button type="button"
|
|
752
|
-
onClick={duplicateRequest}
|
|
753
|
-
className="px-4 py-2 bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-lg font-bold text-sm hover:bg-[var(--kyro-surface)] transition-colors"
|
|
754
|
-
title="Duplicate"
|
|
625
|
+
className="px-4 py-2 bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-primary)] rounded-lg font-bold hover:bg-[var(--kyro-surface)] border border-[var(--kyro-border)]"
|
|
755
626
|
>
|
|
756
|
-
|
|
757
|
-
className="w-4 h-4"
|
|
758
|
-
fill="none"
|
|
759
|
-
stroke="currentColor"
|
|
760
|
-
viewBox="0 0 24 24"
|
|
761
|
-
>
|
|
762
|
-
<path
|
|
763
|
-
strokeLinecap="round"
|
|
764
|
-
strokeLinejoin="round"
|
|
765
|
-
strokeWidth="2"
|
|
766
|
-
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
|
|
767
|
-
/>
|
|
768
|
-
</svg>
|
|
627
|
+
Save
|
|
769
628
|
</button>
|
|
770
629
|
</div>
|
|
771
630
|
|
|
772
|
-
{/* Tabs */}
|
|
631
|
+
{/* Request Tabs */}
|
|
773
632
|
<div className="flex border-b border-[var(--kyro-border)]">
|
|
774
|
-
{(["
|
|
633
|
+
{(["params", "headers", "body"] as const).map((tab) => (
|
|
775
634
|
<button type="button"
|
|
776
635
|
key={tab}
|
|
777
636
|
onClick={() => setActiveTab(tab)}
|
|
778
|
-
className={`px-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
}`}
|
|
637
|
+
className={`px-6 py-2 text-xs font-bold tracking-widest ${activeTab === tab
|
|
638
|
+
? "text-pink-500 border-b-2 border-pink-500"
|
|
639
|
+
: "text-[var(--kyro-text-muted)]"
|
|
640
|
+
}`}
|
|
783
641
|
>
|
|
784
|
-
{tab
|
|
785
|
-
{tab === "headers" && (
|
|
786
|
-
<span className="ml-2 text-xs text-[var(--kyro-text-muted)]">
|
|
787
|
-
({Object.keys(currentRequest.headers).filter((k) => k).length}
|
|
788
|
-
)
|
|
789
|
-
</span>
|
|
790
|
-
)}
|
|
642
|
+
{tab}
|
|
791
643
|
</button>
|
|
792
644
|
))}
|
|
793
645
|
</div>
|
|
794
646
|
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
className="flex-1 px-3 py-2 bg-[var(--kyro-surface)] border border-[var(--kyro-border)] rounded text-sm focus:outline-none focus:border-pink-500"
|
|
807
|
-
/>
|
|
808
|
-
<input
|
|
809
|
-
type="text"
|
|
810
|
-
value={value}
|
|
811
|
-
onChange={(e) => updateHeader(key, e.target.value)}
|
|
812
|
-
placeholder="Value"
|
|
813
|
-
className="flex-1 px-3 py-2 bg-[var(--kyro-surface)] border border-[var(--kyro-border)] rounded text-sm font-mono focus:outline-none focus:border-pink-500"
|
|
814
|
-
/>
|
|
815
|
-
<button type="button"
|
|
816
|
-
onClick={() => removeHeader(key)}
|
|
817
|
-
className="p-2 text-red-500 hover:text-red-600"
|
|
818
|
-
>
|
|
819
|
-
<svg
|
|
820
|
-
className="w-4 h-4"
|
|
821
|
-
fill="none"
|
|
822
|
-
stroke="currentColor"
|
|
823
|
-
viewBox="0 0 24 24"
|
|
824
|
-
>
|
|
825
|
-
<path
|
|
826
|
-
strokeLinecap="round"
|
|
827
|
-
strokeLinejoin="round"
|
|
828
|
-
strokeWidth="2"
|
|
829
|
-
d="M6 18L18 6M6 6l12 12"
|
|
830
|
-
/>
|
|
831
|
-
</svg>
|
|
832
|
-
</button>
|
|
647
|
+
<div className="flex-1 flex flex-col overflow-hidden">
|
|
648
|
+
<div className="flex-1 p-4 overflow-y-auto">
|
|
649
|
+
{activeTab === "params" && (
|
|
650
|
+
<div className="text-xs text-[var(--kyro-text-muted)]">
|
|
651
|
+
Use query parameters in the URL (e.g. ?limit=10)
|
|
652
|
+
</div>
|
|
653
|
+
)}
|
|
654
|
+
{activeTab === "headers" && (
|
|
655
|
+
<div className="space-y-2">
|
|
656
|
+
<div className="text-xs text-[var(--kyro-text-muted)] mb-4">
|
|
657
|
+
Add custom headers as JSON
|
|
833
658
|
</div>
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
</
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
</div>
|
|
659
|
+
<textarea
|
|
660
|
+
value={JSON.stringify(currentRequest.headers, null, 2)}
|
|
661
|
+
onChange={(e) => {
|
|
662
|
+
try {
|
|
663
|
+
const headers = JSON.parse(e.target.value);
|
|
664
|
+
setCurrentRequest({ ...currentRequest, headers });
|
|
665
|
+
} catch (e) { }
|
|
666
|
+
}}
|
|
667
|
+
className="w-full h-40 bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-lg p-3 font-mono text-xs focus:outline-none focus:border-pink-500"
|
|
668
|
+
placeholder='{ "X-Custom-Header": "value" }'
|
|
669
|
+
/>
|
|
670
|
+
</div>
|
|
671
|
+
)}
|
|
672
|
+
{activeTab === "body" && (
|
|
673
|
+
<div className="h-full flex flex-col">
|
|
674
|
+
<div className="text-xs text-[var(--kyro-text-muted)] mb-4">
|
|
675
|
+
Request Body (JSON)
|
|
676
|
+
</div>
|
|
677
|
+
<textarea
|
|
678
|
+
value={currentRequest.body}
|
|
679
|
+
onChange={(e) =>
|
|
680
|
+
setCurrentRequest({ ...currentRequest, body: e.target.value })
|
|
681
|
+
}
|
|
682
|
+
className="flex-1 bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-lg p-3 font-mono text-xs focus:outline-none focus:border-pink-500 min-h-[200px]"
|
|
683
|
+
placeholder='{ "key": "value" }'
|
|
684
|
+
/>
|
|
685
|
+
</div>
|
|
686
|
+
)}
|
|
687
|
+
</div>
|
|
864
688
|
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
className={
|
|
876
|
-
|
|
877
|
-
{responseStatus}
|
|
878
|
-
</span>
|
|
879
|
-
{responseTime && (
|
|
880
|
-
<span className="text-xs text-[var(--kyro-text-muted)]">
|
|
881
|
-
{responseTime}ms
|
|
689
|
+
{/* Response Pane */}
|
|
690
|
+
<div className="h-1/2 border-t border-[var(--kyro-border)] flex flex-col overflow-hidden bg-[var(--kyro-bg-secondary)]">
|
|
691
|
+
<div className="px-4 py-2 bg-[var(--kyro-surface)] border-b border-[var(--kyro-border)] flex items-center justify-between">
|
|
692
|
+
<span className="text-xs font-bold text-[var(--kyro-text-secondary)] tracking-widest">
|
|
693
|
+
Response
|
|
694
|
+
</span>
|
|
695
|
+
{response && (
|
|
696
|
+
<div className="flex gap-4">
|
|
697
|
+
<span className="text-[10px] font-bold">
|
|
698
|
+
STATUS:{" "}
|
|
699
|
+
<span className={response.status < 400 ? "text-green-500" : "text-red-500"}>
|
|
700
|
+
{response.status}
|
|
882
701
|
</span>
|
|
883
|
-
|
|
884
|
-
|
|
702
|
+
</span>
|
|
703
|
+
<span className="text-[10px] font-bold text-[var(--kyro-text-muted)]">
|
|
704
|
+
TIME: {response.duration}ms
|
|
705
|
+
</span>
|
|
706
|
+
<span className="text-[10px] font-bold text-[var(--kyro-text-muted)]">
|
|
707
|
+
SIZE: {response.size}B
|
|
708
|
+
</span>
|
|
709
|
+
</div>
|
|
710
|
+
)}
|
|
711
|
+
</div>
|
|
712
|
+
<div className="flex-1 p-4 overflow-auto">
|
|
713
|
+
{loading ? (
|
|
714
|
+
<div className="flex items-center justify-center h-full">
|
|
715
|
+
<div className="animate-spin w-8 h-8 border-2 border-pink-500 border-t-transparent rounded-full" />
|
|
716
|
+
</div>
|
|
717
|
+
) : error ? (
|
|
718
|
+
<div className="text-red-500 text-sm font-bold">{error}</div>
|
|
719
|
+
) : response ? (
|
|
720
|
+
<pre className="text-xs font-mono text-[var(--kyro-text-primary)]">
|
|
721
|
+
{JSON.stringify(response.data, null, 2)}
|
|
722
|
+
</pre>
|
|
723
|
+
) : (
|
|
724
|
+
<div className="h-full flex items-center justify-center text-[var(--kyro-text-muted)] text-sm italic">
|
|
725
|
+
Send a request to see the response
|
|
726
|
+
</div>
|
|
885
727
|
)}
|
|
886
728
|
</div>
|
|
887
729
|
</div>
|
|
888
|
-
<pre className="max-h-64 overflow-auto p-4 text-xs font-mono bg-[var(--kyro-surface)]">
|
|
889
|
-
{response || "Send a request to see the response"}
|
|
890
|
-
</pre>
|
|
891
730
|
</div>
|
|
892
731
|
</div>
|
|
893
732
|
|
|
894
|
-
{/*
|
|
733
|
+
{/* Modals */}
|
|
734
|
+
{showFolderModal && (
|
|
735
|
+
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-[100] p-4">
|
|
736
|
+
<div className="surface-tile w-full max-w-md p-6 rounded-2xl shadow-2xl border border-[var(--kyro-border)]">
|
|
737
|
+
<h2 className="text-xl font-bold mb-4">Create Folder</h2>
|
|
738
|
+
<input
|
|
739
|
+
type="text"
|
|
740
|
+
value={newFolderName}
|
|
741
|
+
onChange={(e) => setNewFolderName(e.target.value)}
|
|
742
|
+
placeholder="Folder name..."
|
|
743
|
+
className="w-full bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-lg px-4 py-2 mb-6"
|
|
744
|
+
/>
|
|
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 kyro-btn-primary bg-pink-500 border-pink-500 text-white hover:bg-pink-600 hover:border-pink-600">Create</button>
|
|
748
|
+
</div>
|
|
749
|
+
</div>
|
|
750
|
+
</div>
|
|
751
|
+
)}
|
|
752
|
+
|
|
895
753
|
{showSaveModal && (
|
|
896
|
-
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-
|
|
897
|
-
<div className="
|
|
898
|
-
<
|
|
899
|
-
Save Request
|
|
900
|
-
</h3>
|
|
754
|
+
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-[100] p-4">
|
|
755
|
+
<div className="surface-tile w-full max-w-md p-6 rounded-2xl shadow-2xl border border-[var(--kyro-border)]">
|
|
756
|
+
<h2 className="text-xl font-bold mb-4">Save Request</h2>
|
|
901
757
|
<div className="space-y-4">
|
|
902
758
|
<div>
|
|
903
|
-
<label className="
|
|
904
|
-
Name
|
|
905
|
-
</label>
|
|
759
|
+
<label className="text-[10px] font-bold tracking-widest text-[var(--kyro-text-muted)] block mb-1">Request Name</label>
|
|
906
760
|
<input
|
|
907
761
|
type="text"
|
|
908
|
-
value={
|
|
909
|
-
onChange={(e) =>
|
|
910
|
-
placeholder=
|
|
911
|
-
className="w-full
|
|
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"
|
|
912
766
|
/>
|
|
913
767
|
</div>
|
|
914
768
|
<div>
|
|
915
|
-
<label className="
|
|
916
|
-
Folder
|
|
917
|
-
</label>
|
|
769
|
+
<label className="text-[10px] font-bold tracking-widest text-[var(--kyro-text-muted)] block mb-1">Folder</label>
|
|
918
770
|
<select
|
|
919
|
-
value={
|
|
920
|
-
onChange={(e) =>
|
|
921
|
-
className="w-full
|
|
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"
|
|
922
774
|
>
|
|
923
|
-
<option value="">
|
|
924
|
-
{folders.map(
|
|
925
|
-
<option key={f.id} value={f.id}>
|
|
926
|
-
{f.name}
|
|
927
|
-
</option>
|
|
928
|
-
))}
|
|
775
|
+
<option value="">Select Folder...</option>
|
|
776
|
+
{folders.map(f => <option key={f.id} value={f.id}>{f.name}</option>)}
|
|
929
777
|
</select>
|
|
930
778
|
</div>
|
|
931
779
|
</div>
|
|
932
|
-
<div className="flex gap-
|
|
933
|
-
<button type="button"
|
|
934
|
-
|
|
935
|
-
className="flex-1 px-4 py-2 bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-lg font-bold text-sm hover:bg-[var(--kyro-surface)]"
|
|
936
|
-
>
|
|
937
|
-
Cancel
|
|
938
|
-
</button>
|
|
939
|
-
<button type="button"
|
|
940
|
-
onClick={saveRequest}
|
|
941
|
-
className="flex-1 px-4 py-2 bg-pink-500 text-white rounded-lg font-bold text-sm hover:bg-pink-600"
|
|
942
|
-
>
|
|
943
|
-
Save
|
|
944
|
-
</button>
|
|
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 kyro-btn-primary 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>
|
|
945
783
|
</div>
|
|
946
784
|
</div>
|
|
947
785
|
</div>
|