zudoku 0.33.1 → 0.34.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.
Files changed (244) hide show
  1. package/dist/config/validators/common.d.ts +572 -354
  2. package/dist/config/validators/common.js +26 -8
  3. package/dist/config/validators/common.js.map +1 -1
  4. package/dist/config/validators/validate.d.ts +254 -167
  5. package/dist/lib/authentication/hook.d.ts +1 -0
  6. package/dist/lib/authentication/hook.js +11 -1
  7. package/dist/lib/authentication/hook.js.map +1 -1
  8. package/dist/lib/authentication/providers/clerk.js +6 -6
  9. package/dist/lib/authentication/providers/clerk.js.map +1 -1
  10. package/dist/lib/components/AnchorLink.d.ts +2 -2
  11. package/dist/lib/components/AnchorLink.js +4 -4
  12. package/dist/lib/components/AnchorLink.js.map +1 -1
  13. package/dist/lib/components/Banner.js +1 -1
  14. package/dist/lib/components/Banner.js.map +1 -1
  15. package/dist/lib/components/Heading.d.ts +2 -2
  16. package/dist/lib/components/Layout.js +1 -1
  17. package/dist/lib/components/Layout.js.map +1 -1
  18. package/dist/lib/components/context/ZudokuContext.d.ts +1 -1
  19. package/dist/lib/components/index.d.ts +1 -0
  20. package/dist/lib/components/navigation/SidebarItem.js +6 -5
  21. package/dist/lib/components/navigation/SidebarItem.js.map +1 -1
  22. package/dist/lib/core/RouteGuard.js +2 -1
  23. package/dist/lib/core/RouteGuard.js.map +1 -1
  24. package/dist/lib/core/ZudokuContext.d.ts +4 -0
  25. package/dist/lib/core/ZudokuContext.js.map +1 -1
  26. package/dist/lib/plugins/api-catalog/Catalog.d.ts +3 -1
  27. package/dist/lib/plugins/api-catalog/Catalog.js +7 -4
  28. package/dist/lib/plugins/api-catalog/Catalog.js.map +1 -1
  29. package/dist/lib/plugins/api-catalog/index.js +1 -1
  30. package/dist/lib/plugins/api-catalog/index.js.map +1 -1
  31. package/dist/lib/plugins/markdown/MdxPage.js +1 -1
  32. package/dist/lib/plugins/markdown/MdxPage.js.map +1 -1
  33. package/dist/lib/plugins/openapi/OperationList.d.ts +1 -1
  34. package/dist/lib/plugins/openapi/OperationList.js +5 -1
  35. package/dist/lib/plugins/openapi/OperationList.js.map +1 -1
  36. package/dist/lib/plugins/openapi/OperationListItem.d.ts +1 -1
  37. package/dist/lib/plugins/openapi/OperationListItem.js +6 -3
  38. package/dist/lib/plugins/openapi/OperationListItem.js.map +1 -1
  39. package/dist/lib/plugins/openapi/ParameterList.d.ts +2 -1
  40. package/dist/lib/plugins/openapi/ParameterList.js +3 -2
  41. package/dist/lib/plugins/openapi/ParameterList.js.map +1 -1
  42. package/dist/lib/plugins/openapi/PlaygroundDialogWrapper.js +3 -1
  43. package/dist/lib/plugins/openapi/PlaygroundDialogWrapper.js.map +1 -1
  44. package/dist/lib/plugins/openapi/Sidecar.js +1 -1
  45. package/dist/lib/plugins/openapi/Sidecar.js.map +1 -1
  46. package/dist/lib/plugins/openapi/graphql/gql.d.ts +1 -1
  47. package/dist/lib/plugins/openapi/graphql/gql.js +1 -1
  48. package/dist/lib/plugins/openapi/graphql/gql.js.map +1 -1
  49. package/dist/lib/plugins/openapi/graphql/graphql.d.ts +1 -0
  50. package/dist/lib/plugins/openapi/graphql/graphql.js +2 -0
  51. package/dist/lib/plugins/openapi/graphql/graphql.js.map +1 -1
  52. package/dist/lib/plugins/openapi/playground/ExamplesDropdown.d.ts +2 -2
  53. package/dist/lib/plugins/openapi/playground/ExamplesDropdown.js +1 -5
  54. package/dist/lib/plugins/openapi/playground/ExamplesDropdown.js.map +1 -1
  55. package/dist/lib/plugins/openapi/playground/Headers.js +1 -1
  56. package/dist/lib/plugins/openapi/playground/Headers.js.map +1 -1
  57. package/dist/lib/plugins/openapi/playground/IdentityDialog.d.ts +11 -0
  58. package/dist/lib/plugins/openapi/playground/IdentityDialog.js +14 -0
  59. package/dist/lib/plugins/openapi/playground/IdentityDialog.js.map +1 -0
  60. package/dist/lib/plugins/openapi/playground/IdentitySelector.d.ts +7 -0
  61. package/dist/lib/plugins/openapi/playground/IdentitySelector.js +10 -0
  62. package/dist/lib/plugins/openapi/playground/IdentitySelector.js.map +1 -0
  63. package/dist/lib/plugins/openapi/playground/PathParams.d.ts +3 -2
  64. package/dist/lib/plugins/openapi/playground/PathParams.js +3 -2
  65. package/dist/lib/plugins/openapi/playground/PathParams.js.map +1 -1
  66. package/dist/lib/plugins/openapi/playground/Playground.d.ts +13 -2
  67. package/dist/lib/plugins/openapi/playground/Playground.js +80 -26
  68. package/dist/lib/plugins/openapi/playground/Playground.js.map +1 -1
  69. package/dist/lib/plugins/openapi/playground/QueryParams.js +1 -1
  70. package/dist/lib/plugins/openapi/playground/QueryParams.js.map +1 -1
  71. package/dist/lib/plugins/openapi/playground/RequestLoginDialog.d.ts +7 -0
  72. package/dist/lib/plugins/openapi/playground/RequestLoginDialog.js +8 -0
  73. package/dist/lib/plugins/openapi/playground/RequestLoginDialog.js.map +1 -0
  74. package/dist/lib/plugins/openapi/playground/rememberedIdentity.d.ts +17 -0
  75. package/dist/lib/plugins/openapi/playground/rememberedIdentity.js +11 -0
  76. package/dist/lib/plugins/openapi/playground/rememberedIdentity.js.map +1 -0
  77. package/dist/lib/plugins/openapi/playground/result-panel/ResponseTab.js +19 -13
  78. package/dist/lib/plugins/openapi/playground/result-panel/ResponseTab.js.map +1 -1
  79. package/dist/lib/plugins/openapi/playground/result-panel/ResultPanel.d.ts +6 -4
  80. package/dist/lib/plugins/openapi/playground/result-panel/ResultPanel.js +4 -3
  81. package/dist/lib/plugins/openapi/playground/result-panel/ResultPanel.js.map +1 -1
  82. package/dist/lib/plugins/search-pagefind/PagefindSearch.d.ts +6 -0
  83. package/dist/lib/plugins/search-pagefind/PagefindSearch.js +66 -0
  84. package/dist/lib/plugins/search-pagefind/PagefindSearch.js.map +1 -0
  85. package/dist/lib/plugins/search-pagefind/ResultList.d.ts +8 -0
  86. package/dist/lib/plugins/search-pagefind/ResultList.js +31 -0
  87. package/dist/lib/plugins/search-pagefind/ResultList.js.map +1 -0
  88. package/dist/lib/plugins/search-pagefind/get-results.d.ts +3 -0
  89. package/dist/lib/plugins/search-pagefind/get-results.js +37 -0
  90. package/dist/lib/plugins/search-pagefind/get-results.js.map +1 -0
  91. package/dist/lib/plugins/search-pagefind/index.d.ts +8 -0
  92. package/dist/lib/plugins/search-pagefind/index.js +9 -0
  93. package/dist/lib/plugins/search-pagefind/index.js.map +1 -0
  94. package/dist/lib/plugins/search-pagefind/types.d.ts +85 -0
  95. package/dist/lib/plugins/search-pagefind/types.js +2 -0
  96. package/dist/lib/plugins/search-pagefind/types.js.map +1 -0
  97. package/dist/lib/ui/Checkbox.d.ts +2 -8
  98. package/dist/lib/ui/Checkbox.js +1 -13
  99. package/dist/lib/ui/Checkbox.js.map +1 -1
  100. package/dist/lib/ui/Command.d.ts +13 -7
  101. package/dist/lib/ui/Command.js +2 -2
  102. package/dist/lib/ui/Command.js.map +1 -1
  103. package/dist/lib/ui/Select.js +1 -1
  104. package/dist/lib/ui/Select.js.map +1 -1
  105. package/dist/lib/ui/SyntaxHighlight.d.ts +2 -1
  106. package/dist/lib/ui/SyntaxHighlight.js +19 -15
  107. package/dist/lib/ui/SyntaxHighlight.js.map +1 -1
  108. package/dist/lib/util/MdxComponents.d.ts +1 -1
  109. package/dist/lib/util/MdxComponents.js +2 -2
  110. package/dist/lib/util/MdxComponents.js.map +1 -1
  111. package/dist/lib/util/useScrollToAnchor.js +6 -8
  112. package/dist/lib/util/useScrollToAnchor.js.map +1 -1
  113. package/dist/vite/build.js +4 -0
  114. package/dist/vite/build.js.map +1 -1
  115. package/dist/vite/config.js +7 -2
  116. package/dist/vite/config.js.map +1 -1
  117. package/dist/vite/dev-server.js +8 -0
  118. package/dist/vite/dev-server.js.map +1 -1
  119. package/dist/vite/pagefind.d.ts +4 -0
  120. package/dist/vite/pagefind.js +15 -0
  121. package/dist/vite/pagefind.js.map +1 -0
  122. package/dist/vite/plugin-component.js +4 -0
  123. package/dist/vite/plugin-component.js.map +1 -1
  124. package/dist/vite/plugin-search.js +4 -0
  125. package/dist/vite/plugin-search.js.map +1 -1
  126. package/dist/vite/prerender/prerender.js +1 -1
  127. package/dist/vite/prerender/prerender.js.map +1 -1
  128. package/dist/vite/sitemap.js +2 -1
  129. package/dist/vite/sitemap.js.map +1 -1
  130. package/lib/{AuthenticationPlugin-CiO1FM6Q.js → AuthenticationPlugin-4ip08maU.js} +3 -3
  131. package/lib/{AuthenticationPlugin-CiO1FM6Q.js.map → AuthenticationPlugin-4ip08maU.js.map} +1 -1
  132. package/lib/Callout-B_sEhkYd.js +211 -0
  133. package/lib/Callout-B_sEhkYd.js.map +1 -0
  134. package/lib/{Dialog-DIKGQxQc.js → Dialog-sbgekbjb.js} +47 -32
  135. package/lib/{Dialog-DIKGQxQc.js.map → Dialog-sbgekbjb.js.map} +1 -1
  136. package/lib/{Markdown-DePfm7oQ.js → Markdown-DZXjQjpH.js} +4099 -3848
  137. package/lib/Markdown-DZXjQjpH.js.map +1 -0
  138. package/lib/MdxPage-52vRwa_7.js +200 -0
  139. package/lib/MdxPage-52vRwa_7.js.map +1 -0
  140. package/lib/{OasProvider-SzD9mHJc.js → OasProvider-CR2nG1Eg.js} +4 -4
  141. package/lib/{OasProvider-SzD9mHJc.js.map → OasProvider-CR2nG1Eg.js.map} +1 -1
  142. package/lib/{OperationList-DDs9NblY.js → OperationList-DndcCJUG.js} +2069 -1983
  143. package/lib/OperationList-DndcCJUG.js.map +1 -0
  144. package/lib/{Select-Dqtcn53H.js → Select-FAYHOYTy.js} +4 -4
  145. package/lib/{Select-Dqtcn53H.js.map → Select-FAYHOYTy.js.map} +1 -1
  146. package/lib/{SlotletProvider-DdtIOUi6.js → SlotletProvider-TydSHROc.js} +4 -4
  147. package/lib/{SlotletProvider-DdtIOUi6.js.map → SlotletProvider-TydSHROc.js.map} +1 -1
  148. package/lib/{chunk-IR6S3I6Y-D_3UmFIn.js → chunk-HA7DTUK3-ZGg2W6yV.js} +277 -277
  149. package/lib/chunk-HA7DTUK3-ZGg2W6yV.js.map +1 -0
  150. package/lib/hook-CfCFKZ-2.js +350 -0
  151. package/lib/hook-CfCFKZ-2.js.map +1 -0
  152. package/lib/index-DK7IuUyR.js +2201 -0
  153. package/lib/index-DK7IuUyR.js.map +1 -0
  154. package/lib/{index.esm-CQHE3GEU.js → index.esm-CltAN0Tf.js} +259 -239
  155. package/lib/index.esm-CltAN0Tf.js.map +1 -0
  156. package/lib/{mutation-EclmI0is.js → mutation-B81DztCT.js} +2 -2
  157. package/lib/{mutation-EclmI0is.js.map → mutation-B81DztCT.js.map} +1 -1
  158. package/lib/objectEntries-BS7aAgOm.js +12 -0
  159. package/lib/objectEntries-BS7aAgOm.js.map +1 -0
  160. package/lib/ui/Checkbox.js +15 -25
  161. package/lib/ui/Checkbox.js.map +1 -1
  162. package/lib/ui/Command.js +96 -70
  163. package/lib/ui/Command.js.map +1 -1
  164. package/lib/ui/Select.js +1 -1
  165. package/lib/ui/Select.js.map +1 -1
  166. package/lib/ui/SyntaxHighlight.js +483 -502
  167. package/lib/ui/SyntaxHighlight.js.map +1 -1
  168. package/lib/{useExposedProps-RIvey2Oy.js → useExposedProps-BslIn-FE.js} +2 -2
  169. package/lib/{useExposedProps-RIvey2Oy.js.map → useExposedProps-BslIn-FE.js.map} +1 -1
  170. package/lib/useQuery-CQUwWR9i.js +1137 -0
  171. package/lib/useQuery-CQUwWR9i.js.map +1 -0
  172. package/lib/zudoku.auth-auth0.js +1 -1
  173. package/lib/zudoku.auth-clerk.js +29 -29
  174. package/lib/zudoku.auth-clerk.js.map +1 -1
  175. package/lib/zudoku.auth-openid.js +3 -3
  176. package/lib/zudoku.components.js +146 -149
  177. package/lib/zudoku.components.js.map +1 -1
  178. package/lib/zudoku.hooks.js +1 -1
  179. package/lib/zudoku.plugin-api-catalog.js +87 -71
  180. package/lib/zudoku.plugin-api-catalog.js.map +1 -1
  181. package/lib/zudoku.plugin-api-keys.js +16 -15
  182. package/lib/zudoku.plugin-api-keys.js.map +1 -1
  183. package/lib/zudoku.plugin-custom-pages.js +2 -2
  184. package/lib/zudoku.plugin-markdown.js +1 -1
  185. package/lib/zudoku.plugin-openapi.js +3 -3
  186. package/lib/zudoku.plugin-redirect.js +1 -1
  187. package/lib/zudoku.plugin-search-pagefind.js +204 -0
  188. package/lib/zudoku.plugin-search-pagefind.js.map +1 -0
  189. package/package.json +10 -5
  190. package/src/lib/authentication/hook.ts +12 -1
  191. package/src/lib/authentication/providers/clerk.tsx +10 -6
  192. package/src/lib/components/AnchorLink.tsx +7 -7
  193. package/src/lib/components/Banner.tsx +1 -0
  194. package/src/lib/components/Heading.tsx +1 -1
  195. package/src/lib/components/Layout.tsx +1 -0
  196. package/src/lib/components/navigation/SidebarItem.tsx +8 -23
  197. package/src/lib/core/RouteGuard.tsx +2 -1
  198. package/src/lib/core/ZudokuContext.ts +4 -0
  199. package/src/lib/plugins/api-catalog/Catalog.tsx +23 -7
  200. package/src/lib/plugins/api-catalog/index.tsx +1 -0
  201. package/src/lib/plugins/markdown/MdxPage.tsx +5 -1
  202. package/src/lib/plugins/openapi/OperationList.tsx +83 -31
  203. package/src/lib/plugins/openapi/OperationListItem.tsx +107 -86
  204. package/src/lib/plugins/openapi/ParameterList.tsx +4 -0
  205. package/src/lib/plugins/openapi/PlaygroundDialogWrapper.tsx +7 -0
  206. package/src/lib/plugins/openapi/Sidecar.tsx +1 -0
  207. package/src/lib/plugins/openapi/graphql/gql.ts +3 -3
  208. package/src/lib/plugins/openapi/graphql/graphql.ts +3 -0
  209. package/src/lib/plugins/openapi/playground/ExamplesDropdown.tsx +30 -32
  210. package/src/lib/plugins/openapi/playground/Headers.tsx +0 -1
  211. package/src/lib/plugins/openapi/playground/IdentityDialog.tsx +74 -0
  212. package/src/lib/plugins/openapi/playground/IdentitySelector.tsx +54 -0
  213. package/src/lib/plugins/openapi/playground/PathParams.tsx +8 -2
  214. package/src/lib/plugins/openapi/playground/Playground.tsx +175 -88
  215. package/src/lib/plugins/openapi/playground/QueryParams.tsx +0 -1
  216. package/src/lib/plugins/openapi/playground/RequestLoginDialog.tsx +51 -0
  217. package/src/lib/plugins/openapi/playground/rememberedIdentity.ts +26 -0
  218. package/src/lib/plugins/openapi/playground/result-panel/ResponseTab.tsx +24 -4
  219. package/src/lib/plugins/openapi/playground/result-panel/ResultPanel.tsx +66 -45
  220. package/src/lib/plugins/search-pagefind/PagefindSearch.tsx +135 -0
  221. package/src/lib/plugins/search-pagefind/ResultList.tsx +104 -0
  222. package/src/lib/plugins/search-pagefind/get-results.tsx +54 -0
  223. package/src/lib/plugins/search-pagefind/index.tsx +21 -0
  224. package/src/lib/plugins/search-pagefind/types.ts +118 -0
  225. package/src/lib/ui/Checkbox.tsx +8 -24
  226. package/src/lib/ui/Command.tsx +25 -3
  227. package/src/lib/ui/Select.tsx +1 -1
  228. package/src/lib/ui/SyntaxHighlight.tsx +94 -96
  229. package/src/lib/util/MdxComponents.tsx +2 -2
  230. package/src/lib/util/useScrollToAnchor.ts +8 -8
  231. package/lib/Markdown-DePfm7oQ.js.map +0 -1
  232. package/lib/MdxPage-DZTt9ld7.js +0 -193
  233. package/lib/MdxPage-DZTt9ld7.js.map +0 -1
  234. package/lib/OperationList-DDs9NblY.js.map +0 -1
  235. package/lib/chunk-IR6S3I6Y-D_3UmFIn.js.map +0 -1
  236. package/lib/hook-CN__aZIt.js +0 -1464
  237. package/lib/hook-CN__aZIt.js.map +0 -1
  238. package/lib/index-CibzSNks.js +0 -2100
  239. package/lib/index-CibzSNks.js.map +0 -1
  240. package/lib/index.esm-CQHE3GEU.js.map +0 -1
  241. package/lib/objectEntries-yMIkr2mI.js +0 -5
  242. package/lib/objectEntries-yMIkr2mI.js.map +0 -1
  243. package/lib/useScrollToAnchor-C7ilRSts.js +0 -290
  244. package/lib/useScrollToAnchor-C7ilRSts.js.map +0 -1
@@ -0,0 +1,54 @@
1
+ import { Card } from "zudoku/ui/Card.js";
2
+ import { Label } from "zudoku/ui/Label.js";
3
+ import { RadioGroup, RadioGroupItem } from "zudoku/ui/RadioGroup.js";
4
+ import { type ApiIdentity } from "../../../core/ZudokuContext.js";
5
+ import { NO_IDENTITY } from "./Playground.js";
6
+
7
+ const IdentitySelector = ({
8
+ identities,
9
+ setValue,
10
+ value,
11
+ }: {
12
+ identities?: ApiIdentity[];
13
+ setValue: (value: string) => void;
14
+ value?: string;
15
+ }) => {
16
+ return (
17
+ <Card className="w-full overflow-hidden">
18
+ <RadioGroup
19
+ onValueChange={(value) => setValue(value)}
20
+ value={value}
21
+ defaultValue={NO_IDENTITY}
22
+ className="gap-0"
23
+ disabled={identities?.length === 0}
24
+ >
25
+ <Label
26
+ className="h-12 border-b items-center flex p-4 cursor-pointer hover:bg-accent"
27
+ htmlFor="none"
28
+ >
29
+ <RadioGroupItem value={NO_IDENTITY} id="none">
30
+ None
31
+ </RadioGroupItem>
32
+ <Label htmlFor="none" className="ml-2">
33
+ None
34
+ </Label>
35
+ </Label>
36
+ {identities?.map((identity) => (
37
+ <Label
38
+ key={identity.id}
39
+ className="h-12 border-b items-center flex p-4 cursor-pointer hover:bg-accent"
40
+ >
41
+ <RadioGroupItem value={identity.id} id={identity.id}>
42
+ {identity.label}
43
+ </RadioGroupItem>
44
+ <Label htmlFor={identity.id} className="ml-2">
45
+ {identity.label}
46
+ </Label>
47
+ </Label>
48
+ ))}
49
+ </RadioGroup>
50
+ </Card>
51
+ );
52
+ };
53
+
54
+ export default IdentitySelector;
@@ -1,4 +1,4 @@
1
- import { Control, Controller, useFieldArray } from "react-hook-form";
1
+ import { type Control, Controller, useFieldArray } from "react-hook-form";
2
2
  import { Card } from "zudoku/ui/Card.js";
3
3
  import { Input } from "../../../ui/Input.js";
4
4
  import { ColorizedParam } from "../ColorizedParam.js";
@@ -7,18 +7,24 @@ import type { PlaygroundForm } from "./Playground.js";
7
7
 
8
8
  export const PathParams = ({
9
9
  control,
10
+ url,
10
11
  }: {
11
12
  control: Control<PlaygroundForm>;
13
+ url: string;
12
14
  }) => {
13
15
  const { fields } = useFieldArray<PlaygroundForm, "pathParams">({
14
16
  control,
15
17
  name: "pathParams",
16
18
  });
17
19
 
20
+ const sortedFields = [...fields].sort(
21
+ (a, b) => url.indexOf(`{${a.name}}`) - url.indexOf(`{${b.name}}`),
22
+ );
23
+
18
24
  return (
19
25
  <Card className="rounded-lg">
20
26
  <ParamsGrid>
21
- {fields.map((field, i) => (
27
+ {sortedFields.map((field, i) => (
22
28
  <ParamsGridItem key={field.id}>
23
29
  <Controller
24
30
  control={control}
@@ -1,12 +1,10 @@
1
1
  import { useMutation } from "@tanstack/react-query";
2
2
  import { InfoIcon } from "lucide-react";
3
- import { Fragment, useEffect, useRef, useTransition } from "react";
3
+ import { Fragment, useEffect, useRef, useState, useTransition } from "react";
4
4
  import { FormProvider, useForm } from "react-hook-form";
5
5
  import { Alert, AlertDescription, AlertTitle } from "zudoku/ui/Alert.js";
6
6
  import { PathRenderer } from "../../../components/PathRenderer.js";
7
7
 
8
- import { Label } from "zudoku/ui/Label.js";
9
- import { RadioGroup, RadioGroupItem } from "zudoku/ui/RadioGroup.js";
10
8
  import {
11
9
  Select,
12
10
  SelectContent,
@@ -17,16 +15,21 @@ import {
17
15
  import { Textarea } from "zudoku/ui/Textarea.js";
18
16
  import { useSelectedServer } from "../../../authentication/state.js";
19
17
  import { useApiIdentities } from "../../../components/context/ZudokuContext.js";
20
- import { Card } from "../../../ui/Card.js";
21
18
  import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../../ui/Tabs.js";
22
19
  import { cn } from "../../../util/cn.js";
20
+ import { objectEntries } from "../../../util/objectEntries.js";
21
+ import { useLatest } from "../../../util/useLatest.js";
23
22
  import { ColorizedParam } from "../ColorizedParam.js";
24
- import { Content } from "../SidecarExamples.js";
23
+ import { type Content } from "../SidecarExamples.js";
25
24
  import { createUrl } from "./createUrl.js";
26
25
  import ExamplesDropdown from "./ExamplesDropdown.js";
27
26
  import { Headers } from "./Headers.js";
27
+ import { IdentityDialog } from "./IdentityDialog.js";
28
+ import IdentitySelector from "./IdentitySelector.js";
28
29
  import { PathParams } from "./PathParams.js";
29
30
  import { QueryParams } from "./QueryParams.js";
31
+ import { useIdentityStore } from "./rememberedIdentity.js";
32
+ import RequestLoginDialog from "./RequestLoginDialog.js";
30
33
  import { ResultPanel } from "./result-panel/ResultPanel.js";
31
34
  import SubmitButton from "./SubmitButton.js";
32
35
 
@@ -55,8 +58,17 @@ export type PathParam = {
55
58
  isRequired?: boolean;
56
59
  };
57
60
 
61
+ const bodyContentTypeMap = {
62
+ Plain: "text/plain",
63
+ JSON: "application/json",
64
+ XML: "application/xml",
65
+ YAML: "application/yaml",
66
+ CSV: "text/csv",
67
+ } as const;
68
+
58
69
  export type PlaygroundForm = {
59
70
  body: string;
71
+ bodyContentType: keyof typeof bodyContentTypeMap;
60
72
  queryParams: Array<{
61
73
  name: string;
62
74
  value: string;
@@ -97,6 +109,9 @@ export type PlaygroundContentProps = {
97
109
  pathParams?: PathParam[];
98
110
  defaultBody?: string;
99
111
  examples?: Content;
112
+ requiresLogin?: boolean;
113
+ onLogin?: () => void;
114
+ onSignUp?: () => void;
100
115
  };
101
116
 
102
117
  export const Playground = ({
@@ -109,15 +124,27 @@ export const Playground = ({
109
124
  pathParams = [],
110
125
  defaultBody = "",
111
126
  examples,
127
+ requiresLogin = false,
128
+ onLogin,
129
+ onSignUp,
112
130
  }: PlaygroundContentProps) => {
113
131
  const { selectedServer, setSelectedServer } = useSelectedServer(
114
132
  servers.map((url) => ({ url })),
115
133
  );
134
+ const [showSelectIdentity, setShowSelectIdentity] = useState(false);
135
+ const identities = useApiIdentities();
136
+ const { setRememberedIdentity, getRememberedIdentity } = useIdentityStore();
116
137
  const [, startTransition] = useTransition();
138
+ const [skipLogin, setSkipLogin] = useState(false);
139
+ const [showLongRunningWarning, setShowLongRunningWarning] = useState(false);
140
+ const abortControllerRef = useRef<AbortController | undefined>(undefined);
141
+ const latestSetRememberedIdentity = useLatest(setRememberedIdentity);
142
+
117
143
  const { register, control, handleSubmit, watch, setValue, ...form } =
118
144
  useForm<PlaygroundForm>({
119
145
  defaultValues: {
120
146
  body: defaultBody,
147
+ bodyContentType: "JSON",
121
148
  queryParams: queryParams
122
149
  .map((param) => ({
123
150
  name: param.name,
@@ -150,36 +177,42 @@ export const Playground = ({
150
177
  active: false,
151
178
  },
152
179
  ]),
153
- identity: NO_IDENTITY,
180
+ identity: getRememberedIdentity(
181
+ identities.data?.map((i) => i.id) ?? [],
182
+ ),
154
183
  },
155
184
  });
156
185
  const formState = watch();
157
- const identities = useApiIdentities();
186
+ const formRef = useRef<HTMLFormElement>(null);
158
187
 
159
- const setOnce = useRef(false);
160
188
  useEffect(() => {
161
- if (setOnce.current) return;
162
- const firstIdentity = identities.data?.at(0);
163
- if (firstIdentity) {
164
- setValue("identity", firstIdentity.id);
165
- setOnce.current = true;
189
+ if (formState.identity) {
190
+ latestSetRememberedIdentity.current(formState.identity);
166
191
  }
167
- }, [setValue, identities.data]);
168
-
169
- const formRef = useRef<HTMLFormElement>(null);
192
+ }, [latestSetRememberedIdentity, formState.identity]);
170
193
 
171
194
  const queryMutation = useMutation({
172
195
  mutationFn: async (data: PlaygroundForm) => {
173
196
  const start = performance.now();
197
+
198
+ const shouldSetContentType = !data.headers.some(
199
+ (h) => h.active && h.name.toLowerCase() === "content-type",
200
+ );
201
+
202
+ const headers = Object.fromEntries([
203
+ ...data.headers
204
+ .filter((h) => h.name && h.active)
205
+ .map((header) => [header.name, header.value]),
206
+ ...(shouldSetContentType
207
+ ? [["content-type", bodyContentTypeMap[data.bodyContentType]]]
208
+ : []),
209
+ ]);
210
+
174
211
  const request = new Request(
175
212
  createUrl(server ?? selectedServer, url, data),
176
213
  {
177
214
  method: method.toUpperCase(),
178
- headers: Object.fromEntries(
179
- data.headers
180
- .filter((h) => h.name && h.active)
181
- .map((header) => [header.name, header.value]),
182
- ),
215
+ headers,
183
216
  body: data.body ? data.body : undefined,
184
217
  },
185
218
  );
@@ -189,15 +222,23 @@ export const Playground = ({
189
222
  ?.find((i) => i.id === data.identity)
190
223
  ?.authorizeRequest(request);
191
224
  }
225
+
226
+ const warningTimeout = setTimeout(
227
+ () => setShowLongRunningWarning(true),
228
+ 3210,
229
+ );
230
+ abortControllerRef.current = new AbortController();
231
+
192
232
  try {
193
233
  const response = await fetch(request, {
194
- signal: AbortSignal.timeout(5000),
234
+ signal: abortControllerRef.current.signal,
195
235
  });
196
236
 
197
- const time = performance.now() - start;
237
+ clearTimeout(warningTimeout);
238
+ setShowLongRunningWarning(false);
198
239
 
240
+ const time = performance.now() - start;
199
241
  const body = await response.text();
200
-
201
242
  const url = new URL(request.url);
202
243
 
203
244
  return {
@@ -218,6 +259,8 @@ export const Playground = ({
218
259
  },
219
260
  } satisfies PlaygroundResult;
220
261
  } catch (error) {
262
+ clearTimeout(warningTimeout);
263
+ setShowLongRunningWarning(false);
221
264
  if (error instanceof TypeError) {
222
265
  throw new Error(
223
266
  "The request failed, possibly due to network issues or CORS policy.",
@@ -229,6 +272,12 @@ export const Playground = ({
229
272
  },
230
273
  });
231
274
 
275
+ useEffect(() => {
276
+ return () => {
277
+ abortControllerRef.current?.abort();
278
+ };
279
+ }, []);
280
+
232
281
  const path = (
233
282
  <PathRenderer
234
283
  path={url}
@@ -264,7 +313,7 @@ export const Playground = ({
264
313
  const serverSelect = (
265
314
  <div className="inline-block opacity-50 hover:opacity-100 transition">
266
315
  {server ? (
267
- <span>{server.replace(/^https?:\/\//, "")}</span>
316
+ <span>{server.replace(/^https?:\/\//, "").replace(/\/$/, "")}</span>
268
317
  ) : (
269
318
  servers.length > 1 && (
270
319
  <Select
@@ -274,13 +323,13 @@ export const Playground = ({
274
323
  value={selectedServer}
275
324
  defaultValue={selectedServer}
276
325
  >
277
- <SelectTrigger className="p-0 border-none flex-row-reverse bg-transparent text-xs gap-0.5 h-auto">
326
+ <SelectTrigger className="p-0 border-none flex-row-reverse bg-transparent text-xs gap-0.5 h-auto translate-y-[4px]">
278
327
  <SelectValue />
279
328
  </SelectTrigger>
280
329
  <SelectContent>
281
330
  {servers.map((s) => (
282
331
  <SelectItem key={s} value={s}>
283
- {s.replace(/^https?:\/\//, "")}
332
+ {s.replace(/^https?:\/\//, "").replace(/\/$/, "")}
284
333
  </SelectItem>
285
334
  ))}
286
335
  </SelectContent>
@@ -290,14 +339,45 @@ export const Playground = ({
290
339
  </div>
291
340
  );
292
341
 
342
+ const showLogin = requiresLogin && !skipLogin;
343
+ const isBodySupported = ["POST", "PUT", "PATCH", "DELETE"].includes(
344
+ method.toUpperCase(),
345
+ );
346
+
293
347
  return (
294
348
  <FormProvider
295
349
  {...{ register, control, handleSubmit, watch, setValue, ...form }}
296
350
  >
297
351
  <form
298
- onSubmit={handleSubmit((data) => queryMutation.mutateAsync(data))}
352
+ onSubmit={handleSubmit((data) => {
353
+ if (identities.data?.length === 0 || data.identity) {
354
+ queryMutation.mutate(data);
355
+ } else {
356
+ setShowSelectIdentity(true);
357
+ }
358
+ })}
299
359
  ref={formRef}
360
+ className="relative"
300
361
  >
362
+ <IdentityDialog
363
+ identities={identities.data ?? []}
364
+ open={showSelectIdentity}
365
+ onOpenChange={setShowSelectIdentity}
366
+ onSubmit={({ rememberedIdentity, identity }) => {
367
+ if (rememberedIdentity) {
368
+ setValue("identity", identity ?? NO_IDENTITY);
369
+ }
370
+ setShowSelectIdentity(false);
371
+ queryMutation.mutate({ ...formState, identity });
372
+ }}
373
+ />
374
+ <RequestLoginDialog
375
+ open={showLogin}
376
+ setOpen={(open) => setSkipLogin(!open)}
377
+ onSignUp={onSignUp}
378
+ onLogin={onLogin}
379
+ />
380
+
301
381
  <div className="grid grid-cols-2 text-sm h-full">
302
382
  <div className="flex flex-col gap-4 p-4 after:bg-muted-foreground/20 relative after:absolute after:w-px after:inset-0 after:left-auto">
303
383
  <div className="flex gap-2 items-stretch">
@@ -305,7 +385,7 @@ export const Playground = ({
305
385
  <div className="border-r p-2 bg-muted rounded-l-md self-stretch font-semibold font-mono flex items-center">
306
386
  {method.toUpperCase()}
307
387
  </div>
308
- <div className="items-center p-2 font-mono text-xs break-words">
388
+ <div className="items-center px-2 py-0.5 font-mono text-xs break-all leading-6">
309
389
  {serverSelect}
310
390
  {path}
311
391
  {urlQueryParams.length > 0 ? "?" : ""}
@@ -316,7 +396,7 @@ export const Playground = ({
316
396
  <SubmitButton
317
397
  identities={identities.data ?? []}
318
398
  formRef={formRef}
319
- disabled={form.formState.isSubmitting}
399
+ disabled={identities.isLoading || form.formState.isSubmitting}
320
400
  />
321
401
  </div>
322
402
  <Tabs defaultValue="parameters">
@@ -341,7 +421,12 @@ export const Playground = ({
341
421
  <div className="w-2 h-2 rounded-full bg-blue-400 ml-2" />
342
422
  )}
343
423
  </TabsTrigger>
344
- <TabsTrigger value="body">Body</TabsTrigger>
424
+ <TabsTrigger value="body">
425
+ Body
426
+ {formState.body && (
427
+ <div className="w-2 h-2 rounded-full bg-blue-400 ml-2" />
428
+ )}
429
+ </TabsTrigger>
345
430
  </TabsList>
346
431
  </div>
347
432
  <TabsContent value="headers">
@@ -351,7 +436,7 @@ export const Playground = ({
351
436
  {pathParams.length > 0 && (
352
437
  <div className="flex flex-col gap-4 my-4">
353
438
  <span className="font-semibold">Path Parameters</span>
354
- <PathParams control={control} />
439
+ <PathParams url={url} control={control} />
355
440
  </div>
356
441
  )}
357
442
  <div className="flex flex-col gap-4 my-4">
@@ -375,31 +460,58 @@ export const Playground = ({
375
460
  <Textarea
376
461
  {...register("body")}
377
462
  className={cn(
378
- "border w-full rounded-lg p-2 bg-muted h-40 font-mono",
379
- !["POST", "PUT", "PATCH", "DELETE"].includes(
380
- method.toUpperCase(),
381
- ) && "h-20",
463
+ "border w-full rounded-lg bg-muted/40 p-2 h-64 font-mono text-[13px]",
464
+ !isBodySupported && "h-20 bg-muted",
382
465
  )}
383
466
  placeholder={
384
- !["POST", "PUT", "PATCH", "DELETE"].includes(
385
- method.toUpperCase(),
386
- )
467
+ !isBodySupported
387
468
  ? "This request does not support a body"
388
469
  : undefined
389
470
  }
390
- disabled={
391
- !["POST", "PUT", "PATCH", "DELETE"].includes(
392
- method.toUpperCase(),
393
- )
394
- }
471
+ disabled={!isBodySupported}
395
472
  />
396
- {examples && (
397
- <ExamplesDropdown
398
- examples={examples}
399
- onSelect={(example) =>
400
- setValue("body", JSON.stringify(example.value, null, 2))
401
- }
402
- />
473
+ {isBodySupported && (
474
+ <div className="flex items-center gap-2 mt-2 justify-between">
475
+ <Select
476
+ value={formState.bodyContentType}
477
+ onValueChange={(value) =>
478
+ setValue(
479
+ "bodyContentType",
480
+ value as keyof typeof bodyContentTypeMap,
481
+ )
482
+ }
483
+ >
484
+ <SelectTrigger className="w-[100px]">
485
+ <SelectValue />
486
+ </SelectTrigger>
487
+ <SelectContent>
488
+ {Object.keys(bodyContentTypeMap).map((format) => (
489
+ <SelectItem key={format} value={format}>
490
+ {format}
491
+ </SelectItem>
492
+ ))}
493
+ </SelectContent>
494
+ </Select>
495
+ {examples && examples.length > 0 && (
496
+ <ExamplesDropdown
497
+ examples={examples}
498
+ onSelect={(example, mediaType) => {
499
+ setValue(
500
+ "body",
501
+ JSON.stringify(example.value, null, 2),
502
+ );
503
+
504
+ const format = objectEntries(bodyContentTypeMap).find(
505
+ ([_, contentType]) => contentType === mediaType,
506
+ )?.[0];
507
+
508
+ if (format) {
509
+ setValue("bodyContentType", format);
510
+ }
511
+ }}
512
+ />
513
+ )}
514
+ </div>
403
515
  )}
404
516
  </TabsContent>
405
517
  <TabsContent value="auth">
@@ -414,43 +526,11 @@ export const Playground = ({
414
526
  </Alert>
415
527
  )}
416
528
  <div className="flex flex-col items-center gap-2">
417
- <Card className="w-full overflow-hidden">
418
- <RadioGroup
419
- onValueChange={(value) => setValue("identity", value)}
420
- value={formState.identity}
421
- defaultValue={formState.identity}
422
- className="gap-0"
423
- disabled={identities.data?.length === 0}
424
- >
425
- <Label
426
- className="h-12 border-b items-center flex p-4 cursor-pointer hover:bg-accent"
427
- htmlFor="none"
428
- >
429
- <RadioGroupItem value={NO_IDENTITY} id="none">
430
- None
431
- </RadioGroupItem>
432
- <Label htmlFor="none" className="ml-2">
433
- None
434
- </Label>
435
- </Label>
436
- {identities.data?.map((identity) => (
437
- <Label
438
- key={identity.id}
439
- className="h-12 border-b items-center flex p-4 cursor-pointer hover:bg-accent"
440
- >
441
- <RadioGroupItem
442
- value={identity.id}
443
- id={identity.id}
444
- >
445
- {identity.label}
446
- </RadioGroupItem>
447
- <Label htmlFor={identity.id} className="ml-2">
448
- {identity.label}
449
- </Label>
450
- </Label>
451
- ))}
452
- </RadioGroup>
453
- </Card>
529
+ <IdentitySelector
530
+ value={formState.identity}
531
+ identities={identities.data ?? []}
532
+ setValue={(value) => setValue("identity", value)}
533
+ />
454
534
  </div>
455
535
  </div>
456
536
  </TabsContent>
@@ -461,6 +541,13 @@ export const Playground = ({
461
541
  showPathParamsWarning={formState.pathParams.some(
462
542
  (p) => p.value === "",
463
543
  )}
544
+ showLongRunningWarning={showLongRunningWarning}
545
+ onCancel={() => {
546
+ abortControllerRef.current?.abort(
547
+ "Request cancelled by the user",
548
+ );
549
+ setShowLongRunningWarning(false);
550
+ }}
464
551
  />
465
552
  </div>
466
553
  </form>
@@ -43,7 +43,6 @@ export const QueryParams = ({
43
43
  name={`queryParams.${i}.active`}
44
44
  render={({ field }) => (
45
45
  <Checkbox
46
- variant="outline"
47
46
  id={`queryParams.${i}.active`}
48
47
  className="mr-2"
49
48
  checked={field.value}
@@ -0,0 +1,51 @@
1
+ import { Button } from "zudoku/ui/Button.js";
2
+ import {
3
+ Dialog,
4
+ DialogContent,
5
+ DialogDescription,
6
+ DialogFooter,
7
+ DialogTitle,
8
+ } from "zudoku/ui/Dialog.js";
9
+
10
+ const RequestLoginDialog = ({
11
+ open,
12
+ setOpen,
13
+ onSignUp,
14
+ onLogin,
15
+ }: {
16
+ open: boolean;
17
+ onSignUp?: () => void;
18
+ onLogin?: () => void;
19
+ setOpen: (open: boolean) => void;
20
+ }) => {
21
+ return (
22
+ <Dialog open={open} onOpenChange={setOpen}>
23
+ <DialogContent>
24
+ <DialogTitle>Welcome to the Playground!</DialogTitle>
25
+ <DialogDescription>
26
+ The Playground is a tool for developers to test and explore our APIs.
27
+ To use the Playground, you need to login.
28
+ </DialogDescription>
29
+ <DialogFooter className="flex gap-2 sm:justify-between">
30
+ <Button type="button" variant="ghost" onClick={() => setOpen(false)}>
31
+ Skip
32
+ </Button>
33
+ <div className="flex gap-2">
34
+ {onSignUp && (
35
+ <Button type="button" variant="outline" onClick={onSignUp}>
36
+ Sign Up
37
+ </Button>
38
+ )}
39
+ {onLogin && (
40
+ <Button type="button" variant="default" onClick={onLogin}>
41
+ Login
42
+ </Button>
43
+ )}
44
+ </div>
45
+ </DialogFooter>
46
+ </DialogContent>
47
+ </Dialog>
48
+ );
49
+ };
50
+
51
+ export default RequestLoginDialog;
@@ -0,0 +1,26 @@
1
+ import { create } from "zustand";
2
+ import { createJSONStorage, persist } from "zustand/middleware";
3
+
4
+ interface IdentityState {
5
+ rememberedIdentity: string | null;
6
+ setRememberedIdentity: (identity: string | null) => void;
7
+ getRememberedIdentity: (availableIdentities: string[]) => string | undefined;
8
+ }
9
+
10
+ export const useIdentityStore = create<IdentityState>()(
11
+ persist(
12
+ (set, get) => ({
13
+ rememberedIdentity: null,
14
+ setRememberedIdentity: (identity: string | null) =>
15
+ set({ rememberedIdentity: identity }),
16
+ getRememberedIdentity: (availableIdentities: string[]) =>
17
+ availableIdentities.find(
18
+ (identity) => identity === get().rememberedIdentity,
19
+ ),
20
+ }),
21
+ {
22
+ name: "identity-storage",
23
+ storage: createJSONStorage(() => sessionStorage),
24
+ },
25
+ ),
26
+ );