Просмотр исходного кода

Merge pull request #23697 from overleaf/dp-layout-switcher

Add layout switcher button to toolbar

GitOrigin-RevId: 33bf664c2e2cb5c2f992b70fd9ca90b5dfe2ee44
David 1 год назад
Родитель
Сommit
2d02111e06

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

@@ -463,6 +463,7 @@
   "editor_and_pdf": "",
   "editor_disconected_click_to_reconnect": "",
   "editor_limit_exceeded_in_this_project": "",
+  "editor_only": "",
   "editor_only_hide_pdf": "",
   "editor_settings": "",
   "editor_theme": "",
@@ -858,6 +859,7 @@
   "latex_places_figures_according_to_a_special_algorithm": "",
   "latex_places_tables_according_to_a_special_algorithm": "",
   "layout": "",
+  "layout_options": "",
   "layout_processing": "",
   "learn_more": "",
   "learn_more_about_account": "",
@@ -1091,6 +1093,7 @@
   "open_file": "",
   "open_link": "",
   "open_path": "",
+  "open_pdf_in_separate_tab": "",
   "open_project": "",
   "open_survey": "",
   "open_target": "",
@@ -1150,6 +1153,7 @@
   "pdf_compile_try_again": "",
   "pdf_couldnt_compile": "",
   "pdf_in_separate_tab": "",
+  "pdf_only": "",
   "pdf_only_hide_editor": "",
   "pdf_preview_error": "",
   "pdf_rendering_error": "",
@@ -1531,6 +1535,7 @@
   "sort_projects": "",
   "source": "",
   "spell_check": "",
+  "split_view": "",
   "sso": "",
   "sso_active": "",
   "sso_already_setup_good_to_go": "",

BIN
services/web/frontend/fonts/material-symbols/MaterialSymbolsRoundedUnfilledPartialSlice.woff2


+ 1 - 0
services/web/frontend/fonts/material-symbols/unfilled-symbols.mjs

@@ -18,6 +18,7 @@ export default /** @type {const} */ ([
   'rate_review',
   'report',
   'settings',
+  'space_dashboard',
   'table_chart',
   'upload_file',
   'web_asset',

+ 34 - 10
services/web/frontend/js/features/ide-redesign/components/main-layout.tsx

@@ -7,17 +7,26 @@ import { RailLayout } from './rail'
 import { Toolbar } from './toolbar/toolbar'
 import { HorizontalToggler } from '@/features/ide-react/components/resize/horizontal-toggler'
 import { useTranslation } from 'react-i18next'
-import { usePdfPane } from '../hooks/use-pdf-pane'
+import { usePdfPane } from '@/features/ide-react/hooks/use-pdf-pane'
+import { useLayoutContext } from '@/shared/context/layout-context'
+import { useState } from 'react'
 
 export default function MainLayout() {
+  const [resizing, setResizing] = useState(false)
   const {
-    isOpen: isPdfOpen,
-    setIsOpen: setIsPdfOpen,
-    panelRef: pdfPanelRef,
-    handlePaneCollapse: handlePdfPaneCollapse,
-    handlePaneExpand: handlePdfPaneExpand,
-    togglePane: togglePdfPane,
+    togglePdfPane,
+    handlePdfPaneExpand,
+    handlePdfPaneCollapse,
+    setPdfIsOpen: setIsPdfOpen,
+    pdfIsOpen: isPdfOpen,
+    pdfPanelRef,
   } = usePdfPane()
+
+  const { view, pdfLayout } = useLayoutContext()
+
+  const editorIsOpen =
+    view === 'editor' || view === 'file' || pdfLayout === 'sideBySide'
+
   const { t } = useTranslation()
   return (
     <div className="ide-redesign-main">
@@ -27,19 +36,32 @@ export default function MainLayout() {
           autoSaveId="ide-redesign-outer-layout"
           direction="horizontal"
           className={classNames('ide-redesign-inner', {
-            'ide-panel-group-resizing': false,
+            'ide-panel-group-resizing': resizing,
           })}
         >
           <RailLayout />
-          <Panel id="ide-redesign-editor-panel" order={2}>
+          <Panel
+            id="ide-redesign-editor-panel"
+            order={1}
+            className={classNames({
+              'ide-panel-group-resizing': resizing,
+              hidden: !editorIsOpen,
+            })}
+            minSize={5}
+            defaultSize={50}
+          >
             <div className="ide-redesign-editor-container">
               <Editor />
             </div>
           </Panel>
           <HorizontalResizeHandle
-            resizable
+            resizable={pdfLayout === 'sideBySide'}
+            onDragging={setResizing}
             onDoubleClick={togglePdfPane}
             hitAreaMargins={{ coarse: 0, fine: 0 }}
+            className={classNames({
+              hidden: !editorIsOpen,
+            })}
           >
             <HorizontalToggler
               id="ide-redesign-pdf-panel"
@@ -55,6 +77,8 @@ export default function MainLayout() {
             className="ide-redesign-pdf-container"
             id="ide-redesign-pdf-panel"
             order={2}
+            defaultSize={50}
+            minSize={5}
             ref={pdfPanelRef}
             onExpand={handlePdfPaneExpand}
             onCollapse={handlePdfPaneCollapse}

+ 46 - 0
services/web/frontend/js/features/ide-redesign/components/toolbar/change-layout-button.tsx

@@ -0,0 +1,46 @@
+import OLButton from '@/features/ui/components/ol/ol-button'
+import MaterialIcon from '@/shared/components/material-icon'
+import {
+  Dropdown,
+  DropdownMenu,
+  DropdownToggle,
+} from '@/features/ui/components/bootstrap-5/dropdown-menu'
+import React, { forwardRef } from 'react'
+import ChangeLayoutOptions from './change-layout-options'
+
+const LayoutDropdownToggleButton = forwardRef<
+  HTMLButtonElement,
+  {
+    onClick: (e: React.MouseEvent<HTMLButtonElement>) => void
+  }
+>(({ onClick }, ref) => {
+  return (
+    <OLButton
+      size="sm"
+      variant="ghost"
+      className="ide-redesign-toolbar-button-subdued"
+      ref={ref}
+      onClick={onClick}
+      leadingIcon={<MaterialIcon unfilled type="space_dashboard" />}
+    />
+  )
+})
+
+LayoutDropdownToggleButton.displayName = 'LayoutDropdownToggleButton'
+
+export default function ChangeLayoutButton() {
+  return (
+    <div className="ide-redesign-toolbar-button-container">
+      <Dropdown className="toolbar-item layout-dropdown" align="end">
+        <DropdownToggle
+          id="layout-dropdown-btn"
+          className="btn-full-height"
+          as={LayoutDropdownToggleButton}
+        />
+        <DropdownMenu>
+          <ChangeLayoutOptions />
+        </DropdownMenu>
+      </Dropdown>
+    </div>
+  )
+}

+ 175 - 0
services/web/frontend/js/features/ide-redesign/components/toolbar/change-layout-options.tsx

@@ -0,0 +1,175 @@
+import {
+  DropdownItem,
+  DropdownHeader,
+} from '@/features/ui/components/bootstrap-5/dropdown-menu'
+import {
+  IdeLayout,
+  IdeView,
+  useLayoutContext,
+} from '@/shared/context/layout-context'
+import React, { useCallback } from 'react'
+import { useTranslation } from 'react-i18next'
+import * as eventTracking from '../../../../infrastructure/event-tracking'
+import useEventListener from '@/shared/hooks/use-event-listener'
+import { DetachRole } from '@/shared/context/detach-context'
+import { Spinner } from 'react-bootstrap-5'
+
+type LayoutOption = 'sideBySide' | 'editorOnly' | 'pdfOnly' | 'detachedPdf'
+
+const getActiveLayoutOption = ({
+  pdfLayout,
+  view,
+  detachRole,
+}: {
+  pdfLayout: IdeLayout
+  view: IdeView | null
+  detachRole?: DetachRole
+}): LayoutOption | null => {
+  if (view === 'history') {
+    return null
+  }
+
+  if (detachRole === 'detacher') {
+    return 'detachedPdf'
+  }
+
+  if (pdfLayout === 'flat' && (view === 'editor' || view === 'file')) {
+    return 'editorOnly'
+  }
+
+  if (pdfLayout === 'flat' && view === 'pdf') {
+    return 'pdfOnly'
+  }
+
+  if (pdfLayout === 'sideBySide') {
+    return 'sideBySide'
+  }
+
+  return null
+}
+
+const LayoutDropdownItem = ({
+  active,
+  disabled = false,
+  processing = false,
+  leadingIcon,
+  onClick,
+  children,
+}: {
+  active: boolean
+  leadingIcon: string
+  onClick: () => void
+  children: React.ReactNode
+  processing?: boolean
+  disabled?: boolean
+}) => {
+  let trailingIcon: string | React.ReactNode | null = null
+  if (processing) {
+    trailingIcon = (
+      <Spinner animation="border" aria-hidden="true" size="sm" role="status" />
+    )
+  } else if (active) {
+    trailingIcon = 'check'
+  }
+
+  return (
+    <DropdownItem
+      active={active}
+      aria-current={active}
+      trailingIcon={trailingIcon}
+      disabled={disabled}
+      onClick={onClick}
+      leadingIcon={leadingIcon}
+    >
+      {children}
+    </DropdownItem>
+  )
+}
+
+export default function ChangeLayoutOptions() {
+  const {
+    reattach,
+    detach,
+    detachIsLinked,
+    detachRole,
+    changeLayout,
+    view,
+    pdfLayout,
+  } = useLayoutContext()
+
+  const handleDetach = useCallback(() => {
+    detach()
+    eventTracking.sendMB('project-layout-detach')
+  }, [detach])
+
+  const handleReattach = useCallback(() => {
+    if (detachRole !== 'detacher') {
+      return
+    }
+    reattach()
+    eventTracking.sendMB('project-layout-reattach')
+  }, [detachRole, reattach])
+
+  // reattach when the PDF pane opens
+  useEventListener('ui:pdf-open', handleReattach)
+
+  const handleChangeLayout = useCallback(
+    (newLayout: IdeLayout, newView?: IdeView) => {
+      handleReattach()
+      changeLayout(newLayout, newView)
+      eventTracking.sendMB('project-layout-change', {
+        layout: newLayout,
+        view: newView,
+      })
+    },
+    [changeLayout, handleReattach]
+  )
+
+  const { t } = useTranslation()
+
+  const detachable = 'BroadcastChannel' in window
+
+  const activeLayoutOption = getActiveLayoutOption({
+    pdfLayout,
+    view,
+    detachRole,
+  })
+
+  const waitingForDetachedLink = !detachIsLinked && detachRole === 'detacher'
+
+  return (
+    <>
+      <DropdownHeader>{t('layout_options')}</DropdownHeader>
+      <LayoutDropdownItem
+        onClick={() => handleChangeLayout('sideBySide')}
+        active={activeLayoutOption === 'sideBySide'}
+        leadingIcon="splitscreen_right"
+      >
+        {t('split_view')}
+      </LayoutDropdownItem>
+      <LayoutDropdownItem
+        onClick={() => handleChangeLayout('flat', 'editor')}
+        active={activeLayoutOption === 'editorOnly'}
+        leadingIcon="edit"
+      >
+        {t('editor_only')}
+      </LayoutDropdownItem>
+      <LayoutDropdownItem
+        onClick={() => handleChangeLayout('flat', 'pdf')}
+        active={activeLayoutOption === 'pdfOnly'}
+        leadingIcon="picture_as_pdf"
+      >
+        {t('pdf_only')}
+      </LayoutDropdownItem>
+      <LayoutDropdownItem
+        onClick={() => handleDetach()}
+        active={activeLayoutOption === 'detachedPdf' && detachIsLinked}
+        disabled={!detachable}
+        leadingIcon="open_in_new"
+        processing={waitingForDetachedLink}
+      >
+        {t('open_pdf_in_separate_tab')}
+      </LayoutDropdownItem>
+    </>
+  )
+}

+ 2 - 1
services/web/frontend/js/features/ide-redesign/components/toolbar/menu-bar.tsx

@@ -3,6 +3,7 @@ import { MenuBar } from '@/shared/components/menu-bar/menu-bar'
 import { MenuBarDropdown } from '@/shared/components/menu-bar/menu-bar-dropdown'
 import { MenuBarOption } from '@/shared/components/menu-bar/menu-bar-option'
 import { useTranslation } from 'react-i18next'
+import ChangeLayoutOptions from './change-layout-options'
 
 export const ToolbarMenuBar = () => {
   const { t } = useTranslation()
@@ -36,7 +37,7 @@ export const ToolbarMenuBar = () => {
         id="view"
         className="ide-redesign-toolbar-dropdown-toggle-subdued"
       >
-        <MenuBarOption title="PDF only" />
+        <ChangeLayoutOptions />
       </MenuBarDropdown>
       <MenuBarDropdown
         title={t('insert')}

+ 2 - 0
services/web/frontend/js/features/ide-redesign/components/toolbar/toolbar.tsx

@@ -8,6 +8,7 @@ import ShareProjectButton from './share-project-button'
 import importOverleafModules from '../../../../../macros/import-overleaf-module.macro'
 import { useEditorContext } from '@/shared/context/editor-context'
 import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
+import ChangeLayoutButton from './change-layout-button'
 
 const [publishModalModules] = importOverleafModules('publishModal')
 const SubmitProjectButton = publishModalModules?.import.NewPublishToolbarButton
@@ -66,6 +67,7 @@ const ToolbarButtons = () => {
           />
         </OLTooltip>
       </div>
+      <ChangeLayoutButton />
       {shouldDisplaySubmitButton && <SubmitProjectButton />}
       <ShareProjectButton />
     </div>

+ 0 - 35
services/web/frontend/js/features/ide-redesign/hooks/use-pdf-pane.tsx

@@ -1,35 +0,0 @@
-import { useCallback, useRef, useState } from 'react'
-import useCollapsiblePanel from '@/features/ide-react/hooks/use-collapsible-panel'
-import { ImperativePanelHandle } from 'react-resizable-panels'
-
-// FIXME: This is temporary, to avoid clashing with the existing usePdfPane
-// which uses the layout context. That's the correct approach.
-export const usePdfPane = () => {
-  const [isOpen, setIsOpen] = useState(true)
-  const [resizing, setResizing] = useState(false)
-  const panelRef = useRef<ImperativePanelHandle>(null)
-  useCollapsiblePanel(isOpen, panelRef)
-
-  const togglePane = useCallback(() => {
-    setIsOpen(value => !value)
-  }, [])
-
-  const handlePaneExpand = useCallback(() => {
-    setIsOpen(true)
-  }, [])
-
-  const handlePaneCollapse = useCallback(() => {
-    setIsOpen(false)
-  }, [])
-
-  return {
-    isOpen,
-    setIsOpen,
-    panelRef,
-    togglePane,
-    handlePaneExpand,
-    handlePaneCollapse,
-    resizing,
-    setResizing,
-  }
-}

+ 12 - 0
services/web/frontend/stylesheets/bootstrap-5/pages/editor/ide.scss

@@ -45,6 +45,18 @@ $editor-toggler-bg-dark-color: color.adjust(
   }
 }
 
+.ide-redesign-main {
+  .ide-panel-group-resizing {
+    background-color: var(--white);
+
+    // Hide panel contents while resizing
+    .ide-redesign-editor-content,
+    .pdf {
+      display: none !important;
+    }
+  }
+}
+
 .global-alerts {
   height: 0;
   margin-top: var(--spacing-01);

+ 5 - 0
services/web/locales/en.json

@@ -598,6 +598,7 @@
   "editor_and_pdf": "Editor & PDF",
   "editor_disconected_click_to_reconnect": "Editor disconnected, click anywhere to reconnect.",
   "editor_limit_exceeded_in_this_project": "Too many editors in this project",
+  "editor_only": "Editor only",
   "editor_only_hide_pdf": "Editor only <0>(hide PDF)</0>",
   "editor_settings": "Editor settings",
   "editor_theme": "Editor theme",
@@ -1131,6 +1132,7 @@
   "latex_templates_for_journal_articles": "LaTeX templates for journal articles, academic papers, CVs and résumés, presentations, and more.",
   "latex_templates_sentence_case": "LaTeX templates",
   "layout": "Layout",
+  "layout_options": "Layout options",
   "layout_processing": "Layout processing",
   "ldap": "LDAP",
   "ldap_create_admin_instructions": "Choose an email address for the first __appName__ admin account. This should correspond to an account in the LDAP system. You will then be asked to log in with this account.",
@@ -1448,6 +1450,7 @@
   "open_file": "Edit file",
   "open_link": "Go to page",
   "open_path": "Open __path__",
+  "open_pdf_in_separate_tab": "Open PDF in separate tab",
   "open_project": "Open Project",
   "open_survey": "Open survey",
   "open_target": "Go to target",
@@ -1537,6 +1540,7 @@
   "pdf_compile_try_again": "Please wait for your other compile to finish before trying again.",
   "pdf_couldnt_compile": "PDF couldn’t compile",
   "pdf_in_separate_tab": "PDF in separate tab",
+  "pdf_only": "PDF only",
   "pdf_only_hide_editor": "PDF only <0>(hide editor)</0>",
   "pdf_preview_error": "There was a problem displaying the compilation results for this project.",
   "pdf_rendering_error": "PDF Rendering Error",
@@ -2007,6 +2011,7 @@
   "sort_projects": "Sort projects",
   "source": "Source",
   "spell_check": "Spell check",
+  "split_view": "Split view",
   "sso": "SSO",
   "sso_account_already_linked": "Account already linked to another __appName__ user",
   "sso_active": "SSO active",