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

Merge pull request #23344 from overleaf/dp-file-tree-toolbar

 Update file tree toolbar in new editor

GitOrigin-RevId: 11ca678b50e0c525ae60b806dfbc08773b661b7b
David 1 год назад
Родитель
Сommit
f219744fdd

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

@@ -525,6 +525,7 @@
   "file_or_folder_name_already_exists": "",
   "file_or_folder_name_already_exists": "",
   "file_outline": "",
   "file_outline": "",
   "file_size": "",
   "file_size": "",
+  "file_tree": "",
   "files_cannot_include_invalid_characters": "",
   "files_cannot_include_invalid_characters": "",
   "files_selected": "",
   "files_selected": "",
   "fill_in_our_quick_survey": "",
   "fill_in_our_quick_survey": "",
@@ -668,6 +669,7 @@
   "hide_configuration": "",
   "hide_configuration": "",
   "hide_deleted_user": "",
   "hide_deleted_user": "",
   "hide_document_preamble": "",
   "hide_document_preamble": "",
+  "hide_file_tree": "",
   "hide_local_file_contents": "",
   "hide_local_file_contents": "",
   "hide_outline": "",
   "hide_outline": "",
   "history": "",
   "history": "",
@@ -1462,6 +1464,7 @@
   "show_all": "",
   "show_all": "",
   "show_all_projects": "",
   "show_all_projects": "",
   "show_document_preamble": "",
   "show_document_preamble": "",
+  "show_file_tree": "",
   "show_hotkeys": "",
   "show_hotkeys": "",
   "show_in_code": "",
   "show_in_code": "",
   "show_in_pdf": "",
   "show_in_pdf": "",

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


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

@@ -1,20 +1,24 @@
 // @ts-check
 // @ts-check
 // Make sure to run the build-unfilled.mjs script after updating this list
 // Make sure to run the build-unfilled.mjs script after updating this list
 // to update the font file with the latest icons.
 // to update the font file with the latest icons.
+// You may need to hard reload your browser window to see the changes.
 
 
 export default /** @type {const} */ ([
 export default /** @type {const} */ ([
   'book_5',
   'book_5',
   'code',
   'code',
+  'create_new_folder',
   'description',
   'description',
   'forum',
   'forum',
   'help',
   'help',
   'image',
   'image',
   'info',
   'info',
   'integration_instructions',
   'integration_instructions',
+  'note_add',
   'picture_as_pdf',
   'picture_as_pdf',
   'rate_review',
   'rate_review',
   'report',
   'report',
   'settings',
   'settings',
   'table_chart',
   'table_chart',
+  'upload_file',
   'web_asset',
   'web_asset',
 ])
 ])

+ 4 - 1
services/web/frontend/js/features/file-tree/components/file-tree-root.tsx

@@ -6,6 +6,7 @@ import FileTreeContext from './file-tree-context'
 import FileTreeDraggablePreviewLayer from './file-tree-draggable-preview-layer'
 import FileTreeDraggablePreviewLayer from './file-tree-draggable-preview-layer'
 import FileTreeFolderList from './file-tree-folder-list'
 import FileTreeFolderList from './file-tree-folder-list'
 import FileTreeToolbar from './file-tree-toolbar'
 import FileTreeToolbar from './file-tree-toolbar'
+import FileTreeToolbarNew from '@/features/ide-redesign/components/file-tree-toolbar'
 import FileTreeModalDelete from './modals/file-tree-modal-delete'
 import FileTreeModalDelete from './modals/file-tree-modal-delete'
 import FileTreeModalCreateFolder from './modals/file-tree-modal-create-folder'
 import FileTreeModalCreateFolder from './modals/file-tree-modal-create-folder'
 import FileTreeModalError from './modals/file-tree-modal-error'
 import FileTreeModalError from './modals/file-tree-modal-error'
@@ -18,6 +19,7 @@ import FileTreeInner from './file-tree-inner'
 import { useDragLayer } from 'react-dnd'
 import { useDragLayer } from 'react-dnd'
 import classnames from 'classnames'
 import classnames from 'classnames'
 import { pathInFolder } from '@/features/file-tree/util/path'
 import { pathInFolder } from '@/features/file-tree/util/path'
+import { useFeatureFlag } from '@/shared/context/split-test-context'
 
 
 const FileTreeRoot = React.memo<{
 const FileTreeRoot = React.memo<{
   onSelect: () => void
   onSelect: () => void
@@ -41,6 +43,7 @@ const FileTreeRoot = React.memo<{
   const { _id: projectId } = useProjectContext()
   const { _id: projectId } = useProjectContext()
   const { fileTreeData } = useFileTreeData()
   const { fileTreeData } = useFileTreeData()
   const isReady = Boolean(projectId && fileTreeData)
   const isReady = Boolean(projectId && fileTreeData)
+  const newEditor = useFeatureFlag('editor-redesign')
 
 
   useEffect(() => {
   useEffect(() => {
     if (fileTreeContainer) {
     if (fileTreeContainer) {
@@ -97,7 +100,7 @@ const FileTreeRoot = React.memo<{
           fileTreeContainer={fileTreeContainer}
           fileTreeContainer={fileTreeContainer}
         >
         >
           {isConnected ? null : <div className="disconnected-overlay" />}
           {isConnected ? null : <div className="disconnected-overlay" />}
-          <FileTreeToolbar />
+          {newEditor ? <FileTreeToolbarNew /> : <FileTreeToolbar />}
           <FileTreeContextMenu />
           <FileTreeContextMenu />
           <FileTreeInner>
           <FileTreeInner>
             <FileTreeRootFolder onDelete={onDelete} />
             <FileTreeRootFolder onDelete={onDelete} />

+ 13 - 0
services/web/frontend/js/features/ide-react/context/file-tree-open-context.tsx

@@ -30,6 +30,8 @@ const FileTreeOpenContext = createContext<
       handleFileTreeInit: () => void
       handleFileTreeInit: () => void
       handleFileTreeSelect: (selectedEntities: FileTreeFindResult[]) => void
       handleFileTreeSelect: (selectedEntities: FileTreeFindResult[]) => void
       handleFileTreeDelete: (entity: FileTreeFindResult) => void
       handleFileTreeDelete: (entity: FileTreeFindResult) => void
+      fileTreeExpanded: boolean
+      toggleFileTreeExpanded: () => void
     }
     }
   | undefined
   | undefined
 >(undefined)
 >(undefined)
@@ -46,6 +48,13 @@ export const FileTreeOpenProvider: FC = ({ children }) => {
   const [selectedEntityCount, setSelectedEntityCount] = useState(0)
   const [selectedEntityCount, setSelectedEntityCount] = useState(0)
   const [fileTreeReady, setFileTreeReady] = useState(false)
   const [fileTreeReady, setFileTreeReady] = useState(false)
 
 
+  // NOTE: Only used in editor redesign
+  const [fileTreeExpanded, setFileTreeExpanded] = useState(true)
+
+  const toggleFileTreeExpanded = useCallback(() => {
+    setFileTreeExpanded(prev => !prev)
+  }, [])
+
   const handleFileTreeInit = useCallback(() => {
   const handleFileTreeInit = useCallback(() => {
     setFileTreeReady(true)
     setFileTreeReady(true)
   }, [])
   }, [])
@@ -129,6 +138,8 @@ export const FileTreeOpenProvider: FC = ({ children }) => {
       handleFileTreeInit,
       handleFileTreeInit,
       handleFileTreeSelect,
       handleFileTreeSelect,
       handleFileTreeDelete,
       handleFileTreeDelete,
+      fileTreeExpanded,
+      toggleFileTreeExpanded,
     }
     }
   }, [
   }, [
     handleFileTreeDelete,
     handleFileTreeDelete,
@@ -136,6 +147,8 @@ export const FileTreeOpenProvider: FC = ({ children }) => {
     handleFileTreeSelect,
     handleFileTreeSelect,
     openEntity,
     openEntity,
     selectedEntityCount,
     selectedEntityCount,
+    fileTreeExpanded,
+    toggleFileTreeExpanded,
   ])
   ])
 
 
   return (
   return (

+ 11 - 4
services/web/frontend/js/features/ide-redesign/components/file-tree-outline-panel.tsx

@@ -3,35 +3,42 @@ import { FileTree } from '@/features/ide-react/components/file-tree'
 import { OutlineContainer } from '@/features/outline/components/outline-container'
 import { OutlineContainer } from '@/features/outline/components/outline-container'
 import { VerticalResizeHandle } from '@/features/ide-react/components/resize/vertical-resize-handle'
 import { VerticalResizeHandle } from '@/features/ide-react/components/resize/vertical-resize-handle'
 import { useOutlinePane } from '@/features/ide-react/hooks/use-outline-pane'
 import { useOutlinePane } from '@/features/ide-react/hooks/use-outline-pane'
+import useCollapsibleFileTree from '../hooks/use-collapsible-file-tree'
+import classNames from 'classnames'
 
 
 function FileTreeOutlinePanel() {
 function FileTreeOutlinePanel() {
   const { outlineEnabled, outlinePanelRef } = useOutlinePane()
   const { outlineEnabled, outlinePanelRef } = useOutlinePane()
+  const { fileTreeExpanded, fileTreePanelRef } = useCollapsibleFileTree()
 
 
   return (
   return (
     <PanelGroup
     <PanelGroup
+      className="file-tree-outline-panel-group"
       autoSaveId="ide-redesign-file-tree-outline"
       autoSaveId="ide-redesign-file-tree-outline"
       direction="vertical"
       direction="vertical"
     >
     >
       <Panel
       <Panel
+        className={classNames('file-tree-panel', {
+          'file-tree-panel-collapsed': !fileTreeExpanded,
+        })}
         defaultSize={50}
         defaultSize={50}
-        minSize={25}
         id="ide-redesign-file-tree"
         id="ide-redesign-file-tree"
         order={1}
         order={1}
+        collapsible
+        ref={fileTreePanelRef}
       >
       >
         <FileTree />
         <FileTree />
       </Panel>
       </Panel>
       <VerticalResizeHandle
       <VerticalResizeHandle
         hitAreaMargins={{ coarse: 0, fine: 0 }}
         hitAreaMargins={{ coarse: 0, fine: 0 }}
-        disabled={!outlineEnabled}
+        disabled={!outlineEnabled || !fileTreeExpanded}
       />
       />
       <Panel
       <Panel
+        className="file-outline-panel"
         defaultSize={50}
         defaultSize={50}
-        maxSize={75}
         id="ide-redesign-file-outline"
         id="ide-redesign-file-outline"
         order={2}
         order={2}
         collapsible
         collapsible
         ref={outlinePanelRef}
         ref={outlinePanelRef}
-        style={{ minHeight: 36 }} // keep the header visible
       >
       >
         <OutlineContainer />
         <OutlineContainer />
       </Panel>
       </Panel>

+ 112 - 0
services/web/frontend/js/features/ide-redesign/components/file-tree-toolbar.tsx

@@ -0,0 +1,112 @@
+import { useTranslation } from 'react-i18next'
+import * as eventTracking from '../../../infrastructure/event-tracking'
+import { useFileTreeActionable } from '@/features/file-tree/contexts/file-tree-actionable'
+import { useFileTreeData } from '@/shared/context/file-tree-data-context'
+import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
+import MaterialIcon, {
+  AvailableUnfilledIcon,
+} from '@/shared/components/material-icon'
+import React from 'react'
+import useCollapsibleFileTree from '../hooks/use-collapsible-file-tree'
+
+function FileTreeToolbar() {
+  const { t } = useTranslation()
+  const { fileTreeExpanded, toggleFileTreeExpanded } = useCollapsibleFileTree()
+
+  return (
+    <div className="file-tree-toolbar">
+      <button
+        className="file-tree-expand-collapse-button"
+        onClick={toggleFileTreeExpanded}
+        aria-label={
+          fileTreeExpanded ? t('hide_file_tree') : t('show_file_tree')
+        }
+      >
+        <MaterialIcon
+          type={
+            fileTreeExpanded ? 'keyboard_arrow_down' : 'keyboard_arrow_right'
+          }
+        />
+        <h4>{t('file_tree')}</h4>
+      </button>
+      <FileTreeActionButtons />
+    </div>
+  )
+}
+
+function FileTreeActionButtons() {
+  const { t } = useTranslation()
+  const { fileTreeReadOnly } = useFileTreeData()
+
+  const {
+    canCreate,
+    startCreatingFolder,
+    startCreatingDocOrFile,
+    startUploadingDocOrFile,
+  } = useFileTreeActionable()
+
+  if (!canCreate || fileTreeReadOnly) return null
+
+  const createWithAnalytics = () => {
+    eventTracking.sendMB('new-file-click', { location: 'toolbar' })
+    startCreatingDocOrFile()
+  }
+
+  const uploadWithAnalytics = () => {
+    eventTracking.sendMB('upload-click', { location: 'toolbar' })
+    startUploadingDocOrFile()
+  }
+
+  return (
+    <div className="file-tree-toolbar-action-buttons">
+      <FileTreeActionButton
+        id="new-file"
+        description={t('new_file')}
+        onClick={createWithAnalytics}
+        iconType="note_add"
+      />
+      <FileTreeActionButton
+        id="new-folder"
+        description={t('new_folder')}
+        onClick={startCreatingFolder}
+        iconType="create_new_folder"
+      />
+      <FileTreeActionButton
+        id="upload"
+        description={t('upload')}
+        onClick={uploadWithAnalytics}
+        iconType="upload_file"
+      />
+    </div>
+  )
+}
+
+function FileTreeActionButton({
+  id,
+  description,
+  onClick,
+  iconType,
+}: {
+  id: string
+  description: string
+  onClick: () => void
+  iconType: AvailableUnfilledIcon
+}) {
+  return (
+    <OLTooltip
+      id={id}
+      description={description}
+      overlayProps={{ placement: 'bottom' }}
+    >
+      <button className="btn file-tree-toolbar-action-button" onClick={onClick}>
+        <MaterialIcon
+          unfilled
+          type={iconType}
+          accessibilityLabel={description}
+        />
+      </button>
+    </OLTooltip>
+  )
+}
+
+export default FileTreeToolbar

+ 12 - 0
services/web/frontend/js/features/ide-redesign/hooks/use-collapsible-file-tree.tsx

@@ -0,0 +1,12 @@
+import { ImperativePanelHandle } from 'react-resizable-panels'
+import { useRef } from 'react'
+import useCollapsiblePanel from '@/features/ide-react/hooks/use-collapsible-panel'
+import { useFileTreeOpenContext } from '@/features/ide-react/context/file-tree-open-context'
+
+export default function useCollapsibleFileTree() {
+  const { fileTreeExpanded, toggleFileTreeExpanded } = useFileTreeOpenContext()
+  const fileTreePanelRef = useRef<ImperativePanelHandle>(null)
+  useCollapsiblePanel(fileTreeExpanded, fileTreePanelRef)
+
+  return { fileTreeExpanded, fileTreePanelRef, toggleFileTreeExpanded }
+}

+ 58 - 0
services/web/frontend/stylesheets/bootstrap-5/pages/editor/file-tree.scss

@@ -35,6 +35,63 @@
   --file-tree-item-dragging-preview-bg: #{rgb($bg-light-secondary, 0.6)};
   --file-tree-item-dragging-preview-bg: #{rgb($bg-light-secondary, 0.6)};
   --file-tree-item-dragging-preview-colour: #{rgb($content-primary, 0.6)};
   --file-tree-item-dragging-preview-colour: #{rgb($content-primary, 0.6)};
 
 
+  .file-tree-outline-panel-group {
+    background-color: var(--file-tree-bg);
+  }
+
+  .file-tree-toolbar {
+    display: flex;
+    justify-content: space-between;
+    height: 28px;
+    margin: var(--spacing-02);
+  }
+
+  .file-tree-panel {
+    min-height: 36px;
+  }
+
+  .file-tree-panel-collapsed {
+    max-height: 36px;
+  }
+
+  .file-tree-expand-collapse-button {
+    border-radius: var(--border-radius-base);
+    color: var(--outline-content-color);
+    display: flex;
+    align-items: center;
+    background-color: transparent;
+    border: 0;
+    padding: 0 var(--spacing-02);
+    flex-grow: 1;
+
+    &:hover {
+      background-color: var(--file-tree-item-hover-bg);
+    }
+
+    h4 {
+      font-size: var(--font-size-02);
+      margin: 0;
+      font-weight: normal;
+    }
+  }
+
+  .file-tree-toolbar-action-buttons {
+    display: flex;
+  }
+
+  .file-tree-toolbar-action-button {
+    padding: var(--spacing-02);
+    border-radius: var(--border-radius-full);
+
+    &:hover {
+      background-color: var(--file-tree-item-hover-bg);
+    }
+
+    .material-symbols {
+      font-size: 16px;
+    }
+  }
+
   .file-tree {
   .file-tree {
     background-color: var(--file-tree-bg);
     background-color: var(--file-tree-bg);
   }
   }
@@ -47,6 +104,7 @@
     border-left: 1px solid
     border-left: 1px solid
       color-mix(in srgb, var(--border-primary) 24%, transparent);
       color-mix(in srgb, var(--border-primary) 24%, transparent);
     margin-left: 14px !important;
     margin-left: 14px !important;
+    margin-top: 0;
 
 
     &.file-tree-list {
     &.file-tree-list {
       border-left: none;
       border-left: none;

+ 4 - 0
services/web/frontend/stylesheets/bootstrap-5/pages/editor/outline.scss

@@ -37,6 +37,10 @@
   );
   );
   --outline-container-color-bg: var(--bg-light-primary);
   --outline-container-color-bg: var(--bg-light-primary);
 
 
+  .file-outline-panel {
+    min-height: 36px;
+  }
+
   .outline-pane {
   .outline-pane {
     padding: 4px;
     padding: 4px;
   }
   }

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

@@ -701,6 +701,7 @@
   "file_outline": "File outline",
   "file_outline": "File outline",
   "file_size": "File size",
   "file_size": "File size",
   "file_too_large": "File too large",
   "file_too_large": "File too large",
+  "file_tree": "File tree",
   "files_cannot_include_invalid_characters": "File name is empty or contains invalid characters",
   "files_cannot_include_invalid_characters": "File name is empty or contains invalid characters",
   "files_selected": "files selected.",
   "files_selected": "files selected.",
   "fill_in_our_quick_survey": "Fill in our quick survey.",
   "fill_in_our_quick_survey": "Fill in our quick survey.",
@@ -892,6 +893,7 @@
   "hide_configuration": "Hide configuration",
   "hide_configuration": "Hide configuration",
   "hide_deleted_user": "Hide deleted users",
   "hide_deleted_user": "Hide deleted users",
   "hide_document_preamble": "Hide document preamble",
   "hide_document_preamble": "Hide document preamble",
+  "hide_file_tree": "Hide file tree",
   "hide_local_file_contents": "Hide Local File Contents",
   "hide_local_file_contents": "Hide Local File Contents",
   "hide_outline": "Hide File outline",
   "hide_outline": "Hide File outline",
   "history": "History",
   "history": "History",
@@ -1933,6 +1935,7 @@
   "show_all": "show all",
   "show_all": "show all",
   "show_all_projects": "Show all projects",
   "show_all_projects": "Show all projects",
   "show_document_preamble": "Show document preamble",
   "show_document_preamble": "Show document preamble",
+  "show_file_tree": "Show file tree",
   "show_hotkeys": "Show Hotkeys",
   "show_hotkeys": "Show Hotkeys",
   "show_in_code": "Show in code",
   "show_in_code": "Show in code",
   "show_in_pdf": "Show in PDF",
   "show_in_pdf": "Show in PDF",