@kyro-cms/admin 0.11.5 → 0.12.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.
- package/dist/index.cjs +18 -18
- package/dist/index.js +18 -18
- package/package.json +1 -1
- package/src/components/AutoForm.tsx +54 -22
- package/src/components/GraphQLPlayground.tsx +259 -12
- package/src/components/SessionsManager.tsx +1 -1
- package/src/components/WebhookManager.tsx +338 -188
- package/src/components/autoform/AutoFormHeader.tsx +19 -8
- package/src/components/autoform/ErrorBoundary.tsx +93 -0
- package/src/components/blocks/HeadingSubheadingBlock.tsx +2 -2
- package/src/components/blocks/HeroBlock.tsx +2 -2
- package/src/components/fields/BlocksField.tsx +1 -1
- package/src/components/fields/HeadingSubheadingField.tsx +2 -2
- package/src/components/fields/HeroField.tsx +4 -4
- package/src/components/fields/RelationshipField.tsx +2 -2
- package/src/components/fields/extensions/blocksStore.ts +1 -1
- package/src/hooks/useAutoFormState.ts +51 -1
- package/src/integration.ts +4 -4
- package/src/pages/settings/[slug].astro +1 -1
package/package.json
CHANGED
|
@@ -43,6 +43,7 @@ import { AutoFormHeader } from "./autoform/AutoFormHeader";
|
|
|
43
43
|
import { AutoFormEditView } from "./autoform/AutoFormEditView";
|
|
44
44
|
import { AutoFormVersionView } from "./autoform/AutoFormVersionView";
|
|
45
45
|
import { AutoFormApiView } from "./autoform/AutoFormApiView";
|
|
46
|
+
import { ErrorBoundary } from "./autoform/ErrorBoundary";
|
|
46
47
|
|
|
47
48
|
interface AutoFormProps {
|
|
48
49
|
config: CollectionConfig | GlobalConfig;
|
|
@@ -147,6 +148,7 @@ export function AutoForm({
|
|
|
147
148
|
setAutoSaveStatus,
|
|
148
149
|
fetchVersions,
|
|
149
150
|
saveDocument,
|
|
151
|
+
forceSave,
|
|
150
152
|
autoSaveSkipRef,
|
|
151
153
|
lastAutoSaveTimeRef,
|
|
152
154
|
documentStatus,
|
|
@@ -460,11 +462,14 @@ export function AutoForm({
|
|
|
460
462
|
message: "Delete this document? This cannot be undone. Are you absolutely sure?",
|
|
461
463
|
variant: "danger",
|
|
462
464
|
onConfirm: async () => {
|
|
465
|
+
autoSaveSkipRef.current = true;
|
|
463
466
|
try {
|
|
464
467
|
await apiDelete(`/api/${collectionSlug}/${formData.id}`);
|
|
465
468
|
window.location.href = `${ADMIN_BASE}/${collectionSlug}`;
|
|
466
469
|
} catch (err) {
|
|
467
470
|
toast.error((err as Error).message || "Failed to delete document");
|
|
471
|
+
} finally {
|
|
472
|
+
autoSaveSkipRef.current = false;
|
|
468
473
|
}
|
|
469
474
|
},
|
|
470
475
|
});
|
|
@@ -475,6 +480,7 @@ export function AutoForm({
|
|
|
475
480
|
title: "Unpublish Document",
|
|
476
481
|
message: "Unpublish this document?",
|
|
477
482
|
onConfirm: async () => {
|
|
483
|
+
autoSaveSkipRef.current = true;
|
|
478
484
|
try {
|
|
479
485
|
const response = await saveDocument(
|
|
480
486
|
{ ...formData, status: 'draft' } as Record<string, unknown>,
|
|
@@ -493,6 +499,8 @@ export function AutoForm({
|
|
|
493
499
|
}
|
|
494
500
|
} catch (err) {
|
|
495
501
|
toast.error("Failed to unpublish");
|
|
502
|
+
} finally {
|
|
503
|
+
autoSaveSkipRef.current = false;
|
|
496
504
|
}
|
|
497
505
|
},
|
|
498
506
|
});
|
|
@@ -533,7 +541,7 @@ export function AutoForm({
|
|
|
533
541
|
);
|
|
534
542
|
if (isPost) {
|
|
535
543
|
setTimeout(() => {
|
|
536
|
-
window.location.href = `${ADMIN_BASE}/${collectionSlug}`;
|
|
544
|
+
window.location.href = `${ADMIN_BASE}/${collectionSlug}/${result.data.id}`;
|
|
537
545
|
}, 800);
|
|
538
546
|
}
|
|
539
547
|
} else {
|
|
@@ -593,7 +601,22 @@ export function AutoForm({
|
|
|
593
601
|
if (response?.ok) {
|
|
594
602
|
setLocalSaveStatus("saved");
|
|
595
603
|
onActionSuccess?.("Published successfully");
|
|
604
|
+
if (isNewDoc && !globalSlug && dataToPublish.id) {
|
|
605
|
+
setTimeout(() => {
|
|
606
|
+
window.location.href = `${ADMIN_BASE}/${collectionSlug}/${dataToPublish.id}`;
|
|
607
|
+
}, 800);
|
|
608
|
+
}
|
|
596
609
|
} else {
|
|
610
|
+
// Two-step publish: if POST succeeded but PATCH failed,
|
|
611
|
+
// document exists as draft — navigate and inform user
|
|
612
|
+
if (isNewDoc && !globalSlug && dataToPublish.id) {
|
|
613
|
+
const error = await response?.json().catch(() => ({}));
|
|
614
|
+
toast.warning("Document saved as draft. Publishing failed: " + (error?.error || "Unknown error"));
|
|
615
|
+
setTimeout(() => {
|
|
616
|
+
window.location.href = `${ADMIN_BASE}/${collectionSlug}/${dataToPublish.id}`;
|
|
617
|
+
}, 1200);
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
597
620
|
const error = await response?.json().catch(() => ({}));
|
|
598
621
|
if (response?.status === 409) setAutoSaveStatus("conflict");
|
|
599
622
|
setLocalSaveStatus("error");
|
|
@@ -1012,7 +1035,9 @@ export function AutoForm({
|
|
|
1012
1035
|
handleUnpublish={handleUnpublish}
|
|
1013
1036
|
handleDelete={handleDelete}
|
|
1014
1037
|
handlePublish={handlePublish}
|
|
1038
|
+
handleSaveDraft={handleSaveDraft}
|
|
1015
1039
|
handleSchedulePublish={handleSchedulePublish}
|
|
1040
|
+
handleConflictOverride={() => forceSave()}
|
|
1016
1041
|
/>
|
|
1017
1042
|
)}
|
|
1018
1043
|
{layout === "single" && (
|
|
@@ -1068,7 +1093,9 @@ export function AutoForm({
|
|
|
1068
1093
|
type="button"
|
|
1069
1094
|
style={{ width: 0, height: 0, opacity: 0, padding: 0, margin: 0, border: 'none', position: 'absolute' }}
|
|
1070
1095
|
onClick={async () => {
|
|
1096
|
+
autoSaveSkipRef.current = true;
|
|
1071
1097
|
try {
|
|
1098
|
+
window.dispatchEvent(new Event("kyro:global-save-start"));
|
|
1072
1099
|
const response = await saveDocument(formData);
|
|
1073
1100
|
if (response.ok) {
|
|
1074
1101
|
const result = await response.json();
|
|
@@ -1080,33 +1107,38 @@ export function AutoForm({
|
|
|
1080
1107
|
} catch (e) {
|
|
1081
1108
|
console.error("Save error exception:", e);
|
|
1082
1109
|
onActionError?.("Save failed: " + (e as Error).message);
|
|
1110
|
+
} finally {
|
|
1111
|
+
autoSaveSkipRef.current = false;
|
|
1112
|
+
window.dispatchEvent(new Event("kyro:global-save-end"));
|
|
1083
1113
|
}
|
|
1084
1114
|
}}
|
|
1085
1115
|
/>
|
|
1086
1116
|
</>
|
|
1087
1117
|
)}
|
|
1088
1118
|
<main className="w-full pt-6 md:pt-0">
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1119
|
+
<ErrorBoundary>
|
|
1120
|
+
{view === "edit" && (
|
|
1121
|
+
<AutoFormEditView
|
|
1122
|
+
config={config}
|
|
1123
|
+
layout={layout}
|
|
1124
|
+
collectionSlug={collectionSlug}
|
|
1125
|
+
renderField={renderField}
|
|
1126
|
+
/>
|
|
1127
|
+
)}
|
|
1128
|
+
{view === "version" && (
|
|
1129
|
+
<AutoFormVersionView
|
|
1130
|
+
handleRestoreVersion={handleRestoreVersion}
|
|
1131
|
+
handleCompareVersions={handleCompareVersions}
|
|
1132
|
+
toggleCompareSelection={toggleCompareSelection}
|
|
1133
|
+
/>
|
|
1134
|
+
)}
|
|
1135
|
+
{view === "api" && (
|
|
1136
|
+
<AutoFormApiView
|
|
1137
|
+
collectionSlug={collectionSlug}
|
|
1138
|
+
globalSlug={globalSlug}
|
|
1139
|
+
/>
|
|
1140
|
+
)}
|
|
1141
|
+
</ErrorBoundary>
|
|
1110
1142
|
</main>
|
|
1111
1143
|
</div>
|
|
1112
1144
|
);
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
Play,
|
|
24
24
|
Clock,
|
|
25
25
|
Terminal,
|
|
26
|
+
Search,
|
|
26
27
|
} from "./ui/icons";
|
|
27
28
|
|
|
28
29
|
function ChevronDown({ className }: { className?: string }) {
|
|
@@ -246,14 +247,42 @@ function generateSkeletonQuery(field: FieldInfo, schema: SchemaInfo, isMutation:
|
|
|
246
247
|
const returnTypeName = resolveTypeName(field.type);
|
|
247
248
|
const returnType = schema.types.find((t) => t.name === returnTypeName);
|
|
248
249
|
|
|
249
|
-
function
|
|
250
|
+
function buildTypeMap(): Map<string, TypeInfo> {
|
|
251
|
+
const m = new Map<string, TypeInfo>();
|
|
252
|
+
for (const t of schema.types) m.set(t.name, t);
|
|
253
|
+
return m;
|
|
254
|
+
}
|
|
255
|
+
const localTypeMap = buildTypeMap();
|
|
256
|
+
|
|
257
|
+
function collectFieldsWithUnions(fields: FieldInfo[], indent: number, _typeMap: Map<string, TypeInfo>): string[] {
|
|
250
258
|
const lines: string[] = [];
|
|
251
259
|
for (const f of fields) {
|
|
252
260
|
if (f.isDeprecated) continue;
|
|
253
261
|
const typeName = resolveTypeName(f.type);
|
|
262
|
+
const returnInfo = _typeMap.get(typeName);
|
|
254
263
|
const pad = " ".repeat(indent);
|
|
255
264
|
if (isScalarType(typeName) || typeName === "JSON") {
|
|
256
265
|
lines.push(`${pad}${f.name}`);
|
|
266
|
+
} else if (returnInfo && (returnInfo.kind === "UNION" || returnInfo.kind === "INTERFACE")) {
|
|
267
|
+
lines.push(`${pad}${f.name} {`);
|
|
268
|
+
lines.push(`${pad} __typename`);
|
|
269
|
+
for (const pt of returnInfo.possibleTypes || []) {
|
|
270
|
+
const ptInfo = _typeMap.get(pt.name);
|
|
271
|
+
if (!ptInfo?.fields) continue;
|
|
272
|
+
lines.push(`${pad} ... on ${pt.name} {`);
|
|
273
|
+
for (const pf of ptInfo.fields) {
|
|
274
|
+
if (pf.isDeprecated) continue;
|
|
275
|
+
const pfTypeName = resolveTypeName(pf.type);
|
|
276
|
+
const pfPad = " ".repeat(indent + 2);
|
|
277
|
+
if (isScalarType(pfTypeName) || pfTypeName === "JSON") {
|
|
278
|
+
lines.push(`${pfPad}${pf.name}`);
|
|
279
|
+
} else {
|
|
280
|
+
lines.push(`${pfPad}${pf.name} { id }`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
lines.push(`${pad} }`);
|
|
284
|
+
}
|
|
285
|
+
lines.push(`${pad}}`);
|
|
257
286
|
} else {
|
|
258
287
|
lines.push(`${pad}${f.name} { id }`);
|
|
259
288
|
}
|
|
@@ -269,12 +298,12 @@ function generateSkeletonQuery(field: FieldInfo, schema: SchemaInfo, isMutation:
|
|
|
269
298
|
if (docField && isMutation) {
|
|
270
299
|
const docTypeName = resolveTypeName(docField.type);
|
|
271
300
|
const docType = schema.types.find((t) => t.name === docTypeName);
|
|
272
|
-
const docLines =
|
|
301
|
+
const docLines = collectFieldsWithUnions(docType?.fields?.filter((f) => !f.isDeprecated) || [], 1, localTypeMap);
|
|
273
302
|
selection = [" doc {", ...docLines, " }"]
|
|
274
303
|
.concat(messageField ? [" message"] : [])
|
|
275
304
|
.join("\n");
|
|
276
305
|
} else {
|
|
277
|
-
selection =
|
|
306
|
+
selection = collectFieldsWithUnions(returnType?.fields || [], 1, localTypeMap).join("\n");
|
|
278
307
|
}
|
|
279
308
|
|
|
280
309
|
const args = field.args
|
|
@@ -358,6 +387,8 @@ interface TypeInfo {
|
|
|
358
387
|
fields?: FieldInfo[];
|
|
359
388
|
inputFields?: FieldInfo[];
|
|
360
389
|
enumValues?: { name: string; description?: string; isDeprecated: boolean }[];
|
|
390
|
+
possibleTypes?: { name: string; kind: string }[];
|
|
391
|
+
interfaces?: { name: string; kind: string }[];
|
|
361
392
|
isDeprecated?: boolean;
|
|
362
393
|
}
|
|
363
394
|
|
|
@@ -502,6 +533,8 @@ export function GraphQLPlayground({
|
|
|
502
533
|
description
|
|
503
534
|
isDeprecated
|
|
504
535
|
}
|
|
536
|
+
possibleTypes { name kind }
|
|
537
|
+
interfaces { name kind }
|
|
505
538
|
}
|
|
506
539
|
}
|
|
507
540
|
}
|
|
@@ -524,6 +557,93 @@ export function GraphQLPlayground({
|
|
|
524
557
|
if (showDocs && !schema) fetchSchema();
|
|
525
558
|
}, [showDocs, schema, fetchSchema]);
|
|
526
559
|
|
|
560
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
561
|
+
|
|
562
|
+
const typeMap = useMemo(() => {
|
|
563
|
+
const m = new Map<string, TypeInfo>();
|
|
564
|
+
if (!schema) return m;
|
|
565
|
+
for (const t of schema.types) {
|
|
566
|
+
m.set(t.name, t);
|
|
567
|
+
}
|
|
568
|
+
return m;
|
|
569
|
+
}, [schema]);
|
|
570
|
+
|
|
571
|
+
interface SearchResult {
|
|
572
|
+
typeName: string;
|
|
573
|
+
fieldPath: string;
|
|
574
|
+
typeInfo: TypeInfo;
|
|
575
|
+
fieldInfo?: FieldInfo;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const searchResults = useMemo(() => {
|
|
579
|
+
if (!searchQuery.trim() || !schema) return [];
|
|
580
|
+
const q = searchQuery.toLowerCase();
|
|
581
|
+
const results: SearchResult[] = [];
|
|
582
|
+
const seenLabels = new Set<string>();
|
|
583
|
+
|
|
584
|
+
for (const type of schema.types) {
|
|
585
|
+
if (type.name.startsWith("__")) continue;
|
|
586
|
+
const typeMatch = type.name.toLowerCase().includes(q);
|
|
587
|
+
|
|
588
|
+
if (typeMatch && !seenLabels.has(type.name)) {
|
|
589
|
+
seenLabels.add(type.name);
|
|
590
|
+
results.push({ typeName: type.name, fieldPath: type.name, typeInfo: type });
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (type.fields) {
|
|
594
|
+
for (const field of type.fields) {
|
|
595
|
+
const fieldMatch = field.name.toLowerCase().includes(q);
|
|
596
|
+
if (fieldMatch) {
|
|
597
|
+
const label = `${type.name} > ${field.name}`;
|
|
598
|
+
if (!seenLabels.has(label)) {
|
|
599
|
+
seenLabels.add(label);
|
|
600
|
+
results.push({ typeName: type.name, fieldPath: label, typeInfo: type, fieldInfo: field });
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (q.length >= 2) {
|
|
605
|
+
const rtName = resolveTypeName(field.type);
|
|
606
|
+
const rt = typeMap.get(rtName);
|
|
607
|
+
if (rt?.fields && (fieldMatch || results.some(r => r.fieldInfo === field))) {
|
|
608
|
+
for (const sub of rt.fields) {
|
|
609
|
+
if (sub.name.toLowerCase().includes(q)) {
|
|
610
|
+
const label = `${type.name} > ${field.name} > ${sub.name}`;
|
|
611
|
+
if (!seenLabels.has(label)) {
|
|
612
|
+
seenLabels.add(label);
|
|
613
|
+
results.push({ typeName: type.name, fieldPath: label, typeInfo: type, fieldInfo: field });
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
return results;
|
|
624
|
+
}, [searchQuery, schema, typeMap]);
|
|
625
|
+
|
|
626
|
+
const handleSearchSelect = useCallback((result: SearchResult) => {
|
|
627
|
+
const targetType = result.fieldInfo
|
|
628
|
+
? typeMap.get(resolveTypeName(result.fieldInfo.type)) || result.typeInfo
|
|
629
|
+
: result.typeInfo;
|
|
630
|
+
setSelectedType(targetType);
|
|
631
|
+
setSearchQuery("");
|
|
632
|
+
}, [typeMap]);
|
|
633
|
+
|
|
634
|
+
function highlightMatch(text: string, query: string) {
|
|
635
|
+
if (!query.trim()) return text;
|
|
636
|
+
const idx = text.toLowerCase().indexOf(query.toLowerCase());
|
|
637
|
+
if (idx === -1) return text;
|
|
638
|
+
return (
|
|
639
|
+
<>
|
|
640
|
+
{text.slice(0, idx)}
|
|
641
|
+
<mark className="bg-[var(--kyro-primary)]/20 text-[var(--kyro-primary)] rounded px-0.5">{text.slice(idx, idx + query.length)}</mark>
|
|
642
|
+
{text.slice(idx + query.length)}
|
|
643
|
+
</>
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
|
|
527
647
|
const handleRun = useCallback(async () => {
|
|
528
648
|
setIsLoading(true);
|
|
529
649
|
setError(null);
|
|
@@ -850,9 +970,23 @@ export function GraphQLPlayground({
|
|
|
850
970
|
<div className="flex-1 overflow-hidden">
|
|
851
971
|
{rightTab === "docs" ? (
|
|
852
972
|
<div className="h-full flex flex-col overflow-hidden bg-[var(--kyro-surface)]">
|
|
853
|
-
<div className="flex items-center
|
|
854
|
-
<
|
|
855
|
-
|
|
973
|
+
<div className="flex items-center gap-2 px-3 py-1.5 border-b border-[var(--kyro-border)]">
|
|
974
|
+
<div className="relative flex-1">
|
|
975
|
+
<Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3 h-3 text-[var(--kyro-text-muted)]" />
|
|
976
|
+
<input
|
|
977
|
+
type="text"
|
|
978
|
+
value={searchQuery}
|
|
979
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
980
|
+
placeholder="Search types and fields..."
|
|
981
|
+
className="w-full pl-7 pr-6 py-1 text-[10px] bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-md text-[var(--kyro-text-primary)] placeholder:text-[var(--kyro-text-muted)] focus:outline-none focus:border-[var(--kyro-primary)]"
|
|
982
|
+
/>
|
|
983
|
+
{searchQuery && (
|
|
984
|
+
<button onClick={() => setSearchQuery("")} className="absolute right-1.5 top-1/2 -translate-y-1/2 text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)]">
|
|
985
|
+
<X className="w-3 h-3" />
|
|
986
|
+
</button>
|
|
987
|
+
)}
|
|
988
|
+
</div>
|
|
989
|
+
<button onClick={() => setShowDocs(false)} className="text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] shrink-0">
|
|
856
990
|
<X className="w-3.5 h-3.5" />
|
|
857
991
|
</button>
|
|
858
992
|
</div>
|
|
@@ -863,7 +997,39 @@ export function GraphQLPlayground({
|
|
|
863
997
|
</div>
|
|
864
998
|
) : schema ? (
|
|
865
999
|
<div className="space-y-4">
|
|
866
|
-
{
|
|
1000
|
+
{searchQuery ? (
|
|
1001
|
+
<div className="space-y-1">
|
|
1002
|
+
{searchResults.length === 0 ? (
|
|
1003
|
+
<p className="text-[10px] text-[var(--kyro-text-muted)] py-4 text-center">No results for "{searchQuery}"</p>
|
|
1004
|
+
) : (
|
|
1005
|
+
searchResults.map((r, i) => {
|
|
1006
|
+
const targetType = r.fieldInfo ? typeMap.get(resolveTypeName(r.fieldInfo.type)) || r.typeInfo : r.typeInfo;
|
|
1007
|
+
return (
|
|
1008
|
+
<button
|
|
1009
|
+
key={r.fieldPath + i}
|
|
1010
|
+
onClick={() => handleSearchSelect(r)}
|
|
1011
|
+
className="w-full flex items-center justify-between px-3 py-1.5 hover:bg-[var(--kyro-surface-accent)] rounded-lg transition-all text-left group"
|
|
1012
|
+
>
|
|
1013
|
+
<span className="text-[10px] font-mono text-[var(--kyro-text-muted)]">
|
|
1014
|
+
{r.fieldPath.split(" > ").map((part, j) => (
|
|
1015
|
+
<span key={j}>
|
|
1016
|
+
{j > 0 && <span className="mx-1 opacity-40">›</span>}
|
|
1017
|
+
<span className={j === r.fieldPath.split(" > ").length - 1 ? "text-[var(--kyro-text-primary)] font-semibold" : "text-[var(--kyro-text-muted)]"}>
|
|
1018
|
+
{highlightMatch(part, searchQuery)}
|
|
1019
|
+
</span>
|
|
1020
|
+
</span>
|
|
1021
|
+
))}
|
|
1022
|
+
{targetType && (targetType.kind === "UNION" || targetType.kind === "INTERFACE") && (
|
|
1023
|
+
<span className="ml-2 text-[9px] font-mono text-[var(--kyro-primary)] bg-[var(--kyro-primary)]/10 px-1 py-0.5 rounded">⧉</span>
|
|
1024
|
+
)}
|
|
1025
|
+
</span>
|
|
1026
|
+
<ChevronRight className="w-3 h-3 opacity-30 group-hover:opacity-60" />
|
|
1027
|
+
</button>
|
|
1028
|
+
);
|
|
1029
|
+
})
|
|
1030
|
+
)}
|
|
1031
|
+
</div>
|
|
1032
|
+
) : selectedType ? (
|
|
867
1033
|
<div className="space-y-3">
|
|
868
1034
|
<button
|
|
869
1035
|
onClick={() => setSelectedType(null)}
|
|
@@ -878,6 +1044,46 @@ export function GraphQLPlayground({
|
|
|
878
1044
|
<p className="mt-1.5 text-[11px] text-[var(--kyro-text-secondary)] leading-relaxed">{selectedType.description}</p>
|
|
879
1045
|
)}
|
|
880
1046
|
</div>
|
|
1047
|
+
{selectedType.kind === "UNION" && selectedType.possibleTypes && (
|
|
1048
|
+
<div className="space-y-2">
|
|
1049
|
+
<h4 className="text-[10px] font-semibold tracking-wider text-[var(--kyro-text-muted)] pt-3">Possible Types <span className="text-[8px] font-mono text-[var(--kyro-primary)] bg-[var(--kyro-primary)]/10 px-1 py-0.5 rounded ml-1">⧉ fragment</span></h4>
|
|
1050
|
+
<div className="space-y-1">
|
|
1051
|
+
{selectedType.possibleTypes.map(pt => (
|
|
1052
|
+
<button
|
|
1053
|
+
key={pt.name}
|
|
1054
|
+
onClick={() => {
|
|
1055
|
+
const t = typeMap.get(pt.name);
|
|
1056
|
+
if (t) setSelectedType(t);
|
|
1057
|
+
}}
|
|
1058
|
+
className="w-full flex items-center justify-between px-3 py-1.5 hover:bg-[var(--kyro-surface-accent)] rounded-lg transition-all text-left group"
|
|
1059
|
+
>
|
|
1060
|
+
<span className="text-[11px] font-medium text-[var(--kyro-text-primary)]">{pt.name}</span>
|
|
1061
|
+
<ChevronRight className="w-3 h-3 opacity-30 group-hover:opacity-60" />
|
|
1062
|
+
</button>
|
|
1063
|
+
))}
|
|
1064
|
+
</div>
|
|
1065
|
+
</div>
|
|
1066
|
+
)}
|
|
1067
|
+
{selectedType.kind === "INTERFACE" && selectedType.possibleTypes && (
|
|
1068
|
+
<div className="space-y-2">
|
|
1069
|
+
<h4 className="text-[10px] font-semibold tracking-wider text-[var(--kyro-text-muted)] pt-3">Implementing Types <span className="text-[8px] font-mono text-[var(--kyro-text-muted)] bg-[var(--kyro-surface)] px-1 py-0.5 rounded border border-[var(--kyro-border)] ml-1">interface</span></h4>
|
|
1070
|
+
<div className="space-y-1">
|
|
1071
|
+
{selectedType.possibleTypes.map(pt => (
|
|
1072
|
+
<button
|
|
1073
|
+
key={pt.name}
|
|
1074
|
+
onClick={() => {
|
|
1075
|
+
const t = typeMap.get(pt.name);
|
|
1076
|
+
if (t) setSelectedType(t);
|
|
1077
|
+
}}
|
|
1078
|
+
className="w-full flex items-center justify-between px-3 py-1.5 hover:bg-[var(--kyro-surface-accent)] rounded-lg transition-all text-left group"
|
|
1079
|
+
>
|
|
1080
|
+
<span className="text-[11px] font-medium text-[var(--kyro-text-primary)]">{pt.name}</span>
|
|
1081
|
+
<ChevronRight className="w-3 h-3 opacity-30 group-hover:opacity-60" />
|
|
1082
|
+
</button>
|
|
1083
|
+
))}
|
|
1084
|
+
</div>
|
|
1085
|
+
</div>
|
|
1086
|
+
)}
|
|
881
1087
|
{selectedType.fields && (
|
|
882
1088
|
<div className="space-y-2">
|
|
883
1089
|
<h4 className="text-[10px] font-semibold tracking-wider text-[var(--kyro-text-muted)] pt-3">Fields</h4>
|
|
@@ -892,9 +1098,46 @@ export function GraphQLPlayground({
|
|
|
892
1098
|
>
|
|
893
1099
|
<div className="flex items-center justify-between gap-2">
|
|
894
1100
|
<span className="font-semibold text-[11px] text-[var(--kyro-text-primary)]">{f.name}</span>
|
|
895
|
-
<span className="
|
|
1101
|
+
<span className="flex items-center gap-1">
|
|
1102
|
+
{(() => {
|
|
1103
|
+
const rtName = resolveTypeName(f.type);
|
|
1104
|
+
const rt = typeMap.get(rtName);
|
|
1105
|
+
const isUnionish = rt && (rt.kind === "UNION" || rt.kind === "INTERFACE");
|
|
1106
|
+
return isUnionish ? (
|
|
1107
|
+
<span className="text-[9px] font-mono text-[var(--kyro-primary)] bg-[var(--kyro-primary)]/10 px-1.5 py-0.5 rounded flex items-center gap-1">
|
|
1108
|
+
⧉
|
|
1109
|
+
<span className="text-[8px] opacity-70">fragment</span>
|
|
1110
|
+
</span>
|
|
1111
|
+
) : null;
|
|
1112
|
+
})()}
|
|
1113
|
+
<span className="text-[9px] font-mono text-[var(--kyro-primary)] bg-[var(--kyro-primary)]/10 px-1.5 py-0.5 rounded">{renderType(f.type)}</span>
|
|
1114
|
+
</span>
|
|
896
1115
|
</div>
|
|
897
1116
|
{f.description && <p className="text-[10px] text-[var(--kyro-text-secondary)] mt-1">{f.description}</p>}
|
|
1117
|
+
{(() => {
|
|
1118
|
+
const rtName = resolveTypeName(f.type);
|
|
1119
|
+
const rt = typeMap.get(rtName);
|
|
1120
|
+
if (rt && (rt.kind === "UNION" || rt.kind === "INTERFACE") && rt.possibleTypes?.length) {
|
|
1121
|
+
return (
|
|
1122
|
+
<div className="mt-1.5 pl-3 border-l-2 border-[var(--kyro-primary)]/30 space-y-0.5">
|
|
1123
|
+
<span className="text-[8px] font-semibold text-[var(--kyro-text-muted)] uppercase tracking-wider">Possible Types</span>
|
|
1124
|
+
<div className="flex flex-wrap gap-1 mt-0.5">
|
|
1125
|
+
{rt.possibleTypes.map(pt => {
|
|
1126
|
+
const ptInfo = typeMap.get(pt.name);
|
|
1127
|
+
const hasFragment = ptInfo && (ptInfo.kind === "OBJECT" || ptInfo.kind === "INTERFACE");
|
|
1128
|
+
return (
|
|
1129
|
+
<span key={pt.name} className="text-[9px] font-mono text-[var(--kyro-text-muted)] bg-[var(--kyro-surface)] px-1.5 py-0.5 rounded border border-[var(--kyro-border)]">
|
|
1130
|
+
{pt.name}
|
|
1131
|
+
{hasFragment && <span className="ml-1 text-[8px] text-[var(--kyro-primary)]">⧉</span>}
|
|
1132
|
+
</span>
|
|
1133
|
+
);
|
|
1134
|
+
})}
|
|
1135
|
+
</div>
|
|
1136
|
+
</div>
|
|
1137
|
+
);
|
|
1138
|
+
}
|
|
1139
|
+
return null;
|
|
1140
|
+
})()}
|
|
898
1141
|
{f.args && f.args.length > 0 && (
|
|
899
1142
|
<div className="mt-1.5 pl-3 border-l-2 border-[var(--kyro-border)] space-y-0.5">
|
|
900
1143
|
{f.args.map(a => (
|
|
@@ -934,14 +1177,18 @@ export function GraphQLPlayground({
|
|
|
934
1177
|
</div>
|
|
935
1178
|
<div className="space-y-1.5">
|
|
936
1179
|
<h4 className="text-[10px] font-semibold tracking-wider text-[var(--kyro-text-muted)] pt-3">All Types</h4>
|
|
937
|
-
{schema.types.filter(t => !t.name.startsWith("__") && t.kind === "OBJECT").map(t => (
|
|
1180
|
+
{schema.types.filter(t => !t.name.startsWith("__") && (t.kind === "OBJECT" || t.kind === "UNION" || t.kind === "INTERFACE")).map(t => (
|
|
938
1181
|
<button
|
|
939
1182
|
key={t.name}
|
|
940
1183
|
onClick={() => setSelectedType(t)}
|
|
941
|
-
className="w-full flex items-center justify-between px-3 py-1.5 hover:bg-[var(--kyro-surface-accent)] rounded-lg transition-all text-left"
|
|
1184
|
+
className="w-full flex items-center justify-between px-3 py-1.5 hover:bg-[var(--kyro-surface-accent)] rounded-lg transition-all text-left group"
|
|
942
1185
|
>
|
|
943
|
-
<
|
|
944
|
-
|
|
1186
|
+
<div className="flex items-center gap-2">
|
|
1187
|
+
<span className="text-[11px] font-medium text-[var(--kyro-text-primary)]">{t.name}</span>
|
|
1188
|
+
{t.kind === "UNION" && <span className="text-[8px] font-mono text-[var(--kyro-primary)] bg-[var(--kyro-primary)]/10 px-1 py-0.5 rounded">union</span>}
|
|
1189
|
+
{t.kind === "INTERFACE" && <span className="text-[8px] font-mono text-[var(--kyro-text-muted)] bg-[var(--kyro-surface)] px-1 py-0.5 rounded border border-[var(--kyro-border)]">interface</span>}
|
|
1190
|
+
</div>
|
|
1191
|
+
<ChevronRight className="w-3 h-3 opacity-30 group-hover:opacity-60" />
|
|
945
1192
|
</button>
|
|
946
1193
|
))}
|
|
947
1194
|
</div>
|
|
@@ -2,7 +2,7 @@ import React, { useState, useEffect } from "react";
|
|
|
2
2
|
import { apiGet, apiDelete } from "../lib/api";
|
|
3
3
|
import { Shield, Monitor, Trash2, Clock, AlertTriangle, Info, LogOut, Globe, Activity, RefreshCcw, Smartphone, Laptop } from "./ui/icons";
|
|
4
4
|
import { PageHeader } from "./ui/PageHeader";
|
|
5
|
-
import toast from "
|
|
5
|
+
import { toast } from "../lib/stores";
|
|
6
6
|
import { Badge } from "./ui/Badge";
|
|
7
7
|
|
|
8
8
|
interface Session {
|