Commit 2c86ed79 authored by Stefan Probst's avatar Stefan Probst
Browse files

feat: link to item versions

parent 74534396
......@@ -8,7 +8,7 @@ export function Title({
className,
...props
}: TitleProps): JSX.Element {
const classNames = cx('font-medium text-3xl', className)
const classNames = cx('font-medium text-3xl leading-9', className)
return (
<h1 className={classNames} {...props}>
......
import ProtectedScreen from '@/modules/auth/ProtectedScreen'
import DatasetVersionScreen from '@/screens/item/dataset/DatasetVersionScreen'
/**
* Dataset version detail page.
*/
export default function DatasetVersionPage(): JSX.Element {
return (
<ProtectedScreen>
<DatasetVersionScreen />
</ProtectedScreen>
)
}
import ProtectedScreen from '@/modules/auth/ProtectedScreen'
import PublicationVersionScreen from '@/screens/item/publication/PublicationVersionScreen'
/**
* Publication version detail page.
*/
export default function PublicationVersionPage(): JSX.Element {
return (
<ProtectedScreen>
<PublicationVersionScreen />
</ProtectedScreen>
)
}
import ProtectedScreen from '@/modules/auth/ProtectedScreen'
import ToolVersionScreen from '@/screens/item/tool/ToolVersionScreen'
/**
* Tool version detail page.
*/
export default function ToolVersionPage(): JSX.Element {
return (
<ProtectedScreen>
<ToolVersionScreen />
</ProtectedScreen>
)
}
import ProtectedScreen from '@/modules/auth/ProtectedScreen'
import TrainingMaterialVersionScreen from '@/screens/item/training-material/TrainingMaterialVersionScreen'
/**
* TrainingMaterial version detail page.
*/
export default function TrainingMaterialVersionPage(): JSX.Element {
return (
<ProtectedScreen>
<TrainingMaterialVersionScreen />
</ProtectedScreen>
)
}
import ProtectedScreen from '@/modules/auth/ProtectedScreen'
import WorkflowVersionScreen from '@/screens/item/workflow/WorkflowVersionScreen'
/**
* Workflow version detail page.
*/
export default function WorkflowVersionPage(): JSX.Element {
return (
<ProtectedScreen>
<WorkflowVersionScreen />
</ProtectedScreen>
)
}
import Link from 'next/link'
import { Fragment } from 'react'
import type {
......@@ -56,15 +57,33 @@ interface ItemVersionProps {
| Exclude<ItemHistoryProps['item']['olderVersions'], undefined>[number]
}
/**
* TODO: which versions is a user is allowed to see?
* i.e. which requests for an item version will not error?
*/
function ItemVersion(props: ItemVersionProps) {
const { version } = props
return (
<article className="px-4 py-4 border border-gray-200 rounded bg-gray-75">
<h3 className="font-bold text-primary-750">
{version.label}
{version.version != null ? ` ${`${version.version}`}` : null}
</h3>
<Link
href={{
pathname: [
'',
version.category,
version.persistentId,
'version',
version.id,
].join('/'),
}}
>
<a>
<h3 className="font-bold text-primary-750">
{version.label}
{version.version != null ? ` ${`${version.version}`}` : null}
</h3>
</a>
</Link>
</article>
)
}
import { useRouter } from 'next/router'
import { useGetDatasetVersion } from '@/api/sshoc'
import { ProgressSpinner } from '@/elements/ProgressSpinner/ProgressSpinner'
import { toast } from '@/elements/Toast/useToast'
import { useQueryParam } from '@/lib/hooks/useQueryParam'
import { useAuth } from '@/modules/auth/AuthContext'
import { useErrorHandlers } from '@/modules/error/useErrorHandlers'
import ItemLayout from '@/screens/item/ItemLayout'
/**
* Dataset version screen.
*/
export default function DatasetVersionScreen(): JSX.Element {
const router = useRouter()
const id = useQueryParam('id', false)
const versionId = useQueryParam('versionId', false, Number)
const auth = useAuth()
const handleError = useErrorHandlers()
const dataset = useGetDatasetVersion(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
{ id: id!, versionId: versionId! },
{
enabled: id != null && versionId != null && !Number.isNaN(versionId),
onError(error) {
toast.error('Failed to fetch dataset')
router.push('/')
if (error instanceof Error) {
handleError(error)
}
},
},
{
token: auth.session?.accessToken,
},
)
if (dataset.data === undefined) {
return (
<div>
<ProgressSpinner />
</div>
)
}
return <ItemLayout item={dataset.data} />
}
import { useRouter } from 'next/router'
import { useGetPublicationVersion } from '@/api/sshoc'
import { ProgressSpinner } from '@/elements/ProgressSpinner/ProgressSpinner'
import { toast } from '@/elements/Toast/useToast'
import { useQueryParam } from '@/lib/hooks/useQueryParam'
import { useAuth } from '@/modules/auth/AuthContext'
import { useErrorHandlers } from '@/modules/error/useErrorHandlers'
import ItemLayout from '@/screens/item/ItemLayout'
/**
* Publication version screen.
*/
export default function PublicationVersionScreen(): JSX.Element {
const router = useRouter()
const id = useQueryParam('id', false)
const versionId = useQueryParam('versionId', false, Number)
const auth = useAuth()
const handleError = useErrorHandlers()
const publication = useGetPublicationVersion(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
{ id: id!, versionId: versionId! },
{
enabled: id != null && versionId != null && !Number.isNaN(versionId),
onError(error) {
toast.error('Failed to fetch publication')
router.push('/')
if (error instanceof Error) {
handleError(error)
}
},
},
{
token: auth.session?.accessToken,
},
)
if (publication.data === undefined) {
return (
<div>
<ProgressSpinner />
</div>
)
}
return <ItemLayout item={publication.data} />
}
import { useRouter } from 'next/router'
import { useGetToolVersion } from '@/api/sshoc'
import { ProgressSpinner } from '@/elements/ProgressSpinner/ProgressSpinner'
import { toast } from '@/elements/Toast/useToast'
import { useQueryParam } from '@/lib/hooks/useQueryParam'
import { useAuth } from '@/modules/auth/AuthContext'
import { useErrorHandlers } from '@/modules/error/useErrorHandlers'
import ItemLayout from '@/screens/item/ItemLayout'
/**
* Tool version screen.
*/
export default function ToolVersionScreen(): JSX.Element {
const router = useRouter()
const id = useQueryParam('id', false)
const versionId = useQueryParam('versionId', false, Number)
const auth = useAuth()
const handleError = useErrorHandlers()
const tool = useGetToolVersion(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
{ id: id!, versionId: versionId! },
{
enabled: id != null && versionId != null && !Number.isNaN(versionId),
onError(error) {
toast.error('Failed to fetch tool')
router.push('/')
if (error instanceof Error) {
handleError(error)
}
},
},
{
token: auth.session?.accessToken,
},
)
if (tool.data === undefined) {
return (
<div>
<ProgressSpinner />
</div>
)
}
return <ItemLayout item={tool.data} />
}
import { useRouter } from 'next/router'
import { useGetTrainingMaterialVersion } from '@/api/sshoc'
import { ProgressSpinner } from '@/elements/ProgressSpinner/ProgressSpinner'
import { toast } from '@/elements/Toast/useToast'
import { useQueryParam } from '@/lib/hooks/useQueryParam'
import { useAuth } from '@/modules/auth/AuthContext'
import { useErrorHandlers } from '@/modules/error/useErrorHandlers'
import ItemLayout from '@/screens/item/ItemLayout'
/**
* Training material version screen.
*/
export default function TrainingMaterialVersionScreen(): JSX.Element {
const router = useRouter()
const id = useQueryParam('id', false)
const versionId = useQueryParam('versionId', false, Number)
const auth = useAuth()
const handleError = useErrorHandlers()
const trainingMaterial = useGetTrainingMaterialVersion(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
{ id: id!, versionId: versionId! },
{
enabled: id != null && versionId != null && !Number.isNaN(versionId),
onError(error) {
toast.error('Failed to fetch trainingMaterial')
router.push('/')
if (error instanceof Error) {
handleError(error)
}
},
},
{
token: auth.session?.accessToken,
},
)
if (trainingMaterial.data === undefined) {
return (
<div>
<ProgressSpinner />
</div>
)
}
return <ItemLayout item={trainingMaterial.data} />
}
import {
Disclosure,
DisclosureButton,
DisclosurePanel,
} from '@reach/disclosure'
import cx from 'clsx'
import dynamic from 'next/dynamic'
import { useState } from 'react'
import type { WorkflowDto } from '@/api/sshoc'
import ItemMetadata from '@/modules/item/ItemMetadata'
import RelatedItems from '@/modules/item/RelatedItems'
import HStack from '@/modules/layout/HStack'
import VStack from '@/modules/layout/VStack'
import Triangle from '@/modules/ui/Triangle'
import { SectionTitle } from '@/modules/ui/typography/SectionTitle'
const Markdown = dynamic(() => import('@/modules/markdown/Markdown'))
type Steps = WorkflowDto['composedOf']
type Step = Exclude<Steps, undefined>[number]
export default function Steps({ steps }: { steps: Steps }): JSX.Element | null {
if (steps === undefined) return null
/** undocumented in openapi doc */
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (steps === null) return null
if (steps.length === 0) return null
return (
<div
className="grid mb-6"
style={{
gridColumn: '1 / span 9',
/** "sub-grid" */
gridTemplateColumns:
'calc(50vw - (var(--screen-3xl) / 2)) repeat(8, minmax(0, calc(var(--screen, var(--screen-3xl)) / 12)))',
}}
>
<HStack
className="items-baseline p-6 space-x-2"
style={{ gridColumn: '3 / span 7' }}
>
<SectionTitle>Steps</SectionTitle>
<span className="text-xl text-gray-500">({steps.length})</span>
</HStack>
{steps.map((step, index) => {
return <Step key={step.id} step={step} index={index} />
})}
</div>
)
}
function Step({ step, index }: { step: Step; index: number }) {
const [isOpen, setOpen] = useState(false)
function toggleOpen() {
setOpen((prev) => !prev)
}
return (
<Disclosure defaultOpen={false}>
<b style={{ gridColumn: '1 / span 2' }} className="bg-gray-100" />
<HStack
as="header"
className="items-center justify-between p-6 bg-gray-100"
style={{ gridColumn: '3 / span 7' }}
>
<HStack className="items-center space-x-6">
<span className="text-2xl font-bold">{index + 1}</span>
<div className="self-stretch w-px bg-gray-300" />
<span>
<h3 className="text-lg font-bold">{step.label}</h3>
</span>
</HStack>
<DisclosureButton
className={cx(
'w-36 inline-flex items-center justify-between px-2 py-3 rounded space-x-2 transition-colors duration-150 flex-shrink-0',
isOpen
? 'text-gray-100 bg-gray-500'
: 'text-white bg-secondary-600 hover:bg-secondary-500',
)}
onClick={toggleOpen}
>
<span className="px-4">{isOpen ? 'Collapse' : 'Expand'}</span>{' '}
<span className={isOpen ? 'transform rotate-180' : ''}>
<Triangle />
</span>
</DisclosureButton>
</HStack>
<b style={{ gridColumn: '1 / span 2' }} className="bg-gray-50" />
<DisclosurePanel
style={{ gridColumn: '3 / span 7' }}
className="px-6 py-12 space-y-4 bg-gray-50"
>
<ItemMetadata item={step} />
<div className="leading-8">
<Markdown text={step.description} />
</div>
<Related items={step.relatedItems} />
</DisclosurePanel>
</Disclosure>
)
}
function Related({ items }: { items?: Array<any> }) {
if (items == null) return null
// TODO: relations to steps
const relatedItems = items.filter((item) => item.category !== 'step')
if (relatedItems.length === 0) return null
return (
<VStack>
<h4 className="sr-only">Resources</h4>
<RelatedItems items={relatedItems} />
</VStack>
)
}
import {
Disclosure,
DisclosureButton,
DisclosurePanel,
} from '@reach/disclosure'
import { JsonLd } from '@stefanprobst/next-page-metadata'
import cx from 'clsx'
import dynamic from 'next/dynamic'
import { Fragment, useState } from 'react'
import { Fragment } from 'react'
import type { DeepRequired } from 'utility-types'
import type { WorkflowDto } from '@/api/sshoc'
import { useGetWorkflow } from '@/api/sshoc'
import ItemMetadata from '@/modules/item/ItemMetadata'
import RelatedItems from '@/modules/item/RelatedItems'
import HStack from '@/modules/layout/HStack'
import VStack from '@/modules/layout/VStack'
import Triangle from '@/modules/ui/Triangle'
import { SectionTitle } from '@/modules/ui/typography/SectionTitle'
import type { PageProps } from '@/pages/workflow/[id]/index'
import ItemLayout from '@/screens/item/ItemLayout'
const Markdown = dynamic(() => import('@/modules/markdown/Markdown'))
import Steps from '@/screens/item/workflow/Steps'
/**
* Workflow screen.
......@@ -72,103 +58,3 @@ export default function WorkflowScreen({
</Fragment>
)
}
type Steps = WorkflowDto['composedOf']
type Step = Exclude<Steps, undefined>[number]
function Steps({ steps }: { steps: Steps }) {
if (steps === undefined) return null
/** undocumented in openapi doc */
if (steps === null) return null
if (steps.length === 0) return null
return (
<div
className="grid mb-6"
style={{
gridColumn: '1 / span 9',
/** "sub-grid" */
gridTemplateColumns:
'calc(50vw - (var(--screen-3xl) / 2)) repeat(8, minmax(0, calc(var(--screen, var(--screen-3xl)) / 12)))',
}}
>
<HStack
className="items-baseline p-6 space-x-2"
style={{ gridColumn: '3 / span 7' }}
>
<SectionTitle>Steps</SectionTitle>
<span className="text-xl text-gray-500">({steps.length})</span>
</HStack>
{steps.map((step, index) => {
return <Step key={step.id} step={step} index={index} />
})}
</div>
)
}
function Step({ step, index }: { step: Step; index: number }) {
const [isOpen, setOpen] = useState(false)
function toggleOpen() {
setOpen((prev) => !prev)
}
return (
<Disclosure defaultOpen={false}>
<b style={{ gridColumn: '1 / span 2' }} className="bg-gray-100" />
<HStack
as="header"
className="items-center justify-between p-6 bg-gray-100"
style={{ gridColumn: '3 / span 7' }}
>
<HStack className="items-center space-x-6">
<span className="text-2xl font-bold">{index + 1}</span>
<div className="self-stretch w-px bg-gray-300" />
<span>
<h3 className="text-lg font-bold">{step.label}</h3>
</span>
</HStack>
<DisclosureButton
className={cx(
'w-36 inline-flex items-center justify-between px-2 py-3 rounded space-x-2 transition-colors duration-150 flex-shrink-0',
isOpen
? 'text-gray-100 bg-gray-500'
: 'text-white bg-secondary-600 hover:bg-secondary-500',
)}
onClick={toggleOpen}
>
<span className="px-4">{isOpen ? 'Collapse' : 'Expand'}</span>{' '}
<span className={isOpen ? 'transform rotate-180' : ''}>
<Triangle />
</span>
</DisclosureButton>
</HStack>
<b style={{ gridColumn: '1 / span 2' }} className="bg-gray-50" />
<DisclosurePanel
style={{ gridColumn: '3 / span 7' }}
className="px-6 py-12 space-y-4 bg-gray-50"
>
<ItemMetadata item={step} />
<div className="leading-8">
<Markdown text={step.description} />
</div>
<Related items={step.relatedItems} />
</DisclosurePanel>
</Disclosure>
)
}
function Related({ items }: { items?: Array<any> }) {
if (items == null) return null
// TODO: relations to steps
const relatedItems = items.filter((item) => item.category !== 'step')
if (relatedItems.length === 0) return null
return (
<VStack>
<h4 className="sr-only">Resources</h4>
<RelatedItems items={relatedItems} />
</VStack>
)
}