@brainfish-ai/devdoc 0.1.22 → 0.1.24
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/README.md +7 -3
- package/dist/cli/commands/create.js +19 -49
- package/dist/cli/commands/deploy.js +174 -14
- package/dist/cli/commands/dev.d.ts +1 -0
- package/dist/cli/commands/dev.js +175 -18
- package/dist/cli/commands/init.d.ts +2 -1
- package/dist/cli/commands/init.js +26 -64
- package/dist/cli/index.js +3 -1
- package/package.json +1 -1
- package/renderer/components/docs-viewer/index.tsx +198 -54
- package/renderer/components/docs-viewer/sidebar/index.tsx +3 -95
- package/renderer/lib/api-docs/factories.ts +45 -26
- package/renderer/lib/api-docs/parsers/graphql/parser.ts +1 -1
- package/renderer/lib/api-docs/parsers/openapi/transformer.ts +1 -0
- package/renderer/lib/docs/config/schema.ts +11 -1
- package/renderer/components/docs-viewer/content/introduction.tsx +0 -21
|
@@ -7,7 +7,6 @@ import { DocsSidebar } from './sidebar'
|
|
|
7
7
|
import { ApiPlayground } from './playground'
|
|
8
8
|
import type { DebugContext } from './playground/response-viewer'
|
|
9
9
|
import { RightSidebar } from './sidebar/right-sidebar'
|
|
10
|
-
import { Introduction } from './content/introduction'
|
|
11
10
|
import { RequestDetails } from './content/request-details'
|
|
12
11
|
import { DocPage } from './content/doc-page'
|
|
13
12
|
import { ChangelogPage } from './content/changelog-page'
|
|
@@ -241,6 +240,53 @@ function findRequestById(collection: BrainfishCollection, id: string): Brainfish
|
|
|
241
240
|
return null
|
|
242
241
|
}
|
|
243
242
|
|
|
243
|
+
// Helper to get the first endpoint from collection
|
|
244
|
+
function getFirstEndpoint(collection: BrainfishCollection): BrainfishRESTRequest | null {
|
|
245
|
+
// Check direct requests first
|
|
246
|
+
if (collection.requests.length > 0) {
|
|
247
|
+
return collection.requests[0]
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Check folders recursively
|
|
251
|
+
for (const folder of collection.folders) {
|
|
252
|
+
const firstInFolder = getFirstEndpoint(folder)
|
|
253
|
+
if (firstInFolder) return firstInFolder
|
|
254
|
+
}
|
|
255
|
+
return null
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Helper to check if a doc group belongs to a specific tab
|
|
259
|
+
// Group IDs are formatted as: group-{tab-id}-{group-name}
|
|
260
|
+
function isGroupForTab(groupId: string, tabId: string): boolean {
|
|
261
|
+
// Check if the group ID starts with "group-{tabId}-"
|
|
262
|
+
const prefix = `group-${tabId}-`
|
|
263
|
+
return groupId.startsWith(prefix)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Helper to check if a tab has doc groups (MDX pages)
|
|
267
|
+
function hasDocGroupsForTab(
|
|
268
|
+
docGroups: BrainfishDocGroup[] | undefined,
|
|
269
|
+
tabId: string
|
|
270
|
+
): boolean {
|
|
271
|
+
if (!docGroups || docGroups.length === 0) return false
|
|
272
|
+
|
|
273
|
+
// Check if any doc group belongs to this tab and has pages
|
|
274
|
+
return docGroups.some(g => isGroupForTab(g.id, tabId) && g.pages.length > 0)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Helper to get the first page from doc groups for a tab
|
|
278
|
+
function getFirstDocPageForTab(
|
|
279
|
+
docGroups: BrainfishDocGroup[] | undefined,
|
|
280
|
+
tabId: string
|
|
281
|
+
): string | null {
|
|
282
|
+
if (!docGroups || docGroups.length === 0) return null
|
|
283
|
+
|
|
284
|
+
const tabDocGroup = docGroups.find(g => isGroupForTab(g.id, tabId) && g.pages.length > 0)
|
|
285
|
+
|
|
286
|
+
return tabDocGroup?.pages[0]?.slug || null
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
|
|
244
290
|
// API version
|
|
245
291
|
interface ApiVersion {
|
|
246
292
|
version: string
|
|
@@ -391,19 +437,21 @@ function DocsContent() {
|
|
|
391
437
|
const navigateToHash = useCallback((collectionData: BrainfishCollection) => {
|
|
392
438
|
const hash = window.location.hash.slice(1) // Remove #
|
|
393
439
|
|
|
394
|
-
console.log('[Docs] Navigating to hash:', hash)
|
|
395
|
-
|
|
396
440
|
if (!hash) {
|
|
397
|
-
// No hash -
|
|
398
|
-
setSelectedRequest(null)
|
|
441
|
+
// No hash - auto-select first endpoint
|
|
399
442
|
setSelectedDocSection(null)
|
|
400
443
|
setSelectedDocPage(null)
|
|
444
|
+
const firstEndpoint = getFirstEndpoint(collectionData)
|
|
445
|
+
if (firstEndpoint) {
|
|
446
|
+
setSelectedRequest(firstEndpoint)
|
|
447
|
+
} else {
|
|
448
|
+
setSelectedRequest(null)
|
|
449
|
+
}
|
|
401
450
|
return
|
|
402
451
|
}
|
|
403
452
|
|
|
404
453
|
// Notes mode is handled by ModeContext, just clear API selection
|
|
405
454
|
if (hash === 'notes' || hash.startsWith('notes/')) {
|
|
406
|
-
console.log('[Docs] Notes mode detected, clearing API selection')
|
|
407
455
|
setSelectedRequest(null)
|
|
408
456
|
setSelectedDocSection(null)
|
|
409
457
|
setSelectedDocPage(null)
|
|
@@ -428,9 +476,7 @@ function DocsContent() {
|
|
|
428
476
|
const actualId = id || (legacyType ? hash.replace(`${legacyType}/`, '') : null)
|
|
429
477
|
|
|
430
478
|
if (actualType === 'endpoint' && actualId) {
|
|
431
|
-
console.log('[Docs] Looking for endpoint:', actualId)
|
|
432
479
|
const request = findRequestById(collectionData, actualId)
|
|
433
|
-
console.log('[Docs] Found request:', request?.name || 'NOT FOUND')
|
|
434
480
|
if (request) {
|
|
435
481
|
setSelectedRequest(request)
|
|
436
482
|
setSelectedDocSection(null)
|
|
@@ -598,18 +644,55 @@ function DocsContent() {
|
|
|
598
644
|
// Reset mode to docs when switching tabs (exit sandbox/api-client mode)
|
|
599
645
|
switchToDocs()
|
|
600
646
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
updateUrlHash('', tabId)
|
|
607
|
-
} else if (tabId === 'changelog') {
|
|
647
|
+
// Find the tab config to check its type
|
|
648
|
+
const tabConfig = collection?.navigationTabs?.find(t => t.id === tabId)
|
|
649
|
+
const isApiTab = tabConfig?.type === 'openapi' || tabConfig?.type === 'graphql' || tabId === 'api-reference'
|
|
650
|
+
|
|
651
|
+
if (tabId === 'changelog') {
|
|
608
652
|
// Switch to Changelog tab
|
|
609
653
|
setSelectedDocPage(null)
|
|
610
654
|
setSelectedRequest(null)
|
|
611
655
|
setSelectedDocSection(null)
|
|
612
656
|
updateUrlHash('', tabId)
|
|
657
|
+
} else if (isApiTab) {
|
|
658
|
+
// API Reference or GraphQL tab
|
|
659
|
+
setSelectedDocSection(null)
|
|
660
|
+
|
|
661
|
+
// Check if there are doc groups for this tab (MDX pages)
|
|
662
|
+
const hasGroups = hasDocGroupsForTab(collection?.docGroups, tabId)
|
|
663
|
+
|
|
664
|
+
if (hasGroups) {
|
|
665
|
+
// Has doc groups - select the first page from groups
|
|
666
|
+
const firstPage = getFirstDocPageForTab(collection?.docGroups, tabId)
|
|
667
|
+
if (firstPage) {
|
|
668
|
+
setSelectedDocPage(firstPage)
|
|
669
|
+
setSelectedRequest(null)
|
|
670
|
+
updateUrlHash(`page/${firstPage}`, tabId)
|
|
671
|
+
switchToDocs()
|
|
672
|
+
} else {
|
|
673
|
+
// No pages in groups - auto-select first endpoint
|
|
674
|
+
setSelectedDocPage(null)
|
|
675
|
+
const firstEndpoint = collection ? getFirstEndpoint(collection) : null
|
|
676
|
+
if (firstEndpoint) {
|
|
677
|
+
setSelectedRequest(firstEndpoint)
|
|
678
|
+
updateUrlHash(`endpoint/${firstEndpoint.id}`, tabId)
|
|
679
|
+
} else {
|
|
680
|
+
setSelectedRequest(null)
|
|
681
|
+
updateUrlHash('', tabId)
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
} else {
|
|
685
|
+
// No doc groups - auto-select first endpoint
|
|
686
|
+
setSelectedDocPage(null)
|
|
687
|
+
const firstEndpoint = collection ? getFirstEndpoint(collection) : null
|
|
688
|
+
if (firstEndpoint) {
|
|
689
|
+
setSelectedRequest(firstEndpoint)
|
|
690
|
+
updateUrlHash(`endpoint/${firstEndpoint.id}`, tabId)
|
|
691
|
+
} else {
|
|
692
|
+
setSelectedRequest(null)
|
|
693
|
+
updateUrlHash('', tabId)
|
|
694
|
+
}
|
|
695
|
+
}
|
|
613
696
|
} else {
|
|
614
697
|
// Switch to a doc group tab - find and select the first page in that tab
|
|
615
698
|
setSelectedRequest(null)
|
|
@@ -617,10 +700,7 @@ function DocsContent() {
|
|
|
617
700
|
|
|
618
701
|
// Find the first doc group for this tab and select its first page
|
|
619
702
|
if (collection?.docGroups) {
|
|
620
|
-
const tabDocGroup = collection.docGroups.find(g =>
|
|
621
|
-
const groupTabPart = g.id.replace('group-', '').split('-')[0]
|
|
622
|
-
return groupTabPart === tabId
|
|
623
|
-
})
|
|
703
|
+
const tabDocGroup = collection.docGroups.find(g => isGroupForTab(g.id, tabId))
|
|
624
704
|
|
|
625
705
|
if (tabDocGroup && tabDocGroup.pages.length > 0) {
|
|
626
706
|
const firstPage = tabDocGroup.pages[0]
|
|
@@ -733,8 +813,6 @@ function DocsContent() {
|
|
|
733
813
|
// Also update shortcut icon and apple-touch-icon
|
|
734
814
|
updateFaviconLink('shortcut icon', faviconPath)
|
|
735
815
|
updateFaviconLink('apple-touch-icon', faviconPath)
|
|
736
|
-
|
|
737
|
-
console.log('[Docs] Set favicon to:', faviconPath)
|
|
738
816
|
}, [collection?.docsFavicon])
|
|
739
817
|
|
|
740
818
|
useEffect(() => {
|
|
@@ -761,16 +839,6 @@ function DocsContent() {
|
|
|
761
839
|
}
|
|
762
840
|
|
|
763
841
|
const data = await response.json()
|
|
764
|
-
|
|
765
|
-
console.log('[Docs] Collection data:', JSON.stringify({
|
|
766
|
-
hasData: !!data,
|
|
767
|
-
name: data?.name,
|
|
768
|
-
requestsCount: data?.requests?.length || 0,
|
|
769
|
-
foldersCount: data?.folders?.length || 0,
|
|
770
|
-
apiVersions: data?.apiVersions?.length || 0,
|
|
771
|
-
selectedVersion: data?.selectedApiVersion,
|
|
772
|
-
}, null, 2))
|
|
773
|
-
|
|
774
842
|
setCollection(data)
|
|
775
843
|
|
|
776
844
|
// Set initial API version if not already set
|
|
@@ -793,24 +861,60 @@ function DocsContent() {
|
|
|
793
861
|
// Use setTimeout to ensure state is set before navigation
|
|
794
862
|
setTimeout(() => {
|
|
795
863
|
const hash = window.location.hash.slice(1)
|
|
796
|
-
console.log('[Docs] Initial hash:', hash)
|
|
797
864
|
|
|
798
865
|
if (!hash) {
|
|
799
866
|
// No hash - set URL to initial tab
|
|
800
|
-
updateUrlHash('', initialTabId)
|
|
801
|
-
setSelectedRequest(null)
|
|
802
867
|
setSelectedDocSection(null)
|
|
803
868
|
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
869
|
+
// Find the tab config to check its type
|
|
870
|
+
const tabConfig = data.navigationTabs?.find((t: NavigationTab) => t.id === initialTabId)
|
|
871
|
+
const isApiTab = tabConfig?.type === 'openapi' || tabConfig?.type === 'graphql' || initialTabId === 'api-reference'
|
|
872
|
+
|
|
873
|
+
if (isApiTab) {
|
|
874
|
+
// API Reference or GraphQL tab
|
|
875
|
+
setSelectedDocSection(null)
|
|
876
|
+
|
|
877
|
+
// Check if there are doc groups for this tab (MDX pages)
|
|
878
|
+
const hasGroups = hasDocGroupsForTab(data.docGroups, initialTabId)
|
|
879
|
+
|
|
880
|
+
if (hasGroups) {
|
|
881
|
+
// Has doc groups - select the first page from groups
|
|
882
|
+
const firstPage = getFirstDocPageForTab(data.docGroups, initialTabId)
|
|
883
|
+
if (firstPage) {
|
|
884
|
+
setSelectedDocPage(firstPage)
|
|
885
|
+
setSelectedRequest(null)
|
|
886
|
+
updateUrlHash(`page/${firstPage}`, initialTabId)
|
|
887
|
+
} else {
|
|
888
|
+
// No pages in groups - auto-select first endpoint
|
|
889
|
+
setSelectedDocPage(null)
|
|
890
|
+
const firstEndpoint = getFirstEndpoint(data)
|
|
891
|
+
if (firstEndpoint) {
|
|
892
|
+
setSelectedRequest(firstEndpoint)
|
|
893
|
+
updateUrlHash(`endpoint/${firstEndpoint.id}`, initialTabId)
|
|
894
|
+
} else {
|
|
895
|
+
setSelectedRequest(null)
|
|
896
|
+
updateUrlHash('', initialTabId)
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
} else {
|
|
900
|
+
// No doc groups - auto-select first endpoint
|
|
901
|
+
setSelectedDocPage(null)
|
|
902
|
+
const firstEndpoint = getFirstEndpoint(data)
|
|
903
|
+
if (firstEndpoint) {
|
|
904
|
+
setSelectedRequest(firstEndpoint)
|
|
905
|
+
updateUrlHash(`endpoint/${firstEndpoint.id}`, initialTabId)
|
|
906
|
+
} else {
|
|
907
|
+
setSelectedRequest(null)
|
|
908
|
+
updateUrlHash('', initialTabId)
|
|
909
|
+
}
|
|
910
|
+
}
|
|
807
911
|
switchToDocs()
|
|
808
912
|
} else {
|
|
809
913
|
// Doc group tab - select first page
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
914
|
+
setSelectedRequest(null)
|
|
915
|
+
const tabDocGroup = data.docGroups?.find((g: BrainfishDocGroup) =>
|
|
916
|
+
isGroupForTab(g.id, initialTabId)
|
|
917
|
+
)
|
|
814
918
|
|
|
815
919
|
if (tabDocGroup && tabDocGroup.pages.length > 0) {
|
|
816
920
|
const firstPage = tabDocGroup.pages[0]
|
|
@@ -819,12 +923,12 @@ function DocsContent() {
|
|
|
819
923
|
switchToDocs()
|
|
820
924
|
} else {
|
|
821
925
|
setSelectedDocPage(null)
|
|
926
|
+
updateUrlHash('', initialTabId)
|
|
822
927
|
switchToDocs()
|
|
823
928
|
}
|
|
824
929
|
}
|
|
825
930
|
} else if (hash === 'notes' || hash.startsWith('notes/')) {
|
|
826
931
|
// Notes mode - handled by ModeContext, just clear API selection
|
|
827
|
-
console.log('[Docs] Initial hash is notes mode')
|
|
828
932
|
setSelectedRequest(null)
|
|
829
933
|
setSelectedDocSection(null)
|
|
830
934
|
} else {
|
|
@@ -846,14 +950,13 @@ function DocsContent() {
|
|
|
846
950
|
const actualId = isLegacyFormat ? parts.slice(1).join('/') : hashId
|
|
847
951
|
|
|
848
952
|
if (actualType === 'endpoint' && actualId) {
|
|
849
|
-
console.log('[Docs] Looking for endpoint on load:', actualId)
|
|
850
953
|
const request = findRequestById(data, actualId)
|
|
851
|
-
console.log('[Docs] Found:', request?.name || 'NOT FOUND')
|
|
852
954
|
if (request) {
|
|
853
955
|
setSelectedRequest(request)
|
|
854
956
|
setSelectedDocSection(null)
|
|
855
957
|
setSelectedDocPage(null)
|
|
856
958
|
switchToDocs()
|
|
959
|
+
return
|
|
857
960
|
} else {
|
|
858
961
|
setSelectedRequest(null)
|
|
859
962
|
setSelectedDocSection(null)
|
|
@@ -878,9 +981,49 @@ function DocsContent() {
|
|
|
878
981
|
}, 100)
|
|
879
982
|
} else if (hashTab && !hashType) {
|
|
880
983
|
// Just a tab, show its default content
|
|
881
|
-
setSelectedRequest(null)
|
|
882
984
|
setSelectedDocSection(null)
|
|
883
|
-
|
|
985
|
+
|
|
986
|
+
// Check if this is an API tab
|
|
987
|
+
const tabConfig = data.navigationTabs?.find((t: NavigationTab) => t.id === hashTab)
|
|
988
|
+
const isApiTab = tabConfig?.type === 'openapi' || tabConfig?.type === 'graphql' || hashTab === 'api-reference'
|
|
989
|
+
|
|
990
|
+
if (isApiTab) {
|
|
991
|
+
// Check if there are doc groups for this tab (MDX pages)
|
|
992
|
+
const hasGroups = hasDocGroupsForTab(data.docGroups, hashTab)
|
|
993
|
+
|
|
994
|
+
if (hasGroups) {
|
|
995
|
+
// Has doc groups - select the first page from groups
|
|
996
|
+
const firstPage = getFirstDocPageForTab(data.docGroups, hashTab)
|
|
997
|
+
if (firstPage) {
|
|
998
|
+
setSelectedDocPage(firstPage)
|
|
999
|
+
setSelectedRequest(null)
|
|
1000
|
+
updateUrlHash(`page/${firstPage}`, hashTab)
|
|
1001
|
+
} else {
|
|
1002
|
+
// No pages in groups - auto-select first endpoint
|
|
1003
|
+
setSelectedDocPage(null)
|
|
1004
|
+
const firstEndpoint = getFirstEndpoint(data)
|
|
1005
|
+
if (firstEndpoint) {
|
|
1006
|
+
setSelectedRequest(firstEndpoint)
|
|
1007
|
+
updateUrlHash(`endpoint/${firstEndpoint.id}`, hashTab)
|
|
1008
|
+
} else {
|
|
1009
|
+
setSelectedRequest(null)
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
} else {
|
|
1013
|
+
// No doc groups - auto-select first endpoint
|
|
1014
|
+
setSelectedDocPage(null)
|
|
1015
|
+
const firstEndpoint = getFirstEndpoint(data)
|
|
1016
|
+
if (firstEndpoint) {
|
|
1017
|
+
setSelectedRequest(firstEndpoint)
|
|
1018
|
+
updateUrlHash(`endpoint/${firstEndpoint.id}`, hashTab)
|
|
1019
|
+
} else {
|
|
1020
|
+
setSelectedRequest(null)
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
} else {
|
|
1024
|
+
setSelectedDocPage(null)
|
|
1025
|
+
setSelectedRequest(null)
|
|
1026
|
+
}
|
|
884
1027
|
switchToDocs()
|
|
885
1028
|
}
|
|
886
1029
|
}
|
|
@@ -1170,17 +1313,13 @@ function DocsWithMode({
|
|
|
1170
1313
|
// Find the active tab's config
|
|
1171
1314
|
const activeTabConfig = collection.navigationTabs?.find(t => t.id === activeTab)
|
|
1172
1315
|
const activeTabType = activeTabConfig?.type || 'docs'
|
|
1173
|
-
// Use the tab name for page header
|
|
1174
|
-
const activeTabTitle = activeTabConfig?.tab || 'Documentation'
|
|
1175
1316
|
|
|
1176
1317
|
|
|
1177
1318
|
// Filter doc groups for sidebar based on active tab
|
|
1178
1319
|
// Group IDs are like "group-guides-getting-started", tab IDs are like "guides"
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
return groupTabPart === activeTab
|
|
1183
|
-
}) || []
|
|
1320
|
+
// Filter doc groups for sidebar based on active tab
|
|
1321
|
+
// Group IDs are formatted as: group-{tab-id}-{group-name}
|
|
1322
|
+
const filteredDocGroups = collection.docGroups?.filter(g => isGroupForTab(g.id, activeTab)) || []
|
|
1184
1323
|
|
|
1185
1324
|
// Show endpoints in sidebar only when OpenAPI tab is active
|
|
1186
1325
|
const showEndpoints = activeTabType === 'openapi'
|
|
@@ -1386,7 +1525,12 @@ function DocsWithMode({
|
|
|
1386
1525
|
) : selectedDocPage ? (
|
|
1387
1526
|
<DocPage slug={selectedDocPage} />
|
|
1388
1527
|
) : (
|
|
1389
|
-
<
|
|
1528
|
+
<div className="flex-1 flex items-center justify-center bg-background">
|
|
1529
|
+
<div className="text-center text-muted-foreground">
|
|
1530
|
+
<Book className="h-12 w-12 mx-auto mb-3 opacity-40" />
|
|
1531
|
+
<p className="text-sm">Select an endpoint from the sidebar</p>
|
|
1532
|
+
</div>
|
|
1533
|
+
</div>
|
|
1390
1534
|
)}
|
|
1391
1535
|
</div>
|
|
1392
1536
|
</DocsNavigationProvider>
|
|
@@ -15,7 +15,6 @@ import { CollectionTree } from './collection-tree'
|
|
|
15
15
|
import { SidebarSection } from './sidebar-section'
|
|
16
16
|
import { SidebarItem, SlidingIndicatorProvider } from './sidebar-item'
|
|
17
17
|
import { SidebarGroup } from './sidebar-group'
|
|
18
|
-
import { extractMarkdownHeadings, type MarkdownHeading } from '@/lib/api-docs/utils'
|
|
19
18
|
import { useMobile } from '@/lib/api-docs/mobile-context'
|
|
20
19
|
import { cn } from '@/lib/utils'
|
|
21
20
|
|
|
@@ -84,92 +83,8 @@ export function DocsSidebar({
|
|
|
84
83
|
}
|
|
85
84
|
}
|
|
86
85
|
|
|
87
|
-
// Extract documentation headings from collection description
|
|
88
|
-
const docHeadings = useMemo(() => {
|
|
89
|
-
return collection.description
|
|
90
|
-
? extractMarkdownHeadings(collection.description)
|
|
91
|
-
: []
|
|
92
|
-
}, [collection.description])
|
|
93
|
-
|
|
94
86
|
// Get doc groups from collection
|
|
95
87
|
const docGroups = collection.docGroups || []
|
|
96
|
-
|
|
97
|
-
const handleDocClick = (headingId: string) => {
|
|
98
|
-
handleDocClickWithClose(headingId)
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Determine if Introduction should be highlighted
|
|
102
|
-
const isIntroductionSelected = !selectedRequest && !selectedDocSection && !selectedDocPage
|
|
103
|
-
|
|
104
|
-
// Check if a heading or any of its children is selected
|
|
105
|
-
const isHeadingSelected = (heading: MarkdownHeading): boolean => {
|
|
106
|
-
if (selectedDocSection === heading.id) return true
|
|
107
|
-
if (heading.children?.some(child => selectedDocSection === child.id)) return true
|
|
108
|
-
return false
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Get the selected child heading ID for a heading group
|
|
112
|
-
const getSelectedChildHeading = (heading: MarkdownHeading): string | null => {
|
|
113
|
-
if (selectedDocSection === heading.id) return heading.id
|
|
114
|
-
const selected = heading.children?.find(child => selectedDocSection === child.id)
|
|
115
|
-
return selected?.id || null
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Render a documentation heading item (may have children)
|
|
119
|
-
const renderHeading = (heading: MarkdownHeading, index: number) => {
|
|
120
|
-
const hasChildren = heading.children && heading.children.length > 0
|
|
121
|
-
const isSelected = selectedDocSection === heading.id
|
|
122
|
-
|
|
123
|
-
// If heading is "Introduction", handle it specially
|
|
124
|
-
if (heading.text === 'Introduction') {
|
|
125
|
-
return (
|
|
126
|
-
<SidebarItem
|
|
127
|
-
key={`${heading.id}-${index}`}
|
|
128
|
-
itemId={`heading-introduction`}
|
|
129
|
-
selected={isIntroductionSelected}
|
|
130
|
-
onClick={() => handleDocClick('introduction')}
|
|
131
|
-
>
|
|
132
|
-
Introduction
|
|
133
|
-
</SidebarItem>
|
|
134
|
-
)
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if (hasChildren) {
|
|
138
|
-
return (
|
|
139
|
-
<SidebarGroup
|
|
140
|
-
key={`${heading.id}-${index}`}
|
|
141
|
-
title={heading.text}
|
|
142
|
-
defaultOpen={isHeadingSelected(heading)}
|
|
143
|
-
selected={isSelected}
|
|
144
|
-
selectedChildSlug={getSelectedChildHeading(heading)}
|
|
145
|
-
onClick={() => handleDocClick(heading.id)}
|
|
146
|
-
>
|
|
147
|
-
{heading.children!.map((child, childIndex) => (
|
|
148
|
-
<SidebarItem
|
|
149
|
-
key={`${child.id}-${childIndex}`}
|
|
150
|
-
itemId={`heading-${child.id}`}
|
|
151
|
-
selected={selectedDocSection === child.id}
|
|
152
|
-
indent={1}
|
|
153
|
-
onClick={() => handleDocClick(child.id)}
|
|
154
|
-
>
|
|
155
|
-
{child.text}
|
|
156
|
-
</SidebarItem>
|
|
157
|
-
))}
|
|
158
|
-
</SidebarGroup>
|
|
159
|
-
)
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return (
|
|
163
|
-
<SidebarItem
|
|
164
|
-
key={`${heading.id}-${index}`}
|
|
165
|
-
itemId={`heading-${heading.id}`}
|
|
166
|
-
selected={isSelected}
|
|
167
|
-
onClick={() => handleDocClick(heading.id)}
|
|
168
|
-
>
|
|
169
|
-
{heading.text}
|
|
170
|
-
</SidebarItem>
|
|
171
|
-
)
|
|
172
|
-
}
|
|
173
88
|
|
|
174
89
|
// Check if any child in a group is selected
|
|
175
90
|
const isChildSelected = (page: BrainfishDocPage): boolean => {
|
|
@@ -300,14 +215,7 @@ export function DocsSidebar({
|
|
|
300
215
|
|
|
301
216
|
{/* Scrollable content with sliding indicator */}
|
|
302
217
|
<SlidingIndicatorProvider className="docs-sidebar-content flex-1 overflow-y-auto overflow-x-hidden custom-scroll">
|
|
303
|
-
{/* Documentation Section (from
|
|
304
|
-
{activeTab === 'api-reference' && docHeadings.length > 0 && (
|
|
305
|
-
<SidebarSection title="Documentation">
|
|
306
|
-
{docHeadings.map((heading, index) => renderHeading(heading, index))}
|
|
307
|
-
</SidebarSection>
|
|
308
|
-
)}
|
|
309
|
-
|
|
310
|
-
{/* Documentation Pages Section (from docs.json) */}
|
|
218
|
+
{/* Documentation Pages Section (from docs.json groups) */}
|
|
311
219
|
{docGroups.length > 0 && (
|
|
312
220
|
<>
|
|
313
221
|
{docGroups.map((group, index) => (
|
|
@@ -315,7 +223,7 @@ export function DocsSidebar({
|
|
|
315
223
|
key={group.id}
|
|
316
224
|
title={group.title}
|
|
317
225
|
icon={group.icon}
|
|
318
|
-
className={index > 0
|
|
226
|
+
className={index > 0 ? '' : ''}
|
|
319
227
|
>
|
|
320
228
|
{group.pages.map(renderDocPage)}
|
|
321
229
|
</SidebarSection>
|
|
@@ -327,7 +235,7 @@ export function DocsSidebar({
|
|
|
327
235
|
{(collection.folders.length > 0 || collection.requests.length > 0) && (
|
|
328
236
|
<SidebarSection
|
|
329
237
|
title="Endpoints"
|
|
330
|
-
className={
|
|
238
|
+
className={docGroups.length > 0 ? 'border-t border-sidebar-border' : ''}
|
|
331
239
|
>
|
|
332
240
|
<CollectionTree
|
|
333
241
|
collection={collection}
|
|
@@ -11,39 +11,61 @@ import type {
|
|
|
11
11
|
} from './types'
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* Generates a
|
|
15
|
-
*
|
|
14
|
+
* Generates a URL-friendly slug from a string
|
|
15
|
+
* Converts to lowercase, replaces spaces and special chars with hyphens
|
|
16
16
|
*/
|
|
17
|
-
function
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
17
|
+
function slugify(str: string): string {
|
|
18
|
+
return str
|
|
19
|
+
.toLowerCase()
|
|
20
|
+
.replace(/[{}]/g, '') // Remove braces from path params
|
|
21
|
+
.replace(/[^a-z0-9]+/g, '-') // Replace non-alphanumeric with hyphens
|
|
22
|
+
.replace(/^-+|-+$/g, '') // Remove leading/trailing hyphens
|
|
23
|
+
.replace(/-+/g, '-') // Collapse multiple hyphens
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Generates a human-readable, deterministic ID for requests
|
|
28
|
+
* Uses operationId if available, otherwise method + path
|
|
29
|
+
*/
|
|
30
|
+
function generateRequestId(method: string, path: string, operationId?: string): string {
|
|
31
|
+
if (operationId) {
|
|
32
|
+
// Use operationId directly (it's already unique per spec)
|
|
33
|
+
return slugify(operationId)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Generate from method + path: "get-users-id" for "GET /users/{id}"
|
|
37
|
+
const pathSlug = slugify(path)
|
|
38
|
+
return `${method.toLowerCase()}-${pathSlug}`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Generates a unique ID for collections
|
|
43
|
+
* If a seed is provided, generates a deterministic slug-based ID
|
|
44
|
+
*/
|
|
45
|
+
function generateCollectionId(name?: string): string {
|
|
46
|
+
if (name) {
|
|
47
|
+
return `coll-${slugify(name)}`
|
|
29
48
|
}
|
|
30
|
-
return
|
|
49
|
+
return `coll_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
|
31
50
|
}
|
|
32
51
|
|
|
33
52
|
/**
|
|
34
53
|
* Creates a BrainfishRESTRequest with defaults
|
|
35
|
-
*
|
|
54
|
+
* Generates human-readable, deterministic IDs based on operationId or method+path
|
|
36
55
|
*/
|
|
37
56
|
export function makeBrainfishRESTRequest(
|
|
38
|
-
request: Omit<BrainfishRESTRequest, 'id'>
|
|
57
|
+
request: Omit<BrainfishRESTRequest, 'id'> & { operationId?: string }
|
|
39
58
|
): BrainfishRESTRequest {
|
|
40
|
-
//
|
|
41
|
-
const
|
|
42
|
-
?
|
|
43
|
-
:
|
|
59
|
+
// Extract path from endpoint (remove base URL)
|
|
60
|
+
const path = request.endpoint
|
|
61
|
+
? request.endpoint.replace(/^https?:\/\/[^/]+/, '') || '/'
|
|
62
|
+
: '/'
|
|
63
|
+
|
|
64
|
+
// Generate human-readable ID
|
|
65
|
+
const id = generateRequestId(request.method || 'GET', path, request.operationId)
|
|
44
66
|
|
|
45
67
|
return {
|
|
46
|
-
id
|
|
68
|
+
id,
|
|
47
69
|
name: request.name || 'Untitled Request',
|
|
48
70
|
description: request.description ?? null,
|
|
49
71
|
method: request.method || 'GET',
|
|
@@ -65,11 +87,8 @@ export function makeBrainfishRESTRequest(
|
|
|
65
87
|
export function makeBrainfishCollection(
|
|
66
88
|
collection: Omit<BrainfishCollection, 'id'>
|
|
67
89
|
): BrainfishCollection {
|
|
68
|
-
// Generate deterministic ID from collection name
|
|
69
|
-
const idSeed = collection.name ? `collection:${collection.name}` : undefined
|
|
70
|
-
|
|
71
90
|
return {
|
|
72
|
-
id:
|
|
91
|
+
id: generateCollectionId(collection.name),
|
|
73
92
|
name: collection.name || 'Untitled Collection',
|
|
74
93
|
description: collection.description ?? null,
|
|
75
94
|
folders: collection.folders || [],
|
|
@@ -238,7 +238,7 @@ function extractOperations(
|
|
|
238
238
|
const fields = rootType.getFields()
|
|
239
239
|
|
|
240
240
|
return Object.values(fields).map(field => ({
|
|
241
|
-
id: `${operationType}-${field.name}
|
|
241
|
+
id: `${operationType}-${field.name}`.toLowerCase(),
|
|
242
242
|
name: field.name,
|
|
243
243
|
description: field.description || null,
|
|
244
244
|
operationType,
|
|
@@ -58,6 +58,16 @@ const openapiTabSchema = z.object({
|
|
|
58
58
|
path: z.string().optional(),
|
|
59
59
|
versions: z.array(openapiVersionSchema).optional(),
|
|
60
60
|
spec: z.string().optional(),
|
|
61
|
+
groups: z.array(groupSchema).optional(), // Doc groups alongside API reference
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const graphqlTabSchema = z.object({
|
|
65
|
+
tab: z.string(),
|
|
66
|
+
type: z.literal('graphql'),
|
|
67
|
+
path: z.string().optional(),
|
|
68
|
+
schema: z.string(), // GraphQL schema file path
|
|
69
|
+
endpoint: z.string().optional(), // GraphQL endpoint URL
|
|
70
|
+
groups: z.array(groupSchema).optional(), // Doc groups alongside API reference
|
|
61
71
|
})
|
|
62
72
|
|
|
63
73
|
const changelogTabSchema = z.object({
|
|
@@ -66,7 +76,7 @@ const changelogTabSchema = z.object({
|
|
|
66
76
|
path: z.string().optional(),
|
|
67
77
|
})
|
|
68
78
|
|
|
69
|
-
const tabSchema = z.union([docsTabSchema, openapiTabSchema, changelogTabSchema])
|
|
79
|
+
const tabSchema = z.union([docsTabSchema, openapiTabSchema, graphqlTabSchema, changelogTabSchema])
|
|
70
80
|
|
|
71
81
|
const navigationSchema = z.object({
|
|
72
82
|
tabs: z.array(tabSchema).optional(),
|