Przeglądaj źródła

Add hybrid toolbar to migrated PDF preview (#5414)

GitOrigin-RevId: 6266028091229c819aee3c8d4bd3bff2e2417125
Alf Eaton 4 lat temu
rodzic
commit
e26d47cb41

+ 5 - 3
services/web/frontend/extracted-translations.json

@@ -55,8 +55,8 @@
   "conflicting_paths_found": "",
   "conflicting_paths_found": "",
   "connected_users": "",
   "connected_users": "",
   "contact_message_label": "",
   "contact_message_label": "",
-  "continue_github_merge": "",
   "contact_us": "",
   "contact_us": "",
+  "continue_github_merge": "",
   "copy": "",
   "copy": "",
   "copy_project": "",
   "copy_project": "",
   "copying": "",
   "copying": "",
@@ -179,9 +179,10 @@
   "log_entry_maximum_entries_message": "",
   "log_entry_maximum_entries_message": "",
   "log_entry_maximum_entries_title": "",
   "log_entry_maximum_entries_title": "",
   "log_hint_extra_info": "",
   "log_hint_extra_info": "",
+  "log_viewer_error": "",
+  "logs_and_output_files": "",
   "logs_pane_info_message": "",
   "logs_pane_info_message": "",
   "logs_pane_info_message_popup": "",
   "logs_pane_info_message_popup": "",
-  "log_viewer_error": "",
   "main_file_not_found": "",
   "main_file_not_found": "",
   "make_private": "",
   "make_private": "",
   "manage_files_from_your_dropbox_folder": "",
   "manage_files_from_your_dropbox_folder": "",
@@ -257,7 +258,7 @@
   "project_too_large": "",
   "project_too_large": "",
   "project_too_large_please_reduce": "",
   "project_too_large_please_reduce": "",
   "project_too_much_editable_text": "",
   "project_too_much_editable_text": "",
-  "project_url" : "",
+  "project_url": "",
   "public": "",
   "public": "",
   "pull_github_changes_into_sharelatex": "",
   "pull_github_changes_into_sharelatex": "",
   "push_sharelatex_changes_to_github": "",
   "push_sharelatex_changes_to_github": "",
@@ -269,6 +270,7 @@
   "recent_commits_in_github": "",
   "recent_commits_in_github": "",
   "recompile": "",
   "recompile": "",
   "recompile_from_scratch": "",
   "recompile_from_scratch": "",
+  "recompile_pdf": "",
   "reconnect": "",
   "reconnect": "",
   "reference_error_relink_hint": "",
   "reference_error_relink_hint": "",
   "refresh": "",
   "refresh": "",

+ 30 - 11
services/web/frontend/js/features/pdf-preview/components/pdf-compile-button.js

@@ -1,4 +1,10 @@
-import { Button, Dropdown, MenuItem } from 'react-bootstrap'
+import {
+  Button,
+  Dropdown,
+  MenuItem,
+  OverlayTrigger,
+  Tooltip,
+} from 'react-bootstrap'
 import Icon from '../../../shared/components/icon'
 import Icon from '../../../shared/components/icon'
 import ControlledDropdown from '../../../shared/components/controlled-dropdown'
 import ControlledDropdown from '../../../shared/components/controlled-dropdown'
 import { useTranslation } from 'react-i18next'
 import { useTranslation } from 'react-i18next'
@@ -6,6 +12,8 @@ import { usePdfPreviewContext } from '../contexts/pdf-preview-context'
 import { memo } from 'react'
 import { memo } from 'react'
 import classnames from 'classnames'
 import classnames from 'classnames'
 
 
+const modifierKey = /Mac/i.test(navigator.platform) ? 'Cmd' : 'Ctrl'
+
 function PdfCompileButton() {
 function PdfCompileButton() {
   const {
   const {
     autoCompile,
     autoCompile,
@@ -34,17 +42,28 @@ function PdfCompileButton() {
       })}
       })}
       id="pdf-recompile-dropdown"
       id="pdf-recompile-dropdown"
     >
     >
-      <Button
-        className="btn-recompile"
-        bsStyle="success"
-        onClick={compiling ? stopCompile : startCompile}
-        aria-label={compileButtonLabel}
+      <OverlayTrigger
+        placement="bottom"
+        delayShow={500}
+        overlay={
+          <Tooltip id="tooltip-logs-toggle" className="keyboard-tooltip">
+            {t('recompile_pdf')}{' '}
+            <span className="keyboard-shortcut">({modifierKey} + Enter)</span>
+          </Tooltip>
+        }
       >
       >
-        <Icon type="refresh" spin={compiling} />
-        <span className="toolbar-text toolbar-hide-medium toolbar-hide-small">
-          {compileButtonLabel}
-        </span>
-      </Button>
+        <Button
+          className="btn-recompile"
+          bsStyle="success"
+          onClick={compiling ? stopCompile : startCompile}
+          aria-label={compileButtonLabel}
+        >
+          <Icon type="refresh" spin={compiling} />
+          <span className="toolbar-hide-medium toolbar-hide-small btn-recompile-label">
+            {compileButtonLabel}
+          </span>
+        </Button>
+      </OverlayTrigger>
 
 
       <Dropdown.Toggle
       <Dropdown.Toggle
         aria-label={t('toggle_compile_options_menu')}
         aria-label={t('toggle_compile_options_menu')}

+ 43 - 0
services/web/frontend/js/features/pdf-preview/components/pdf-hybrid-code-check-button.js

@@ -0,0 +1,43 @@
+import { memo, useCallback } from 'react'
+import { usePdfPreviewContext } from '../contexts/pdf-preview-context'
+import { sendMBOnce } from '../../../infrastructure/event-tracking'
+import { Button } from 'react-bootstrap'
+import Icon from '../../../shared/components/icon'
+import { useTranslation } from 'react-i18next'
+
+function PdfHybridCodeCheckButton() {
+  const { codeCheckFailed, error, setShowLogs } = usePdfPreviewContext()
+
+  const { t } = useTranslation()
+
+  const handleClick = useCallback(() => {
+    setShowLogs(value => {
+      if (!value) {
+        sendMBOnce('ide-open-logs-once')
+      }
+
+      return !value
+    })
+  }, [setShowLogs])
+
+  if (!codeCheckFailed) {
+    return null
+  }
+
+  return (
+    <Button
+      bsSize="xsmall"
+      bsStyle="danger"
+      disabled={Boolean(error)}
+      className="btn-toggle-logs toolbar-item"
+      onClick={handleClick}
+    >
+      <Icon type="exclamation-triangle" />
+      <span className="toolbar-text toolbar-hide-small">
+        {t('code_check_failed')}
+      </span>
+    </Button>
+  )
+}
+
+export default memo(PdfHybridCodeCheckButton)

+ 36 - 0
services/web/frontend/js/features/pdf-preview/components/pdf-hybrid-download-button.js

@@ -0,0 +1,36 @@
+import { useTranslation } from 'react-i18next'
+import { Button, OverlayTrigger, Tooltip } from 'react-bootstrap'
+import Icon from '../../../shared/components/icon'
+import { usePdfPreviewContext } from '../contexts/pdf-preview-context'
+import { memo } from 'react'
+
+function PdfHybridDownloadButton() {
+  const { pdfDownloadUrl } = usePdfPreviewContext()
+
+  const { t } = useTranslation()
+
+  return (
+    <OverlayTrigger
+      placement="bottom"
+      overlay={
+        <Tooltip id="tooltip-logs-toggle">
+          {pdfDownloadUrl
+            ? t('download_pdf')
+            : t('please_compile_pdf_before_download')}
+        </Tooltip>
+      }
+    >
+      <Button
+        bsStyle="link"
+        disabled={!pdfDownloadUrl}
+        download
+        href={pdfDownloadUrl || '#'}
+        target="_blank"
+      >
+        <Icon type="download" modifier="fw" />
+      </Button>
+    </OverlayTrigger>
+  )
+}
+
+export default memo(PdfHybridDownloadButton)

+ 55 - 0
services/web/frontend/js/features/pdf-preview/components/pdf-hybrid-logs-button.js

@@ -0,0 +1,55 @@
+import { memo, useCallback } from 'react'
+import { useTranslation } from 'react-i18next'
+import { Button, Label, OverlayTrigger, Tooltip } from 'react-bootstrap'
+import { usePdfPreviewContext } from '../contexts/pdf-preview-context'
+import { sendMBOnce } from '../../../infrastructure/event-tracking'
+import Icon from '../../../shared/components/icon'
+
+function PdfHybridLogsButton() {
+  const { error, logEntries, setShowLogs, showLogs } = usePdfPreviewContext()
+
+  const { t } = useTranslation()
+
+  const handleClick = useCallback(() => {
+    setShowLogs(value => {
+      if (!value) {
+        sendMBOnce('ide-open-logs-once')
+      }
+
+      return !value
+    })
+  }, [setShowLogs])
+
+  const errorCount = Number(logEntries?.errors?.length)
+  const warningCount = Number(logEntries?.warnings?.length)
+  const totalCount = errorCount + warningCount
+
+  return (
+    <OverlayTrigger
+      placement="bottom"
+      overlay={
+        <Tooltip id="tooltip-logs-toggle">{t('logs_and_output_files')}</Tooltip>
+      }
+    >
+      <Button
+        bsStyle="link"
+        disabled={Boolean(error)}
+        active={showLogs}
+        className="toolbar-item log-btn"
+        onClick={handleClick}
+        style={{ position: 'relative' }}
+        aria-label={showLogs ? t('view_pdf') : t('view_logs')}
+      >
+        <Icon type="file-text-o" modifier="fw" />
+
+        {!showLogs && totalCount > 0 && (
+          <Label bsStyle={errorCount === 0 ? 'warning' : 'danger'}>
+            {totalCount}
+          </Label>
+        )}
+      </Button>
+    </OverlayTrigger>
+  )
+}
+
+export default memo(PdfHybridLogsButton)

+ 5 - 15
services/web/frontend/js/features/pdf-preview/components/pdf-logs-viewer.js

@@ -1,4 +1,3 @@
-import Icon from '../../../shared/components/icon'
 import { useTranslation } from 'react-i18next'
 import { useTranslation } from 'react-i18next'
 import PreviewLogsPaneEntry from '../../preview/components/preview-logs-pane-entry'
 import PreviewLogsPaneEntry from '../../preview/components/preview-logs-pane-entry'
 import { usePdfPreviewContext } from '../contexts/pdf-preview-context'
 import { usePdfPreviewContext } from '../contexts/pdf-preview-context'
@@ -11,7 +10,8 @@ import PdfDownloadFilesButton from './pdf-download-files-button'
 import PdfLogsEntries from './pdf-logs-entries'
 import PdfLogsEntries from './pdf-logs-entries'
 import withErrorBoundary from '../../../infrastructure/error-boundary'
 import withErrorBoundary from '../../../infrastructure/error-boundary'
 import ErrorBoundaryFallback from './error-boundary-fallback'
 import ErrorBoundaryFallback from './error-boundary-fallback'
-import { LogsPaneInfoNotice } from '../../preview/components/preview-logs-pane'
+import PdfCodeCheckFailedNotice from '../../preview/components/pdf-code-check-failed-notice'
+import PdfLogsPaneInfoNotice from '../../preview/components/pdf-logs-pane-info-notice'
 
 
 function PdfLogsViewer() {
 function PdfLogsViewer() {
   const {
   const {
@@ -27,19 +27,9 @@ function PdfLogsViewer() {
   return (
   return (
     <div className="logs-pane">
     <div className="logs-pane">
       <div className="logs-pane-content">
       <div className="logs-pane-content">
-        <LogsPaneInfoNotice />
-        {codeCheckFailed && (
-          <div className="log-entry">
-            <div className="log-entry-header log-entry-header-error">
-              <div className="log-entry-header-icon-container">
-                <Icon type="exclamation-triangle" modifier="fw" />
-              </div>
-              <h3 className="log-entry-header-title">
-                {t('code_check_failed_explanation')}
-              </h3>
-            </div>
-          </div>
-        )}
+        <PdfLogsPaneInfoNotice />
+
+        {codeCheckFailed && <PdfCodeCheckFailedNotice />}
 
 
         {error && <PdfPreviewError error={error} />}
         {error && <PdfPreviewError error={error} />}
 
 

+ 25 - 0
services/web/frontend/js/features/pdf-preview/components/pdf-preview-hybrid-toolbar.js

@@ -0,0 +1,25 @@
+import { memo } from 'react'
+import { ButtonToolbar } from 'react-bootstrap'
+import PdfCompileButton from './pdf-compile-button'
+import PdfExpandButton from './pdf-expand-button'
+import PdfHybridLogsButton from './pdf-hybrid-logs-button'
+import PdfHybridDownloadButton from './pdf-hybrid-download-button'
+import PdfHybridCodeCheckButton from './pdf-hybrid-code-check-button'
+
+function PdfPreviewHybridToolbar() {
+  return (
+    <ButtonToolbar className="toolbar toolbar-pdf toolbar-pdf-hybrid">
+      <div className="toolbar-pdf-left">
+        <PdfCompileButton />
+        <PdfHybridLogsButton />
+        <PdfHybridDownloadButton />
+      </div>
+      <div className="toolbar-pdf-right">
+        <PdfHybridCodeCheckButton />
+        <PdfExpandButton />
+      </div>
+    </ButtonToolbar>
+  )
+}
+
+export default memo(PdfPreviewHybridToolbar)

+ 7 - 2
services/web/frontend/js/features/pdf-preview/components/pdf-preview-pane.js

@@ -2,15 +2,20 @@ import { memo, Suspense } from 'react'
 import PdfLogsViewer from './pdf-logs-viewer'
 import PdfLogsViewer from './pdf-logs-viewer'
 import PdfViewer from './pdf-viewer'
 import PdfViewer from './pdf-viewer'
 import { usePdfPreviewContext } from '../contexts/pdf-preview-context'
 import { usePdfPreviewContext } from '../contexts/pdf-preview-context'
-import PdfPreviewToolbar from './pdf-preview-toolbar'
 import LoadingSpinner from '../../../shared/components/loading-spinner'
 import LoadingSpinner from '../../../shared/components/loading-spinner'
+import PdfHybridPreviewToolbar from './pdf-preview-hybrid-toolbar'
+import PdfPreviewToolbar from './pdf-preview-toolbar'
+
+const newPreviewToolbar = new URLSearchParams(window.location.search).has(
+  'new_preview_toolbar'
+)
 
 
 function PdfPreviewPane() {
 function PdfPreviewPane() {
   const { showLogs } = usePdfPreviewContext()
   const { showLogs } = usePdfPreviewContext()
 
 
   return (
   return (
     <div className="pdf full-size">
     <div className="pdf full-size">
-      <PdfPreviewToolbar />
+      {newPreviewToolbar ? <PdfPreviewToolbar /> : <PdfHybridPreviewToolbar />}
       <Suspense fallback={<LoadingPreview />}>
       <Suspense fallback={<LoadingPreview />}>
         <div className="pdf-viewer">
         <div className="pdf-viewer">
           <PdfViewer />
           <PdfViewer />

+ 22 - 0
services/web/frontend/js/features/preview/components/pdf-code-check-failed-notice.js

@@ -0,0 +1,22 @@
+import { memo } from 'react'
+import { useTranslation } from 'react-i18next'
+import Icon from '../../../shared/components/icon'
+
+function PdfCodeCheckFailedNotice() {
+  const { t } = useTranslation()
+
+  return (
+    <div className="log-entry">
+      <div className="log-entry-header log-entry-header-error">
+        <div className="log-entry-header-icon-container">
+          <Icon type="exclamation-triangle" modifier="fw" />
+        </div>
+        <h3 className="log-entry-header-title">
+          {t('code_check_failed_explanation')}
+        </h3>
+      </div>
+    </div>
+  )
+}
+
+export default memo(PdfCodeCheckFailedNotice)

+ 49 - 0
services/web/frontend/js/features/preview/components/pdf-logs-pane-info-notice.js

@@ -0,0 +1,49 @@
+import { memo } from 'react'
+import { useTranslation } from 'react-i18next'
+import usePersistedState from '../../../shared/hooks/use-persisted-state'
+
+function PdfLogsPaneInfoNotice() {
+  const { t } = useTranslation()
+
+  const [dismissed, setDismissed] = usePersistedState(
+    'logs_pane.dismissed_info_notice',
+    false
+  )
+
+  if (dismissed) {
+    return null
+  }
+
+  return (
+    <div className="log-entry">
+      <div className="log-entry-header log-entry-header-raw">
+        <div className="log-entry-header-icon-container">
+          <span className="info-badge" />
+        </div>
+        <h3 className="log-entry-header-title">
+          {t('logs_pane_info_message')}
+        </h3>
+        <a
+          href="https://forms.gle/zYByeRPcDtA6nDS19"
+          target="_blank"
+          rel="noopener noreferrer"
+          className="log-entry-header-link log-entry-header-link-raw"
+        >
+          <span className="log-entry-header-link-location">
+            {t('give_feedback')}
+          </span>
+        </a>
+        <button
+          className="btn-inline-link log-entry-header-link"
+          type="button"
+          aria-label={t('dismiss')}
+          onClick={() => setDismissed(true)}
+        >
+          <span aria-hidden="true">&times;</span>
+        </button>
+      </div>
+    </div>
+  )
+}
+
+export default memo(PdfLogsPaneInfoNotice)

+ 10 - 10
services/web/frontend/js/features/preview/components/preview-logs-pane-entry.js

@@ -78,6 +78,16 @@ function PreviewLogEntryContent({
 
 
   return (
   return (
     <div className="log-entry-content">
     <div className="log-entry-content">
+      {formattedContent ? (
+        <div className="log-entry-formatted-content">{formattedContent}</div>
+      ) : null}
+      {extraInfoURL ? (
+        <div className="log-entry-content-link">
+          <a href={extraInfoURL} target="_blank" rel="noopener">
+            {t('log_hint_extra_info')}
+          </a>
+        </div>
+      ) : null}
       {rawContent ? (
       {rawContent ? (
         <div className="log-entry-content-raw-container">
         <div className="log-entry-content-raw-container">
           <div {...expandableProps}>
           <div {...expandableProps}>
@@ -104,16 +114,6 @@ function PreviewLogEntryContent({
           ) : null}
           ) : null}
         </div>
         </div>
       ) : null}
       ) : null}
-      {formattedContent ? (
-        <div className="log-entry-formatted-content">{formattedContent}</div>
-      ) : null}
-      {extraInfoURL ? (
-        <div className="log-entry-content-link">
-          <a href={extraInfoURL} target="_blank" rel="noopener">
-            {t('log_hint_extra_info')}
-          </a>
-        </div>
-      ) : null}
     </div>
     </div>
   )
   )
 }
 }

+ 18 - 0
services/web/frontend/stories/pdf-preview.stories.js

@@ -14,6 +14,7 @@ import { buildFileList } from '../js/features/pdf-preview/util/file-list'
 import PdfLogsViewer from '../js/features/pdf-preview/components/pdf-logs-viewer'
 import PdfLogsViewer from '../js/features/pdf-preview/components/pdf-logs-viewer'
 import examplePdf from './fixtures/storybook-example.pdf'
 import examplePdf from './fixtures/storybook-example.pdf'
 import PdfPreviewError from '../js/features/pdf-preview/components/pdf-preview-error'
 import PdfPreviewError from '../js/features/pdf-preview/components/pdf-preview-error'
+import PdfPreviewHybridToolbar from '../js/features/pdf-preview/components/pdf-preview-hybrid-toolbar'
 
 
 setupContext()
 setupContext()
 
 
@@ -22,6 +23,7 @@ export default {
   component: PdfPreview,
   component: PdfPreview,
   subcomponents: {
   subcomponents: {
     PdfPreviewToolbar,
     PdfPreviewToolbar,
+    PdfPreviewHybridToolbar,
     PdfFileList,
     PdfFileList,
     PdfPreviewError,
     PdfPreviewError,
   },
   },
@@ -496,6 +498,22 @@ export const Toolbar = () => {
   )
   )
 }
 }
 
 
+export const HybridToolbar = () => {
+  useFetchMock(fetchMock => {
+    mockCompile(fetchMock, 500)
+    mockBuildFile(fetchMock)
+  })
+
+  return withContextRoot(
+    <PdfPreviewProvider>
+      <div className="pdf">
+        <PdfPreviewHybridToolbar />
+      </div>
+    </PdfPreviewProvider>,
+    scope
+  )
+}
+
 export const FileList = () => {
 export const FileList = () => {
   const fileList = useMemo(() => {
   const fileList = useMemo(() => {
     return buildFileList(outputFiles)
     return buildFileList(outputFiles)

+ 1 - 0
services/web/frontend/stylesheets/app/editor/logs.less

@@ -137,6 +137,7 @@
   background-color: @ol-blue-gray-1;
   background-color: @ol-blue-gray-1;
   border-radius: @border-radius-base;
   border-radius: @border-radius-base;
   overflow: hidden;
   overflow: hidden;
+  margin-top: @margin-sm;
 }
 }
 
 
 .log-entry-content-raw {
 .log-entry-content-raw {

+ 56 - 0
services/web/frontend/stylesheets/app/editor/pdf.less

@@ -12,6 +12,7 @@
   .toolbar-small-mixin;
   .toolbar-small-mixin;
   .toolbar-alt-mixin;
   .toolbar-alt-mixin;
   padding-right: 5px;
   padding-right: 5px;
+  margin-left: 0;
   &.changes-to-autocompile {
   &.changes-to-autocompile {
     // prettier-ignore
     // prettier-ignore
     #gradient > .striped(@color: rgba(255, 255, 255, 0.1), @angle: -45deg);
     #gradient > .striped(@color: rgba(255, 255, 255, 0.1), @angle: -45deg);
@@ -56,6 +57,61 @@
   padding-left: @line-height-computed / 4;
   padding-left: @line-height-computed / 4;
 }
 }
 
 
+.toolbar-pdf-hybrid {
+  .btn:not(.btn-recompile) {
+    display: inline-block;
+    color: @toolbar-btn-color;
+    background-color: transparent;
+    padding: 4px 2px;
+    line-height: 1;
+    height: 24px;
+    border-radius: 2px;
+
+    &:hover,
+    &:active,
+    &:focus {
+      color: @toolbar-btn-color;
+    }
+
+    .label {
+      position: absolute;
+      top: 0;
+      right: 0;
+      padding: 0.15em 0.6em 0.2em;
+      font-size: 60%;
+      pointer-events: none;
+    }
+
+    .btn {
+      display: inline-block;
+      color: #fff;
+      background-color: transparent;
+      padding: 4px 2px;
+      line-height: 1;
+      height: 24px;
+      border-radius: 2px;
+    }
+
+    &.log-btn {
+      margin-right: 3px;
+
+      &.active {
+        color: white;
+        background-color: @link-color;
+        box-shadow: @toolbar-icon-btn-hover-boxshadow;
+
+        &:hover {
+          color: white;
+        }
+      }
+
+      &:focus {
+        outline: none;
+      }
+    }
+  }
+}
+
 .pdf {
 .pdf {
   background-color: @pdf-bg;
   background-color: @pdf-bg;
 }
 }

+ 2 - 2
services/web/test/frontend/features/pdf-preview/components/pdf-preview.test.js

@@ -317,7 +317,7 @@ describe('<PdfPreview/>', function () {
     await screen.findByRole('button', { name: 'Recompile' })
     await screen.findByRole('button', { name: 'Recompile' })
 
 
     const logsButton = screen.getByRole('button', {
     const logsButton = screen.getByRole('button', {
-      name: 'This project has an error',
+      name: 'View logs',
     })
     })
     logsButton.click()
     logsButton.click()
 
 
@@ -350,7 +350,7 @@ describe('<PdfPreview/>', function () {
 
 
     // show the logs UI
     // show the logs UI
     const logsButton = screen.getByRole('button', {
     const logsButton = screen.getByRole('button', {
-      name: 'This project has an error',
+      name: 'View logs',
     })
     })
     logsButton.click()
     logsButton.click()