@brainfish-ai/devdoc 0.1.21
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/LICENSE +33 -0
- package/README.md +415 -0
- package/bin/devdoc.js +13 -0
- package/dist/cli/commands/build.d.ts +5 -0
- package/dist/cli/commands/build.js +87 -0
- package/dist/cli/commands/check.d.ts +1 -0
- package/dist/cli/commands/check.js +143 -0
- package/dist/cli/commands/create.d.ts +24 -0
- package/dist/cli/commands/create.js +387 -0
- package/dist/cli/commands/deploy.d.ts +9 -0
- package/dist/cli/commands/deploy.js +433 -0
- package/dist/cli/commands/dev.d.ts +6 -0
- package/dist/cli/commands/dev.js +139 -0
- package/dist/cli/commands/init.d.ts +11 -0
- package/dist/cli/commands/init.js +238 -0
- package/dist/cli/commands/keys.d.ts +12 -0
- package/dist/cli/commands/keys.js +165 -0
- package/dist/cli/commands/start.d.ts +5 -0
- package/dist/cli/commands/start.js +56 -0
- package/dist/cli/commands/upload.d.ts +13 -0
- package/dist/cli/commands/upload.js +238 -0
- package/dist/cli/commands/whoami.d.ts +8 -0
- package/dist/cli/commands/whoami.js +91 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +106 -0
- package/dist/config/index.d.ts +80 -0
- package/dist/config/index.js +133 -0
- package/dist/constants.d.ts +9 -0
- package/dist/constants.js +13 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +12 -0
- package/dist/utils/logger.d.ts +16 -0
- package/dist/utils/logger.js +61 -0
- package/dist/utils/paths.d.ts +16 -0
- package/dist/utils/paths.js +50 -0
- package/package.json +51 -0
- package/renderer/app/api/assets/[...path]/route.ts +123 -0
- package/renderer/app/api/assets/route.ts +124 -0
- package/renderer/app/api/assets/upload/route.ts +177 -0
- package/renderer/app/api/auth-schemes/route.ts +77 -0
- package/renderer/app/api/chat/route.ts +858 -0
- package/renderer/app/api/codegen/route.ts +72 -0
- package/renderer/app/api/collections/route.ts +1016 -0
- package/renderer/app/api/debug/route.ts +53 -0
- package/renderer/app/api/deploy/route.ts +234 -0
- package/renderer/app/api/device/route.ts +42 -0
- package/renderer/app/api/docs/route.ts +187 -0
- package/renderer/app/api/keys/regenerate/route.ts +80 -0
- package/renderer/app/api/openapi-spec/route.ts +151 -0
- package/renderer/app/api/projects/[slug]/route.ts +153 -0
- package/renderer/app/api/projects/[slug]/stats/route.ts +96 -0
- package/renderer/app/api/projects/register/route.ts +152 -0
- package/renderer/app/api/proxy/route.ts +149 -0
- package/renderer/app/api/proxy-stream/route.ts +168 -0
- package/renderer/app/api/redirects/route.ts +47 -0
- package/renderer/app/api/schema/route.ts +65 -0
- package/renderer/app/api/subdomains/check/route.ts +172 -0
- package/renderer/app/api/suggestions/route.ts +144 -0
- package/renderer/app/favicon.ico +0 -0
- package/renderer/app/globals.css +1103 -0
- package/renderer/app/layout.tsx +47 -0
- package/renderer/app/llms-full.txt/route.ts +346 -0
- package/renderer/app/llms.txt/route.ts +279 -0
- package/renderer/app/page.tsx +14 -0
- package/renderer/app/robots.txt/route.ts +84 -0
- package/renderer/app/sitemap.xml/route.ts +199 -0
- package/renderer/components/docs/index.ts +12 -0
- package/renderer/components/docs/mdx/accordion.tsx +169 -0
- package/renderer/components/docs/mdx/badge.tsx +132 -0
- package/renderer/components/docs/mdx/callouts.tsx +154 -0
- package/renderer/components/docs/mdx/cards.tsx +213 -0
- package/renderer/components/docs/mdx/changelog.tsx +120 -0
- package/renderer/components/docs/mdx/code-block.tsx +186 -0
- package/renderer/components/docs/mdx/code-group.tsx +421 -0
- package/renderer/components/docs/mdx/file-embeds.tsx +105 -0
- package/renderer/components/docs/mdx/frame.tsx +112 -0
- package/renderer/components/docs/mdx/highlight.tsx +151 -0
- package/renderer/components/docs/mdx/iframe.tsx +134 -0
- package/renderer/components/docs/mdx/image.tsx +235 -0
- package/renderer/components/docs/mdx/index.ts +204 -0
- package/renderer/components/docs/mdx/mermaid.tsx +240 -0
- package/renderer/components/docs/mdx/param-field.tsx +200 -0
- package/renderer/components/docs/mdx/steps.tsx +113 -0
- package/renderer/components/docs/mdx/tabs.tsx +86 -0
- package/renderer/components/docs/mdx-renderer.tsx +100 -0
- package/renderer/components/docs/navigation/breadcrumbs.tsx +76 -0
- package/renderer/components/docs/navigation/index.ts +8 -0
- package/renderer/components/docs/navigation/page-nav.tsx +64 -0
- package/renderer/components/docs/navigation/sidebar.tsx +515 -0
- package/renderer/components/docs/navigation/toc.tsx +113 -0
- package/renderer/components/docs/notice.tsx +105 -0
- package/renderer/components/docs-header.tsx +274 -0
- package/renderer/components/docs-viewer/agent/agent-chat.tsx +2076 -0
- package/renderer/components/docs-viewer/agent/cards/debug-context-card.tsx +90 -0
- package/renderer/components/docs-viewer/agent/cards/endpoint-context-card.tsx +49 -0
- package/renderer/components/docs-viewer/agent/cards/index.tsx +50 -0
- package/renderer/components/docs-viewer/agent/cards/response-options-card.tsx +212 -0
- package/renderer/components/docs-viewer/agent/cards/types.ts +84 -0
- package/renderer/components/docs-viewer/agent/chat-message.tsx +17 -0
- package/renderer/components/docs-viewer/agent/index.tsx +6 -0
- package/renderer/components/docs-viewer/agent/messages/assistant-message.tsx +119 -0
- package/renderer/components/docs-viewer/agent/messages/chat-message.tsx +46 -0
- package/renderer/components/docs-viewer/agent/messages/index.ts +17 -0
- package/renderer/components/docs-viewer/agent/messages/tool-call-display.tsx +721 -0
- package/renderer/components/docs-viewer/agent/messages/types.ts +61 -0
- package/renderer/components/docs-viewer/agent/messages/typing-indicator.tsx +24 -0
- package/renderer/components/docs-viewer/agent/messages/user-message.tsx +51 -0
- package/renderer/components/docs-viewer/code-editor/index.tsx +2 -0
- package/renderer/components/docs-viewer/code-editor/notes-mode.tsx +1283 -0
- package/renderer/components/docs-viewer/content/changelog-page.tsx +331 -0
- package/renderer/components/docs-viewer/content/doc-page.tsx +285 -0
- package/renderer/components/docs-viewer/content/documentation-viewer.tsx +17 -0
- package/renderer/components/docs-viewer/content/index.tsx +29 -0
- package/renderer/components/docs-viewer/content/introduction.tsx +21 -0
- package/renderer/components/docs-viewer/content/request-details.tsx +330 -0
- package/renderer/components/docs-viewer/content/sections/auth.tsx +69 -0
- package/renderer/components/docs-viewer/content/sections/body.tsx +66 -0
- package/renderer/components/docs-viewer/content/sections/headers.tsx +43 -0
- package/renderer/components/docs-viewer/content/sections/overview.tsx +40 -0
- package/renderer/components/docs-viewer/content/sections/parameters.tsx +43 -0
- package/renderer/components/docs-viewer/content/sections/responses.tsx +87 -0
- package/renderer/components/docs-viewer/global-auth-modal.tsx +352 -0
- package/renderer/components/docs-viewer/index.tsx +1466 -0
- package/renderer/components/docs-viewer/playground/auth-editor.tsx +280 -0
- package/renderer/components/docs-viewer/playground/body-editor.tsx +221 -0
- package/renderer/components/docs-viewer/playground/code-editor.tsx +224 -0
- package/renderer/components/docs-viewer/playground/code-snippet.tsx +387 -0
- package/renderer/components/docs-viewer/playground/graphql-playground.tsx +745 -0
- package/renderer/components/docs-viewer/playground/index.tsx +671 -0
- package/renderer/components/docs-viewer/playground/key-value-editor.tsx +261 -0
- package/renderer/components/docs-viewer/playground/method-selector.tsx +60 -0
- package/renderer/components/docs-viewer/playground/request-builder.tsx +179 -0
- package/renderer/components/docs-viewer/playground/request-tabs.tsx +237 -0
- package/renderer/components/docs-viewer/playground/response-cards/idle-card.tsx +21 -0
- package/renderer/components/docs-viewer/playground/response-cards/index.tsx +93 -0
- package/renderer/components/docs-viewer/playground/response-cards/loading-card.tsx +16 -0
- package/renderer/components/docs-viewer/playground/response-cards/network-error-card.tsx +23 -0
- package/renderer/components/docs-viewer/playground/response-cards/response-body-card.tsx +268 -0
- package/renderer/components/docs-viewer/playground/response-cards/types.ts +82 -0
- package/renderer/components/docs-viewer/playground/response-viewer.tsx +43 -0
- package/renderer/components/docs-viewer/search/index.ts +2 -0
- package/renderer/components/docs-viewer/search/search-dialog.tsx +331 -0
- package/renderer/components/docs-viewer/search/use-search.ts +117 -0
- package/renderer/components/docs-viewer/shared/markdown-renderer.tsx +431 -0
- package/renderer/components/docs-viewer/shared/method-badge.tsx +41 -0
- package/renderer/components/docs-viewer/shared/schema-viewer.tsx +349 -0
- package/renderer/components/docs-viewer/sidebar/collection-tree.tsx +239 -0
- package/renderer/components/docs-viewer/sidebar/endpoint-options.tsx +316 -0
- package/renderer/components/docs-viewer/sidebar/index.tsx +343 -0
- package/renderer/components/docs-viewer/sidebar/right-sidebar.tsx +202 -0
- package/renderer/components/docs-viewer/sidebar/sidebar-group.tsx +118 -0
- package/renderer/components/docs-viewer/sidebar/sidebar-item.tsx +226 -0
- package/renderer/components/docs-viewer/sidebar/sidebar-section.tsx +52 -0
- package/renderer/components/theme-provider.tsx +11 -0
- package/renderer/components/theme-toggle.tsx +76 -0
- package/renderer/components/ui/badge.tsx +46 -0
- package/renderer/components/ui/button.tsx +59 -0
- package/renderer/components/ui/dialog.tsx +118 -0
- package/renderer/components/ui/dropdown-menu.tsx +257 -0
- package/renderer/components/ui/input.tsx +21 -0
- package/renderer/components/ui/label.tsx +24 -0
- package/renderer/components/ui/navigation-menu.tsx +168 -0
- package/renderer/components/ui/select.tsx +190 -0
- package/renderer/components/ui/spinner.tsx +114 -0
- package/renderer/components/ui/tabs.tsx +66 -0
- package/renderer/components/ui/tooltip.tsx +61 -0
- package/renderer/hooks/use-code-copy.ts +88 -0
- package/renderer/hooks/use-openapi-title.ts +44 -0
- package/renderer/lib/api-docs/agent/index.ts +6 -0
- package/renderer/lib/api-docs/agent/indexer.ts +323 -0
- package/renderer/lib/api-docs/agent/spec-summary.ts +335 -0
- package/renderer/lib/api-docs/agent/types.ts +116 -0
- package/renderer/lib/api-docs/auth/auth-context.tsx +225 -0
- package/renderer/lib/api-docs/auth/auth-storage.ts +87 -0
- package/renderer/lib/api-docs/auth/crypto.ts +89 -0
- package/renderer/lib/api-docs/auth/index.ts +4 -0
- package/renderer/lib/api-docs/code-editor/db.ts +164 -0
- package/renderer/lib/api-docs/code-editor/hooks.ts +266 -0
- package/renderer/lib/api-docs/code-editor/index.ts +6 -0
- package/renderer/lib/api-docs/code-editor/mode-context.tsx +207 -0
- package/renderer/lib/api-docs/code-editor/types.ts +105 -0
- package/renderer/lib/api-docs/codegen/definitions.ts +297 -0
- package/renderer/lib/api-docs/codegen/har.ts +251 -0
- package/renderer/lib/api-docs/codegen/index.ts +159 -0
- package/renderer/lib/api-docs/factories.ts +151 -0
- package/renderer/lib/api-docs/index.ts +17 -0
- package/renderer/lib/api-docs/mobile-context.tsx +112 -0
- package/renderer/lib/api-docs/navigation-context.tsx +88 -0
- package/renderer/lib/api-docs/parsers/graphql/README.md +129 -0
- package/renderer/lib/api-docs/parsers/graphql/index.ts +91 -0
- package/renderer/lib/api-docs/parsers/graphql/parser.ts +491 -0
- package/renderer/lib/api-docs/parsers/graphql/transformer.ts +246 -0
- package/renderer/lib/api-docs/parsers/graphql/types.ts +283 -0
- package/renderer/lib/api-docs/parsers/openapi/README.md +32 -0
- package/renderer/lib/api-docs/parsers/openapi/dereferencer.ts +60 -0
- package/renderer/lib/api-docs/parsers/openapi/extractors/auth.ts +574 -0
- package/renderer/lib/api-docs/parsers/openapi/extractors/body.ts +403 -0
- package/renderer/lib/api-docs/parsers/openapi/extractors/index.ts +232 -0
- package/renderer/lib/api-docs/parsers/openapi/index.ts +171 -0
- package/renderer/lib/api-docs/parsers/openapi/transformer.ts +277 -0
- package/renderer/lib/api-docs/parsers/openapi/validator.ts +31 -0
- package/renderer/lib/api-docs/playground/context.tsx +107 -0
- package/renderer/lib/api-docs/playground/navigation-context.tsx +124 -0
- package/renderer/lib/api-docs/playground/request-builder.ts +223 -0
- package/renderer/lib/api-docs/playground/request-runner.ts +282 -0
- package/renderer/lib/api-docs/playground/types.ts +35 -0
- package/renderer/lib/api-docs/types.ts +269 -0
- package/renderer/lib/api-docs/utils.ts +311 -0
- package/renderer/lib/cache.ts +193 -0
- package/renderer/lib/docs/config/index.ts +29 -0
- package/renderer/lib/docs/config/loader.ts +142 -0
- package/renderer/lib/docs/config/schema.ts +298 -0
- package/renderer/lib/docs/index.ts +12 -0
- package/renderer/lib/docs/mdx/compiler.ts +176 -0
- package/renderer/lib/docs/mdx/frontmatter.ts +80 -0
- package/renderer/lib/docs/mdx/index.ts +26 -0
- package/renderer/lib/docs/navigation/generator.ts +348 -0
- package/renderer/lib/docs/navigation/index.ts +12 -0
- package/renderer/lib/docs/navigation/types.ts +123 -0
- package/renderer/lib/docs-navigation-context.tsx +80 -0
- package/renderer/lib/multi-tenant/context.ts +105 -0
- package/renderer/lib/storage/blob.ts +845 -0
- package/renderer/lib/utils.ts +6 -0
- package/renderer/next.config.ts +76 -0
- package/renderer/package.json +66 -0
- package/renderer/postcss.config.mjs +5 -0
- package/renderer/public/assets/images/screenshot.png +0 -0
- package/renderer/public/assets/logo/dark.svg +9 -0
- package/renderer/public/assets/logo/light.svg +9 -0
- package/renderer/public/assets/logo.svg +9 -0
- package/renderer/public/file.svg +1 -0
- package/renderer/public/globe.svg +1 -0
- package/renderer/public/icon.png +0 -0
- package/renderer/public/logo.svg +9 -0
- package/renderer/public/window.svg +1 -0
- package/renderer/tsconfig.json +28 -0
- package/templates/basic/README.md +139 -0
- package/templates/basic/assets/favicon.svg +4 -0
- package/templates/basic/assets/logo.svg +9 -0
- package/templates/basic/docs.json +47 -0
- package/templates/basic/guides/configuration.mdx +149 -0
- package/templates/basic/guides/overview.mdx +96 -0
- package/templates/basic/index.mdx +39 -0
- package/templates/basic/package.json +14 -0
- package/templates/basic/quickstart.mdx +92 -0
- package/templates/basic/vercel.json +6 -0
- package/templates/graphql/README.md +139 -0
- package/templates/graphql/api-reference/schema.graphql +305 -0
- package/templates/graphql/assets/favicon.svg +4 -0
- package/templates/graphql/assets/logo.svg +9 -0
- package/templates/graphql/docs.json +54 -0
- package/templates/graphql/guides/configuration.mdx +149 -0
- package/templates/graphql/guides/overview.mdx +96 -0
- package/templates/graphql/index.mdx +39 -0
- package/templates/graphql/package.json +14 -0
- package/templates/graphql/quickstart.mdx +92 -0
- package/templates/graphql/vercel.json +6 -0
- package/templates/openapi/README.md +139 -0
- package/templates/openapi/api-reference/openapi.json +419 -0
- package/templates/openapi/assets/favicon.svg +4 -0
- package/templates/openapi/assets/logo.svg +9 -0
- package/templates/openapi/docs.json +61 -0
- package/templates/openapi/guides/configuration.mdx +149 -0
- package/templates/openapi/guides/overview.mdx +96 -0
- package/templates/openapi/index.mdx +39 -0
- package/templates/openapi/package.json +14 -0
- package/templates/openapi/quickstart.mdx +92 -0
- package/templates/openapi/vercel.json +6 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAPI Importer
|
|
3
|
+
*
|
|
4
|
+
* Main entry point for importing OpenAPI specifications
|
|
5
|
+
* Ported from Hoppscotch's openapi/index.ts
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import yaml from 'js-yaml'
|
|
9
|
+
import type { OpenAPI, OpenAPIV2, OpenAPIV3 } from 'openapi-types'
|
|
10
|
+
import type { BrainfishCollection } from '../../types'
|
|
11
|
+
import { objectHasProperty } from '../../utils'
|
|
12
|
+
import { validateOpenAPISpec } from './validator'
|
|
13
|
+
import { dereferenceOpenAPISpec, hasUnresolvedRefs } from './dereferencer'
|
|
14
|
+
import { convertOpenAPIToCollection } from './transformer'
|
|
15
|
+
|
|
16
|
+
export const OPENAPI_DEREF_ERROR = 'openapi/deref_error' as const
|
|
17
|
+
export const IMPORTER_INVALID_FILE_FORMAT = 'importer/invalid_file_format' as const
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Safely parses JSON string
|
|
21
|
+
*/
|
|
22
|
+
function safeParseJSON(str: string): unknown | null {
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(str)
|
|
25
|
+
} catch {
|
|
26
|
+
return null
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Safely parses YAML string
|
|
32
|
+
*/
|
|
33
|
+
function safeParseYAML(str: string): unknown | null {
|
|
34
|
+
try {
|
|
35
|
+
return yaml.load(str)
|
|
36
|
+
} catch {
|
|
37
|
+
return null
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Parses OpenAPI document content (JSON or YAML)
|
|
43
|
+
*/
|
|
44
|
+
function parseOpenAPIDocContent(str: string): unknown | null {
|
|
45
|
+
const jsonResult = safeParseJSON(str)
|
|
46
|
+
if (jsonResult !== null) {
|
|
47
|
+
return jsonResult
|
|
48
|
+
}
|
|
49
|
+
return safeParseYAML(str)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Checks if document is OpenAPI v2
|
|
54
|
+
*/
|
|
55
|
+
function isOpenAPIV2Document(doc: unknown): doc is OpenAPIV2.Document {
|
|
56
|
+
return (
|
|
57
|
+
objectHasProperty(doc, 'swagger') &&
|
|
58
|
+
typeof doc.swagger === 'string' &&
|
|
59
|
+
doc.swagger === '2.0'
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Checks if document is OpenAPI v3
|
|
65
|
+
*/
|
|
66
|
+
function isOpenAPIV3Document(
|
|
67
|
+
doc: unknown
|
|
68
|
+
): doc is OpenAPIV3.Document {
|
|
69
|
+
return (
|
|
70
|
+
objectHasProperty(doc, 'openapi') &&
|
|
71
|
+
typeof doc.openapi === 'string' &&
|
|
72
|
+
doc.openapi.startsWith('3.')
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Imports OpenAPI specification from file contents or document
|
|
78
|
+
*
|
|
79
|
+
* @param input - OpenAPI document object or array of file contents (strings)
|
|
80
|
+
* @returns Promise resolving to BrainfishCollection array
|
|
81
|
+
*/
|
|
82
|
+
export async function importOpenAPISpec(
|
|
83
|
+
input: OpenAPI.Document | string | string[]
|
|
84
|
+
): Promise<BrainfishCollection[]> {
|
|
85
|
+
let docArr: unknown[]
|
|
86
|
+
|
|
87
|
+
// Handle different input types
|
|
88
|
+
if (typeof input === 'string') {
|
|
89
|
+
// Single file content string
|
|
90
|
+
const parsed = parseOpenAPIDocContent(input)
|
|
91
|
+
if (!parsed) {
|
|
92
|
+
throw new Error(IMPORTER_INVALID_FILE_FORMAT)
|
|
93
|
+
}
|
|
94
|
+
docArr = [parsed]
|
|
95
|
+
} else if (Array.isArray(input)) {
|
|
96
|
+
// Array of file content strings
|
|
97
|
+
docArr = input
|
|
98
|
+
.map((str) => parseOpenAPIDocContent(str))
|
|
99
|
+
.filter((doc): doc is unknown => doc !== null)
|
|
100
|
+
|
|
101
|
+
if (docArr.length === 0) {
|
|
102
|
+
throw new Error(IMPORTER_INVALID_FILE_FORMAT)
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
// Already a parsed OpenAPI document
|
|
106
|
+
docArr = [input]
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Validate documents
|
|
110
|
+
const validatedDocs: OpenAPI.Document[] = []
|
|
111
|
+
|
|
112
|
+
for (const docObj of docArr) {
|
|
113
|
+
try {
|
|
114
|
+
// More lenient check - if it has paths, we'll try to import it
|
|
115
|
+
const isValidOpenAPISpec =
|
|
116
|
+
objectHasProperty(docObj, 'paths') &&
|
|
117
|
+
(isOpenAPIV2Document(docObj) ||
|
|
118
|
+
isOpenAPIV3Document(docObj) ||
|
|
119
|
+
objectHasProperty(docObj, 'info'))
|
|
120
|
+
|
|
121
|
+
if (!isValidOpenAPISpec) {
|
|
122
|
+
throw new Error('INVALID_OPENAPI_SPEC')
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const validatedDoc = await validateOpenAPISpec(docObj)
|
|
127
|
+
validatedDocs.push(validatedDoc)
|
|
128
|
+
} catch (validationError) {
|
|
129
|
+
// If validation fails but it has basic OpenAPI structure, add it anyway
|
|
130
|
+
if (objectHasProperty(docObj, 'paths')) {
|
|
131
|
+
validatedDocs.push(docObj as OpenAPI.Document)
|
|
132
|
+
} else {
|
|
133
|
+
throw validationError
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
} catch (err) {
|
|
137
|
+
if (err instanceof Error && err.message === 'INVALID_OPENAPI_SPEC') {
|
|
138
|
+
throw new Error('INVALID_OPENAPI_SPEC')
|
|
139
|
+
}
|
|
140
|
+
// Continue with other documents
|
|
141
|
+
console.warn('Failed to validate document:', err)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (validatedDocs.length === 0) {
|
|
146
|
+
throw new Error(IMPORTER_INVALID_FILE_FORMAT)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Dereference documents
|
|
150
|
+
const dereferencedDocs: OpenAPI.Document[] = []
|
|
151
|
+
|
|
152
|
+
for (const docObj of validatedDocs) {
|
|
153
|
+
try {
|
|
154
|
+
const dereferencedDoc = await dereferenceOpenAPISpec(docObj)
|
|
155
|
+
dereferencedDocs.push(dereferencedDoc)
|
|
156
|
+
} catch {
|
|
157
|
+
// Check if the document has unresolved references
|
|
158
|
+
if (hasUnresolvedRefs(docObj)) {
|
|
159
|
+
console.warn(
|
|
160
|
+
'Document contains unresolved references which may affect import quality'
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// If dereferencing fails, use the original document
|
|
165
|
+
dereferencedDocs.push(docObj)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Convert to BrainfishCollections
|
|
170
|
+
return convertOpenAPIToCollection(dereferencedDocs)
|
|
171
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAPI Transformer
|
|
3
|
+
*
|
|
4
|
+
* Ported from Hoppscotch's openapi/index.ts
|
|
5
|
+
* Converts OpenAPI Document to BrainfishCollection
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
OpenAPI,
|
|
12
|
+
OpenAPIV2,
|
|
13
|
+
OpenAPIV3,
|
|
14
|
+
} from 'openapi-types'
|
|
15
|
+
import type {
|
|
16
|
+
BrainfishCollection,
|
|
17
|
+
BrainfishRESTRequest,
|
|
18
|
+
BrainfishRESTResponseOriginalRequest,
|
|
19
|
+
} from '../../types'
|
|
20
|
+
import {
|
|
21
|
+
makeBrainfishCollection,
|
|
22
|
+
makeBrainfishRESTRequest,
|
|
23
|
+
makeBrainfishRESTResponseOriginalRequest,
|
|
24
|
+
} from '../../factories'
|
|
25
|
+
import { objectHasProperty, cloneDeep } from '../../utils'
|
|
26
|
+
import { hasUnresolvedRefs } from './dereferencer'
|
|
27
|
+
import {
|
|
28
|
+
replaceOpenApiPathTemplating,
|
|
29
|
+
parseOpenAPIParams,
|
|
30
|
+
parseOpenAPIHeaders,
|
|
31
|
+
parseOpenAPIVariables,
|
|
32
|
+
parseOpenAPIResponses,
|
|
33
|
+
type OpenAPIPathInfoType,
|
|
34
|
+
type OpenAPIOperationType,
|
|
35
|
+
} from './extractors'
|
|
36
|
+
import { parseOpenAPIBody } from './extractors/body'
|
|
37
|
+
import { parseOpenAPIAuth, extractSecurityHeaders, getDefaultAuth } from './extractors/auth'
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Formats an operation name for display
|
|
41
|
+
* Converts snake_case/kebab-case operationIds to readable Title Case
|
|
42
|
+
* Prefers summary over operationId if available
|
|
43
|
+
*/
|
|
44
|
+
function formatOperationName(info: any): string {
|
|
45
|
+
const summary = info.summary?.trim()
|
|
46
|
+
const operationId = info.operationId?.trim()
|
|
47
|
+
|
|
48
|
+
// Prefer summary as it's usually more human-readable
|
|
49
|
+
if (summary) {
|
|
50
|
+
return summary
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// If only operationId is available, convert to readable format
|
|
54
|
+
if (operationId) {
|
|
55
|
+
return operationId
|
|
56
|
+
// Replace underscores and hyphens with spaces
|
|
57
|
+
.replace(/[_-]/g, ' ')
|
|
58
|
+
// Handle camelCase by adding space before capital letters
|
|
59
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
60
|
+
// Capitalize first letter of each word
|
|
61
|
+
.replace(/\b\w/g, (char: string) => char.toUpperCase())
|
|
62
|
+
// Clean up multiple spaces
|
|
63
|
+
.replace(/\s+/g, ' ')
|
|
64
|
+
.trim()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return 'Untitled Request'
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Parses OpenAPI URL from document (v2 or v3)
|
|
72
|
+
*/
|
|
73
|
+
function parseOpenAPIUrl(
|
|
74
|
+
doc: OpenAPI.Document | OpenAPIV2.Document | OpenAPIV3.Document
|
|
75
|
+
): string {
|
|
76
|
+
// OpenAPI V2 has host and basePath in document properties
|
|
77
|
+
if (objectHasProperty(doc, 'swagger')) {
|
|
78
|
+
const host = (doc as OpenAPIV2.Document).host?.trim() || '<<baseUrl>>'
|
|
79
|
+
const basePath = (doc as OpenAPIV2.Document).basePath?.trim() || ''
|
|
80
|
+
return `${host}${basePath}`
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// OpenAPI V3 has servers array
|
|
84
|
+
if (objectHasProperty(doc, 'servers')) {
|
|
85
|
+
const serverUrl = (doc as OpenAPIV3.Document).servers?.[0]?.url
|
|
86
|
+
return !serverUrl || serverUrl === './' ? '<<baseUrl>>' : serverUrl
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Fallback
|
|
90
|
+
return '<<baseUrl>>'
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Converts an OpenAPI path to Brainfish requests
|
|
95
|
+
*/
|
|
96
|
+
function convertPathToBrainfishReqs(
|
|
97
|
+
doc: OpenAPI.Document,
|
|
98
|
+
pathName: string,
|
|
99
|
+
pathObj: OpenAPIPathInfoType
|
|
100
|
+
): Array<{
|
|
101
|
+
request: BrainfishRESTRequest
|
|
102
|
+
metadata: {
|
|
103
|
+
tags: string[]
|
|
104
|
+
}
|
|
105
|
+
}> {
|
|
106
|
+
const methods = ['get', 'head', 'post', 'put', 'delete', 'options', 'patch'] as const
|
|
107
|
+
|
|
108
|
+
return methods
|
|
109
|
+
.filter((method) => !!pathObj[method])
|
|
110
|
+
.map((method) => {
|
|
111
|
+
const info = pathObj[method]! as OpenAPIOperationType
|
|
112
|
+
const openAPIUrl = parseOpenAPIUrl(doc)
|
|
113
|
+
const openAPIPath = replaceOpenApiPathTemplating(pathName)
|
|
114
|
+
|
|
115
|
+
const endpoint =
|
|
116
|
+
openAPIUrl.endsWith('/') && openAPIPath.startsWith('/')
|
|
117
|
+
? openAPIUrl + openAPIPath.slice(1)
|
|
118
|
+
: openAPIUrl + openAPIPath
|
|
119
|
+
|
|
120
|
+
const params = parseOpenAPIParams(
|
|
121
|
+
(info.parameters as any) ?? []
|
|
122
|
+
)
|
|
123
|
+
const baseHeaders = parseOpenAPIHeaders(
|
|
124
|
+
(info.parameters as any) ?? []
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
// Get the primary auth key (handled by global auth) to exclude from additional headers
|
|
128
|
+
const defaultAuth = getDefaultAuth(doc)
|
|
129
|
+
const primaryAuthKey = defaultAuth?.authType === 'api-key' ? defaultAuth.key : undefined
|
|
130
|
+
|
|
131
|
+
// Extract additional security headers (e.g., agent-key when access-token is primary)
|
|
132
|
+
const securityHeaders = extractSecurityHeaders(doc, info, primaryAuthKey)
|
|
133
|
+
|
|
134
|
+
// Merge headers: base headers + security headers (avoiding duplicates)
|
|
135
|
+
const existingKeys = new Set(baseHeaders.map(h => h.key.toLowerCase()))
|
|
136
|
+
const additionalSecurityHeaders = securityHeaders
|
|
137
|
+
.filter(h => !existingKeys.has(h.key.toLowerCase()))
|
|
138
|
+
.map(h => ({
|
|
139
|
+
key: h.key,
|
|
140
|
+
value: h.value,
|
|
141
|
+
active: h.active,
|
|
142
|
+
description: h.description,
|
|
143
|
+
}))
|
|
144
|
+
|
|
145
|
+
const headers = [...baseHeaders, ...additionalSecurityHeaders]
|
|
146
|
+
|
|
147
|
+
const requestVariables = parseOpenAPIVariables(
|
|
148
|
+
(info.parameters as any) ?? []
|
|
149
|
+
)
|
|
150
|
+
const auth = parseOpenAPIAuth(doc, info)
|
|
151
|
+
const body = parseOpenAPIBody(doc, info)
|
|
152
|
+
|
|
153
|
+
const displayName = formatOperationName(info)
|
|
154
|
+
|
|
155
|
+
const originalRequest: BrainfishRESTResponseOriginalRequest =
|
|
156
|
+
makeBrainfishRESTResponseOriginalRequest({
|
|
157
|
+
name: displayName,
|
|
158
|
+
method: method.toUpperCase() as any,
|
|
159
|
+
endpoint,
|
|
160
|
+
params,
|
|
161
|
+
headers,
|
|
162
|
+
body,
|
|
163
|
+
auth,
|
|
164
|
+
requestVariables,
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
const responses = parseOpenAPIResponses(doc, info, originalRequest)
|
|
168
|
+
|
|
169
|
+
const request = makeBrainfishRESTRequest({
|
|
170
|
+
name: displayName,
|
|
171
|
+
description: (info as any).description ?? null,
|
|
172
|
+
method: method.toUpperCase() as any,
|
|
173
|
+
endpoint,
|
|
174
|
+
params,
|
|
175
|
+
headers,
|
|
176
|
+
auth,
|
|
177
|
+
body,
|
|
178
|
+
requestVariables,
|
|
179
|
+
responses,
|
|
180
|
+
tags: (info as any).tags ?? [],
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
request,
|
|
185
|
+
metadata: {
|
|
186
|
+
tags: (info as any).tags ?? [],
|
|
187
|
+
},
|
|
188
|
+
}
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Converts OpenAPI documents to BrainfishCollections
|
|
194
|
+
*/
|
|
195
|
+
export function convertOpenAPIToCollection(
|
|
196
|
+
docs: OpenAPI.Document[]
|
|
197
|
+
): BrainfishCollection[] {
|
|
198
|
+
// Check for unresolved references before conversion
|
|
199
|
+
for (const doc of docs) {
|
|
200
|
+
if (hasUnresolvedRefs(doc)) {
|
|
201
|
+
console.warn(
|
|
202
|
+
'Document contains unresolved references which may affect import quality'
|
|
203
|
+
)
|
|
204
|
+
// Continue anyway to provide a best-effort import
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const collections = docs.map((doc) => {
|
|
209
|
+
const name = doc.info.title
|
|
210
|
+
const description = doc.info.description ?? null
|
|
211
|
+
|
|
212
|
+
// Extract tag descriptions from OpenAPI spec
|
|
213
|
+
const tagDescriptions: Record<string, string> = {}
|
|
214
|
+
if ('tags' in doc && Array.isArray(doc.tags)) {
|
|
215
|
+
doc.tags.forEach((tag: any) => {
|
|
216
|
+
if (tag.name && tag.description) {
|
|
217
|
+
tagDescriptions[tag.name] = tag.description
|
|
218
|
+
}
|
|
219
|
+
})
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Convert all paths to requests
|
|
223
|
+
const paths = Object.entries(doc.paths ?? {})
|
|
224
|
+
.map(([pathName, pathObj]) =>
|
|
225
|
+
convertPathToBrainfishReqs(doc, pathName, pathObj as OpenAPIPathInfoType)
|
|
226
|
+
)
|
|
227
|
+
.flat()
|
|
228
|
+
|
|
229
|
+
// Organize requests by tags
|
|
230
|
+
const requestsByTags: Record<string, BrainfishRESTRequest[]> = {}
|
|
231
|
+
const requestsWithoutTags: BrainfishRESTRequest[] = []
|
|
232
|
+
|
|
233
|
+
paths.forEach(({ metadata, request }) => {
|
|
234
|
+
const tags = metadata.tags
|
|
235
|
+
|
|
236
|
+
if (tags.length === 0) {
|
|
237
|
+
requestsWithoutTags.push(request)
|
|
238
|
+
return
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
for (const tag of tags) {
|
|
242
|
+
if (!requestsByTags[tag]) {
|
|
243
|
+
requestsByTags[tag] = []
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Clone request for each tag (request can belong to multiple tags)
|
|
247
|
+
requestsByTags[tag].push(cloneDeep(request))
|
|
248
|
+
}
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
// Create folders (collections) for each tag
|
|
252
|
+
const folders = Object.entries(requestsByTags).map(([tagName, requests]) =>
|
|
253
|
+
makeBrainfishCollection({
|
|
254
|
+
name: tagName,
|
|
255
|
+
description: tagDescriptions[tagName] ?? null,
|
|
256
|
+
requests,
|
|
257
|
+
folders: [],
|
|
258
|
+
variables: [],
|
|
259
|
+
auth: { authType: 'inherit', authActive: true },
|
|
260
|
+
headers: [],
|
|
261
|
+
})
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
// Create main collection
|
|
265
|
+
return makeBrainfishCollection({
|
|
266
|
+
name,
|
|
267
|
+
description,
|
|
268
|
+
folders,
|
|
269
|
+
requests: requestsWithoutTags,
|
|
270
|
+
variables: [],
|
|
271
|
+
auth: { authType: 'inherit', authActive: true },
|
|
272
|
+
headers: [],
|
|
273
|
+
})
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
return collections
|
|
277
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAPI Validator
|
|
3
|
+
*
|
|
4
|
+
* Ported from Hoppscotch's openapi-import-worker.ts
|
|
5
|
+
* Converts Web Worker pattern to direct function calls for Next.js
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import SwaggerParser from '@apidevtools/swagger-parser'
|
|
9
|
+
import type { OpenAPI } from 'openapi-types'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Validates an OpenAPI document using SwaggerParser
|
|
13
|
+
*
|
|
14
|
+
* @param docs - The OpenAPI document to validate
|
|
15
|
+
* @returns Promise resolving to validated OpenAPI document
|
|
16
|
+
* @throws Error if validation fails
|
|
17
|
+
*/
|
|
18
|
+
export async function validateOpenAPISpec(
|
|
19
|
+
docs: unknown
|
|
20
|
+
): Promise<OpenAPI.Document> {
|
|
21
|
+
try {
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
23
|
+
const validated = await SwaggerParser.validate(docs as any, {
|
|
24
|
+
// @ts-expect-error - this is a valid option, but types may not be updated
|
|
25
|
+
continueOnError: true,
|
|
26
|
+
})
|
|
27
|
+
return validated as unknown as OpenAPI.Document
|
|
28
|
+
} catch {
|
|
29
|
+
throw new Error('COULD_NOT_VALIDATE')
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext, useState, useCallback, type ReactNode } from 'react'
|
|
4
|
+
import type { PrefillData } from '@/lib/api-docs/agent/types'
|
|
5
|
+
|
|
6
|
+
/** Current request payload from the playground */
|
|
7
|
+
export interface CurrentRequestPayload {
|
|
8
|
+
method: string
|
|
9
|
+
endpoint: string
|
|
10
|
+
params: Array<{ key: string; value: string; active: boolean }>
|
|
11
|
+
headers: Array<{ key: string; value: string; active: boolean }>
|
|
12
|
+
body: string | null
|
|
13
|
+
bodyContentType: string | null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface PlaygroundContextValue {
|
|
17
|
+
/** Pending prefill data to be applied */
|
|
18
|
+
pendingPrefill: PrefillData | null
|
|
19
|
+
/** Set prefill data - will be consumed by playground */
|
|
20
|
+
setPrefill: (data: PrefillData) => void
|
|
21
|
+
/** Clear pending prefill after it's been applied */
|
|
22
|
+
clearPrefill: () => void
|
|
23
|
+
/** Current request payload from the playground */
|
|
24
|
+
currentRequestPayload: CurrentRequestPayload | null
|
|
25
|
+
/** Set current request payload - called by playground when values change */
|
|
26
|
+
setCurrentRequestPayload: (payload: CurrentRequestPayload | null) => void
|
|
27
|
+
/** Trigger to send request from agent */
|
|
28
|
+
triggerSend: boolean
|
|
29
|
+
/** Call to trigger send from agent */
|
|
30
|
+
requestSend: () => void
|
|
31
|
+
/** Clear send trigger after playground handles it */
|
|
32
|
+
clearSendTrigger: () => void
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const PlaygroundContext = createContext<PlaygroundContextValue | null>(null)
|
|
36
|
+
|
|
37
|
+
export function PlaygroundProvider({ children }: { children: ReactNode }) {
|
|
38
|
+
const [pendingPrefill, setPendingPrefill] = useState<PrefillData | null>(null)
|
|
39
|
+
const [currentRequestPayload, setCurrentRequestPayloadState] = useState<CurrentRequestPayload | null>(null)
|
|
40
|
+
const [triggerSend, setTriggerSend] = useState(false)
|
|
41
|
+
|
|
42
|
+
const setPrefill = useCallback((data: PrefillData) => {
|
|
43
|
+
setPendingPrefill(data)
|
|
44
|
+
}, [])
|
|
45
|
+
|
|
46
|
+
const clearPrefill = useCallback(() => {
|
|
47
|
+
setPendingPrefill(null)
|
|
48
|
+
}, [])
|
|
49
|
+
|
|
50
|
+
const setCurrentRequestPayload = useCallback((payload: CurrentRequestPayload | null) => {
|
|
51
|
+
setCurrentRequestPayloadState(payload)
|
|
52
|
+
}, [])
|
|
53
|
+
|
|
54
|
+
const requestSend = useCallback(() => {
|
|
55
|
+
setTriggerSend(true)
|
|
56
|
+
}, [])
|
|
57
|
+
|
|
58
|
+
const clearSendTrigger = useCallback(() => {
|
|
59
|
+
setTriggerSend(false)
|
|
60
|
+
}, [])
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<PlaygroundContext.Provider value={{
|
|
64
|
+
pendingPrefill,
|
|
65
|
+
setPrefill,
|
|
66
|
+
clearPrefill,
|
|
67
|
+
currentRequestPayload,
|
|
68
|
+
setCurrentRequestPayload,
|
|
69
|
+
triggerSend,
|
|
70
|
+
requestSend,
|
|
71
|
+
clearSendTrigger,
|
|
72
|
+
}}>
|
|
73
|
+
{children}
|
|
74
|
+
</PlaygroundContext.Provider>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function usePlaygroundPrefill() {
|
|
79
|
+
const context = useContext(PlaygroundContext)
|
|
80
|
+
if (!context) {
|
|
81
|
+
throw new Error('usePlaygroundPrefill must be used within a PlaygroundProvider')
|
|
82
|
+
}
|
|
83
|
+
return context
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function useCurrentRequestPayload() {
|
|
87
|
+
const context = useContext(PlaygroundContext)
|
|
88
|
+
if (!context) {
|
|
89
|
+
throw new Error('useCurrentRequestPayload must be used within a PlaygroundProvider')
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
currentRequestPayload: context.currentRequestPayload,
|
|
93
|
+
setCurrentRequestPayload: context.setCurrentRequestPayload,
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function useSendTrigger() {
|
|
98
|
+
const context = useContext(PlaygroundContext)
|
|
99
|
+
if (!context) {
|
|
100
|
+
throw new Error('useSendTrigger must be used within a PlaygroundProvider')
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
triggerSend: context.triggerSend,
|
|
104
|
+
requestSend: context.requestSend,
|
|
105
|
+
clearSendTrigger: context.clearSendTrigger,
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext, useState, useCallback, ReactNode } from 'react'
|
|
4
|
+
|
|
5
|
+
// Tabs that can be navigated to
|
|
6
|
+
export type PlaygroundTab = 'params' | 'body' | 'headers' | 'auth'
|
|
7
|
+
|
|
8
|
+
// Fields that can be highlighted
|
|
9
|
+
export type HighlightField =
|
|
10
|
+
| { type: 'param'; key?: string }
|
|
11
|
+
| { type: 'header'; key?: string }
|
|
12
|
+
| { type: 'body' }
|
|
13
|
+
| { type: 'auth'; field?: 'apiKey' | 'bearerToken' | 'basicUsername' | 'basicPassword' }
|
|
14
|
+
|
|
15
|
+
interface PlaygroundNavigationState {
|
|
16
|
+
/** Currently active tab (controlled externally) */
|
|
17
|
+
activeTab: PlaygroundTab | null
|
|
18
|
+
/** Field to highlight */
|
|
19
|
+
highlightField: HighlightField | null
|
|
20
|
+
/** Whether to show error styling on highlighted field */
|
|
21
|
+
showError: boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface PlaygroundNavigationContextType {
|
|
25
|
+
state: PlaygroundNavigationState
|
|
26
|
+
/** Navigate to a specific tab */
|
|
27
|
+
navigateToTab: (tab: PlaygroundTab) => void
|
|
28
|
+
/** Navigate to tab and highlight a field */
|
|
29
|
+
navigateAndHighlight: (tab: PlaygroundTab, field?: HighlightField, showError?: boolean) => void
|
|
30
|
+
/** Clear highlight */
|
|
31
|
+
clearHighlight: () => void
|
|
32
|
+
/** Set active tab (called by RequestTabs) */
|
|
33
|
+
setActiveTab: (tab: PlaygroundTab) => void
|
|
34
|
+
/** Reset navigation state (called when endpoint changes) */
|
|
35
|
+
resetNavigation: () => void
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const PlaygroundNavigationContext = createContext<PlaygroundNavigationContextType | undefined>(undefined)
|
|
39
|
+
|
|
40
|
+
export function PlaygroundNavigationProvider({ children }: { children: ReactNode }) {
|
|
41
|
+
const [state, setState] = useState<PlaygroundNavigationState>({
|
|
42
|
+
activeTab: null,
|
|
43
|
+
highlightField: null,
|
|
44
|
+
showError: false,
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const navigateToTab = useCallback((tab: PlaygroundTab) => {
|
|
48
|
+
setState(prev => ({
|
|
49
|
+
...prev,
|
|
50
|
+
activeTab: tab,
|
|
51
|
+
highlightField: null,
|
|
52
|
+
showError: false,
|
|
53
|
+
}))
|
|
54
|
+
}, [])
|
|
55
|
+
|
|
56
|
+
const navigateAndHighlight = useCallback((
|
|
57
|
+
tab: PlaygroundTab,
|
|
58
|
+
field?: HighlightField,
|
|
59
|
+
showError: boolean = true
|
|
60
|
+
) => {
|
|
61
|
+
setState({
|
|
62
|
+
activeTab: tab,
|
|
63
|
+
highlightField: field || null,
|
|
64
|
+
showError,
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
// Auto-clear highlight after a delay
|
|
68
|
+
if (field) {
|
|
69
|
+
setTimeout(() => {
|
|
70
|
+
setState(prev => ({
|
|
71
|
+
...prev,
|
|
72
|
+
highlightField: null,
|
|
73
|
+
showError: false,
|
|
74
|
+
}))
|
|
75
|
+
}, 3000)
|
|
76
|
+
}
|
|
77
|
+
}, [])
|
|
78
|
+
|
|
79
|
+
const clearHighlight = useCallback(() => {
|
|
80
|
+
setState(prev => ({
|
|
81
|
+
...prev,
|
|
82
|
+
highlightField: null,
|
|
83
|
+
showError: false,
|
|
84
|
+
}))
|
|
85
|
+
}, [])
|
|
86
|
+
|
|
87
|
+
const setActiveTab = useCallback((tab: PlaygroundTab) => {
|
|
88
|
+
setState(prev => ({
|
|
89
|
+
...prev,
|
|
90
|
+
activeTab: tab,
|
|
91
|
+
}))
|
|
92
|
+
}, [])
|
|
93
|
+
|
|
94
|
+
const resetNavigation = useCallback(() => {
|
|
95
|
+
setState({
|
|
96
|
+
activeTab: null,
|
|
97
|
+
highlightField: null,
|
|
98
|
+
showError: false,
|
|
99
|
+
})
|
|
100
|
+
}, [])
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<PlaygroundNavigationContext.Provider
|
|
104
|
+
value={{
|
|
105
|
+
state,
|
|
106
|
+
navigateToTab,
|
|
107
|
+
navigateAndHighlight,
|
|
108
|
+
clearHighlight,
|
|
109
|
+
setActiveTab,
|
|
110
|
+
resetNavigation,
|
|
111
|
+
}}
|
|
112
|
+
>
|
|
113
|
+
{children}
|
|
114
|
+
</PlaygroundNavigationContext.Provider>
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function usePlaygroundNavigation() {
|
|
119
|
+
const context = useContext(PlaygroundNavigationContext)
|
|
120
|
+
if (context === undefined) {
|
|
121
|
+
throw new Error('usePlaygroundNavigation must be used within a PlaygroundNavigationProvider')
|
|
122
|
+
}
|
|
123
|
+
return context
|
|
124
|
+
}
|