diff --git a/services/web/app/views/project/editor.pug b/services/web/app/views/project/editor.pug index f3e7139098..48a1e98986 100644 --- a/services/web/app/views/project/editor.pug +++ b/services/web/app/views/project/editor.pug @@ -179,8 +179,9 @@ block append meta meta(name="ol-gitBridgePublicBaseUrl" content=gitBridgePublicBaseUrl) //- Set base path for Ace scripts loaded on demand/workers and don't use cdn meta(name="ol-aceBasePath" content="/js/" + lib('ace')) - //- Set path for PDFjs CMaps + //- Set path for PDFjs CMaps and images meta(name="ol-pdfCMapsPath" content="/js/cmaps/") + meta(name="ol-pdfImageResourcesPath" content="/images/") //- enable doc hash checking for all projects //- used in public/js/libs/sharejs.js meta(name="ol-useShareJsHash" data-type="boolean" content=true) diff --git a/services/web/app/views/project/editor/editor.pug b/services/web/app/views/project/editor/editor.pug index c8fdef22bf..f8c0451ebd 100644 --- a/services/web/app/views/project/editor/editor.pug +++ b/services/web/app/views/project/editor/editor.pug @@ -18,14 +18,17 @@ div.full-size( include ./editor-no-symbol-palette .ui-layout-east - div(ng-if="ui.pdfLayout == 'sideBySide'") - if showNewPdfPreview - pdf-preview-pane() - else + // The pdf-preview component needs to always be rendered, even when the editor is in "full-width" mode and it's not visible. + // It doesn't recompile while hidden, due to the ui.pdfHidden flag, but maintains its state for when it's shown again. + if showNewPdfPreview + div(ng-show="ui.pdfLayout == 'sideBySide'") + pdf-preview() + else + div(ng-if="ui.pdfLayout == 'sideBySide'") include ./pdf .ui-layout-resizer-controls.synctex-controls( - ng-show="!!pdf.url && settings.pdfViewer == 'pdfjs'" + ng-show="!!pdf.url && settings.pdfViewer !== 'native'" ng-controller="PdfSynctexController" ) a.btn.btn-default.btn-xs.synctex-control.synctex-control-goto-pdf( @@ -52,7 +55,7 @@ div.full-size( ng-show="ui.view == 'pdf'" ) if showNewPdfPreview - pdf-preview-pane() + pdf-preview() else include ./pdf diff --git a/services/web/frontend/js/features/pdf-preview/components/pdf-clear-cache-button.js b/services/web/frontend/js/features/pdf-preview/components/pdf-clear-cache-button.js new file mode 100644 index 0000000000..cc76e9463b --- /dev/null +++ b/services/web/frontend/js/features/pdf-preview/components/pdf-clear-cache-button.js @@ -0,0 +1,27 @@ +import Icon from '../../../shared/components/icon' +import { Button } from 'react-bootstrap' +import { usePdfPreviewContext } from '../contexts/pdf-preview-context' +import { useTranslation } from 'react-i18next' +import { memo } from 'react' + +function PdfClearCacheButton() { + const { compiling, clearCache, clearingCache } = usePdfPreviewContext() + + const { t } = useTranslation() + + return ( + + ) +} + +export default memo(PdfClearCacheButton) diff --git a/services/web/frontend/js/features/pdf-preview/components/pdf-compile-button.js b/services/web/frontend/js/features/pdf-preview/components/pdf-compile-button.js new file mode 100644 index 0000000000..ee4d5bd9a8 --- /dev/null +++ b/services/web/frontend/js/features/pdf-preview/components/pdf-compile-button.js @@ -0,0 +1,118 @@ +import { Button, Dropdown, MenuItem } from 'react-bootstrap' +import Icon from '../../../shared/components/icon' +import ControlledDropdown from '../../../shared/components/controlled-dropdown' +import { useTranslation } from 'react-i18next' +import { usePdfPreviewContext } from '../contexts/pdf-preview-context' +import { memo, useCallback } from 'react' +import classnames from 'classnames' + +function PdfCompileButton() { + const { + autoCompile, + compiling, + draft, + hasChanges, + recompile, + setAutoCompile, + setDraft, + setStopOnValidationError, + stopCompile, + stopOnValidationError, + recompileFromScratch, + } = usePdfPreviewContext() + + const { t } = useTranslation() + + const compileButtonLabel = compiling ? t('compiling') + '…' : t('recompile') + + const startCompile = useCallback(() => { + recompile() + }, [recompile]) + + return ( + + + + + + + {t('auto_compile')} + + setAutoCompile(true)}> + + {t('on')} + + + setAutoCompile(false)}> + + {t('off')} + + + {t('compile_mode')} + + setDraft(false)}> + + {t('normal')} + + + setDraft(true)}> + + {t('fast')} [draft] + + + Syntax Checks + + setStopOnValidationError(true)}> + + {t('stop_on_validation_error')} + + + setStopOnValidationError(false)}> + + {t('ignore_validation_errors')} + + + + + + {t('stop_compile')} + + + + {t('recompile_from_scratch')} + + + + ) +} + +export default memo(PdfCompileButton) diff --git a/services/web/frontend/js/features/pdf-preview/components/pdf-download-button.js b/services/web/frontend/js/features/pdf-preview/components/pdf-download-button.js new file mode 100644 index 0000000000..4814416f74 --- /dev/null +++ b/services/web/frontend/js/features/pdf-preview/components/pdf-download-button.js @@ -0,0 +1,50 @@ +import { useTranslation } from 'react-i18next' +import { Button, Dropdown } from 'react-bootstrap' +import Icon from '../../../shared/components/icon' +import ControlledDropdown from '../../../shared/components/controlled-dropdown' +import PdfFileList from './pdf-file-list' +import { usePdfPreviewContext } from '../contexts/pdf-preview-context' +import { memo } from 'react' + +function PdfDownloadButton() { + const { compiling, pdfDownloadUrl, fileList } = usePdfPreviewContext() + + const { t } = useTranslation() + + const disabled = compiling || !pdfDownloadUrl + + return ( + + + + + + + + + + ) +} + +export default memo(PdfDownloadButton) diff --git a/services/web/frontend/js/features/pdf-preview/components/pdf-download-files-button.js b/services/web/frontend/js/features/pdf-preview/components/pdf-download-files-button.js new file mode 100644 index 0000000000..e7c8de1884 --- /dev/null +++ b/services/web/frontend/js/features/pdf-preview/components/pdf-download-files-button.js @@ -0,0 +1,33 @@ +import { Dropdown } from 'react-bootstrap' +import PdfFileList from './pdf-file-list' +import ControlledDropdown from '../../../shared/components/controlled-dropdown' +import { memo } from 'react' +import { usePdfPreviewContext } from '../contexts/pdf-preview-context' +import { useTranslation } from 'react-i18next' + +function PdfDownloadFilesButton() { + const { compiling, fileList } = usePdfPreviewContext() + + const { t } = useTranslation() + + return ( + + + + + + + ) +} + +export default memo(PdfDownloadFilesButton) diff --git a/services/web/frontend/js/features/pdf-preview/components/pdf-expand-button.js b/services/web/frontend/js/features/pdf-preview/components/pdf-expand-button.js new file mode 100644 index 0000000000..3534e43904 --- /dev/null +++ b/services/web/frontend/js/features/pdf-preview/components/pdf-expand-button.js @@ -0,0 +1,32 @@ +import { Button, OverlayTrigger, Tooltip } from 'react-bootstrap' +import { useTranslation } from 'react-i18next' +import Icon from '../../../shared/components/icon' +import { usePdfPreviewContext } from '../contexts/pdf-preview-context' +import { memo, useMemo } from 'react' + +function PdfExpandButton() { + const { pdfLayout, switchLayout } = usePdfPreviewContext() + + const { t } = useTranslation() + + const text = useMemo(() => { + return pdfLayout === 'sideBySide' ? t('full_screen') : t('split_screen') + }, [pdfLayout, t]) + + return ( + {text}} + > + + + ) +} + +export default memo(PdfExpandButton) diff --git a/services/web/frontend/js/features/pdf-preview/components/pdf-file-list.js b/services/web/frontend/js/features/pdf-preview/components/pdf-file-list.js new file mode 100644 index 0000000000..e039e2f4e9 --- /dev/null +++ b/services/web/frontend/js/features/pdf-preview/components/pdf-file-list.js @@ -0,0 +1,50 @@ +import { MenuItem } from 'react-bootstrap' +import { useTranslation } from 'react-i18next' +import { memo } from 'react' +import PropTypes from 'prop-types' + +function PdfFileList({ fileList }) { + const { t } = useTranslation() + + if (!fileList) { + return null + } + + return ( + <> + {t('other_output_files')} + + {fileList.top.map(file => ( + + {file.path} + + ))} + + {fileList.other.length > 0 && fileList.top.length > 0 && ( + + )} + + {fileList.other.map(file => ( + + {file.path} + + ))} + + ) +} + +const FilesArray = PropTypes.arrayOf( + PropTypes.shape({ + path: PropTypes.string.isRequired, + url: PropTypes.string.isRequired, + }) +) + +PdfFileList.propTypes = { + fileList: PropTypes.shape({ + top: FilesArray, + other: FilesArray, + }), +} + +export default memo(PdfFileList) diff --git a/services/web/frontend/js/features/pdf-preview/components/pdf-js-viewer.js b/services/web/frontend/js/features/pdf-preview/components/pdf-js-viewer.js new file mode 100644 index 0000000000..c6e5e9c771 --- /dev/null +++ b/services/web/frontend/js/features/pdf-preview/components/pdf-js-viewer.js @@ -0,0 +1,239 @@ +import PropTypes from 'prop-types' +import { memo, useCallback, useEffect, useState } from 'react' +import { debounce } from 'lodash' +import { Alert } from 'react-bootstrap' +import PdfViewerControls from './pdf-viewer-controls' +import { useProjectContext } from '../../../shared/context/project-context' +import usePersistedState from '../../../shared/hooks/use-persisted-state' +import useScopeValue from '../../../shared/context/util/scope-value-hook' +import { buildHighlightElement } from '../util/highlights' +import PDFJSWrapper from '../util/pdf-js-wrapper' + +function PdfJsViewer({ url }) { + const { _id: projectId } = useProjectContext() + + // state values persisted in localStorage to restore on load + const [scale, setScale] = usePersistedState( + `pdf-viewer-scale:${projectId}`, + 'page-width' + ) + const [, setScrollTop] = usePersistedState( + `pdf-viewer-scroll-top:${projectId}`, + 0 + ) + + // state values shared with Angular scope (highlights => editor, position => synctex buttons + const [highlights] = useScopeValue('pdf.highlights') + const [, setPosition] = useScopeValue('pdf.position') + + // local state values + const [pdfJsWrapper, setPdfJsWrapper] = useState() + const [initialised, setInitialised] = useState(false) + const [error, setError] = useState() + + // create the viewer when the container is mounted + const handleContainer = useCallback(parent => { + setPdfJsWrapper(parent ? new PDFJSWrapper(parent.firstChild) : undefined) + }, []) + + // listen for initialize event + useEffect(() => { + if (pdfJsWrapper) { + pdfJsWrapper.eventBus.on('pagesinit', () => { + setInitialised(true) + }) + } + }, [pdfJsWrapper]) + + // load the PDF document from the URL + useEffect(() => { + if (pdfJsWrapper && url) { + setInitialised(false) + setError(undefined) + // TODO: anything else to be reset? + + pdfJsWrapper.loadDocument(url).catch(error => setError(error)) + } + }, [pdfJsWrapper, url]) + + useEffect(() => { + if (pdfJsWrapper) { + // listen for 'pdf:scroll-to-position' events + const eventListener = event => { + pdfJsWrapper.container.scrollTop = event.data.position + } + + window.addEventListener('pdf:scroll-to-position', eventListener) + + return () => { + window.removeEventListener('pdf:scroll-to-position', eventListener) + } + } + }, [pdfJsWrapper]) + + // listen for scroll events + useEffect(() => { + if (pdfJsWrapper) { + // store the scroll position in localStorage, for the synctex button + const storePosition = debounce(pdfViewer => { + // set position for "sync to code" button + try { + setPosition(pdfViewer.currentPosition) + } catch (error) { + // console.error(error) // TODO + } + }, 500) + + // store the scroll position in localStorage, for use when reloading + const storeScrollTop = debounce(pdfViewer => { + // set position for "sync to code" button + setScrollTop(pdfJsWrapper.container.scrollTop) + }, 500) + + storePosition(pdfJsWrapper) + + const scrollListener = () => { + storeScrollTop(pdfJsWrapper) + storePosition(pdfJsWrapper) + } + + pdfJsWrapper.container.addEventListener('scroll', scrollListener) + + return () => { + pdfJsWrapper.container.removeEventListener('scroll', scrollListener) + } + } + }, [setPosition, setScrollTop, pdfJsWrapper]) + + // listen for double-click events + useEffect(() => { + if (pdfJsWrapper) { + pdfJsWrapper.eventBus.on('textlayerrendered', textLayer => { + const pageElement = textLayer.source.textLayerDiv.closest('.page') + + const doubleClickListener = event => { + window.dispatchEvent( + new CustomEvent('synctex:sync-to-position', { + detail: pdfJsWrapper.clickPosition(event, pageElement, textLayer), + }) + ) + } + + pageElement.addEventListener('dblclick', doubleClickListener) + }) + } + }, [pdfJsWrapper]) + + // restore the saved scale and scroll position + useEffect(() => { + if (initialised && pdfJsWrapper) { + setScale(scale => { + pdfJsWrapper.viewer.currentScaleValue = scale + return scale + }) + + // restore the scroll position + setScrollTop(scrollTop => { + if (scrollTop > 0) { + pdfJsWrapper.container.scrollTop = scrollTop + } + return scrollTop + }) + } + }, [initialised, setScale, setScrollTop, pdfJsWrapper]) + + // transmit scale value to the viewer when it changes + useEffect(() => { + if (pdfJsWrapper) { + pdfJsWrapper.viewer.currentScaleValue = scale + } + }, [scale, pdfJsWrapper]) + + // when highlights are created, build the highlight elements + useEffect(() => { + if (pdfJsWrapper && highlights?.length) { + const elements = highlights.map(highlight => + buildHighlightElement(highlight, pdfJsWrapper.viewer) + ) + + // scroll to the first highlighted element + elements[0].scrollIntoView({ + block: 'start', + inline: 'nearest', + behavior: 'smooth', + }) + + return () => { + for (const element of elements) { + element.remove() + } + } + } + }, [highlights, pdfJsWrapper]) + + // set the scale in response to zoom option changes + const setZoom = useCallback( + zoom => { + switch (zoom) { + case 'fit-width': + setScale('page-width') + break + + case 'fit-height': + setScale('page-height') + break + + case 'zoom-in': + setScale(pdfJsWrapper.viewer.currentScale * 1.25) + break + + case 'zoom-out': + setScale(pdfJsWrapper.viewer.currentScale * 0.75) + break + } + }, + [pdfJsWrapper, setScale] + ) + + // adjust the scale when the container is resized + useEffect(() => { + if (pdfJsWrapper) { + const resizeListener = () => { + pdfJsWrapper.updateOnResize() + } + + const resizeObserver = new ResizeObserver(resizeListener) + resizeObserver.observe(pdfJsWrapper.container) + + window.addEventListener('resize', resizeListener) + + return () => { + resizeObserver.disconnect() + window.removeEventListener('resize', resizeListener) + } + } + }, [pdfJsWrapper]) + + /* eslint-disable jsx-a11y/no-noninteractive-tabindex */ + return ( +
+
+
+
+
+ +
+ {error && ( +
+ {error.message} +
+ )} +
+ ) +} + +PdfJsViewer.propTypes = { + url: PropTypes.string.isRequired, +} + +export default memo(PdfJsViewer) diff --git a/services/web/frontend/js/features/pdf-preview/components/pdf-logs-button-content.js b/services/web/frontend/js/features/pdf-preview/components/pdf-logs-button-content.js new file mode 100644 index 0000000000..86d7987936 --- /dev/null +++ b/services/web/frontend/js/features/pdf-preview/components/pdf-logs-button-content.js @@ -0,0 +1,67 @@ +import { useTranslation } from 'react-i18next' +import Icon from '../../../shared/components/icon' +import PropTypes from 'prop-types' +import { memo } from 'react' + +export function PdfLogsButtonContent({ + showLogs, + logEntries, + autoCompileLintingError, +}) { + const { t } = useTranslation() + + if (showLogs) { + return ( + <> + + {t('view_pdf')} + + ) + } + + if (autoCompileLintingError) { + return ( + <> + + + {t('code_check_failed')} + + + ) + } + + const count = logEntries?.errors?.length || logEntries?.warnings?.length + + if (!count) { + return ( + <> + + + {t('view_logs')} + + + ) + } + + return ( + <> + + + {logEntries.errors?.length + ? t('your_project_has_an_error', { count }) + : t('view_warning', { count })} + + {count > 1 && ` (${count > 99 ? '99+' : count})`} + + + + ) +} + +PdfLogsButtonContent.propTypes = { + autoCompileLintingError: PropTypes.bool, + showLogs: PropTypes.bool, + logEntries: PropTypes.object, +} + +export default memo(PdfLogsButtonContent) diff --git a/services/web/frontend/js/features/pdf-preview/components/pdf-logs-button.js b/services/web/frontend/js/features/pdf-preview/components/pdf-logs-button.js new file mode 100644 index 0000000000..d0a03c2e00 --- /dev/null +++ b/services/web/frontend/js/features/pdf-preview/components/pdf-logs-button.js @@ -0,0 +1,68 @@ +import { memo, useCallback, useMemo } from 'react' +import { Button } from 'react-bootstrap' +import { usePdfPreviewContext } from '../contexts/pdf-preview-context' +import { PdfLogsButtonContent } from './pdf-logs-button-content' +import { sendMBOnce } from '../../../infrastructure/event-tracking' + +function PdfLogsButton() { + const { + autoCompileLintingError, + stopOnValidationError, + error, + logEntries, + showLogs, + setShowLogs, + } = usePdfPreviewContext() + + const buttonStyle = useMemo(() => { + if (showLogs) { + return 'default' + } + + if (autoCompileLintingError && stopOnValidationError) { + return 'danger' + } + + if (logEntries) { + if (logEntries.errors?.length) { + return 'danger' + } + + if (logEntries.warnings?.length) { + return 'warning' + } + } + + return 'default' + }, [autoCompileLintingError, logEntries, showLogs, stopOnValidationError]) + + const handleClick = useCallback(() => { + setShowLogs(value => { + if (!value) { + sendMBOnce('ide-open-logs-once') + } + + return !value + }) + }, [setShowLogs]) + + return ( + + ) +} + +export default memo(PdfLogsButton) diff --git a/services/web/frontend/js/features/pdf-preview/components/pdf-logs-entries.js b/services/web/frontend/js/features/pdf-preview/components/pdf-logs-entries.js new file mode 100644 index 0000000000..5187614377 --- /dev/null +++ b/services/web/frontend/js/features/pdf-preview/components/pdf-logs-entries.js @@ -0,0 +1,46 @@ +import { memo, useCallback } from 'react' +import PropTypes from 'prop-types' +import { useTranslation } from 'react-i18next' +import PreviewLogsPaneEntry from '../../preview/components/preview-logs-pane-entry' + +function PdfLogsEntries({ entries }) { + const { t } = useTranslation() + + const syncToEntry = useCallback(entry => { + window.dispatchEvent( + new CustomEvent('synctex:sync-to-entry', { + detail: entry, + }) + ) + }, []) + + return ( + <> + {entries.map(logEntry => ( + + ))} + + ) +} +PdfLogsEntries.propTypes = { + entries: PropTypes.arrayOf(PropTypes.object), +} + +export default memo(PdfLogsEntries) diff --git a/services/web/frontend/js/features/pdf-preview/components/pdf-logs-viewer.js b/services/web/frontend/js/features/pdf-preview/components/pdf-logs-viewer.js new file mode 100644 index 0000000000..e782d445c1 --- /dev/null +++ b/services/web/frontend/js/features/pdf-preview/components/pdf-logs-viewer.js @@ -0,0 +1,70 @@ +import Icon from '../../../shared/components/icon' +import { useTranslation } from 'react-i18next' +import PreviewLogsPaneEntry from '../../preview/components/preview-logs-pane-entry' +import { usePdfPreviewContext } from '../contexts/pdf-preview-context' +import { memo } from 'react' +import PdfValidationIssue from './pdf-validation-issue' +import TimeoutUpgradePrompt from './timeout-upgrade-prompt' +import PdfPreviewError from './pdf-preview-error' +import PdfClearCacheButton from './pdf-clear-cache-button' +import PdfDownloadFilesButton from './pdf-download-files-button' +import PdfLogsEntries from './pdf-logs-entries' + +function PdfLogsViewer() { + const { + autoCompileLintingError, + stopOnValidationError, + error, + logEntries, + rawLog, + validationIssues, + } = usePdfPreviewContext() + + const { t } = useTranslation() + + return ( +
+
+ {autoCompileLintingError && stopOnValidationError && ( +
+
+
+ +
+

+ {t('code_check_failed_explanation')} +

+
+
+ )} + + {error && } + + {error === 'timedout' && } + + {validationIssues && + Object.entries(validationIssues).map(([name, issue]) => ( + + ))} + + {logEntries?.all && } + + {rawLog && ( + + )} + +
+ + +
+
+
+ ) +} + +export default memo(PdfLogsViewer) diff --git a/services/web/frontend/js/features/pdf-preview/components/pdf-preview-error.js b/services/web/frontend/js/features/pdf-preview/components/pdf-preview-error.js new file mode 100644 index 0000000000..b0eeb923eb --- /dev/null +++ b/services/web/frontend/js/features/pdf-preview/components/pdf-preview-error.js @@ -0,0 +1,155 @@ +import PropTypes from 'prop-types' +import { useTranslation, Trans } from 'react-i18next' +import PreviewLogsPaneEntry from '../../preview/components/preview-logs-pane-entry' +import { memo } from 'react' + +function PdfPreviewError({ error }) { + const { t } = useTranslation() + + switch (error) { + case 'rendering-error': + return ( + + {t('something_went_wrong_rendering_pdf')} + + ) + + case 'clsi-maintenance': + return ( + + {t('clsi_maintenance')} + + ) + + case 'clsi-unavailable': + return ( + + {t('clsi_unavailable')} + + ) + + case 'too-recently-compiled': + return ( + + {t('too_recently_compiled')} + + ) + + case 'terminated': + return ( + + {t('compile_terminated_by_user')} + + ) + + case 'rate-limited': + return ( + + {t('project_flagged_too_many_compiles')} + + ) + + case 'compile-in-progress': + return ( + + {t('pdf_compile_try_again')} + + ) + + case 'auto-compile-disabled': + return ( + + {t('autocompile_disabled_reason')} + + ) + + case 'project-too-large': + return ( + + {t('project_too_much_editable_text')} + + ) + + case 'timedout': + return ( + + {t('proj_timed_out_reason')} + + + + ) + + case 'failure': + return ( + + {t('no_pdf_error_explanation')} + +
    +
  • {t('no_pdf_error_reason_unrecoverable_error')}
  • +
  • + }} + /> +
  • +
  • + }} + /> +
  • +
+
+ ) + + case 'clear-cache': + return ( + + {t('somthing_went_wrong_compiling')} + + ) + + case 'validation-problems': + return null // handled elsewhere + + case 'error': + default: + return ( + + {t('somthing_went_wrong_compiling')} + + ) + } +} + +PdfPreviewError.propTypes = { + error: PropTypes.string.isRequired, +} + +export default memo(PdfPreviewError) + +function ErrorLogEntry({ title, children }) { + const { t } = useTranslation() + + return ( + + ) +} + +ErrorLogEntry.propTypes = { + title: PropTypes.string.isRequired, + children: PropTypes.any.isRequired, +} diff --git a/services/web/frontend/js/features/pdf-preview/components/pdf-preview-pane.js b/services/web/frontend/js/features/pdf-preview/components/pdf-preview-pane.js index 609685bdcb..19999f8d38 100644 --- a/services/web/frontend/js/features/pdf-preview/components/pdf-preview-pane.js +++ b/services/web/frontend/js/features/pdf-preview/components/pdf-preview-pane.js @@ -1,7 +1,24 @@ -import { memo } from 'react' +import { memo, Suspense } from 'react' +import PdfLogsViewer from './pdf-logs-viewer' +import PdfViewer from './pdf-viewer' +import { usePdfPreviewContext } from '../contexts/pdf-preview-context' +import withErrorBoundary from '../../../infrastructure/error-boundary' +import PdfPreviewToolbar from './pdf-preview-toolbar' function PdfPreviewPane() { - return
PDF Preview
+ const { showLogs } = usePdfPreviewContext() + + return ( +
+ + Loading…
}> +
+ +
+ + {showLogs && } +
+ ) } -export default memo(PdfPreviewPane) +export default memo(withErrorBoundary(PdfPreviewPane)) diff --git a/services/web/frontend/js/features/pdf-preview/components/pdf-preview-toolbar.js b/services/web/frontend/js/features/pdf-preview/components/pdf-preview-toolbar.js new file mode 100644 index 0000000000..3ade98ab66 --- /dev/null +++ b/services/web/frontend/js/features/pdf-preview/components/pdf-preview-toolbar.js @@ -0,0 +1,32 @@ +import PdfCompileButton from './pdf-compile-button' +import PdfDownloadButton from './pdf-download-button' +import PdfLogsButton from './pdf-logs-button' +import PdfExpandButton from './pdf-expand-button' +import { ButtonToolbar } from 'react-bootstrap' +import { memo, useState } from 'react' +import useToolbarBreakpoint from '../hooks/use-toolbar-breakpoint' + +const isPreview = new URLSearchParams(window.location.search).get('preview') + +function PdfPreviewToolbar() { + const [element, setElement] = useState() + + const toolbarClasses = useToolbarBreakpoint(element) + + return ( +
setElement(element)}> + +
+ + +
+
+ + {!isPreview && } +
+
+
+ ) +} + +export default memo(PdfPreviewToolbar) diff --git a/services/web/frontend/js/features/pdf-preview/components/pdf-preview.js b/services/web/frontend/js/features/pdf-preview/components/pdf-preview.js new file mode 100644 index 0000000000..a68e33f76b --- /dev/null +++ b/services/web/frontend/js/features/pdf-preview/components/pdf-preview.js @@ -0,0 +1,13 @@ +import PdfPreviewProvider from '../contexts/pdf-preview-context' +import PdfPreviewPane from './pdf-preview-pane' +import { memo } from 'react' + +function PdfPreview() { + return ( + + + + ) +} + +export default memo(PdfPreview) diff --git a/services/web/frontend/js/features/pdf-preview/components/pdf-validation-issue.js b/services/web/frontend/js/features/pdf-preview/components/pdf-validation-issue.js new file mode 100644 index 0000000000..fcb68cf2d7 --- /dev/null +++ b/services/web/frontend/js/features/pdf-preview/components/pdf-validation-issue.js @@ -0,0 +1,71 @@ +import { memo } from 'react' +import { useTranslation } from 'react-i18next' +import PropTypes from 'prop-types' +import PreviewLogsPaneEntry from '../../preview/components/preview-logs-pane-entry' + +PdfValidationIssue.propTypes = { + name: PropTypes.string.isRequired, + issue: PropTypes.any, +} + +function PdfValidationIssue({ issue, name }) { + const { t } = useTranslation() + + switch (name) { + case 'sizeCheck': + return ( + +
{t('project_too_large_please_reduce')}
+ + + } + entryAriaLabel={t('validation_issue_entry_description')} + level="error" + /> + ) + + case 'conflictedPaths': + return ( + +
{t('following_paths_conflict')}
+
    + {issue.map(detail => ( +
  • /{detail.path}
  • + ))} +
+ + } + entryAriaLabel={t('validation_issue_entry_description')} + level="error" + /> + ) + + case 'mainFile': + return ( + + ) + + default: + return null + } +} + +export default memo(PdfValidationIssue) diff --git a/services/web/frontend/js/features/pdf-preview/components/pdf-viewer-controls.js b/services/web/frontend/js/features/pdf-preview/components/pdf-viewer-controls.js new file mode 100644 index 0000000000..732e6cba9a --- /dev/null +++ b/services/web/frontend/js/features/pdf-preview/components/pdf-viewer-controls.js @@ -0,0 +1,38 @@ +import { ButtonGroup } from 'react-bootstrap' +import PropTypes from 'prop-types' +import Button from 'react-bootstrap/lib/Button' +import Icon from '../../../shared/components/icon' +import { memo } from 'react' + +function PdfViewerControls({ setZoom }) { + return ( + + + + + + + ) +} + +PdfViewerControls.propTypes = { + setZoom: PropTypes.func.isRequired, +} + +export default memo(PdfViewerControls) diff --git a/services/web/frontend/js/features/pdf-preview/components/pdf-viewer.js b/services/web/frontend/js/features/pdf-preview/components/pdf-viewer.js new file mode 100644 index 0000000000..6015285e85 --- /dev/null +++ b/services/web/frontend/js/features/pdf-preview/components/pdf-viewer.js @@ -0,0 +1,38 @@ +import useScopeValue from '../../../shared/context/util/scope-value-hook' +import { usePdfPreviewContext } from '../contexts/pdf-preview-context' +import { lazy, memo, useEffect } from 'react' + +const PdfJsViewer = lazy(() => + import(/* webpackChunkName: "pdf-js-viewer" */ './pdf-js-viewer') +) + +const params = new URLSearchParams(window.location.search) + +function PdfViewer() { + const [pdfViewer, setPdfViewer] = useScopeValue('settings.pdfViewer') + + useEffect(() => { + const viewer = params.get('viewer') + + if (viewer) { + setPdfViewer(viewer) + } + }, [setPdfViewer]) + + const { pdfUrl } = usePdfPreviewContext() + + if (!pdfUrl) { + return null + } + + switch (pdfViewer) { + case 'native': + return