Skip to content

Commit 68c42d8

Browse files
committed
Add scroll-to-change navigation from diff sidebar
Clicking on a change in the diff sidebar now scrolls to that operation in the documentation viewer. Supports all three visualizers: - SwaggerUI: DOM-based scrolling with operation expansion - Stoplight: Hash-based navigation - Redocly: Hash set on iframe Also removes unused DiffDialog component.
1 parent 0d907a2 commit 68c42d8

File tree

9 files changed

+105
-147
lines changed

9 files changed

+105
-147
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { scrollToOperation } from "./scrollToOperation"
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { DocumentationVisualizer } from "@/features/settings/domain"
2+
3+
/**
4+
* Finds and scrolls to a SwaggerUI operation element.
5+
* SwaggerUI elements have IDs in the format: operations-{tag}-{operationId}
6+
* Since we may not know the tag, we search for elements containing the operationId.
7+
*/
8+
function scrollToSwaggerOperation(operationId: string): boolean {
9+
// SwaggerUI uses element IDs in the format: operations-{tag}-{operationId}
10+
const allOpBlocks = document.querySelectorAll('[id^="operations-"]')
11+
12+
for (let i = 0; i < allOpBlocks.length; i++) {
13+
const block = allOpBlocks[i]
14+
if (block.id.endsWith(`-${operationId}`)) {
15+
block.scrollIntoView({ behavior: "smooth", block: "start" })
16+
// Expand the operation by clicking on its control button
17+
const button = block.querySelector(".opblock-summary-control")
18+
if (button instanceof HTMLElement) {
19+
button.click()
20+
}
21+
return true
22+
}
23+
}
24+
25+
return false
26+
}
27+
28+
/**
29+
* Scrolls to an operation in Redocly by setting the hash on the iframe.
30+
* Redocly uses hash format: #operation/{operationId}
31+
*/
32+
function scrollToRedoclyOperation(operationId: string): void {
33+
const iframe = document.querySelector("iframe")
34+
if (!iframe?.contentWindow) {
35+
return
36+
}
37+
iframe.contentWindow.location.hash = `operation/${operationId}`
38+
}
39+
40+
/**
41+
* Scrolls to an operation in the documentation viewer.
42+
* Each visualizer has its own mechanism for deep linking.
43+
*
44+
* For SwaggerUI: Uses DOM-based scrolling to find operations-{tag}-{operationId} elements
45+
* For Stoplight: #/operations/{operationId}
46+
* For Redocly: #operation/{operationId} (set on iframe)
47+
*/
48+
export function scrollToOperation(
49+
visualizer: DocumentationVisualizer,
50+
operationId?: string,
51+
method?: string,
52+
path?: string
53+
): void {
54+
if (!operationId && (!method || !path)) {
55+
return
56+
}
57+
58+
switch (visualizer) {
59+
case DocumentationVisualizer.SWAGGER:
60+
// SwaggerUI: Use DOM-based scrolling since hash changes don't trigger navigation
61+
// after initial page load
62+
if (operationId) {
63+
scrollToSwaggerOperation(operationId)
64+
}
65+
break
66+
67+
case DocumentationVisualizer.STOPLIGHT:
68+
// Stoplight uses hash-based navigation: #/operations/{operationId}
69+
if (operationId) {
70+
window.location.hash = `/operations/${operationId}`
71+
}
72+
break
73+
74+
case DocumentationVisualizer.REDOCLY:
75+
// Redocly runs in an iframe and uses hash format: #tag/{tag}/operation/{operationId}
76+
// We need to set the hash on the iframe's content window
77+
if (operationId) {
78+
scrollToRedoclyOperation(operationId)
79+
}
80+
break
81+
}
82+
}

src/features/projects/view/toolbar/MobileToolbar.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,11 @@ const MobileToolbar = () => {
2828
sx={{ width: "100%" }}
2929
/>
3030
<Selector
31-
items={version.specifications}
31+
items={version.specifications.map(spec => ({
32+
id: spec.id,
33+
name: spec.name,
34+
hasChanges: !!spec.diffURL
35+
}))}
3236
selection={specification.id}
3337
onSelect={selectSpecification}
3438
sx={{ width: "100%" }}

src/features/projects/view/toolbar/TrailingToolbarItem.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,11 @@ const TrailingToolbarItem = () => {
5959
/>
6060
<Typography variant="h6" sx={{ marginRight: 1 }}>/</Typography>
6161
<Selector
62-
items={version.specifications}
62+
items={version.specifications.map(spec => ({
63+
id: spec.id,
64+
name: spec.name,
65+
hasChanges: !!spec.diffURL
66+
}))}
6367
selection={specification.id}
6468
onSelect={selectSpecification}
6569
sx={{ marginRight: 0.5 }}

src/features/sidebar/view/internal/diffbar/DiffContent.tsx

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
11
"use client"
22

33
import { Alert, Box, Typography, Link } from "@mui/material"
4-
import { useState } from "react"
54
import useDiff from "@/features/sidebar/data/useDiff"
65
import { useProjectSelection } from "@/features/projects/data"
6+
import useDocumentationVisualizer from "@/features/settings/data/useDocumentationVisualizer"
7+
import { scrollToOperation } from "@/features/docs/navigation"
78
import DiffList, { DiffListStatus } from "./components/DiffList"
8-
import DiffDialog from "./components/DiffDialog"
9+
import { DiffChange } from "@/features/diff/domain/DiffChange"
910

1011
const DiffContent = () => {
1112
const { data, loading, changes, error, isNewFile } = useDiff()
1213
const { specification } = useProjectSelection()
13-
const [selectedChange, setSelectedChange] = useState<number | null>(null)
14+
const [visualizer] = useDocumentationVisualizer()
1415

15-
const closeModal = () => setSelectedChange(null)
16+
const handleChangeClick = (change: DiffChange) => {
17+
scrollToOperation(visualizer, change.operationId, change.operation, change.path)
18+
}
1619

1720
const hasData = Boolean(data)
1821
const hasChanges = changes.length > 0
@@ -101,17 +104,10 @@ const DiffContent = () => {
101104
<DiffList
102105
changes={changes}
103106
status={diffStatus}
104-
selectedChange={selectedChange}
105-
onClick={(i) => setSelectedChange(i)}
107+
onClick={handleChangeClick}
106108
/>
107109
</Box>
108110
)}
109-
110-
<DiffDialog
111-
open={selectedChange !== null}
112-
change={selectedChange !== null ? changes[selectedChange] : null}
113-
onClose={closeModal}
114-
/>
115111
</Box>
116112
)
117113
}

src/features/sidebar/view/internal/diffbar/components/DiffDialog.tsx

Lines changed: 0 additions & 120 deletions
This file was deleted.

src/features/sidebar/view/internal/diffbar/components/DiffList.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,11 @@ export type DiffListStatus = "idle" | "loading" | "empty" | "ready" | "error"
99
const DiffList = ({
1010
changes,
1111
status,
12-
selectedChange,
1312
onClick,
1413
}: {
1514
changes: DiffChange[]
1615
status: DiffListStatus
17-
selectedChange: number | null
18-
onClick: (i: number) => void
16+
onClick: (change: DiffChange) => void
1917
}) => {
2018
if (status === "loading") {
2119
return (
@@ -37,7 +35,6 @@ const DiffList = ({
3735
return (
3836
<PopulatedDiffList
3937
changes={changes}
40-
selectedChange={selectedChange}
4138
onClick={onClick}
4239
/>
4340
)

src/features/sidebar/view/internal/diffbar/components/DiffListItem.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,19 @@ const DiffListItem = ({
1010
text,
1111
level,
1212
operation,
13-
selected,
1413
onClick,
1514
}: {
1615
path?: string
1716
text?: string
1817
level?: number
1918
operation?: string
20-
selected: boolean
2119
onClick: () => void
2220
}) => {
2321
const levelConfig = getLevelConfig((level ?? 1) as Level)
2422

2523
return (
2624
<ListItem disableGutters disablePadding>
2725
<ListItemButton
28-
selected={selected}
2926
onClick={onClick}
3027
disableGutters
3128
sx={{ width: "100%", px: 0, alignItems: "flex-start" }}

src/features/sidebar/view/internal/diffbar/components/PopulatedDiffList.tsx

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,10 @@ import { DiffChange } from "@/features/diff/domain/DiffChange"
66

77
const PopulatedDiffList = ({
88
changes,
9-
selectedChange,
109
onClick,
1110
}: {
1211
changes: DiffChange[]
13-
selectedChange: number | null
14-
onClick: (i: number) => void
12+
onClick: (change: DiffChange) => void
1513
}) => {
1614
return (
1715
<SpacedList
@@ -21,15 +19,14 @@ const PopulatedDiffList = ({
2119
"& .menu-item-highlight": { px: 1, py: 1.25 },
2220
}}
2321
>
24-
{changes.map((change, i) => (
22+
{changes.map((change) => (
2523
<DiffListItem
2624
key={change.id}
2725
path={change?.path}
2826
text={change?.text}
2927
level={change?.level}
3028
operation={change?.operation}
31-
selected={selectedChange === i}
32-
onClick={() => onClick(i)}
29+
onClick={() => onClick(change)}
3330
/>
3431
))}
3532
</SpacedList>

0 commit comments

Comments
 (0)