zudoku 0.28.0 → 0.28.1

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.
Files changed (75) hide show
  1. package/dist/app/main.js +1 -2
  2. package/dist/app/main.js.map +1 -1
  3. package/dist/config/validators/InputSidebarSchema.d.ts +2 -2
  4. package/dist/lib/components/PathRenderer.d.ts +11 -0
  5. package/dist/lib/components/PathRenderer.js +25 -0
  6. package/dist/lib/components/PathRenderer.js.map +1 -0
  7. package/dist/lib/components/ThemeSwitch.js +4 -4
  8. package/dist/lib/components/ThemeSwitch.js.map +1 -1
  9. package/dist/lib/components/index.d.ts +1 -0
  10. package/dist/lib/components/index.js +4 -2
  11. package/dist/lib/components/index.js.map +1 -1
  12. package/dist/lib/components/navigation/SidebarCategory.js +16 -14
  13. package/dist/lib/components/navigation/SidebarCategory.js.map +1 -1
  14. package/dist/lib/oas/graphql/circular.js +17 -6
  15. package/dist/lib/oas/graphql/circular.js.map +1 -1
  16. package/dist/lib/plugins/openapi/ColorizedParam.js +3 -1
  17. package/dist/lib/plugins/openapi/ColorizedParam.js.map +1 -1
  18. package/dist/lib/plugins/openapi/OperationListItem.js +1 -1
  19. package/dist/lib/plugins/openapi/OperationListItem.js.map +1 -1
  20. package/dist/lib/plugins/openapi/ParameterListItem.js +1 -1
  21. package/dist/lib/plugins/openapi/ParameterListItem.js.map +1 -1
  22. package/dist/lib/plugins/openapi/Sidecar.js +5 -10
  23. package/dist/lib/plugins/openapi/Sidecar.js.map +1 -1
  24. package/dist/lib/plugins/openapi/playground/PathParams.js +1 -1
  25. package/dist/lib/plugins/openapi/playground/PathParams.js.map +1 -1
  26. package/dist/lib/plugins/openapi/playground/Playground.js +7 -15
  27. package/dist/lib/plugins/openapi/playground/Playground.js.map +1 -1
  28. package/dist/lib/plugins/openapi/schema/{SchemaComponents.js → SchemaPropertyItem.js} +10 -8
  29. package/dist/lib/plugins/openapi/schema/SchemaPropertyItem.js.map +1 -0
  30. package/dist/lib/plugins/openapi/schema/SchemaView.js +1 -1
  31. package/dist/lib/plugins/openapi/schema/SchemaView.js.map +1 -1
  32. package/dist/lib/plugins/openapi/schema/utils.d.ts +1 -0
  33. package/dist/lib/plugins/openapi/schema/utils.js +2 -0
  34. package/dist/lib/plugins/openapi/schema/utils.js.map +1 -1
  35. package/dist/vite/plugin-api.js +4 -2
  36. package/dist/vite/plugin-api.js.map +1 -1
  37. package/dist/vite/prerender.js +0 -1
  38. package/dist/vite/prerender.js.map +1 -1
  39. package/lib/{MdxPage-DewragjB.js → MdxPage-BuG8Tuwc.js} +2 -2
  40. package/lib/{MdxPage-DewragjB.js.map → MdxPage-BuG8Tuwc.js.map} +1 -1
  41. package/lib/{OperationList-D_ejrepA.js → OperationList-CDt1xdc4.js} +978 -970
  42. package/lib/OperationList-CDt1xdc4.js.map +1 -0
  43. package/lib/{circular-Dgpd6AN-.js → circular-DxaIIlWD.js} +251 -239
  44. package/lib/{circular-Dgpd6AN-.js.map → circular-DxaIIlWD.js.map} +1 -1
  45. package/lib/{createServer-BydbkTsd.js → createServer-CjNktZzL.js} +2 -2
  46. package/lib/{createServer-BydbkTsd.js.map → createServer-CjNktZzL.js.map} +1 -1
  47. package/lib/{index-DGugJOLc.js → index-Eb1oiHbM.js} +583 -560
  48. package/lib/index-Eb1oiHbM.js.map +1 -0
  49. package/lib/{useScrollToAnchor-eRM9tVvD.js → useScrollToAnchor-BZsGmBng.js} +84 -89
  50. package/lib/useScrollToAnchor-BZsGmBng.js.map +1 -0
  51. package/lib/zudoku.components.js +334 -317
  52. package/lib/zudoku.components.js.map +1 -1
  53. package/lib/zudoku.plugin-markdown.js +1 -1
  54. package/lib/zudoku.plugin-openapi.js +1 -1
  55. package/package.json +1 -1
  56. package/src/app/main.tsx +1 -2
  57. package/src/lib/components/PathRenderer.tsx +59 -0
  58. package/src/lib/components/ThemeSwitch.tsx +15 -14
  59. package/src/lib/components/index.ts +7 -5
  60. package/src/lib/components/navigation/SidebarCategory.tsx +43 -41
  61. package/src/lib/oas/graphql/circular.ts +27 -6
  62. package/src/lib/plugins/openapi/ColorizedParam.tsx +3 -3
  63. package/src/lib/plugins/openapi/OperationListItem.tsx +6 -1
  64. package/src/lib/plugins/openapi/ParameterListItem.tsx +1 -1
  65. package/src/lib/plugins/openapi/Sidecar.tsx +17 -26
  66. package/src/lib/plugins/openapi/playground/PathParams.tsx +1 -1
  67. package/src/lib/plugins/openapi/playground/Playground.tsx +23 -33
  68. package/src/lib/plugins/openapi/schema/{SchemaComponents.tsx → SchemaPropertyItem.tsx} +10 -6
  69. package/src/lib/plugins/openapi/schema/SchemaView.tsx +4 -1
  70. package/src/lib/plugins/openapi/schema/utils.ts +4 -0
  71. package/dist/lib/plugins/openapi/schema/SchemaComponents.js.map +0 -1
  72. package/lib/OperationList-D_ejrepA.js.map +0 -1
  73. package/lib/index-DGugJOLc.js.map +0 -1
  74. package/lib/useScrollToAnchor-eRM9tVvD.js.map +0 -1
  75. /package/dist/lib/plugins/openapi/schema/{SchemaComponents.d.ts → SchemaPropertyItem.d.ts} +0 -0
@@ -74,7 +74,7 @@ const C = (n) => ({
74
74
  const h = {
75
75
  path: r,
76
76
  lazy: async () => {
77
- const { MdxPage: l } = await import("./MdxPage-DewragjB.js"), { default: p, ...g } = await a();
77
+ const { MdxPage: l } = await import("./MdxPage-BuG8Tuwc.js"), { default: p, ...g } = await a();
78
78
  return {
79
79
  element: /* @__PURE__ */ P.jsx(
80
80
  l,
@@ -5,7 +5,7 @@ import "./chunk-SYFQ2XB5-BPvC-soB.js";
5
5
  import "./hook-FT3SJLe_.js";
6
6
  import "./ui/Button.js";
7
7
  import "./joinUrl-nLx9pD-Z.js";
8
- import { o as f } from "./index-DGugJOLc.js";
8
+ import { o as f } from "./index-Eb1oiHbM.js";
9
9
  export {
10
10
  f as openApiPlugin
11
11
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zudoku",
3
- "version": "0.28.0",
3
+ "version": "0.28.1",
4
4
  "type": "module",
5
5
  "homepage": "https://zudoku.dev",
6
6
  "repository": {
package/src/app/main.tsx CHANGED
@@ -11,12 +11,11 @@ import { configuredRedirectPlugin } from "virtual:zudoku-redirect-plugin";
11
11
  import { configuredSearchPlugin } from "virtual:zudoku-search-plugin";
12
12
  import { configuredSidebar } from "virtual:zudoku-sidebar";
13
13
  import "virtual:zudoku-theme.css";
14
- import { Layout, RouterError, Zudoku } from "zudoku/components";
14
+ import { Layout, RouteGuard, RouterError, Zudoku } from "zudoku/components";
15
15
  import type { ZudokuConfig } from "../config/config.js";
16
16
  import { StatusPage } from "../lib/components/StatusPage.js";
17
17
  import type { ZudokuContextOptions } from "../lib/core/ZudokuContext.js";
18
18
  import { isNavigationPlugin } from "../lib/core/plugins.js";
19
- import { RouteGuard } from "../lib/core/RouteGuard.js";
20
19
 
21
20
  export const convertZudokuConfigToOptions = (
22
21
  config: ZudokuConfig,
@@ -0,0 +1,59 @@
1
+ import { Fragment, type ReactNode } from "react";
2
+
3
+ type PathParamProps = {
4
+ name: string;
5
+ index: number;
6
+ originalValue?: string;
7
+ };
8
+
9
+ export const PathRenderer = ({
10
+ path,
11
+ renderParam,
12
+ }: {
13
+ path: string;
14
+ renderParam: (props: PathParamProps) => ReactNode;
15
+ }) =>
16
+ path.split("/").map((part, i, arr) => {
17
+ const matches = Array.from(part.matchAll(/{([^}]+)}/g));
18
+ const elements: ReactNode[] = [];
19
+ let lastIndex = 0;
20
+
21
+ matches.forEach((match, matchIndex) => {
22
+ const [originalValue, name] = match;
23
+ if (!name) return;
24
+ const startIndex = match.index!;
25
+
26
+ if (startIndex > lastIndex) {
27
+ elements.push(
28
+ <Fragment key={`text-${lastIndex}-${startIndex}`}>
29
+ {part.slice(lastIndex, startIndex)}
30
+ </Fragment>,
31
+ );
32
+ }
33
+
34
+ elements.push(
35
+ <Fragment key={`param-${name}`}>
36
+ {renderParam({ name, originalValue, index: matchIndex })}
37
+ </Fragment>,
38
+ );
39
+
40
+ lastIndex = startIndex + originalValue.length;
41
+ });
42
+
43
+ if (lastIndex < part.length) {
44
+ elements.push(
45
+ <Fragment key={`text-${lastIndex}-${part.length}`}>
46
+ {part.slice(lastIndex)}
47
+ </Fragment>,
48
+ );
49
+ }
50
+
51
+ return (
52
+ // eslint-disable-next-line react/no-array-index-key
53
+ <Fragment key={`${part}-${i}`}>
54
+ {elements}
55
+ {i < arr.length - 1 && "/"}
56
+ <wbr />
57
+ </Fragment>
58
+ );
59
+ });
@@ -1,25 +1,26 @@
1
1
  import { MoonStarIcon, SunIcon } from "lucide-react";
2
2
  import { useTheme } from "next-themes";
3
3
  import { Button } from "zudoku/ui/Button.js";
4
- import { cn } from "../util/cn.js";
4
+ import { ClientOnly } from "./ClientOnly.js";
5
5
 
6
6
  export const ThemeSwitch = () => {
7
7
  const { resolvedTheme, setTheme } = useTheme();
8
8
  const ThemeIcon = resolvedTheme === "dark" ? MoonStarIcon : SunIcon;
9
9
 
10
10
  return (
11
- <Button
12
- variant="ghost"
13
- size="icon"
14
- aria-label={
15
- resolvedTheme === "dark"
16
- ? "Switch to light mode"
17
- : "Switch to dark mode"
18
- }
19
- className={cn(resolvedTheme ? "opacity-100" : "opacity-0")}
20
- onClick={() => setTheme(resolvedTheme === "dark" ? "light" : "dark")}
21
- >
22
- <ThemeIcon size={18} />
23
- </Button>
11
+ <ClientOnly fallback={<Button variant="ghost" size="icon" />}>
12
+ <Button
13
+ variant="ghost"
14
+ size="icon"
15
+ aria-label={
16
+ resolvedTheme === "dark"
17
+ ? "Switch to light mode"
18
+ : "Switch to dark mode"
19
+ }
20
+ onClick={() => setTheme(resolvedTheme === "dark" ? "light" : "dark")}
21
+ >
22
+ <ThemeIcon size={18} />
23
+ </Button>
24
+ </ClientOnly>
24
25
  );
25
26
  };
@@ -2,6 +2,7 @@ import { useMDXComponents as useMDXComponentsImport } from "@mdx-js/react";
2
2
  import { Helmet } from "@zudoku/react-helmet-async";
3
3
  import { Link as LinkImport } from "react-router";
4
4
  import { useAuth as useAuthImport } from "../authentication/hook.js";
5
+ import { RouteGuard as RouteGuardImport } from "../core/RouteGuard.js";
5
6
  import { RouterError as RouterErrorImport } from "../errors/RouterError.js";
6
7
  import { ServerError as ServerErrorImport } from "../errors/ServerError.js";
7
8
  import { Button as ButtonImport } from "../ui/Button.js";
@@ -10,16 +11,16 @@ import {
10
11
  Bootstrap as BootstrapImport,
11
12
  BootstrapStatic as BootstrapStaticImport,
12
13
  } from "./Bootstrap.js";
13
- import { ClientOnly as ClientOnlyImport } from "./ClientOnly.js";
14
- import { Layout as LayoutImport } from "./Layout.js";
15
- import { Markdown as MarkdownImport } from "./Markdown.js";
16
- import { Spinner as SpinnerImport } from "./Spinner.js";
17
- import { Zudoku as ZudokuImport } from "./Zudoku.js";
18
14
  import {
19
15
  CACHE_KEYS as CACHE_KEYS_IMPORT,
20
16
  useCache as useCacheImport,
21
17
  } from "./cache.js";
18
+ import { ClientOnly as ClientOnlyImport } from "./ClientOnly.js";
22
19
  import { useZudoku as useZudokuImport } from "./context/ZudokuContext.js";
20
+ import { Layout as LayoutImport } from "./Layout.js";
21
+ import { Markdown as MarkdownImport } from "./Markdown.js";
22
+ import { Spinner as SpinnerImport } from "./Spinner.js";
23
+ import { Zudoku as ZudokuImport } from "./Zudoku.js";
23
24
 
24
25
  export const useMDXComponents = /*@__PURE__*/ useMDXComponentsImport;
25
26
  export const Layout = /*@__PURE__*/ LayoutImport;
@@ -27,6 +28,7 @@ export const RouterError = /*@__PURE__*/ RouterErrorImport;
27
28
  export const ServerError = /*@__PURE__*/ ServerErrorImport;
28
29
  export const Bootstrap = /*@__PURE__*/ BootstrapImport;
29
30
  export const BootstrapStatic = /*@__PURE__*/ BootstrapStaticImport;
31
+ export const RouteGuard = /*@__PURE__*/ RouteGuardImport;
30
32
 
31
33
  export const Head = /*@__PURE__*/ Helmet;
32
34
 
@@ -53,6 +53,22 @@ export const SidebarCategory = ({
53
53
  </button>
54
54
  );
55
55
 
56
+ const icon = category.icon && (
57
+ <category.icon
58
+ size={16}
59
+ className={cn("align-[-0.125em] ", isActive && "text-primary")}
60
+ />
61
+ );
62
+
63
+ const styles = navigationListItem({
64
+ className: [
65
+ "text-start font-medium",
66
+ isCollapsible || typeof category.link !== "undefined"
67
+ ? "cursor-pointer"
68
+ : "cursor-default hover:bg-transparent",
69
+ ],
70
+ });
71
+
56
72
  return (
57
73
  <Collapsible.Root
58
74
  className="flex flex-col"
@@ -61,52 +77,38 @@ export const SidebarCategory = ({
61
77
  onOpenChange={() => setOpen(true)}
62
78
  >
63
79
  <Collapsible.Trigger className="group" asChild disabled={!isCollapsible}>
64
- <div
65
- onClick={() => setHasInteracted(true)}
66
- className={navigationListItem({
67
- isActive: false,
68
- className: [
69
- "text-start font-medium",
70
- isCollapsible || typeof category.link !== "undefined"
71
- ? "cursor-pointer"
72
- : "cursor-default hover:bg-transparent",
73
- ],
74
- })}
75
- >
76
- {category.icon && (
77
- <category.icon
78
- size={16}
79
- className={cn("align-[-0.125em] ", isActive && "text-primary")}
80
- />
81
- )}
82
- {category.link?.type === "doc" ? (
83
- <NavLink
84
- to={joinPath(category.link.id)}
85
- className="flex-1 truncate"
86
- onClick={() => {
87
- // if it is the current path and closed then open it because there's no path change to trigger the open
88
- if (isActive && !open) {
89
- setOpen(true);
90
- }
91
- }}
80
+ {category.link?.type === "doc" ? (
81
+ <NavLink
82
+ to={joinPath(category.link.id)}
83
+ className={styles}
84
+ onClick={() => {
85
+ setHasInteracted(true);
86
+ // if it is the current path and closed then open it because there's no path change to trigger the open
87
+ if (isActive && !open) {
88
+ setOpen(true);
89
+ }
90
+ }}
91
+ >
92
+ {icon}
93
+ <div
94
+ className={cn(
95
+ "flex items-center gap-2 justify-between w-full",
96
+ isActive ? "text-primary" : "text-foreground/80",
97
+ )}
92
98
  >
93
- <div
94
- className={cn(
95
- "flex items-center gap-2 justify-between w-full",
96
- isActive ? "text-primary" : "text-foreground/80",
97
- )}
98
- >
99
- <div className="truncate">{category.label}</div>
100
- {ToggleButton}
101
- </div>
102
- </NavLink>
103
- ) : (
99
+ <div className="truncate">{category.label}</div>
100
+ {ToggleButton}
101
+ </div>
102
+ </NavLink>
103
+ ) : (
104
+ <div onClick={() => setHasInteracted(true)} className={styles}>
105
+ {icon}
104
106
  <div className="flex items-center justify-between w-full">
105
107
  <div className="flex gap-2 truncate w-full">{category.label}</div>
106
108
  {ToggleButton}
107
109
  </div>
108
- )}
109
- </div>
110
+ </div>
111
+ )}
110
112
  </Collapsible.Trigger>
111
113
  <Collapsible.Content
112
114
  className={cn(
@@ -1,25 +1,46 @@
1
1
  import { GraphQLJSON } from "graphql-type-json";
2
2
  import { GraphQLScalarType } from "graphql/index.js";
3
+ import { RecordAny } from "../../util/traverse.js";
3
4
 
4
5
  export const CIRCULAR_REF = "$[Circular Reference]";
5
- const handleCircularRefs = (obj: any, visited = new WeakSet()): any => {
6
+
7
+ const handleCircularRefs = (
8
+ obj: any,
9
+ visited = new Map<string, string[]>(),
10
+ path: string[] = [],
11
+ ): any => {
6
12
  if (obj === CIRCULAR_REF) return CIRCULAR_REF;
7
13
  if (obj === null || typeof obj !== "object") return obj;
8
14
 
9
- if (visited.has(obj)) return CIRCULAR_REF;
15
+ const currentPath = path.join(".");
16
+
17
+ if (obj.type === "object" && obj.properties) {
18
+ const schemaKey = Object.keys(obj.properties).sort().join("-");
10
19
 
11
- visited.add(obj);
20
+ if (visited.has(schemaKey)) {
21
+ const prevPaths = visited.get(schemaKey)!;
22
+ if (prevPaths.some((prev) => currentPath.startsWith(prev))) {
23
+ return CIRCULAR_REF;
24
+ }
25
+ visited.set(schemaKey, [...prevPaths, currentPath]);
26
+ } else {
27
+ visited.set(schemaKey, [currentPath]);
28
+ }
29
+ }
12
30
 
13
31
  if (Array.isArray(obj)) {
14
- return obj.map((item) => handleCircularRefs(item, visited));
32
+ return obj.map((item, index) =>
33
+ handleCircularRefs(item, visited, [...path, `${index}`]),
34
+ );
15
35
  }
16
36
 
17
- const result: Record<string, any> = {};
37
+ const result: RecordAny = {};
18
38
  for (const [key, value] of Object.entries(obj)) {
19
- result[key] = handleCircularRefs(value, visited);
39
+ result[key] = handleCircularRefs(value, visited, [...path, key]);
20
40
  }
21
41
  return result;
22
42
  };
43
+
23
44
  export const GraphQLJSONSchema = new GraphQLScalarType({
24
45
  ...GraphQLJSON,
25
46
  name: "JSONSchema",
@@ -89,10 +89,10 @@ export const ColorizedParam = ({
89
89
  <span
90
90
  {...{ [DATA_ATTR]: normalizedSlug }}
91
91
  className={cn(
92
- "relative inline-block rounded transition-all duration-100",
93
- "rounded-lg",
92
+ // This may not contain (inline-)flex or (inline-)block otherwise it breaks the browser's full text search
93
+ "relative rounded transition-all duration-100 rounded-lg",
94
94
  "border border-[--border-color] p-0.5 text-[--param-color] bg-[--background-color]",
95
- "data-[active=true]:border-[--param-color] data-[active=true]:shadow data-[active=true]:-translate-y-px",
95
+ "data-[active=true]:border-[--param-color] data-[active=true]:shadow data-[active=true]:bottom-px",
96
96
  className,
97
97
  )}
98
98
  title={title}
@@ -37,7 +37,12 @@ export const OperationListItem = ({
37
37
  className="grid grid-cols-1 lg:grid-cols-[minmax(0,4fr)_minmax(0,3fr)] gap-8 items-start border-b-2 mb-16 pb-16"
38
38
  >
39
39
  <div className="flex flex-col gap-4">
40
- <Heading level={2} id={operation.slug} registerSidebarAnchor>
40
+ <Heading
41
+ level={2}
42
+ id={operation.slug}
43
+ registerSidebarAnchor
44
+ className="break-all"
45
+ >
41
46
  {operation.summary}
42
47
  </Heading>
43
48
  <div className="text-sm flex gap-2 font-mono">
@@ -40,7 +40,7 @@ export const ParameterListItem = ({
40
40
  name={parameter.name}
41
41
  backgroundOpacity="15%"
42
42
  className="px-1"
43
- slug={id + "-" + parameter.name.toLocaleLowerCase()}
43
+ slug={`${id}-${parameter.name}`}
44
44
  />
45
45
  ) : (
46
46
  parameter.name
@@ -1,8 +1,9 @@
1
1
  import { useSuspenseQuery } from "@tanstack/react-query";
2
2
  import { HTTPSnippet } from "@zudoku/httpsnippet";
3
- import { Fragment, useMemo, useState, useTransition } from "react";
3
+ import { useMemo, useState, useTransition } from "react";
4
4
  import { useSearchParams } from "react-router";
5
5
  import { useSelectedServerStore } from "../../authentication/state.js";
6
+ import { PathRenderer } from "../../components/PathRenderer.js";
6
7
  import { SyntaxHighlight } from "../../components/SyntaxHighlight.js";
7
8
  import type { SchemaObject } from "../../oas/parser/index.js";
8
9
  import { cn } from "../../util/cn.js";
@@ -114,31 +115,21 @@ export const Sidecar = ({
114
115
 
115
116
  const requestBodyContent = operation.requestBody?.content;
116
117
 
117
- const path = operation.path.split("/").map((part, i, arr) => {
118
- const isParam =
119
- (part.startsWith("{") && part.endsWith("}")) || part.startsWith(":");
120
- const paramName = isParam ? part.replace(/[:{}]/g, "") : undefined;
121
-
122
- return (
123
- // eslint-disable-next-line react/no-array-index-key
124
- <Fragment key={part + i}>
125
- {paramName ? (
126
- <ColorizedParam
127
- name={paramName}
128
- backgroundOpacity="0"
129
- // same as in `ParameterListItem`
130
- slug={`${operation.slug}-${paramName.toLocaleLowerCase()}`}
131
- >
132
- {part}
133
- </ColorizedParam>
134
- ) : (
135
- part
136
- )}
137
- {i < arr.length - 1 ? "/" : null}
138
- <wbr />
139
- </Fragment>
140
- );
141
- });
118
+ const path = (
119
+ <PathRenderer
120
+ path={operation.path}
121
+ renderParam={({ name }) => (
122
+ <ColorizedParam
123
+ name={name}
124
+ backgroundOpacity="0"
125
+ // same as in `ParameterListItem`
126
+ slug={`${operation.slug}-${name}`}
127
+ >
128
+ {`{${name}}`}
129
+ </ColorizedParam>
130
+ )}
131
+ />
132
+ );
142
133
 
143
134
  const { selectedServer } = useSelectedServerStore();
144
135
 
@@ -22,7 +22,7 @@ export const PathParams = ({
22
22
  <>
23
23
  <Controller
24
24
  control={control}
25
- name={`pathParams.${i}.value`}
25
+ name={`pathParams.${i}.name`}
26
26
  render={() => (
27
27
  <div>
28
28
  <ColorizedParam
@@ -3,6 +3,7 @@ import { InfoIcon } from "lucide-react";
3
3
  import { Fragment, useEffect, useRef, useTransition } from "react";
4
4
  import { FormProvider, useForm } from "react-hook-form";
5
5
  import { Alert, AlertDescription, AlertTitle } from "zudoku/ui/Alert.js";
6
+ import { PathRenderer } from "../../../components/PathRenderer.js";
6
7
 
7
8
  import { Label } from "zudoku/ui/Label.js";
8
9
  import { RadioGroup, RadioGroupItem } from "zudoku/ui/RadioGroup.js";
@@ -222,36 +223,27 @@ export const Playground = ({
222
223
  },
223
224
  });
224
225
 
225
- const path = url.split("/").map((part, i, arr) => {
226
- const isPathParam =
227
- (part.startsWith("{") && part.endsWith("}")) || part.startsWith(":");
228
- const replaced = part.replace(/[:{}]/g, "");
229
- const value = formState.pathParams.find((p) => p.name === replaced)?.value;
226
+ const path = (
227
+ <PathRenderer
228
+ path={url}
229
+ renderParam={({ name, originalValue, index }) => {
230
+ const formValue = formState.pathParams.find(
231
+ (param) => param.name === name,
232
+ )?.value;
230
233
 
231
- const pathParamValue = (
232
- <ColorizedParam
233
- backgroundOpacity="25%"
234
- name={part}
235
- slug={part}
236
- title={
237
- !value
238
- ? `Missing value for path parameter \`${replaced}\``
239
- : undefined
240
- }
241
- >
242
- {value ? encodeURIComponent(value) : part}
243
- </ColorizedParam>
244
- );
245
-
246
- return (
247
- // eslint-disable-next-line react/no-array-index-key
248
- <Fragment key={part + i}>
249
- {isPathParam ? pathParamValue : part}
250
- {i < arr.length - 1 && "/"}
251
- <wbr />
252
- </Fragment>
253
- );
254
- });
234
+ return (
235
+ <ColorizedParam
236
+ name={name}
237
+ backgroundOpacity="0"
238
+ slug={name}
239
+ onClick={() => form.setFocus(`pathParams.${index}.value`)}
240
+ >
241
+ {formValue || originalValue}
242
+ </ColorizedParam>
243
+ );
244
+ }}
245
+ />
246
+ );
255
247
 
256
248
  const urlQueryParams = formState.queryParams
257
249
  .filter((p) => p.active)
@@ -268,9 +260,7 @@ export const Playground = ({
268
260
  {servers && servers.length > 1 ? (
269
261
  <Select
270
262
  onValueChange={(value) => {
271
- startTransition(() => {
272
- setSelectedServer(value);
273
- });
263
+ startTransition(() => setSelectedServer(value));
274
264
  }}
275
265
  value={selectedServer}
276
266
  defaultValue={selectedServer}
@@ -307,7 +297,7 @@ export const Playground = ({
307
297
  <div className="border-r p-2 bg-muted rounded-l-md self-stretch font-semibold font-mono flex items-center">
308
298
  {method.toUpperCase()}
309
299
  </div>
310
- <div className="flex items-center flex-wrap p-2 font-mono text-xs break-all">
300
+ <div className="items-center p-2 font-mono text-xs break-words">
311
301
  {serverSelect}
312
302
  {path}
313
303
  {urlQueryParams.length > 0 ? "?" : ""}
@@ -3,7 +3,6 @@ import { ListPlusIcon, RefreshCcwDotIcon } from "lucide-react";
3
3
  import { useCallback, useState } from "react";
4
4
  import { Badge } from "zudoku/ui/Badge.js";
5
5
  import { Markdown, ProseClasses } from "../../../components/Markdown.js";
6
- import { CIRCULAR_REF } from "../../../oas/graphql/circular.js";
7
6
  import type { SchemaObject } from "../../../oas/parser/index.js";
8
7
  import { Button } from "../../../ui/Button.js";
9
8
  import { cn } from "../../../util/cn.js";
@@ -12,6 +11,7 @@ import { LogicalGroup } from "./LogicalGroup/LogicalGroup.js";
12
11
  import { SchemaView } from "./SchemaView.js";
13
12
  import {
14
13
  hasLogicalGroupings,
14
+ isCircularRef,
15
15
  isComplexType,
16
16
  LogicalSchemaTypeMap,
17
17
  } from "./utils.js";
@@ -41,13 +41,10 @@ export const SchemaLogicalGroup = ({
41
41
  }
42
42
  };
43
43
 
44
- const isCircularRef = (schema: unknown): schema is string =>
45
- schema === CIRCULAR_REF;
46
-
47
44
  const RecursiveIndicator = () => (
48
45
  <div className="flex items-center gap-2 italic text-sm text-muted-foreground font-mono bg-muted px-2 py-0.5 rounded-md">
49
46
  <RefreshCcwDotIcon size={16} />
50
- <span>recursive</span>
47
+ <span>circular</span>
51
48
  </div>
52
49
  );
53
50
 
@@ -74,6 +71,8 @@ export const SchemaPropertyItem = ({
74
71
  <div className="flex flex-col gap-1 justify-between text-sm">
75
72
  <div className="flex gap-2 items-center">
76
73
  <code>{name}</code>
74
+ <Badge variant="muted">object</Badge>
75
+ {group === "optional" && <Badge variant="outline">optional</Badge>}
77
76
  <RecursiveIndicator />
78
77
  </div>
79
78
  </div>
@@ -96,6 +95,9 @@ export const SchemaPropertyItem = ({
96
95
  )}
97
96
  </Badge>
98
97
  {group === "optional" && <Badge variant="outline">optional</Badge>}
98
+ {schema.type === "array" &&
99
+ "items" in schema &&
100
+ isCircularRef(schema.items) && <RecursiveIndicator />}
99
101
  </div>
100
102
 
101
103
  {schema.description && (
@@ -133,7 +135,9 @@ export const SchemaPropertyItem = ({
133
135
  <SchemaView schema={schema} level={level + 1} />
134
136
  ) : (
135
137
  schema.type === "array" &&
136
- typeof schema.items === "object" && (
138
+ "items" in schema &&
139
+ typeof schema.items === "object" &&
140
+ !isCircularRef(schema.items) && (
137
141
  <SchemaView schema={schema.items} level={level + 1} />
138
142
  )
139
143
  )}
@@ -3,7 +3,10 @@ import type { SchemaObject } from "../../../oas/parser/index.js";
3
3
  import { Card, CardContent, CardHeader, CardTitle } from "../../../ui/Card.js";
4
4
  import { cn } from "../../../util/cn.js";
5
5
  import { groupBy } from "../../../util/groupBy.js";
6
- import { SchemaLogicalGroup, SchemaPropertyItem } from "./SchemaComponents.js";
6
+ import {
7
+ SchemaLogicalGroup,
8
+ SchemaPropertyItem,
9
+ } from "./SchemaPropertyItem.js";
7
10
  import { hasLogicalGroupings } from "./utils.js";
8
11
 
9
12
  export const SchemaView = ({
@@ -1,3 +1,4 @@
1
+ import { CIRCULAR_REF } from "../../../oas/graphql/circular.js";
1
2
  import type { SchemaObject } from "../../../oas/parser/index.js";
2
3
 
3
4
  export const isComplexType = (value: SchemaObject) =>
@@ -16,3 +17,6 @@ export const LogicalSchemaTypeMap = {
16
17
  } as const;
17
18
 
18
19
  export type LogicalGroupType = "AND" | "OR" | "ONE";
20
+
21
+ export const isCircularRef = (schema: unknown): schema is string =>
22
+ schema === CIRCULAR_REF;
@@ -1 +0,0 @@
1
- {"version":3,"file":"SchemaComponents.js","sourceRoot":"","sources":["../../../../../src/lib/plugins/openapi/schema/SchemaComponents.tsx"],"names":[],"mappings":"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"}