@kyro-cms/admin 0.9.7 → 0.9.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kyro-cms/admin",
3
- "version": "0.9.7",
3
+ "version": "0.9.8",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },
@@ -120,7 +120,7 @@
120
120
  "vitest": "^4.1.4"
121
121
  },
122
122
  "peerDependencies": {
123
- "@kyro-cms/core": "^0.9.2",
123
+ "@kyro-cms/core": "^0.9.8",
124
124
  "react": "^19.0.0",
125
125
  "react-dom": "^19.0.0"
126
126
  },
@@ -2,6 +2,7 @@ import React, {
2
2
  useState,
3
3
  useCallback,
4
4
  useEffect,
5
+ useMemo,
5
6
  useRef,
6
7
  Suspense,
7
8
  lazy,
@@ -52,6 +53,8 @@ const CodeMirrorEditor = lazy(() =>
52
53
  import("@uiw/react-codemirror").then((mod) => ({ default: mod.default })),
53
54
  );
54
55
  import { javascript } from "@codemirror/lang-javascript";
56
+ import { json } from "@codemirror/lang-json";
57
+ import { CompletionContext, autocompletion } from "@codemirror/autocomplete";
55
58
  import { aura } from "@uiw/codemirror-theme-aura";
56
59
 
57
60
  function prettifyQuery(query: string): string {
@@ -71,6 +74,94 @@ function prettifyQuery(query: string): string {
71
74
  return result.trim();
72
75
  }
73
76
 
77
+ function resolveTypeName(type: Record<string, unknown>): string {
78
+ if (!type) return "Unknown";
79
+ if (type.name) return type.name as string;
80
+ if (type.ofType) return resolveTypeName(type.ofType as Record<string, unknown>);
81
+ return "Unknown";
82
+ }
83
+
84
+ function isScalarType(typeName: string): boolean {
85
+ return ["String", "Int", "Float", "Boolean", "ID"].includes(typeName);
86
+ }
87
+
88
+ function generateSkeletonQuery(field: FieldInfo, schema: SchemaInfo, isMutation: boolean): string {
89
+ const returnTypeName = resolveTypeName(field.type);
90
+ const returnType = schema.types.find((t) => t.name === returnTypeName);
91
+
92
+ const scalarFields = returnType?.fields
93
+ ?.filter((f) => !f.isDeprecated && isScalarType(resolveTypeName(f.type)))
94
+ .map((f) => ` ${f.name}`) || [];
95
+ const listFields = returnType?.fields
96
+ ?.filter((f) => resolveTypeName(f.type).startsWith("["))
97
+ .map((f) => ` ${f.name} { id }`) || [];
98
+ const docField = returnType?.fields?.find((f) => f.name === "doc");
99
+ const messageField = returnType?.fields?.find((f) => f.name === "message");
100
+
101
+ let selection = "";
102
+
103
+ if (docField && isMutation) {
104
+ const docTypeName = resolveTypeName(docField.type);
105
+ const docType = schema.types.find((t) => t.name === docTypeName);
106
+ const docScalars = docType?.fields
107
+ ?.filter((f) => !f.isDeprecated && isScalarType(resolveTypeName(f.type)))
108
+ .map((f) => ` ${f.name}`) || [" id"];
109
+ selection = [" doc {", ...docScalars, " }"]
110
+ .concat(messageField ? [" message"] : [])
111
+ .join("\n");
112
+ } else {
113
+ selection = [...scalarFields, ...listFields].join("\n");
114
+ }
115
+
116
+ const args = field.args
117
+ ?.filter((a) => {
118
+ const t = renderType(a.type);
119
+ return t.endsWith("!");
120
+ })
121
+ .map((a) => {
122
+ const t = renderType(a.type).replace("!", "");
123
+ const val = t === "String" ? '""' : t === "Int" ? "0" : t === "Float" ? "0" : t === "Boolean" ? "false" : '""';
124
+ return `${a.name}: ${val}`;
125
+ })
126
+ .join(", ");
127
+
128
+ const fieldCall = `${field.name}${args ? `(${args})` : ""}`;
129
+
130
+ if (isMutation) {
131
+ return `mutation {\n ${fieldCall} {\n${selection}\n }\n}`;
132
+ }
133
+ return `${fieldCall} {\n${selection}\n}`;
134
+ }
135
+
136
+ function buildSchemaCompletionOverride(schema: SchemaInfo) {
137
+ const queryType = schema.types.find((t) => t.name === schema.queryType.name);
138
+ const mutationType = schema.mutationType
139
+ ? schema.types.find((t) => t.name === schema.mutationType!.name)
140
+ : null;
141
+
142
+ const queryFields = queryType?.fields || [];
143
+ const mutationFields = mutationType?.fields || [];
144
+
145
+ const operationNames = [...queryFields.map((f) => f.name), ...mutationFields.map((f) => f.name)];
146
+
147
+ return (context: CompletionContext) => {
148
+ const word = context.matchBefore(/\w*/);
149
+ if (!word || (word.from === word.to && !context.explicit)) return null;
150
+ return {
151
+ from: word.from,
152
+ options: [
153
+ ...operationNames.map((n) => ({
154
+ label: n,
155
+ type: "function" as const,
156
+ detail: "operation",
157
+ })),
158
+ { label: "query", type: "keyword" as const, detail: "operation type" },
159
+ { label: "mutation", type: "keyword" as const, detail: "operation type" },
160
+ ],
161
+ };
162
+ };
163
+ }
164
+
74
165
  const DEFAULT_QUERY = `# Welcome to Kyro CMS GraphQL Playground
75
166
  # Cmd+Enter to run, Cmd+Shift+P to prettify
76
167
 
@@ -163,6 +254,7 @@ export function GraphQLPlayground({
163
254
  const [splitPos, setSplitPos] = useState(50);
164
255
  const [mobilePanel, setMobilePanel] = useState<"editor" | "response">("editor");
165
256
  const [isDragging, setIsDragging] = useState(false);
257
+ const [isDesktop, setIsDesktop] = useState(false);
166
258
  const containerRef = useRef<HTMLDivElement>(null);
167
259
  const cursorPos = useRef({ line: 1, col: 1 });
168
260
 
@@ -170,6 +262,13 @@ export function GraphQLPlayground({
170
262
  setIsMounted(true);
171
263
  }, []);
172
264
 
265
+ useEffect(() => {
266
+ const check = () => setIsDesktop(window.innerWidth >= 768);
267
+ check();
268
+ window.addEventListener("resize", check);
269
+ return () => window.removeEventListener("resize", check);
270
+ }, []);
271
+
173
272
  const fetchSchema = useCallback(async () => {
174
273
  setLoadingSchema(true);
175
274
  try {
@@ -336,7 +435,24 @@ export function GraphQLPlayground({
336
435
  setTab((prev) => ({ ...prev, query: "" }));
337
436
  };
338
437
 
339
- const extensions = [javascript()];
438
+ const handleInsertQuery = useCallback(
439
+ (field: FieldInfo) => {
440
+ const isMutation = schema?.mutationType?.name
441
+ ? schema.types
442
+ .find((t) => t.name === schema.mutationType!.name)
443
+ ?.fields?.some((f) => f.name === field.name)
444
+ : false;
445
+ if (!schema) return;
446
+ const q = generateSkeletonQuery(field, schema, !!isMutation);
447
+ setTab((prev) => ({ ...prev, query: q }));
448
+ setRightTab("response");
449
+ },
450
+ [schema],
451
+ );
452
+
453
+ const queryExt = useMemo(() => [javascript(), schema ? autocompletion({ override: [buildSchemaCompletionOverride(schema)] }) : []].flat(), [schema]);
454
+ const jsonExt = useMemo(() => [json()], []);
455
+ const extensions = activeEditorTab === "query" ? queryExt : jsonExt;
340
456
  const theme = aura;
341
457
 
342
458
  const renderType = (type: Record<string, unknown>): string => {
@@ -478,7 +594,7 @@ export function GraphQLPlayground({
478
594
  </div>
479
595
 
480
596
  {/* Left: editor */}
481
- <div className={`${mobilePanel === "editor" ? "flex" : "hidden"} md:flex flex-col overflow-hidden border-r border-[var(--kyro-border)] w-full md:w-auto`} style={{ flex: 'none', width: typeof window !== "undefined" && window.innerWidth >= 768 ? `${splitPos}%` : "100%" }}>
597
+ <div className={`${mobilePanel === "editor" ? "flex" : "hidden"} md:flex flex-col overflow-hidden border-r border-[var(--kyro-border)] w-full md:w-auto`} style={{ flex: 'none', width: isDesktop ? `${splitPos}%` : "100%" }}>
482
598
  {/* Editor pills */}
483
599
  <div className="flex gap-0.5 px-3 py-1.5 border-b border-[var(--kyro-border)] bg-[var(--kyro-surface)]">
484
600
  {editorPills.map((p) => (
@@ -533,7 +649,7 @@ export function GraphQLPlayground({
533
649
  </div>
534
650
 
535
651
  {/* Right panel */}
536
- <div className={`${mobilePanel === "response" ? "flex" : "hidden"} md:flex flex-1 flex-col overflow-hidden min-w-0 w-full md:w-auto`} style={{ flex: undefined, width: typeof window !== "undefined" && window.innerWidth >= 768 ? `${100 - splitPos}%` : "100%" }}>
652
+ <div className={`${mobilePanel === "response" ? "flex" : "hidden"} md:flex flex-1 flex-col overflow-hidden min-w-0 w-full md:w-auto`} style={{ flex: undefined, width: isDesktop ? `${100 - splitPos}%` : "100%" }}>
537
653
  {/* Right pills */}
538
654
  <div className="flex gap-0.5 px-3 py-1.5 border-b border-[var(--kyro-border)] bg-[var(--kyro-surface)]">
539
655
  {rightPills.map((p) => (
@@ -584,25 +700,33 @@ export function GraphQLPlayground({
584
700
  {selectedType.fields && (
585
701
  <div className="space-y-2">
586
702
  <h4 className="text-[10px] font-semibold tracking-wider text-[var(--kyro-text-muted)] pt-3">Fields</h4>
587
- {selectedType.fields.map(f => (
588
- <div key={f.name} className="p-2.5 bg-[var(--kyro-surface-accent)] rounded-lg border border-[var(--kyro-border)]">
589
- <div className="flex items-center justify-between gap-2">
590
- <span className="font-semibold text-[11px] text-[var(--kyro-text-primary)]">{f.name}</span>
591
- <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>
592
- </div>
593
- {f.description && <p className="text-[10px] text-[var(--kyro-text-secondary)] mt-1">{f.description}</p>}
594
- {f.args && f.args.length > 0 && (
595
- <div className="mt-1.5 pl-3 border-l-2 border-[var(--kyro-border)] space-y-0.5">
596
- {f.args.map(a => (
597
- <div key={a.name} className="text-[9px]">
598
- <span className="text-[var(--kyro-text-muted)]">{a.name}:</span>{" "}
599
- <span className="text-[var(--kyro-primary)]">{renderType(a.type)}</span>
600
- </div>
601
- ))}
703
+ {selectedType.fields.map(f => {
704
+ const isRootOp = (selectedType.name === "Query" || selectedType.name === "Mutation") && !!schema;
705
+ return (
706
+ <button
707
+ key={f.name}
708
+ type="button"
709
+ onClick={isRootOp ? () => handleInsertQuery(f) : undefined}
710
+ className={`w-full text-left p-2.5 bg-[var(--kyro-surface-accent)] rounded-lg border border-[var(--kyro-border)] transition-all ${isRootOp ? "hover:border-[var(--kyro-primary)] hover:shadow-sm cursor-pointer" : ""}`}
711
+ >
712
+ <div className="flex items-center justify-between gap-2">
713
+ <span className="font-semibold text-[11px] text-[var(--kyro-text-primary)]">{f.name}</span>
714
+ <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>
602
715
  </div>
603
- )}
604
- </div>
605
- ))}
716
+ {f.description && <p className="text-[10px] text-[var(--kyro-text-secondary)] mt-1">{f.description}</p>}
717
+ {f.args && f.args.length > 0 && (
718
+ <div className="mt-1.5 pl-3 border-l-2 border-[var(--kyro-border)] space-y-0.5">
719
+ {f.args.map(a => (
720
+ <div key={a.name} className="text-[9px]">
721
+ <span className="text-[var(--kyro-text-muted)]">{a.name}:</span>{" "}
722
+ <span className="text-[var(--kyro-primary)]">{renderType(a.type)}</span>
723
+ </div>
724
+ ))}
725
+ </div>
726
+ )}
727
+ </button>
728
+ );
729
+ })}
606
730
  </div>
607
731
  )}
608
732
  </div>