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

Merge pull request #22979 from overleaf/dp-file-tree-in-editor

Add file tree to editor

GitOrigin-RevId: 493ecf88d632bed92c6b2b5ae2e5c0b7eef968cc
David 1 год назад
Родитель
Сommit
8acbafcb05

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


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

@@ -3,14 +3,17 @@
 // to update the font file with the latest icons.
 
 export default /** @type {const} */ ([
+  'book_5',
   'code',
   'description',
   'forum',
   'help',
+  'image',
   'integration_instructions',
   'picture_as_pdf',
   'rate_review',
   'report',
   'settings',
+  'table_chart',
   'web_asset',
 ])

+ 1 - 55
services/web/frontend/js/features/file-tree/components/file-tree-doc.tsx

@@ -1,12 +1,6 @@
 import { useSelectableEntity } from '../contexts/file-tree-selectable'
-
+import FileTreeIcon from './file-tree-icon'
 import FileTreeItemInner from './file-tree-item/file-tree-item-inner'
-import { useTranslation } from 'react-i18next'
-import Icon from '../../../shared/components/icon'
-import iconTypeFromName from '../util/icon-type-from-name'
-import classnames from 'classnames'
-import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
-import MaterialIcon from '@/shared/components/material-icon'
 
 function FileTreeDoc({
   name,
@@ -46,52 +40,4 @@ function FileTreeDoc({
   )
 }
 
-export const FileTreeIcon = ({
-  isLinkedFile,
-  name,
-}: {
-  name: string
-  isLinkedFile?: boolean
-}) => {
-  const { t } = useTranslation()
-
-  const className = classnames('file-tree-icon', {
-    'linked-file-icon': isLinkedFile,
-  })
-
-  return (
-    <>
-      &nbsp;
-      <BootstrapVersionSwitcher
-        bs3={
-          <>
-            <Icon type={iconTypeFromName(name)} fw className={className} />
-            {isLinkedFile && (
-              <Icon
-                type="external-link-square"
-                modifier="rotate-180"
-                className="linked-file-highlight"
-                accessibilityLabel={t('linked_file')}
-              />
-            )}
-          </>
-        }
-        bs5={
-          <>
-            <MaterialIcon type={iconTypeFromName(name)} className={className} />
-            {isLinkedFile && (
-              <MaterialIcon
-                type="open_in_new"
-                modifier="rotate-180"
-                className="linked-file-highlight"
-                accessibilityLabel={t('linked_file')}
-              />
-            )}
-          </>
-        }
-      />
-    </>
-  )
-}
-
 export default FileTreeDoc

+ 77 - 0
services/web/frontend/js/features/file-tree/components/file-tree-folder-icons.tsx

@@ -0,0 +1,77 @@
+import { useTranslation } from 'react-i18next'
+import Icon from '../../../shared/components/icon'
+import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
+import MaterialIcon from '@/shared/components/material-icon'
+import { useFeatureFlag } from '@/shared/context/split-test-context'
+
+function FileTreeFolderIcons({
+  expanded,
+  onExpandCollapseClick,
+}: {
+  expanded: boolean
+  onExpandCollapseClick: () => void
+}) {
+  const { t } = useTranslation()
+
+  const newEditor = useFeatureFlag('editor-redesign')
+
+  if (newEditor) {
+    return (
+      <>
+        <button
+          className="folder-expand-collapse-button"
+          onClick={onExpandCollapseClick}
+          aria-label={expanded ? t('collapse') : t('expand')}
+        >
+          <MaterialIcon
+            type={expanded ? 'expand_more' : 'chevron_right'}
+            className="file-tree-expand-icon"
+          />
+        </button>
+      </>
+    )
+  }
+
+  return (
+    <BootstrapVersionSwitcher
+      bs3={
+        <>
+          <button
+            onClick={onExpandCollapseClick}
+            aria-label={expanded ? t('collapse') : t('expand')}
+          >
+            <Icon
+              type={expanded ? 'angle-down' : 'angle-right'}
+              fw
+              className="file-tree-expand-icon"
+            />
+          </button>
+          <Icon
+            type={expanded ? 'folder-open' : 'folder'}
+            fw
+            className="file-tree-folder-icon"
+          />
+        </>
+      }
+      bs5={
+        <>
+          <button
+            onClick={onExpandCollapseClick}
+            aria-label={expanded ? t('collapse') : t('expand')}
+          >
+            <MaterialIcon
+              type={expanded ? 'expand_more' : 'chevron_right'}
+              className="file-tree-expand-icon"
+            />
+          </button>
+          <MaterialIcon
+            type={expanded ? 'folder_open' : 'folder'}
+            className="file-tree-folder-icon"
+          />
+        </>
+      }
+    />
+  )
+}
+
+export default FileTreeFolderIcons

+ 33 - 25
services/web/frontend/js/features/file-tree/components/file-tree-folder-list.tsx

@@ -32,39 +32,47 @@ function FileTreeFolderList({
 
   return (
     <ul
-      className={classNames('list-unstyled', classes.root)}
+      className={classNames(
+        'list-unstyled',
+        'file-tree-folder-list',
+        classes.root
+      )}
       role="tree"
       ref={dropRef}
       data-testid={dataTestId}
     >
-      {folders.sort(compareFunction).map(folder => {
-        return (
-          <FileTreeFolder
-            key={folder._id}
-            name={folder.name}
-            id={folder._id}
-            folders={folder.folders}
-            docs={folder.docs}
-            files={folder.fileRefs}
-          />
-        )
-      })}
-      {docsAndFiles.sort(compareFunction).map(doc => {
-        if ('isFile' in doc) {
+      <div className="file-tree-folder-list-inner">
+        {folders.sort(compareFunction).map(folder => {
           return (
-            <FileTreeDoc
-              key={doc._id}
-              name={doc.name}
-              id={doc._id}
-              isFile={doc.isFile}
-              isLinkedFile={doc.linkedFileData && !!doc.linkedFileData.provider}
+            <FileTreeFolder
+              key={folder._id}
+              name={folder.name}
+              id={folder._id}
+              folders={folder.folders}
+              docs={folder.docs}
+              files={folder.fileRefs}
             />
           )
-        }
+        })}
+        {docsAndFiles.sort(compareFunction).map(doc => {
+          if ('isFile' in doc) {
+            return (
+              <FileTreeDoc
+                key={doc._id}
+                name={doc.name}
+                id={doc._id}
+                isFile={doc.isFile}
+                isLinkedFile={
+                  doc.linkedFileData && !!doc.linkedFileData.provider
+                }
+              />
+            )
+          }
 
-        return <FileTreeDoc key={doc._id} name={doc.name} id={doc._id} />
-      })}
-      {children}
+          return <FileTreeDoc key={doc._id} name={doc.name} id={doc._id} />
+        })}
+        {children}
+      </div>
     </ul>
   )
 }

+ 7 - 48
services/web/frontend/js/features/file-tree/components/file-tree-folder.tsx

@@ -1,8 +1,6 @@
 import { useEffect } from 'react'
-import { useTranslation } from 'react-i18next'
 import classNames from 'classnames'
 
-import Icon from '../../../shared/components/icon'
 import {
   useFileTreeSelectable,
   useSelectableEntity,
@@ -12,11 +10,10 @@ import { useDroppable } from '../contexts/file-tree-draggable'
 import FileTreeItemInner from './file-tree-item/file-tree-item-inner'
 import FileTreeFolderList from './file-tree-folder-list'
 import usePersistedState from '../../../shared/hooks/use-persisted-state'
-import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
-import MaterialIcon from '@/shared/components/material-icon'
 import { Folder } from '../../../../../types/folder'
 import { Doc } from '../../../../../types/doc'
 import { FileRef } from '../../../../../types/file-ref'
+import FileTreeFolderIcons from './file-tree-folder-icons'
 
 function FileTreeFolder({
   name,
@@ -31,8 +28,6 @@ function FileTreeFolder({
   docs: Doc[]
   files: FileRef[]
 }) {
-  const { t } = useTranslation()
-
   const { isSelected, props: selectableEntityProps } = useSelectableEntity(
     id,
     'folder'
@@ -58,47 +53,6 @@ function FileTreeFolder({
   const { isOver: isOverRoot, dropRef: dropRefRoot } = useDroppable(id)
   const { isOver: isOverList, dropRef: dropRefList } = useDroppable(id)
 
-  const icons = (
-    <BootstrapVersionSwitcher
-      bs3={
-        <>
-          <button
-            onClick={handleExpandCollapseClick}
-            aria-label={expanded ? t('collapse') : t('expand')}
-          >
-            <Icon
-              type={expanded ? 'angle-down' : 'angle-right'}
-              fw
-              className="file-tree-expand-icon"
-            />
-          </button>
-          <Icon
-            type={expanded ? 'folder-open' : 'folder'}
-            fw
-            className="file-tree-folder-icon"
-          />
-        </>
-      }
-      bs5={
-        <>
-          <button
-            onClick={handleExpandCollapseClick}
-            aria-label={expanded ? t('collapse') : t('expand')}
-          >
-            <MaterialIcon
-              type={expanded ? 'expand_more' : 'chevron_right'}
-              className="file-tree-expand-icon"
-            />
-          </button>
-          <MaterialIcon
-            type={expanded ? 'folder_open' : 'folder'}
-            className="file-tree-folder-icon"
-          />
-        </>
-      }
-    />
-  )
-
   return (
     <>
       <li
@@ -119,7 +73,12 @@ function FileTreeFolder({
           name={name}
           type="folder"
           isSelected={isSelected}
-          icons={icons}
+          icons={
+            <FileTreeFolderIcons
+              expanded={expanded}
+              onExpandCollapseClick={handleExpandCollapseClick}
+            />
+          }
         />
       </li>
       {expanded ? (

+ 81 - 0
services/web/frontend/js/features/file-tree/components/file-tree-icon.tsx

@@ -0,0 +1,81 @@
+import { useTranslation } from 'react-i18next'
+import Icon from '../../../shared/components/icon'
+import iconTypeFromName, {
+  newEditorIconTypeFromName,
+} from '../util/icon-type-from-name'
+import classnames from 'classnames'
+import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
+import MaterialIcon from '@/shared/components/material-icon'
+import { useFeatureFlag } from '@/shared/context/split-test-context'
+
+function FileTreeIcon({
+  isLinkedFile,
+  name,
+}: {
+  name: string
+  isLinkedFile?: boolean
+}) {
+  const { t } = useTranslation()
+
+  const className = classnames('file-tree-icon', {
+    'linked-file-icon': isLinkedFile,
+  })
+
+  const newEditor = useFeatureFlag('editor-redesign')
+
+  if (newEditor) {
+    return (
+      <>
+        <MaterialIcon
+          unfilled
+          type={newEditorIconTypeFromName(name)}
+          className={className}
+        />
+        {isLinkedFile && (
+          <MaterialIcon
+            type="open_in_new"
+            modifier="rotate-180"
+            className="linked-file-highlight"
+            accessibilityLabel={t('linked_file')}
+          />
+        )}
+      </>
+    )
+  }
+
+  return (
+    <>
+      &nbsp;
+      <BootstrapVersionSwitcher
+        bs3={
+          <>
+            <Icon type={iconTypeFromName(name)} fw className={className} />
+            {isLinkedFile && (
+              <Icon
+                type="external-link-square"
+                modifier="rotate-180"
+                className="linked-file-highlight"
+                accessibilityLabel={t('linked_file')}
+              />
+            )}
+          </>
+        }
+        bs5={
+          <>
+            <MaterialIcon type={iconTypeFromName(name)} className={className} />
+            {isLinkedFile && (
+              <MaterialIcon
+                type="open_in_new"
+                modifier="rotate-180"
+                className="linked-file-highlight"
+                accessibilityLabel={t('linked_file')}
+              />
+            )}
+          </>
+        }
+      />
+    </>
+  )
+}
+
+export default FileTreeIcon

+ 20 - 0
services/web/frontend/js/features/file-tree/util/icon-type-from-name.ts

@@ -1,4 +1,24 @@
 import { isBootstrap5 } from '@/features/utils/bootstrap-5'
+import { AvailableUnfilledIcon } from '@/shared/components/material-icon'
+
+// TODO ide-redesign-cleanup: Make this the default export and remove the legacy version
+export const newEditorIconTypeFromName = (
+  name: string
+): AvailableUnfilledIcon => {
+  let ext = name.split('.').pop()
+  ext = ext ? ext.toLowerCase() : ext
+
+  if (ext && ['png', 'pdf', 'jpg', 'jpeg', 'gif'].includes(ext)) {
+    return 'image'
+  } else if (ext && ['csv', 'xls', 'xlsx'].includes(ext)) {
+    return 'table_chart'
+  } else if (ext && ['py', 'r'].includes(ext)) {
+    return 'code'
+  } else if (ext && ['bib'].includes(ext)) {
+    return 'book_5'
+  }
+  return 'description'
+}
 
 export default function iconTypeFromName(name: string): string {
   let ext = name.split('.').pop()

+ 12 - 6
services/web/frontend/js/features/ide-redesign/components/rail.tsx

@@ -5,6 +5,7 @@ import MaterialIcon, {
 } from '@/shared/components/material-icon'
 import { Panel } from 'react-resizable-panels'
 import { useLayoutContext } from '@/shared/context/layout-context'
+import { FileTree } from '@/features/ide-react/components/file-tree'
 
 type RailElement = {
   icon: AvailableUnfilledIcon
@@ -26,7 +27,12 @@ const RAIL_TABS: RailElement[] = [
   {
     key: 'file-tree',
     icon: 'description',
-    component: <>File tree</>,
+    component: (
+      <>
+        {/* TODO: add panel for file outline */}
+        <FileTree />
+      </>
+    ),
   },
   {
     key: 'integrations',
@@ -127,11 +133,11 @@ const RailTab = ({
 }) => {
   return (
     <NavLink eventKey={eventKey} className="ide-rail-tab-link">
-      <MaterialIcon
-        className="ide-rail-tab-link-icon"
-        type={icon}
-        unfilled={!active}
-      />
+      {active ? (
+        <MaterialIcon className="ide-rail-tab-link-icon" type={icon} />
+      ) : (
+        <MaterialIcon className="ide-rail-tab-link-icon" type={icon} unfilled />
+      )}
     </NavLink>
   )
 }

+ 97 - 4
services/web/frontend/stylesheets/bootstrap-5/pages/editor/file-tree.scss

@@ -5,8 +5,11 @@
   --file-tree-bg: var(--bg-dark-tertiary);
   --file-tree-item-selected-color: var(--content-primary-dark);
   --file-tree-item-dragging-bg: #{rgb($bg-dark-secondary, 0.9)};
+  --file-tree-item-dragging-color: var(--content-primary-dark);
   --file-tree-item-dragging-preview-bg: #{rgb($bg-accent-01, 0.6)};
+  --file-tree-item-dragging-preview-colour: var(--content-primary-dark);
   --file-tree-line-height: 2.05;
+  --file-tree-icon-colour: var(--content-disabled);
 }
 
 @include theme('light') {
@@ -15,6 +18,96 @@
   --file-tree-bg: var(--bg-light-primary);
   --file-tree-item-selected-color: var(--bg-light-primary);
   --file-tree-item-dragging-bg: #{rgb($bg-light-tertiary, 0.9)};
+  --file-tree-item-dragging-color: var(--content-secondary);
+  --file-tree-item-dragging-preview-colour: var(--bg-light-primary);
+}
+
+// TODO ide-redesign-cleanup: Replace the existing styling with these overrides.
+.ide-redesign-main {
+  --file-tree-item-hover-bg: var(--bg-light-secondary);
+  --file-tree-item-selected-bg: var(--bg-dark-primary);
+  --file-tree-item-selected-color: var(--white);
+  --file-tree-item-color: var(--content-primary);
+  --file-tree-bg: var(--white);
+  --file-tree-icon-colour: var(--content-primary);
+  --file-tree-item-dragging-bg: #{rgb($bg-dark-primary, 0.9)};
+  --file-tree-item-dragging-color: var(--white);
+  --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 {
+    background-color: var(--file-tree-bg);
+  }
+
+  .file-tree ul.file-tree-list {
+    margin: var(--spacing-02);
+  }
+
+  .file-tree-folder-list {
+    border-left: 1px solid
+      color-mix(in srgb, var(--border-primary) 24%, transparent);
+    margin-left: 14px !important;
+
+    &.file-tree-list {
+      border-left: none;
+      margin-left: var(--spacing-02) !important;
+
+      > .file-tree-folder-list-inner {
+        margin-left: 0;
+      }
+    }
+  }
+
+  .file-tree-folder-list-inner {
+    margin-left: 10px;
+    display: flex;
+    flex-direction: column;
+    gap: var(--spacing-02);
+  }
+
+  .item-name-button,
+  .folder-expand-collapse-button {
+    display: flex;
+    align-items: center;
+    height: 20px !important;
+  }
+
+  .file-tree ul.file-tree-list li .material-symbols.file-tree-expand-icon {
+    margin-left: 0;
+  }
+
+  .file-tree ul.file-tree-list li .material-symbols.file-tree-icon {
+    margin-left: 0;
+    margin-right: 0;
+  }
+
+  // TODO ide-redesign-cleanup: Remove the !important overrides once
+  // we have replaced the default styling
+  .linked-file-highlight {
+    background-color: var(--file-tree-bg) !important;
+    color: var(--file-tree-icon-colour) !important;
+    left: 14px !important;
+  }
+
+  .entity-name {
+    color: var(--file-tree-item-color);
+    border-radius: var(--border-radius-base);
+    padding: var(--spacing-02);
+
+    // TODO ide-redesign-cleanup: This is here to override the fake-full-width-bg
+    // mixin. We can just remove that mixin when we clean this up.
+    &::before {
+      content: none !important;
+    }
+  }
+
+  .item-name-button {
+    margin-left: var(--spacing-02);
+  }
+
+  .dnd-draggable-preview-item {
+    border-radius: var(--border-radius-base);
+  }
 }
 
 .ide-react-file-tree-panel {
@@ -214,7 +307,7 @@
       }
 
       .material-symbols {
-        color: var(--content-disabled);
+        color: var(--file-tree-icon-colour);
 
         &.file-tree-icon {
           margin-right: var(--spacing-02);
@@ -252,7 +345,7 @@
         width: 24px;
         padding: var(--spacing-03);
         font-size: var(--font-size-03);
-        color: var(--content-disabled);
+        color: var(--file-tree-icon-colour);
       }
 
       .file-tree-dropdown-toggle {
@@ -410,7 +503,7 @@
 
     @include fake-full-width-bg(var(--file-tree-item-dragging-bg));
 
-    color: var(--file-tree-item-color);
+    color: var(--file-tree-item-dragging-color);
 
     .material-symbols {
       color: var(--content-disabled) !important;
@@ -447,7 +540,7 @@
   }
 
   .dnd-draggable-preview-item {
-    color: var(--file-tree-item-selected-color);
+    color: var(--file-tree-item-dragging-preview-colour);
     background-color: var(--file-tree-item-dragging-preview-bg);
     width: 75%;
     padding-left: var(--spacing-08);

+ 1 - 0
services/web/frontend/stylesheets/bootstrap-5/pages/editor/toolbar.scss

@@ -51,6 +51,7 @@
   --toolbar-btn-hover-bg-color: var(--neutral-80);
   --toolbar-btn-hover-color: var(--white);
   --editor-toolbar-bg: var(--white);
+  --toolbar-filetree-bg-color: var(--white);
 
   .toolbar {
     border-bottom: none;

+ 1 - 1
services/web/test/frontend/features/file-tree/components/file-tree-root.spec.tsx

@@ -372,7 +372,7 @@ describe('<FileTreeRoot/>', function () {
 
       cy.findByRole('treeitem', { name: 'abcdef.tex' }).then($itemEl => {
         cy.findByTestId('file-tree-list-root').then($rootEl => {
-          expect($itemEl.get(0).parentNode).to.equal($rootEl.get(0))
+          expect($itemEl.get(0).parentNode?.parentNode).to.equal($rootEl.get(0))
         })
       })
     })