import { withContextRoot } from './utils/with-context-root'
import { useCallback, useEffect, useMemo, useState } from 'react'
import useFetchMock from './hooks/use-fetch-mock'
import { setupContext } from './fixtures/context'
import { Button } from 'react-bootstrap'
import PdfPreviewProvider, {
usePdfPreviewContext,
} from '../js/features/pdf-preview/contexts/pdf-preview-context'
import PdfPreviewPane from '../js/features/pdf-preview/components/pdf-preview-pane'
import PdfPreview from '../js/features/pdf-preview/components/pdf-preview'
import PdfPreviewToolbar from '../js/features/pdf-preview/components/pdf-preview-toolbar'
import PdfFileList from '../js/features/pdf-preview/components/pdf-file-list'
import { buildFileList } from '../js/features/pdf-preview/util/file-list'
import PdfLogsViewer from '../js/features/pdf-preview/components/pdf-logs-viewer'
import examplePdf from './fixtures/storybook-example.pdf'
import PdfPreviewError from '../js/features/pdf-preview/components/pdf-preview-error'
import PdfPreviewHybridToolbar from '../js/features/pdf-preview/components/pdf-preview-hybrid-toolbar'
setupContext()
export default {
title: 'PDF Preview',
component: PdfPreview,
subcomponents: {
PdfPreviewToolbar,
PdfPreviewHybridToolbar,
PdfFileList,
PdfPreviewError,
},
}
const project = {
_id: 'a-project',
name: 'A Project',
features: {},
tokens: {},
owner: {
_id: 'a-user',
email: 'stories@overleaf.com',
},
members: [],
invites: [],
}
const scope = {
project,
settings: {
syntaxValidation: true,
},
hasLintingError: false,
$applyAsync: () => {},
editor: {
sharejs_doc: {
doc_id: 'test-doc',
getSnapshot: () => 'some doc content',
},
},
}
const dispatchProjectJoined = () => {
window.dispatchEvent(new CustomEvent('project:joined', { detail: project }))
}
const dispatchDocChanged = () => {
window.dispatchEvent(
new CustomEvent('doc:changed', { detail: { doc_id: 'foo' } })
)
}
const outputFiles = [
{
path: 'output.pdf',
build: '123',
url: '/build/output.pdf',
type: 'pdf',
},
{
path: 'output.bbl',
build: '123',
url: '/build/output.bbl',
type: 'bbl',
},
{
path: 'output.bib',
build: '123',
url: '/build/output.bib',
type: 'bib',
},
{
path: 'example.txt',
build: '123',
url: '/build/example.txt',
type: 'txt',
},
{
path: 'output.log',
build: '123',
url: '/build/output.log',
type: 'log',
},
{
path: 'output.blg',
build: '123',
url: '/build/output.blg',
type: 'blg',
},
]
const mockCompile = (fetchMock, delay = 1000) =>
fetchMock.post(
'express:/project/:projectId/compile',
{
body: {
status: 'success',
clsiServerId: 'foo',
compileGroup: 'priority',
pdfDownloadDomain: '',
outputFiles,
},
},
{ delay }
)
const mockCompileError = (fetchMock, status = 'success', delay = 1000) =>
fetchMock.post(
'express:/project/:projectId/compile',
{
body: {
status,
clsiServerId: 'foo',
compileGroup: 'priority',
},
},
{ delay, overwriteRoutes: true }
)
const mockCompileValidationIssues = (
fetchMock,
validationProblems,
delay = 1000
) =>
fetchMock.post(
'express:/project/:projectId/compile',
() => {
return {
body: {
status: 'validation-problems',
validationProblems,
clsiServerId: 'foo',
compileGroup: 'priority',
},
}
},
{ delay, overwriteRoutes: true }
)
const mockClearCache = fetchMock =>
fetchMock.delete('express:/project/:projectId/output', 204, {
delay: 1000,
})
const mockBuildFile = fetchMock =>
fetchMock.get(
'express:/build/:file',
(url, options, request) => {
const { pathname } = new URL(url, 'https://example.com')
switch (pathname) {
case '/build/output.blg':
return 'This is BibTeX, Version 4.0' // FIXME
case '/build/output.log':
return `
The LaTeX compiler output
* With a lot of details
Wrapped in an HTML
element with
preformatted text which is to be presented exactly
as written in the HTML file
(whitespace included™)
The text is typically rendered using a non-proportional ("monospace") font.
LaTeX Font Info: External font \`cmex10' loaded for size
(Font) <7> on input line 18.
LaTeX Font Info: External font \`cmex10' loaded for size
(Font) <5> on input line 18.
! Undefined control sequence.
\\Zlpha
main.tex, line 23
`
case '/build/output.pdf':
return new Promise(resolve => {
const xhr = new XMLHttpRequest()
xhr.addEventListener('load', () => {
resolve({
status: 200,
headers: {
'Content-Length': xhr.getResponseHeader('Content-Length'),
'Content-Type': xhr.getResponseHeader('Content-Type'),
},
body: xhr.response,
})
})
xhr.open('GET', examplePdf)
xhr.responseType = 'arraybuffer'
xhr.send()
})
default:
return 404
}
},
{ sendAsJson: false }
)
export const Interactive = () => {
useFetchMock(fetchMock => {
mockCompile(fetchMock)
mockBuildFile(fetchMock)
mockClearCache(fetchMock)
})
useEffect(() => {
dispatchProjectJoined()
}, [])
const Inner = () => {
const context = usePdfPreviewContext()
const { setHasLintingError } = context
const toggleLintingError = useCallback(() => {
setHasLintingError(value => !value)
}, [setHasLintingError])
const values = useMemo(() => {
const entries = Object.entries(context).sort((a, b) => {
return a[0].localeCompare(b[0])
})
const values = { boolean: [], other: [] }
for (const entry of entries) {
const type = typeof entry[1]
if (type === 'boolean') {
values.boolean.push(entry)
} else if (type !== 'function') {
values.other.push(entry)
}
}
return values
}, [context])
return (
{values.boolean.map(([key, value]) => {
return (
{value ? '🟢' : '🔴'} |
{key} |
)
})}
{values.other.map(([key, value]) => {
return (
{key}
|
{JSON.stringify(value, null, 2)}
|
)
})}
)
}
return withContextRoot(
,
scope
)
}
const compileStatuses = [
'autocompile-backoff',
'clear-cache',
'clsi-maintenance',
'compile-in-progress',
'exited',
'failure',
'generic',
'project-too-large',
'rate-limited',
'success',
'terminated',
'timedout',
'too-recently-compiled',
'unavailable',
'validation-problems',
'foo',
]
export const CompileError = () => {
const [status, setStatus] = useState('success')
useFetchMock(fetchMock => {
mockCompileError(fetchMock, status, 0)
mockBuildFile(fetchMock)
})
const Inner = () => {
const { startCompile } = usePdfPreviewContext()
const handleStatusChange = useCallback(
event => {
setStatus(event.target.value)
window.setTimeout(() => {
startCompile()
}, 0)
},
[startCompile]
)
return (
)
}
return withContextRoot(
,
scope
)
}
const compileErrors = [
'autocompile-backoff',
'clear-cache',
'clsi-maintenance',
'compile-in-progress',
'exited',
'failure',
'generic',
'project-too-large',
'rate-limited',
'success',
'terminated',
'timedout',
'too-recently-compiled',
'unavailable',
'validation-problems',
'foo',
]
export const DisplayError = () => {
return withContextRoot(
{compileErrors.map(error => (
))}
,
scope
)
}
export const Toolbar = () => {
useFetchMock(fetchMock => mockCompile(fetchMock, 500))
return withContextRoot(
,
scope
)
}
export const HybridToolbar = () => {
useFetchMock(fetchMock => {
mockCompile(fetchMock, 500)
mockBuildFile(fetchMock)
})
return withContextRoot(
,
scope
)
}
export const FileList = () => {
const fileList = useMemo(() => {
return buildFileList(outputFiles)
}, [])
return (
)
}
export const Logs = () => {
useFetchMock(fetchMock => {
mockCompile(fetchMock, 0)
mockBuildFile(fetchMock)
mockClearCache(fetchMock)
})
useEffect(() => {
dispatchProjectJoined()
}, [])
return withContextRoot(
,
scope
)
}
const validationProblems = {
sizeCheck: {
resources: [
{ path: 'foo/bar', kbSize: 76221 },
{ path: 'bar/baz', kbSize: 2342 },
],
},
mainFile: true,
conflictedPaths: [
{
path: 'foo/bar',
},
{
path: 'foo/baz',
},
],
}
export const ValidationIssues = () => {
useFetchMock(fetchMock => {
mockCompileValidationIssues(fetchMock, validationProblems, 0)
mockBuildFile(fetchMock)
})
useEffect(() => {
dispatchProjectJoined()
}, [])
return withContextRoot(
,
scope
)
}