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 28, 2023
1 parent 2c64b13 commit d65f5bd
Show file tree
Hide file tree
Showing 3 changed files with 172 additions and 121 deletions.
198 changes: 77 additions & 121 deletions web/src/pages/Docs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,31 @@
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 [project, setProject] = useState<string>(projectParam)
const [version, setVersion] = useState<string>(versionParam)
const [page, setPage] = useState<string>(pageParam)
const [hideUi, setHideUi] = useState<boolean>(hideUiParam)

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

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

document.title = `${project} | docat`
Expand All @@ -37,156 +37,112 @@ export default function Docs(): JSX.Element {
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
}

window.history.pushState({}, '', newState)
},
[]
)

updateRoute(project, version, page, hideUi)

useEffect(() => {
if (project === '' || project === 'none') {
setVersions([])
return
}
const updateURL = (newProject: string, newVersion: string, newPage: string, newHideUi: boolean): void => {
const url = `#/${newProject}/${newVersion}/${newPage}${newHideUi ? '?hide-ui=true' : ''}`

if (projects == null || projects.length === 0) {
if (project === newProject && version === newVersion && page === newPage && hideUi === newHideUi) {
// no change
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")
}
const oldVersion = version

return
}
setProject(newProject)
setVersion(newVersion)
setPage(newPage)
setHideUi(newHideUi)

// 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)
if (oldVersion === 'latest' && newVersion !== 'latest') {
// 'latest' was updated to the actual version -> replace the url
window.history.replaceState(null, '', url)
return
}
}, [project, projects, version, page, hideUi, updateRoute])

const handleVersionChange = (v: string): void => {
setVersion(v)
updateRoute(project, v, page, hideUi)
}

const handleHideControls = (): void => {
updateRoute(project, version, page, true)
setHideUi(true)
window.history.pushState(null, '', url)
}

/**
* 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) {
const onIFrameLocationChanged = (url: string): void => {
url = url.split('/doc/')[1]
if (url.length === 0) {
// should never happen
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]
const parts = url.split('/')
const urlProject = parts[0]
const urlVersion = parts[1]
const urlPage = parts.slice(2).join('/')

if (page == null || page.trim().length < 1) {
return
if (urlProject !== project || urlVersion !== version || urlPage !== page) {
updateURL(urlProject, urlVersion, urlPage, hideUi)
}
}

setPage(page)
updateRoute(project, version, page, hideUi)
useEffect(() => {
void (async (): Promise<void> => {
try {
let allVersions = await ProjectRepository.getVersions(project)

// 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')
if (allVersions.length === 0) {
setLoadingFailed(true)
return
}
})
}

if (versions == null || versions.length === 0) {
return <LoadingPage />
}
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.log("Version doesn't exist")
return
}

versionToUse = version
}

updateURL(project, versionToUse, page, hideUi)
setVersions(allVersions)
setLoadingFailed(false)
} catch (e) {
console.error(e)
setLoadingFailed(true)
}
})()
}, [project])

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
72 changes: 72 additions & 0 deletions web/src/tests/repositories/ProjectRepository.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -460,3 +460,75 @@ describe('filterHiddenVersions', () => {
expect(result).toStrictEqual([])
})
})

describe('getLatestVersion', () => {
test('should return latest version by name', () => {
const versions: ProjectDetails[] = [
{
name: '1.0.0',
hidden: false,
tags: []
},
{
name: '2.0.0',
hidden: false,
tags: []
}
]

const latestVersion = ProjectRepository.getLatestVersion(versions)
expect(latestVersion).toStrictEqual(versions[1])
})

test('should return version with latest in name', () => {
const versions: ProjectDetails[] = [
{
name: '1.0.0',
hidden: false,
tags: []
},
{
name: 'latest',
hidden: false,
tags: []
}]

const latestVersion = ProjectRepository.getLatestVersion(versions)
expect(latestVersion).toStrictEqual(versions[1])
})

test('should return version with latest tag', () => {
const versions: ProjectDetails[] = [
{
name: '1.0.0',
hidden: false,
tags: ['latest']
},
{
name: '2.0.0',
hidden: false,
tags: []
}]

const latestVersion = ProjectRepository.getLatestVersion(versions)
expect(latestVersion).toStrictEqual(versions[0])
})

test('should prefer version with latest in name over latest tag', () => {
const versions: ProjectDetails[] = [
{
name: 'latest',
hidden: false,
tags: []
},
{
name: '1.0.0',
hidden: false,
tags: ['latest']
}
]

const latestVersion = ProjectRepository.getLatestVersion(versions)
expect(latestVersion).toStrictEqual(versions[0])
})
})

0 comments on commit d65f5bd

Please sign in to comment.