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

Add file tree to editor

GitOrigin-RevId: 493ecf88d632bed92c6b2b5ae2e5c0b7eef968cc
This commit is contained in:
David
2025-01-27 10:02:30 +00:00
committed by Copybot
parent 377f641dd4
commit 8acbafcb05
12 changed files with 333 additions and 139 deletions
@@ -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,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
@@ -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
@@ -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>
)
}
@@ -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 ? (
@@ -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
@@ -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()
@@ -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>
)
}
@@ -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);
@@ -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;
@@ -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))
})
})
})