Skip to content

Commit

Permalink
Fix: Links to Docs page didn't work and Back button not working
Browse files Browse the repository at this point in the history
The docs page was way too complicated anyway, so I simlified it a bit.

fixes: #476, #477
  • Loading branch information
reglim committed Mar 29, 2023
1 parent 2c64b13 commit 5cf6034
Show file tree
Hide file tree
Showing 3 changed files with 196 additions and 122 deletions.
223 changes: 101 additions & 122 deletions web/src/pages/Docs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,189 +4,168 @@
and we need to use some of it's members, which is unsafe
*/

import React, { useCallback, useEffect, useRef, useState } from 'react'
import React, { useEffect, useRef, useState } from 'react'
import { useParams, useSearchParams } from 'react-router-dom'
import DocumentControlButtons from '../components/DocumentControlButtons'
import { useProjects } from '../data-providers/ProjectDataProvider'
import ProjectDetails from '../models/ProjectDetails'
import ProjectRepository from '../repositories/ProjectRepository'

import styles from './../style/pages/Docs.module.css'
import LoadingPage from './LoadingPage'
import NotFound from './NotFound'

export default function Docs(): JSX.Element {
import styles from './../style/pages/Docs.module.css'

export default function Docs (): JSX.Element {
const projectParam = useParams().project ?? ''
const versionParam = useParams().version ?? 'latest'
const pageParam = useParams().page ?? 'index.html'
const hideUiParam = useSearchParams()[0].get('hide-ui') === 'true'

const [project] = useState<string>(projectParam)
const [version, setVersion] = useState<string>(versionParam)
const [page, setPage] = useState<string>(pageParam)
const [hideUi, setHideUi] = useState<boolean>(hideUiParam)
const [project, setProject] = useState<string>('')
const [version, setVersion] = useState<string>('')
const [page, setPage] = useState<string>('')
const [hideUi, setHideUi] = useState<boolean>(false)

const [versions, setVersions] = useState<ProjectDetails[]>([])
const [loadingFailed, setLoadingFailed] = useState<boolean>(false)

const { projectsWithHiddenVersions: projects } = useProjects()
const iFrameRef = useRef(null)

document.title = `${project} | docat`

if (project === '') {
if (projectParam === '') {
setLoadingFailed(true)
}

const updateRoute = useCallback(
(
project: string,
version: string,
page: string,
hideControls: boolean
): void => {
const newState = `/#/${project}/${version}/${page}${
hideControls ? '?hide-ui=true' : ''
}`

// skip updating the route if the new state is the same as the current one
if (window.location.hash === newState.substring(1)) {
return
}
const updateURL = (newProject: string, newVersion: string, newPage: string, newHideUi: boolean): void => {
const url = `#/${newProject}/${newVersion}/${newPage}${newHideUi ? '?hide-ui=true' : ''}`

window.history.pushState({}, '', newState)
},
[]
)
if (project === newProject && version === newVersion && page === newPage && hideUi === newHideUi) {
// no change
return
}

updateRoute(project, version, page, hideUi)
const oldVersion = version
const oldPage = page

useEffect(() => {
if (project === '' || project === 'none') {
setVersions([])
setProject(newProject)
setVersion(newVersion)
setPage(newPage)
setHideUi(newHideUi)

if (oldVersion === 'latest' && newVersion !== 'latest') {
// replace the url if 'latest' was updated to the actual version
window.history.replaceState(null, '', url)
return
}

if (projects == null || projects.length === 0) {
if (oldPage === '' && newPage !== '') {
// replace the url if the page was updated from '' to the actual page
window.history.replaceState(null, '', url)
return
}

try {
const matchingProjects = projects.filter((p) => p.name === project)

if (matchingProjects.length !== 1) {
setLoadingFailed(true)
return
}

let res = matchingProjects[0].versions

if (res.length === 0) {
setLoadingFailed(true)
return
}

res = res.sort((a, b) => ProjectRepository.compareVersions(a, b))
setVersions(res)

if (version !== 'latest') {
// custom version -> check if it exists
const versionsAndTags = res.map((v) => [v.name, ...v.tags]).flat()

if (!versionsAndTags.includes(version)) {
// version does not exist -> fail
setLoadingFailed(true)
console.log("Version doesn't exist")
}

return
}
window.history.pushState(null, '', url)
}

// latest version -> check if there is a latest tag
const versionWithLatestTag = res.find((v) =>
(v.tags ?? []).includes('latest')
)

// if there is a latest tag, use it,
// otherwise use the latest version by sorting
const latestVersion =
versionWithLatestTag != null
? versionWithLatestTag.name
: res[res.length - 1].name

setVersion(latestVersion)
updateRoute(project, latestVersion, page, hideUi)
} catch (e) {
console.error(e)
setLoadingFailed(true)
const onIFrameLocationChanged = (url: string): void => {
url = url.split('/doc/')[1]
if (url.length === 0) {
// should never happen
return
}
}, [project, projects, version, page, hideUi, updateRoute])

const handleVersionChange = (v: string): void => {
setVersion(v)
updateRoute(project, v, page, hideUi)
}
const parts = url.split('/')
const urlProject = parts[0]
const urlVersion = parts[1]
const urlPage = parts.slice(2).join('/')

const handleHideControls = (): void => {
updateRoute(project, version, page, true)
setHideUi(true)
if (urlProject !== project || urlVersion !== version || urlPage !== page) {
updateURL(urlProject, urlVersion, urlPage, hideUi)
}
}

/**
* This makes all external links in the iFrame open in a new tab
* and updates the page url when the location in the iFrame changes
*/
const onIframeLocationChanged = (): void => {
if (iFrameRef?.current == null) {
useEffect(() => {
if (project === '') {
return
}

// update the path in the url
// @ts-expect-error - ts does not find the location on the iframe
const path: string = iFrameRef.current.contentWindow.location.href as string
const page = path.split(`${version}/`)[1]

if (page == null || page.trim().length < 1) {
return
}
void (async (): Promise<void> => {
try {
let allVersions = await ProjectRepository.getVersions(project)

setPage(page)
updateRoute(project, version, page, hideUi)
if (allVersions.length === 0) {
setLoadingFailed(true)
return
}

// make all links in iframe open in new tab
// @ts-expect-error - ts does not find the document on the iframe
iFrameRef.current.contentDocument
.querySelectorAll('a')
.forEach((a: HTMLAnchorElement) => {
if (!a.href.startsWith(window.location.origin)) {
a.setAttribute('target', '_blank')
allVersions = allVersions.sort((a, b) => ProjectRepository.compareVersions(a, b))
let versionToUse = ''

if (version === 'latest') {
versionToUse = ProjectRepository.getLatestVersion(allVersions).name
} else {
// custom version -> check if it exists
const versionsAndTags = allVersions.map((v) => [v.name, ...v.tags]).flat()
if (!versionsAndTags.includes(version)) {
// version does not exist -> fail
setLoadingFailed(true)
console.error("Version doesn't exist")
return
}

versionToUse = version
}
})
}

if (versions == null || versions.length === 0) {
return <LoadingPage />
}
updateURL(project, versionToUse, page, hideUi)
setVersions(allVersions)
setLoadingFailed(false)
} catch (e) {
console.error(e)
setLoadingFailed(true)
}
})()
}, [project])

useEffect(() => {
// set props equal to url params and update the url with the default values if empty
setProject(p => {
if (p !== '') {
return p
}

updateURL(projectParam, versionParam, pageParam, hideUiParam)
return projectParam
})
}, [])

if (loadingFailed) {
return <NotFound />
}

if (versions.length === 0) {
return <LoadingPage />
}

return (
<>
<iframe
title="docs"
ref={iFrameRef}
src={ProjectRepository.getProjectDocsURL(project, version, page)}
onLoad={onIframeLocationChanged}
title="docs"
className={styles['docs-iframe']}
></iframe>
onLoad={() => {
// @ts-expect-error ts can't find contentWindow
onIFrameLocationChanged(iFrameRef.current?.contentWindow.location.href as string)
}}
/>

{!hideUi && (
<DocumentControlButtons
version={version}
versions={versions}
onVersionChange={handleVersionChange}
onHideUi={handleHideControls}
onVersionChange={(v) => updateURL(project, v, page, hideUi)}
onHideUi={() => updateURL(project, version, page, true)}
/>
)}
</>
Expand Down
23 changes: 23 additions & 0 deletions web/src/repositories/ProjectRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,28 @@ async function getVersions (projectName: string): Promise<ProjectDetails[]> {
return json.versions
}

/**
* Returns the latest version of a project.
* Order of precedence: latest, latest tag, latest version
* @param versions all versions of a project
*/
function getLatestVersion (versions: ProjectDetails[]): ProjectDetails {
const latest = versions.find((v) => v.name.includes('latest'))
if (latest != null) {
return latest
}

const latestTag = versions.find((v) => v.tags.includes('latest'))
if (latestTag != null) {
return latestTag
}

const sortedVersions = versions
.sort((a, b) => compareVersions(a, b))

return sortedVersions[sortedVersions.length - 1]
}

/**
* Returns a SearchResult object containing all projects and versions that contain the search query in their name or tag
* @param {Project[]} projects List of all projects
Expand Down Expand Up @@ -239,6 +261,7 @@ function setFavorite (projectName: string, shouldBeFavorite: boolean): void {

const exp = {
getVersions,
getLatestVersion,
filterHiddenVersions,
search,
getProjectLogoURL,
Expand Down
Loading

0 comments on commit 5cf6034

Please sign in to comment.