From abb59e46031796d62472e9b7e902a8fcceafb2f0 Mon Sep 17 00:00:00 2001 From: Rebeka Dekany <50901361+rebekadekany@users.noreply.github.com> Date: Wed, 25 Sep 2024 15:46:02 +0200 Subject: [PATCH] Merge pull request #20298 from overleaf/rd-ide-filetree [web] Migrate the file tree on the editor page to Bootstrap 5 GitOrigin-RevId: e2efec26242c8cdab37a54bc182b83bfb0f1eb3c --- .../web/frontend/extracted-translations.json | 1 + .../components/file-tree-context-menu.tsx | 74 ++- .../file-tree-create/danger-message.jsx | 4 +- .../file-tree-create-name-input.jsx | 79 ++- .../file-tree-modal-create-file-body.jsx | 9 +- .../file-tree-modal-create-file-footer.jsx | 29 +- .../file-tree-modal-create-file-mode.jsx | 16 +- .../file-tree-upload-conflicts.tsx | 22 +- .../modes/file-tree-import-from-project.tsx | 94 ++- .../modes/file-tree-import-from-url.jsx | 13 +- .../file-tree/components/file-tree-doc.jsx | 42 +- .../file-tree/components/file-tree-error.jsx | 6 +- .../file-tree/components/file-tree-folder.jsx | 57 +- .../file-tree-item/file-tree-item-inner.tsx | 2 +- .../file-tree-item-menu-items.jsx | 102 ++- .../file-tree-item/file-tree-item-menu.jsx | 15 +- .../modals/file-tree-modal-create-file.jsx | 26 +- .../modals/file-tree-modal-create-folder.jsx | 55 +- .../modals/file-tree-modal-delete.jsx | 57 +- .../modals/file-tree-modal-error.jsx | 30 +- .../file-tree/util/icon-type-from-name.js | 10 +- .../components/sidebar/tags-list.tsx | 2 +- .../bootstrap-5/language-picker.tsx | 5 +- .../components/types/dropdown-menu-props.ts | 3 + .../js/shared/components/loading-spinner.tsx | 28 +- .../js/shared/components/material-icon.tsx | 4 +- .../stylesheets/app/editor/file-tree.less | 16 +- .../stylesheets/app/editor/ide-react.less | 5 +- .../bootstrap-5/abstracts/mixins.scss | 13 - .../stylesheets/bootstrap-5/base/layout.scss | 4 + .../bootstrap-5/components/button.scss | 1 + .../bootstrap-5/components/dropdown-menu.scss | 2 +- .../bootstrap-5/pages/editor/file-tree.scss | 603 ++++++++++++++++++ .../bootstrap-5/pages/editor/ide.scss | 141 +++- .../bootstrap-5/pages/editor/outline.scss | 27 +- services/web/locales/en.json | 1 + .../file-tree-item-inner.spec.tsx | 2 +- .../components/file-tree-root.spec.tsx | 17 +- .../file-tree/flows/delete-entity.spec.tsx | 4 +- .../file-tree/flows/rename-entity.spec.tsx | 2 +- 40 files changed, 1297 insertions(+), 326 deletions(-) diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 00c8639ed5..3c3d3b4e4f 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -968,6 +968,7 @@ "only_group_admin_or_managers_can_delete_your_account_5": "", "only_importer_can_refresh": "", "open_a_file_on_the_left": "", + "open_action_menu": "", "open_advanced_reference_search": "", "open_file": "", "open_link": "", diff --git a/services/web/frontend/js/features/file-tree/components/file-tree-context-menu.tsx b/services/web/frontend/js/features/file-tree/components/file-tree-context-menu.tsx index a0e9bcbd6f..7386abb218 100644 --- a/services/web/frontend/js/features/file-tree/components/file-tree-context-menu.tsx +++ b/services/web/frontend/js/features/file-tree/components/file-tree-context-menu.tsx @@ -1,10 +1,15 @@ import React, { useEffect, useRef } from 'react' import ReactDOM from 'react-dom' -import { Dropdown } from 'react-bootstrap' +import { Dropdown as BS3Dropdown } from 'react-bootstrap' +import { + Dropdown, + DropdownMenu, +} from '@/features/ui/components/bootstrap-5/dropdown-menu' import { useFileTreeData } from '@/shared/context/file-tree-data-context' import { useFileTreeMainContext } from '../contexts/file-tree-main' import FileTreeItemMenuItems from './file-tree-item/file-tree-item-menu-items' +import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' function FileTreeContextMenu() { const { fileTreeReadOnly } = useFileTreeData() @@ -24,10 +29,10 @@ function FileTreeContextMenu() { // A11y - Move the focus to the context menu when it opens function focusContextMenu() { - const contextMenu = document.querySelector( + const BS3contextMenu = document.querySelector( '[aria-labelledby="dropdown-file-tree-context-menu"]' ) as HTMLElement | null - contextMenu?.focus() + BS3contextMenu?.focus() } function close() { @@ -48,31 +53,58 @@ function FileTreeContextMenu() { // A11y - Close the context menu when the user presses the Tab key // Focus should move to the next element in the filetree - function handleKeyDown(event: React.KeyboardEvent) { + function handleKeyDown(event: React.KeyboardEvent) { if (event.key === 'Tab') { close() } } return ReactDOM.createPortal( - + + + + + } - className="context-menu" - style={contextMenuCoords} - onKeyDown={handleKeyDown} - > - - - - - , + bs5={ +
+ + + + + +
+ } + />, document.body ) } diff --git a/services/web/frontend/js/features/file-tree/components/file-tree-create/danger-message.jsx b/services/web/frontend/js/features/file-tree/components/file-tree-create/danger-message.jsx index 45553d72d2..ceaafc6fa8 100644 --- a/services/web/frontend/js/features/file-tree/components/file-tree-create/danger-message.jsx +++ b/services/web/frontend/js/features/file-tree/components/file-tree-create/danger-message.jsx @@ -1,8 +1,8 @@ +import OLNotification from '@/features/ui/components/ol/ol-notification' import PropTypes from 'prop-types' -import { Alert } from 'react-bootstrap' export default function DangerMessage({ children }) { - return {children} + return } DangerMessage.propTypes = { children: PropTypes.any.isRequired, diff --git a/services/web/frontend/js/features/file-tree/components/file-tree-create/file-tree-create-name-input.jsx b/services/web/frontend/js/features/file-tree/components/file-tree-create/file-tree-create-name-input.jsx index 1a26d12a8a..a76768c4df 100644 --- a/services/web/frontend/js/features/file-tree/components/file-tree-create/file-tree-create-name-input.jsx +++ b/services/web/frontend/js/features/file-tree/components/file-tree-create/file-tree-create-name-input.jsx @@ -1,7 +1,4 @@ -import ControlLabel from 'react-bootstrap/lib/ControlLabel' -import { Alert, FormControl } from 'react-bootstrap' -import FormGroup from 'react-bootstrap/lib/FormGroup' -import { useCallback } from 'react' +import { useRef, useEffect } from 'react' import { useTranslation } from 'react-i18next' import { useFileTreeCreateName } from '../../contexts/file-tree-create-name' import PropTypes from 'prop-types' @@ -10,6 +7,10 @@ import { DuplicateFilenameError, InvalidFilenameError, } from '../../errors' +import OLFormGroup from '@/features/ui/components/ol/ol-form-group' +import OLFormLabel from '@/features/ui/components/ol/ol-form-label' +import OLFormControl from '@/features/ui/components/ol/ol-form-control' +import OLNotification from '@/features/ui/components/ol/ol-notification' /** * A form component that renders a text input with label, @@ -29,42 +30,46 @@ export default function FileTreeCreateNameInput({ const { name, setName, touchedName, validName } = useFileTreeCreateName() // focus the first part of the filename if needed - const inputRef = useCallback( - element => { - if (element && focusName) { - window.requestAnimationFrame(() => { - element.focus() - element.setSelectionRange(0, element.value.lastIndexOf('.')) - }) - } - }, - [focusName] - ) + const inputRef = useRef(null) + + useEffect(() => { + if (inputRef.current && focusName) { + window.requestAnimationFrame(() => { + if (inputRef.current) { + inputRef.current.focus() + inputRef.current.setSelectionRange( + 0, + inputRef.current.value.lastIndexOf('.') + ) + } + }) + } + }, [focusName]) return ( - - {label || t('file_name')} + + {label || t('file_name')} - setName(event.target.value)} - inputRef={inputRef} + ref={inputRef} disabled={inFlight} /> - - {touchedName && !validName && ( - - {t('files_cannot_include_invalid_characters')} - + )} {error && } - + ) } @@ -89,23 +94,29 @@ function ErrorMessage({ error }) { switch (error.constructor) { case DuplicateFilenameError: return ( - - {t('file_already_exists')} - + ) case InvalidFilenameError: return ( - - {t('files_cannot_include_invalid_characters')} - + ) case BlockedFilenameError: return ( - - {t('blocked_filename')} - + ) default: diff --git a/services/web/frontend/js/features/file-tree/components/file-tree-create/file-tree-modal-create-file-body.jsx b/services/web/frontend/js/features/file-tree/components/file-tree-create/file-tree-modal-create-file-body.jsx index 6aa69372f0..00d1a0aaf2 100644 --- a/services/web/frontend/js/features/file-tree/components/file-tree-create/file-tree-modal-create-file-body.jsx +++ b/services/web/frontend/js/features/file-tree/components/file-tree-create/file-tree-modal-create-file-body.jsx @@ -11,6 +11,7 @@ import importOverleafModules from '../../../../../macros/import-overleaf-module. import { lazy, Suspense } from 'react' import { FullSizeLoadingSpinner } from '@/shared/components/loading-spinner' import getMeta from '@/utils/meta' +import { bsVersion } from '@/features/utils/bootstrap-5' const createFileModeModules = importOverleafModules('createFileModes') @@ -35,11 +36,11 @@ export default function FileTreeModalCreateFileBody() { -
+
    @@ -53,7 +54,7 @@ export default function FileTreeModalCreateFileBody() { hasLinkedProjectOutputFileFeature) && ( )} @@ -75,7 +76,7 @@ export default function FileTreeModalCreateFileBody() {
{newFileCreateMode === 'doc' && ( diff --git a/services/web/frontend/js/features/file-tree/components/file-tree-create/file-tree-modal-create-file-footer.jsx b/services/web/frontend/js/features/file-tree/components/file-tree-create/file-tree-modal-create-file-footer.jsx index 326053c1fa..09bf3b82ec 100644 --- a/services/web/frontend/js/features/file-tree/components/file-tree-create/file-tree-modal-create-file-footer.jsx +++ b/services/web/frontend/js/features/file-tree/components/file-tree-create/file-tree-modal-create-file-footer.jsx @@ -1,9 +1,10 @@ import { useTranslation } from 'react-i18next' -import { Alert, Button } from 'react-bootstrap' import { useFileTreeCreateForm } from '../../contexts/file-tree-create-form' import { useFileTreeActionable } from '../../contexts/file-tree-actionable' import { useFileTreeData } from '../../../../shared/context/file-tree-data-context' import PropTypes from 'prop-types' +import OLButton from '@/features/ui/components/ol/ol-button' +import OLNotification from '@/features/ui/components/ol/ol-notification' export default function FileTreeModalCreateFileFooter() { const { valid } = useFileTreeCreateForm() @@ -40,31 +41,35 @@ export function FileTreeModalCreateFileFooterContent({ )} {fileCount.status === 'error' && ( - + {/* TODO: add parameter for fileCount.limit */} - {t('project_has_too_many_files')} - + )} - + {newFileCreateMode !== 'upload' && ( - + {t('create')} + )} ) diff --git a/services/web/frontend/js/features/file-tree/components/file-tree-create/file-tree-modal-create-file-mode.jsx b/services/web/frontend/js/features/file-tree/components/file-tree-create/file-tree-modal-create-file-mode.jsx index 6fde08ae55..9f5904f0e2 100644 --- a/services/web/frontend/js/features/file-tree/components/file-tree-create/file-tree-modal-create-file-mode.jsx +++ b/services/web/frontend/js/features/file-tree/components/file-tree-create/file-tree-modal-create-file-mode.jsx @@ -1,9 +1,11 @@ import classnames from 'classnames' -import { Button } from 'react-bootstrap' import PropTypes from 'prop-types' import Icon from '../../../../shared/components/icon' import { useFileTreeActionable } from '../../contexts/file-tree-actionable' import * as eventTracking from '../../../../infrastructure/event-tracking' +import OLButton from '@/features/ui/components/ol/ol-button' +import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' +import MaterialIcon from '@/shared/components/material-icon' export default function FileTreeModalCreateFileMode({ mode, icon, label }) { const { newFileCreateMode, startCreatingFile } = useFileTreeActionable() @@ -15,16 +17,18 @@ export default function FileTreeModalCreateFileMode({ mode, icon, label }) { return (
  • - +
  • ) } diff --git a/services/web/frontend/js/features/file-tree/components/file-tree-create/file-tree-upload-conflicts.tsx b/services/web/frontend/js/features/file-tree/components/file-tree-create/file-tree-upload-conflicts.tsx index 814e8a164b..9d8a66122c 100644 --- a/services/web/frontend/js/features/file-tree/components/file-tree-create/file-tree-upload-conflicts.tsx +++ b/services/web/frontend/js/features/file-tree/components/file-tree-create/file-tree-upload-conflicts.tsx @@ -3,8 +3,8 @@ import { useTranslation } from 'react-i18next' import { useProjectContext } from '@/shared/context/project-context' import { useCallback } from 'react' import { syncDelete } from '@/features/file-tree/util/sync-mutation' -import { Button } from 'react-bootstrap' import { TFunction } from 'i18next' +import OLButton from '@/features/ui/components/ol/ol-button' export type Conflict = { entity: FileTreeEntity @@ -48,7 +48,7 @@ export function FileUploadConflicts({ ) return ( -
    +
    {conflicts.length > 0 && ( <>

    {getConflictText(conflicts, t)}

    @@ -70,14 +70,14 @@ export function FileUploadConflicts({ )}

    - +   {!hasFolderConflict && ( - + )}

    @@ -118,7 +118,7 @@ export function FolderUploadConflicts({ }, [setError, conflicts, handleOverwrite, projectId]) return ( -
    +

    {getConflictText(conflicts, t)}

      @@ -140,14 +140,14 @@ export function FolderUploadConflicts({ )}

      - +   {!hasFileConflict && ( - + )}

    diff --git a/services/web/frontend/js/features/file-tree/components/file-tree-create/modes/file-tree-import-from-project.tsx b/services/web/frontend/js/features/file-tree/components/file-tree-create/modes/file-tree-import-from-project.tsx index 5ec1f22595..2ab9486e92 100644 --- a/services/web/frontend/js/features/file-tree/components/file-tree-create/modes/file-tree-import-from-project.tsx +++ b/services/web/frontend/js/features/file-tree/components/file-tree-create/modes/file-tree-import-from-project.tsx @@ -5,7 +5,6 @@ import { useMemo, FormEventHandler, } from 'react' -import { Button, ControlLabel, FormControl, FormGroup } from 'react-bootstrap' import Icon from '../../../../../shared/components/icon' import FileTreeCreateNameInput from '../file-tree-create-name-input' import { useTranslation } from 'react-i18next' @@ -21,6 +20,13 @@ import * as eventTracking from '../../../../../infrastructure/event-tracking' import { File } from '@/features/source-editor/utils/file' import { Project } from '../../../../../../../types/project' import getMeta from '@/utils/meta' +import OLButton from '@/features/ui/components/ol/ol-button' +import OLFormGroup from '@/features/ui/components/ol/ol-form-group' +import OLFormLabel from '@/features/ui/components/ol/ol-form-label' +import OLForm from '@/features/ui/components/ol/ol-form' +import OLFormSelect from '@/features/ui/components/ol/ol-form-select' +import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' +import { Spinner } from 'react-bootstrap-5' export default function FileTreeImportFromProject() { const { t } = useTranslation() @@ -125,7 +131,7 @@ export default function FileTreeImportFromProject() { } return ( -
    + or  - +
    )} @@ -173,7 +179,7 @@ export default function FileTreeImportFromProject() { /> {error && } - + ) } @@ -204,18 +210,27 @@ function SelectProject({ } return ( - - {t('select_a_project')} + + {t('select_a_project')} {loading && (   - + } + bs5={ + )} - { @@ -234,12 +249,12 @@ function SelectProject({ {project.name} ))} - + {filteredData && !filteredData.length && ( {t('no_other_projects_found')} )} - + ) } @@ -263,25 +278,34 @@ function SelectProjectOutputFile({ } return ( - - {t('select_an_output_file')} + {t('select_an_output_file')} {loading && (   - + } + bs5={ + )} - { - const path = (event.target as HTMLSelectElement).value + const path = (event.target as unknown as HTMLSelectElement).value const file = data?.find(item => item.path === path) setSelectedProjectOutputFile(file) }} @@ -296,8 +320,8 @@ function SelectProjectOutputFile({ {file.path} ))} - - + + ) } @@ -321,21 +345,27 @@ function SelectProjectEntity({ } return ( - - {t('select_a_file')} + + {t('select_a_file')} {loading && (   - + } + bs5={ + )} - { @@ -354,7 +384,7 @@ function SelectProjectEntity({ {entity.path.slice(1)} ))} - - + + ) } diff --git a/services/web/frontend/js/features/file-tree/components/file-tree-create/modes/file-tree-import-from-url.jsx b/services/web/frontend/js/features/file-tree/components/file-tree-create/modes/file-tree-import-from-url.jsx index 7318aec78e..9a6f121abc 100644 --- a/services/web/frontend/js/features/file-tree/components/file-tree-create/modes/file-tree-import-from-url.jsx +++ b/services/web/frontend/js/features/file-tree/components/file-tree-create/modes/file-tree-import-from-url.jsx @@ -1,4 +1,3 @@ -import { ControlLabel, FormControl, FormGroup } from 'react-bootstrap' import { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import FileTreeCreateNameInput from '../file-tree-create-name-input' @@ -7,6 +6,9 @@ import { useFileTreeCreateName } from '../../../contexts/file-tree-create-name' import { useFileTreeCreateForm } from '../../../contexts/file-tree-create-form' import ErrorMessage from '../error-message' import * as eventTracking from '../../../../../infrastructure/event-tracking' +import OLFormGroup from '@/features/ui/components/ol/ol-form-group' +import OLFormLabel from '@/features/ui/components/ol/ol-form-label' +import OLFormControl from '@/features/ui/components/ol/ol-form-control' export default function FileTreeImportFromUrl() { const { t } = useTranslation() @@ -54,17 +56,16 @@ export default function FileTreeImportFromUrl() { noValidate onSubmit={handleSubmit} > - - {t('url_to_fetch_the_file_from')} - - + {t('url_to_fetch_the_file_from')} + - + { const { t } = useTranslation() - const className = classnames('spaced', 'file-tree-icon', { + const className = classnames('file-tree-icon', { 'linked-file-icon': isLinkedFile, }) return ( <>   - - {isLinkedFile && ( - - )} + + + {isLinkedFile && ( + + )} + + } + bs5={ + <> + + {isLinkedFile && ( + + )} + + } + /> ) } + FileTreeIcon.propTypes = { name: PropTypes.string.isRequired, isLinkedFile: PropTypes.bool, diff --git a/services/web/frontend/js/features/file-tree/components/file-tree-error.jsx b/services/web/frontend/js/features/file-tree/components/file-tree-error.jsx index c62250b6c6..9093691210 100644 --- a/services/web/frontend/js/features/file-tree/components/file-tree-error.jsx +++ b/services/web/frontend/js/features/file-tree/components/file-tree-error.jsx @@ -1,6 +1,6 @@ -import { Button } from 'react-bootstrap' import { useTranslation } from 'react-i18next' import { useLocation } from '../../../shared/hooks/use-location' +import OLButton from '@/features/ui/components/ol/ol-button' function FileTreeError() { const { t } = useTranslation() @@ -10,9 +10,9 @@ function FileTreeError() {

    {t('generic_something_went_wrong')}

    {t('please_refresh')}

    - +
    ) } diff --git a/services/web/frontend/js/features/file-tree/components/file-tree-folder.jsx b/services/web/frontend/js/features/file-tree/components/file-tree-folder.jsx index 07301c5142..01234dd3bd 100644 --- a/services/web/frontend/js/features/file-tree/components/file-tree-folder.jsx +++ b/services/web/frontend/js/features/file-tree/components/file-tree-folder.jsx @@ -13,6 +13,8 @@ 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' function FileTreeFolder({ name, id, folders, docs, files }) { const { t } = useTranslation() @@ -43,23 +45,44 @@ function FileTreeFolder({ name, id, folders, docs, files }) { const { isOver: isOverList, dropRef: dropRefList } = useDroppable(id) const icons = ( - <> - - - + + + + + } + bs5={ + <> + + + + } + /> ) return ( diff --git a/services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-inner.tsx b/services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-inner.tsx index 7e5a0e71bb..60bbc15ba9 100644 --- a/services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-inner.tsx +++ b/services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-inner.tsx @@ -90,7 +90,7 @@ function FileTreeItemInner({ isSelected={isSelected} setIsDraggable={setIsDraggable} /> - {hasMenu ? : null} + {hasMenu ? : null}
    ) diff --git a/services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-menu-items.jsx b/services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-menu-items.jsx index a769f98756..93da129758 100644 --- a/services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-menu-items.jsx +++ b/services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-menu-items.jsx @@ -4,7 +4,12 @@ import * as eventTracking from '../../../../infrastructure/event-tracking' import { useProjectContext } from '@/shared/context/project-context' import { MenuItem } from 'react-bootstrap' +import { + DropdownDivider, + DropdownItem, +} from '@/features/ui/components/bootstrap-5/dropdown-menu' import { useFileTreeActionable } from '../../contexts/file-tree-actionable' +import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' function FileTreeItemMenuItems() { const { t } = useTranslation() @@ -42,31 +47,82 @@ function FileTreeItemMenuItems() { }, [startUploadingDocOrFile]) return ( - <> - {canRename ? ( - {t('rename')} - ) : null} - {downloadPath ? ( - - {t('download')} - - ) : null} - {canDelete ? ( - {t('delete')} - ) : null} - {canCreate ? ( + -
  • - {t('new_file')} - {t('new_folder')} - {t('upload')} + {canRename ? ( + {t('rename')} + ) : null} + {downloadPath ? ( + + {t('download')} + + ) : null} + {canDelete ? ( + {t('delete')} + ) : null} + {canCreate ? ( + <> +
  • + {t('new_file')} + + {t('new_folder')} + + {t('upload')} + + ) : null} - ) : null} - + } + bs5={ + <> + {canRename ? ( +
  • + {t('rename')} +
  • + ) : null} + {downloadPath ? ( +
  • + + {t('download')} + +
  • + ) : null} + {canDelete ? ( +
  • + {t('delete')} +
  • + ) : null} + {canCreate ? ( + <> + +
  • + + {t('new_file')} + +
  • +
  • + + {t('new_folder')} + +
  • +
  • + + {t('upload')} + +
  • + + ) : null} + + } + /> ) } diff --git a/services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-menu.jsx b/services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-menu.jsx index 32afe6d251..48c5505395 100644 --- a/services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-menu.jsx +++ b/services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-menu.jsx @@ -5,12 +5,16 @@ import { useTranslation } from 'react-i18next' import Icon from '../../../../shared/components/icon' import { useFileTreeMainContext } from '../../contexts/file-tree-main' +import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' +import MaterialIcon from '@/shared/components/material-icon' -function FileTreeItemMenu({ id }) { +function FileTreeItemMenu({ id, name }) { const { t } = useTranslation() const { contextMenuCoords, setContextMenuCoords } = useFileTreeMainContext() const menuButtonRef = useRef() + const isMenuOpen = Boolean(contextMenuCoords) + function handleClick(event) { event.stopPropagation() if (!contextMenuCoords) { @@ -31,8 +35,14 @@ function FileTreeItemMenu({ id }) { id={`menu-button-${id}`} onClick={handleClick} ref={menuButtonRef} + aria-haspopup="true" + aria-expanded={isMenuOpen} + aria-label={t('open_action_menu', { name })} > - + } + bs5={} + /> ) @@ -40,6 +50,7 @@ function FileTreeItemMenu({ id }) { FileTreeItemMenu.propTypes = { id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, } export default FileTreeItemMenu diff --git a/services/web/frontend/js/features/file-tree/components/modals/file-tree-modal-create-file.jsx b/services/web/frontend/js/features/file-tree/components/modals/file-tree-modal-create-file.jsx index 6fd497f79f..e61a6ba2c9 100644 --- a/services/web/frontend/js/features/file-tree/components/modals/file-tree-modal-create-file.jsx +++ b/services/web/frontend/js/features/file-tree/components/modals/file-tree-modal-create-file.jsx @@ -1,10 +1,14 @@ -import { Modal } from 'react-bootstrap' import { useTranslation } from 'react-i18next' import { useFileTreeActionable } from '../../contexts/file-tree-actionable' import FileTreeCreateFormProvider from '../../contexts/file-tree-create-form' import FileTreeModalCreateFileBody from '../file-tree-create/file-tree-modal-create-file-body' import FileTreeModalCreateFileFooter from '../file-tree-create/file-tree-modal-create-file-footer' -import AccessibleModal from '../../../../shared/components/accessible-modal' +import OLModal, { + OLModalBody, + OLModalFooter, + OLModalHeader, + OLModalTitle, +} from '@/features/ui/components/ol/ol-modal' export default function FileTreeModalCreateFile() { const { t } = useTranslation() @@ -17,19 +21,19 @@ export default function FileTreeModalCreateFile() { return ( - - - {t('add_files')} - + + + {t('add_files')} + - + - + - + - - + + ) } diff --git a/services/web/frontend/js/features/file-tree/components/modals/file-tree-modal-create-folder.jsx b/services/web/frontend/js/features/file-tree/components/modals/file-tree-modal-create-folder.jsx index aa65175d54..bf726c8902 100644 --- a/services/web/frontend/js/features/file-tree/components/modals/file-tree-modal-create-folder.jsx +++ b/services/web/frontend/js/features/file-tree/components/modals/file-tree-modal-create-folder.jsx @@ -1,17 +1,17 @@ import { useEffect, useState } from 'react' import PropTypes from 'prop-types' - -import { Button, Modal } from 'react-bootstrap' import { useTranslation } from 'react-i18next' import { useRefWithAutoFocus } from '../../../../shared/hooks/use-ref-with-auto-focus' - -import AccessibleModal from '../../../../shared/components/accessible-modal' - import { useFileTreeActionable } from '../../contexts/file-tree-actionable' - import { DuplicateFilenameError } from '../../errors' - import { isCleanFilename } from '../../util/safe-path' +import OLModal, { + OLModalBody, + OLModalFooter, + OLModalHeader, + OLModalTitle, +} from '@/features/ui/components/ol/ol-modal' +import OLButton from '@/features/ui/components/ol/ol-button' function FileTreeModalCreateFolder() { const { t } = useTranslation() @@ -48,12 +48,12 @@ function FileTreeModalCreateFolder() { } return ( - - - {t('new_folder')} - + + + {t('new_folder')} + - + ) : null} - + - + {inFlight ? ( - + ) : ( <> - - + )} - - + + ) } diff --git a/services/web/frontend/js/features/file-tree/components/modals/file-tree-modal-delete.jsx b/services/web/frontend/js/features/file-tree/components/modals/file-tree-modal-delete.jsx index e4a94fc88e..314729dd1a 100644 --- a/services/web/frontend/js/features/file-tree/components/modals/file-tree-modal-delete.jsx +++ b/services/web/frontend/js/features/file-tree/components/modals/file-tree-modal-delete.jsx @@ -1,9 +1,14 @@ -import { Button, Modal } from 'react-bootstrap' import { useTranslation } from 'react-i18next' -import AccessibleModal from '../../../../shared/components/accessible-modal' - import { useFileTreeActionable } from '../../contexts/file-tree-actionable' +import OLModal, { + OLModalBody, + OLModalFooter, + OLModalHeader, + OLModalTitle, +} from '@/features/ui/components/ol/ol-modal' +import OLButton from '@/features/ui/components/ol/ol-button' +import OLNotification from '@/features/ui/components/ol/ol-notification' function FileTreeModalDelete() { const { t } = useTranslation() @@ -28,12 +33,12 @@ function FileTreeModalDelete() { } return ( - - - {t('delete')} - + + + {t('delete')} + - +

    {t('sure_you_want_to_delete')}

      {actionedEntities.map(entity => ( @@ -41,33 +46,33 @@ function FileTreeModalDelete() { ))}
    {error && ( -
    - {t('generic_something_went_wrong')} -
    + )} -
    + - + {inFlight ? ( - + ) : ( <> - - + )} - -
    + + ) } diff --git a/services/web/frontend/js/features/file-tree/components/modals/file-tree-modal-error.jsx b/services/web/frontend/js/features/file-tree/components/modals/file-tree-modal-error.jsx index 248c371447..5215f6960a 100644 --- a/services/web/frontend/js/features/file-tree/components/modals/file-tree-modal-error.jsx +++ b/services/web/frontend/js/features/file-tree/components/modals/file-tree-modal-error.jsx @@ -1,4 +1,3 @@ -import { Button, Modal } from 'react-bootstrap' import { Trans, useTranslation } from 'react-i18next' import { useFileTreeActionable } from '../../contexts/file-tree-actionable' @@ -9,6 +8,13 @@ import { DuplicateFilenameError, DuplicateFilenameMoveError, } from '../../errors' +import OLModal, { + OLModalBody, + OLModalFooter, + OLModalHeader, + OLModalTitle, +} from '@/features/ui/components/ol/ol-modal' +import OLButton from '@/features/ui/components/ol/ol-button' function FileTreeModalError() { const { t } = useTranslation() @@ -60,23 +66,23 @@ function FileTreeModalError() { } return ( - - - {errorTitle()} - + + + {errorTitle()} + - +
    {errorMessage()}
    -
    + - - - -
    + + + ) } diff --git a/services/web/frontend/js/features/file-tree/util/icon-type-from-name.js b/services/web/frontend/js/features/file-tree/util/icon-type-from-name.js index 7482ea03bb..05f8ed0e1f 100644 --- a/services/web/frontend/js/features/file-tree/util/icon-type-from-name.js +++ b/services/web/frontend/js/features/file-tree/util/icon-type-from-name.js @@ -1,3 +1,5 @@ +import { isBootstrap5 } from '@/features/utils/bootstrap-5' + export default function iconTypeFromName(name) { let ext = name.split('.').pop() ext = ext ? ext.toLowerCase() : ext @@ -5,12 +7,12 @@ export default function iconTypeFromName(name) { if (['png', 'pdf', 'jpg', 'jpeg', 'gif'].includes(ext)) { return 'image' } else if (['csv', 'xls', 'xlsx'].includes(ext)) { - return 'table' + return isBootstrap5() ? 'table_chart' : 'table' } else if (['py', 'r'].includes(ext)) { - return 'file-text' + return isBootstrap5() ? 'code' : 'file-text' } else if (['bib'].includes(ext)) { - return 'book' + return isBootstrap5() ? 'menu_book' : 'book' } else { - return 'file' + return isBootstrap5() ? 'description' : 'file' } } diff --git a/services/web/frontend/js/features/project-list/components/sidebar/tags-list.tsx b/services/web/frontend/js/features/project-list/components/sidebar/tags-list.tsx index bce043a7e5..5beca4895b 100644 --- a/services/web/frontend/js/features/project-list/components/sidebar/tags-list.tsx +++ b/services/web/frontend/js/features/project-list/components/sidebar/tags-list.tsx @@ -82,7 +82,7 @@ export default function TagsList() { - + - + {subdomainLang && Object.entries(subdomainLang).map(([subdomain, subdomainDetails]) => { if ( diff --git a/services/web/frontend/js/features/ui/components/types/dropdown-menu-props.ts b/services/web/frontend/js/features/ui/components/types/dropdown-menu-props.ts index 994886f32e..4e67b22dd4 100644 --- a/services/web/frontend/js/features/ui/components/types/dropdown-menu-props.ts +++ b/services/web/frontend/js/features/ui/components/types/dropdown-menu-props.ts @@ -18,6 +18,8 @@ export type DropdownProps = { show?: boolean autoClose?: boolean | 'inside' | 'outside' drop?: 'up' | 'up-centered' | 'start' | 'end' | 'down' | 'down-centered' + focusFirstItemOnShow?: false | true | 'keyboard' + onKeyDown?: (event: React.KeyboardEvent) => void } export type DropdownItemProps = PropsWithChildren<{ @@ -54,6 +56,7 @@ export type DropdownMenuProps = PropsWithChildren<{ show?: boolean className?: string flip?: boolean + id?: string }> export type DropdownDividerProps = PropsWithChildren<{ diff --git a/services/web/frontend/js/shared/components/loading-spinner.tsx b/services/web/frontend/js/shared/components/loading-spinner.tsx index 3c1fe2d755..fab856d248 100644 --- a/services/web/frontend/js/shared/components/loading-spinner.tsx +++ b/services/web/frontend/js/shared/components/loading-spinner.tsx @@ -1,6 +1,8 @@ import { useTranslation } from 'react-i18next' import Icon from './icon' import { useEffect, useState } from 'react' +import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' +import { Spinner } from 'react-bootstrap-5' function LoadingSpinner({ delay = 0, @@ -28,11 +30,27 @@ function LoadingSpinner({ } return ( -
    - -   - {loadingText || t('loading')}… -
    + + +   + {loadingText || t('loading')}… + + } + bs5={ +
    +
    + } + /> ) } diff --git a/services/web/frontend/js/shared/components/material-icon.tsx b/services/web/frontend/js/shared/components/material-icon.tsx index c7acac7a14..414eac1a2b 100644 --- a/services/web/frontend/js/shared/components/material-icon.tsx +++ b/services/web/frontend/js/shared/components/material-icon.tsx @@ -5,15 +5,17 @@ import { bsVersion } from '@/features/utils/bootstrap-5' type IconProps = React.ComponentProps<'i'> & { type: string accessibilityLabel?: string + modifier?: string } function MaterialIcon({ type, className, accessibilityLabel, + modifier, ...rest }: IconProps) { - const iconClassName = classNames('material-symbols', className) + const iconClassName = classNames('material-symbols', className, modifier) return ( <> diff --git a/services/web/frontend/stylesheets/app/editor/file-tree.less b/services/web/frontend/stylesheets/app/editor/file-tree.less index 4f808b9e01..1d5d1a8b3c 100644 --- a/services/web/frontend/stylesheets/app/editor/file-tree.less +++ b/services/web/frontend/stylesheets/app/editor/file-tree.less @@ -422,7 +422,7 @@ } } } -.modal-new-file--list { +.modal-new-file-list { background-color: @modal-footer-background-color; width: 220px; ul { @@ -473,24 +473,24 @@ } .file-tree-modal-alert { - margin-top: 10px; - margin-bottom: 0px; + margin-top: 12.5px; } .btn.modal-new-file-mode { text-align: left; + width: 100%; } -.modal-new-file--body { +.modal-new-file-body { padding: 20px; padding-top: (@line-height-computed / 4); } -.modal-new-file--body-upload { +.modal-new-file-body-upload { padding-top: 20px; } -.modal-new-file--body-conflict { +.modal-new-file-body-conflict { background-color: @red-10; border: 1px dashed @red-50; min-height: 400px; @@ -512,11 +512,11 @@ } } -.modal-new-file--body-upload .uppy-Root { +.modal-new-file-body-upload .uppy-Root { font-family: inherit; } -.modal-new-file--body-upload .uppy-Dashboard { +.modal-new-file-body-upload .uppy-Dashboard { .uppy-Dashboard-inner { border: none; } diff --git a/services/web/frontend/stylesheets/app/editor/ide-react.less b/services/web/frontend/stylesheets/app/editor/ide-react.less index c2fd23a53b..f6bcfd626d 100644 --- a/services/web/frontend/stylesheets/app/editor/ide-react.less +++ b/services/web/frontend/stylesheets/app/editor/ide-react.less @@ -27,7 +27,6 @@ } .ide-react-main { - //migrated to SCSS height: 100%; display: flex; flex-direction: column; @@ -40,7 +39,6 @@ } .ide-react-body { - //migrated to SCSS flex-grow: 1; background-color: @pdf-bg; overflow-y: hidden; @@ -127,7 +125,6 @@ } .ide-react-editor-sidebar { - //migrated to SCSS height: 100%; background-color: @file-tree-bg; color: var(--neutral-20); @@ -140,7 +137,6 @@ } .ide-react-file-tree-panel { - //migrated to SCSS display: flex; flex-direction: column; @@ -162,6 +158,7 @@ } // Styles for placeholder elements that will eventually be replaced +// Unused, not migrated to SCSS .ide-react-placeholder-chat { background-color: var(--editor-toolbar-bg); color: var(--neutral-20); diff --git a/services/web/frontend/stylesheets/bootstrap-5/abstracts/mixins.scss b/services/web/frontend/stylesheets/bootstrap-5/abstracts/mixins.scss index 3a56cd202a..2110f6541a 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/abstracts/mixins.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/abstracts/mixins.scss @@ -74,19 +74,6 @@ background-color: var(--bg-dark-secondary); } -// Filetree -@mixin file-tree-item-color { - color: var(--content-primary-dark); -} - -@mixin file-tree-item-hover-bg { - background-color: var(--bg-dark-secondary); -} - -@mixin file-tree-bg { - background-color: var(--bg-dark-tertiary); -} - @mixin theme($name) { @if index($themes, $name) { [data-theme='#{$name}'] { diff --git a/services/web/frontend/stylesheets/bootstrap-5/base/layout.scss b/services/web/frontend/stylesheets/bootstrap-5/base/layout.scss index 268cb8cff4..a0756cdf8f 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/base/layout.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/base/layout.scss @@ -54,3 +54,7 @@ hr { .row-spaced-extra-large { margin-top: calc(var(--line-height-03) * 4); } + +.rotate-180 { + transform: rotate(180deg); +} diff --git a/services/web/frontend/stylesheets/bootstrap-5/components/button.scss b/services/web/frontend/stylesheets/bootstrap-5/components/button.scss index af7f274dd9..7f61bb9a0a 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/components/button.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/components/button.scss @@ -104,6 +104,7 @@ text-decoration: underline; font-size: inherit; vertical-align: inherit; + border-radius: 0; &, &:active, diff --git a/services/web/frontend/stylesheets/bootstrap-5/components/dropdown-menu.scss b/services/web/frontend/stylesheets/bootstrap-5/components/dropdown-menu.scss index 1b37e6ed3a..f07165a1db 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/components/dropdown-menu.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/components/dropdown-menu.scss @@ -14,7 +14,7 @@ min-width: 240px; - &.sm { + &.dropdown-menu-sm-width { min-width: 160px; } diff --git a/services/web/frontend/stylesheets/bootstrap-5/pages/editor/file-tree.scss b/services/web/frontend/stylesheets/bootstrap-5/pages/editor/file-tree.scss index 98b42bd95f..7a06a8c927 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/pages/editor/file-tree.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/pages/editor/file-tree.scss @@ -1,3 +1,19 @@ +:root { + --file-tree-item-hover-bg: var(--bg-dark-secondary); + --file-tree-item-selected-bg: var(--bg-accent-01); + --file-tree-item-color: var(--content-primary-dark); + --file-tree-bg: var(--bg-dark-tertiary); + --file-tree-item-selected-color: var(--content-primary-dark); + --file-tree-line-height: 2.05; +} + +@include theme('light') { + --file-tree-item-hover-bg: var(--bg-light-tertiary); + --file-tree-item-color: var(--content-secondary); + --file-tree-bg: var(--bg-light-primary); + --file-tree-item-selected-color: var(--bg-light-primary); +} + .ide-react-file-tree-panel { display: flex; flex-direction: column; @@ -7,3 +23,590 @@ width: 100%; } } + +.context-menu { + position: fixed; + z-index: 100; +} + +.editor-sidebar { + background-color: var(--file-tree-bg); + display: flex; + flex-direction: column; +} + +@mixin fake-full-width-bg($bg-color) { + &::before { + content: '\00a0'; + position: absolute; + width: 9999px; + left: -9999px; + background-color: $bg-color; + } +} + +.file-tree { + display: flex !important; // To work around jQuery layout's inline styles + flex-direction: column; + height: 100%; + + .toolbar.toolbar-filetree { + @include toolbar-sm-height; + @include toolbar-alt-bg; + + padding: 0 var(--spacing-03); + flex-shrink: 0; + } + + > file-tree-root, + .file-tree-inner { + position: relative; + display: flex; + flex-direction: column; + flex-grow: 1; + overflow-y: auto; + width: inherit; + height: inherit; + + &.no-toolbar { + top: 0; + } + } + + // TODO; Consolidate with "Project files" in Overleaf + h3 { + font-size: 1rem; + border-bottom: 1px solid var(--border-primary); + padding-bottom: var(--spacing-02); + margin: var(--spacing-05); + } + + &-history { + .entity-name { + padding-left: var(--spacing-03); + + &.deleted { + text-decoration: line-through; + } + } + + .loading { + padding-left: var(--spacing-03); + color: var(--content-primary-dark); + + .material-symbols { + color: var(--content-primary-dark); + } + } + } + + ul.file-tree-list { + margin: 0; + overflow: hidden auto; + height: 100%; + flex-grow: 1; + position: relative; + + .entity > ul, + ul[role='tree'] { + margin-left: var(--spacing-08); + } + + &::after { + content: ''; + display: block; + min-height: var(--spacing-08); + } + + li { + line-height: var(--file-tree-line-height); + position: relative; + + &:focus { + outline: none; + } + + .entity { + user-select: none; + + &:focus { + outline: none; + } + } + + .entity > .entity-name > button { + background-color: transparent; + border: 0; + padding: 0; + + &:focus { + outline: none; + } + + &.item-name-button { + color: inherit; + width: 100%; + overflow: hidden; + text-overflow: ellipsis; + text-align: left; + padding-right: var(--spacing-09); + white-space: pre; + } + } + + .entity-name { + color: var(--file-tree-item-color); + cursor: pointer; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + &.entity-name-react { + text-overflow: clip; + } + + &:focus { + outline: none; + } + + background-color: transparent; + + @include fake-full-width-bg(transparent); + + &:hover { + background-color: var(--file-tree-item-hover-bg); + + // When the entity is a subfolder, the DOM element is "indented" via margin-left. This makes the + // element not fill the entire file-tree width (as it's spaced from the left-hand side via margin) + // and, in consequence, the background gets clipped. The ::before pseudo-selector is used to fill + // the empty space. + @include fake-full-width-bg(var(--file-tree-item-hover-bg)); + } + + input { + line-height: 1.6; + } + + .entity-menu-toggle > .material-symbols { + color: var(--content-primary-dark); + vertical-align: middle; + } + } + + .material-symbols { + color: var(--content-disabled); + + &.file-tree-icon { + margin-right: var(--spacing-02); + margin-left: var(--spacing-04); + vertical-align: sub; + + &.linked-file-icon { + position: relative; + left: -2px; + + + .linked-file-highlight { + color: inherit; + position: relative; + top: 4px; + width: 0; + left: -5px; + font-size: 12px; + } + } + } + + &.file-tree-folder-icon { + margin-right: var(--spacing-02); + vertical-align: sub; + } + + &.file-tree-expand-icon { + margin-left: var(--spacing-04); + vertical-align: sub; + } + } + + .material-symbols.folder-open, + .material-symbols.fa-folder { + color: var(--content-disabled); + } + + .material-symbols.toggle { + width: 24px; + padding: var(--spacing-03); + font-size: var(--font-size-03); + color: var(--content-disabled); + } + + .file-tree-dropdown-toggle { + display: flex; + align-items: center; + color: var(--content-primary-dark); + line-height: 1.6; + font-size: var(--font-size-05); + padding: 0 var(--font-size-02) 0 var(--font-size-04); + + &:hover, + &:focus { + text-decoration: none; + } + + &::before { + content: '\00B7\00B7\00B7'; + transform: rotate(90deg); + letter-spacing: 0.5px; + } + } + + &.multi-selected { + > .entity { + > .entity-name { + > div > .material-symbols, + > button > .material-symbols, + > .material-symbols, + .entity-menu-toggle .material-symbols { + color: var(--content-primary-dark); + } + + > .material-symbols.linked-file-highlight { + color: var(--bg-info-01); + } + + @include fake-full-width-bg(var(--bg-info-01)); + + color: var(--content-primary-dark); + font-weight: bold; + background-color: var(--bg-info-01); + + &:hover { + background-color: var(--bg-info-02); + + @include fake-full-width-bg(var(--bg-info-02)); + } + } + } + } + + .menu-button { + position: absolute; + right: 0; + top: 3px; + } + + .rename-input { + display: block; + position: absolute; + top: 1px; + left: 32px; + right: 32px; + color: var(--content-primary); + + input { + width: 100%; + } + } + + > .entity > .entity-name { + .entity-menu-toggle { + display: none; + } + } + + .entity-limit-hit { + line-height: var(--file-tree-line-height); + color: var(--file-tree-item-color); + margin-left: var(--spacing-05); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .entity-limit-hit-message { + font-style: italic; + } + + .material-symbols .entity-limit-hit-tooltip-trigger { + margin-left: var(spacing-03); + cursor: pointer; + } + } + } + + &:not(.multi-selected) { + ul.file-tree-list li.selected { + > .entity { + > .entity-name { + background-color: var(--file-tree-item-selected-bg); + color: var(--file-tree-item-selected-color); + + > div > .material-symbols, + > button > .material-symbols, + > .material-symbols, + .entity-menu-toggle .material-symbols { + color: var(--file-tree-item-selected-color); + } + + > .material-symbols.linked-file-highlight { + color: var(--bg-info-01); + } + + @include fake-full-width-bg(var(--file-tree-item-selected-bg)); + + font-weight: bold; + padding-right: var(--spacing-09); + + .entity-menu-toggle { + display: inline-block; + background-color: transparent; + box-shadow: none; + border: 0; + padding-right: var(--spacing-02); + padding-left: var(--spacing-02); + } + } + } + } + } + + // while dragging, the previously selected item gets no highlight + ul.file-tree-list.file-tree-dragging li.selected .entity .entity-name { + @include fake-full-width-bg(transparent); + + font-weight: normal; + background-color: transparent; + color: var(--file-tree-item-color); + + .material-symbols { + color: var(--content-disabled) !important; + } + } + + // the items being dragged get the full "hover" colour + ul.file-tree-list.file-tree-dragging + li + .entity.file-tree-entity-dragging + .entity-name { + background-color: fade(var(--file-tree-item-hover-bg), 90%); + + @include fake-full-width-bg(fade(var(--file-tree-item-hover-bg), 90%)); + + color: var(--file-tree-item-color); + + .material-symbols { + color: var(--content-disabled) !important; + } + } + + // the drop target gets the "selected" colour + ul.file-tree-list.file-tree-dragging + li.dnd-droppable-hover + .entity + .entity-name { + background-color: var(--file-tree-item-selected-bg); + + @include fake-full-width-bg(var(--file-tree-item-selected-bg)); + + color: var(--file-tree-item-selected-color); + + .material-symbols { + color: var(--file-tree-item-selected-color) !important; + } + } + + .dnd-draggable-preview-layer { + position: absolute; + pointer-events: none; + z-index: 100; + width: 100%; + height: 100%; + overflow: hidden; + + &.dnd-droppable-hover { + border: 3px solid var(--file-tree-item-selected-bg); + } + } + + .dnd-draggable-preview-item { + color: var(--file-tree-item-selected-color); + background-color: fade(var(--file-tree-item-selected-bg), 60%); + width: 75%; + padding-left: var(--spacing-08); + line-height: 2.05; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .disconnected-overlay { + background-color: var(--file-tree-bg); + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 10; + opacity: 0.5; + cursor: wait; + } +} + +.modal-new-file { + padding: 0; + + table { + width: 100%; + table-layout: fixed; + + td { + vertical-align: top; + } + } + + .toggle-file-type-button { + font-size: 80%; + margin-top: calc(var(--spacing-05) * -1); + + .btn { + display: inline-block; + padding: 0; + vertical-align: baseline; + font-size: inherit; + } + + .btn:focus-within { + outline: none; + text-decoration: none; + } + } +} + +.modal-new-file-list { + background-color: var(--bg-light-secondary); + width: 220px; + + ul { + li { + /* old modal (a) */ + a { + color: var(--content-secondary); + padding: var(--spacing-03); + display: block; + text-decoration: none; + } + + /* new modal (button) */ + .btn { + color: var(--content-secondary); + padding: var(--spacing-03); + } + + .btn:hover { + text-decoration: none; + } + + .btn:focus { + outline: none; + text-decoration: none; + background-color: white; + } + } + + li.active { + background-color: white; + + /* old modal (a) */ + a { + color: var(--link-ui); + } + + /* new modal (button) */ + .btn { + color: var(--link-ui); + text-decoration: none; + } + } + + li:hover { + background-color: white; + } + } +} + +.file-tree-error { + text-align: center; + color: var(--content-secondary-dark); + padding: 20px; +} + +.file-tree-modal-alert { + margin-top: var(--spacing-06); +} + +.btn.modal-new-file-mode { + justify-content: left; + text-align: left; + text-decoration: none; + width: 100%; +} + +.modal-new-file-body { + padding: 20px; + padding-top: var(--spacing-03); +} + +.modal-new-file-body-upload { + padding-top: 20px; +} + +.modal-new-file-body-conflict { + background-color: var(--bg-danger-03); + border: 1px dashed var(--border-danger); + min-height: 400px; + border-radius: 3px; + color: var(--content-primary); + display: flex; + flex-direction: column; + justify-content: center; + padding: var(--spacing-05); +} + +.modal-footer { + .approaching-file-limit { + font-weight: bold; + } + + .at-file-limit { + text-align: left; + } +} + +/* stylelint-disable selector-class-pattern */ +.modal-new-file-body-upload .uppy-Root { + font-family: inherit; +} + +.modal-new-file-body-upload .uppy-Dashboard { + .uppy-Dashboard-inner { + border: none; + } + + .uppy-Dashboard-dropFilesHereHint { + inset: 0; + } + + .uppy-Dashboard-AddFiles { + margin: 0; + border: 1px dashed var(--border-primary); + height: 100%; + + .uppy-Dashboard-AddFiles-title { + font-size: inherit; + } + } + + .uppy-Dashboard-AddFiles-title { + width: 26em; // sized to create a wrap between the sentences + max-width: 100%; + } +} diff --git a/services/web/frontend/stylesheets/bootstrap-5/pages/editor/ide.scss b/services/web/frontend/stylesheets/bootstrap-5/pages/editor/ide.scss index be6c284cf6..16801b9292 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/pages/editor/ide.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/pages/editor/ide.scss @@ -27,8 +27,7 @@ } .ide-react-editor-sidebar { - @include file-tree-bg; - + background-color: var(--file-tree-bg); height: 100%; color: var(--content-secondary-dark); } @@ -51,3 +50,141 @@ color: var(--neutral-20); } } + +.ide-react-symbol-palette { + height: 100%; + background-color: var(--bg-dark-tertiary); + color: var(--neutral-20); +} + +.ide-react-file-tree-panel { + display: flex; + flex-direction: column; + + // Prevent the file tree expanding beyond the boundary of the panel + .file-tree { + width: 100%; + } +} + +.ide-react-editor-panel { + display: flex; + flex-direction: column; +} + +// Ensure an element with class "full-size", such as the binary file view, stays within the bounds of the panel +.ide-react-panel { + position: relative; + container-type: size; +} + +.ide-panel-group-resizing { + background-color: white; + + // Hide panel contents while resizing + .ide-react-editor-content, + .pdf { + display: none !important; + } +} + +.horizontal-resize-handle { + width: 7px !important; + height: 100%; + + // Enable ::before and ::after pseudo-elements to position themselves correctly + position: relative; + background-color: var(--bg-dark-secondary); + + .custom-toggler { + padding: 0; + border-width: 0; + } + + &.horizontal-resize-handle-enabled { + &::before, + &::after { + // This SVG has the colour hard-coded to the current value of @ol-blue-gray-2, so if we changed @ol-blue-gray-2, + // we'd have to change this SVG too + content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='7' height='18' viewBox='0 0 7 18'%3E%3Cpath d='M2 0h3v3H2zM2 5h3v3H2zM2 10h3v3H2zM2 15h3v3H2z' style='fill:%239da7b7'/%3E%3C/svg%3E"); + display: block; + position: absolute; + text-align: center; + left: 0; + width: 7px; + height: 18px; + } + + &::before { + top: 25%; + } + + &::after { + top: 75%; + } + } + + &:not(.horizontal-resize-handle-enabled) { + cursor: default; + } + + .synctex-controls { + left: -8px; + margin: 0; + + // Ensure that SyncTex controls appear in front of PDF viewer controls and logs pane + z-index: 12; + } +} + +.vertical-resize-handle { + height: 6px; + background-color: var(--bg-dark-secondary); + + &.vertical-resize-handle-enabled { + &:hover { + background-color: var(--bg-dark-primary); + } + } + + &:not(.vertical-resize-handle-enabled) { + opacity: 0.5; + cursor: default; + } + + &::after { + // This SVG has the colour hard-coded to the current value of @ol-blue-gray-2, so if we changed @ol-blue-gray-2, + // we'd have to change this SVG too + content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='6' viewBox='0 0 18 6'%3E%3Cpath d='M0 1.5h3v3H0zM5 1.5h3v3H5zM10 1.5h3v3h-3zM15 1.5h3v3h-3z' style='fill:%239da7b7'/%3E%3C/svg%3E"); + display: block; + text-align: center; + line-height: 0; + } +} + +.vertical-resizable-resizer { + background-color: var(--bg-dark-secondary); + + &:hover { + background-color: var(--bg-dark-primary); + } + + &::after { + @include heading-sm; + + content: '\00b7\00b7\00b7\00b7'; + display: block; + color: var(--content-disabled); + text-align: center; + pointer-events: none; + } +} + +.vertical-resizable-resizer-disabled { + pointer-events: none; + opacity: 0.5; + + &::after { + opacity: 0.5; + } +} diff --git a/services/web/frontend/stylesheets/bootstrap-5/pages/editor/outline.scss b/services/web/frontend/stylesheets/bootstrap-5/pages/editor/outline.scss index 4ea50107d0..b03e87b0de 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/pages/editor/outline.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/pages/editor/outline.scss @@ -51,8 +51,7 @@ } .outline-header-expand-collapse-btn { - @include file-tree-item-color; - + color: var(--file-tree-item-color); display: flex; align-items: center; background-color: transparent; @@ -81,8 +80,8 @@ .outline-header-name { @include body-sm; - @include file-tree-item-color; + color: var(--file-tree-item-color); display: inline-block; font-weight: 700; margin: 0; @@ -93,15 +92,13 @@ } .outline-body { - @include file-tree-bg; - + background-color: var(--file-tree-bg); overflow-y: auto; padding-right: var(--spacing-03); } .outline-body-no-elements { - @include file-tree-item-color; - + color: var(--file-tree-item-color); text-align: center; padding: var(--spacing-08) var(--spacing-08) var(--spacing-11) var(--spacing-08); @@ -109,15 +106,13 @@ } .outline-body-link { - @include file-tree-item-color; - + color: var(--file-tree-item-color); display: block; text-decoration: underline; &:hover, &:focus { - @include file-tree-item-color; - + color: var(--file-tree-item-color); text-decoration: underline; } } @@ -157,8 +152,7 @@ } .outline-item-expand-collapse-btn { - @include file-tree-bg; - + background-color: var(--file-tree-bg); display: inline; border: 0; padding: 0; @@ -176,14 +170,14 @@ } &:hover { - @include file-tree-item-hover-bg; + background-color: var(--file-tree-item-hover-bg); } } .outline-item-link { - @include file-tree-item-color; @include text-truncate; + color: var(--file-tree-item-color); display: inline; background-color: transparent; border: 0; @@ -196,8 +190,7 @@ &:hover, &:focus { - @include file-tree-item-hover-bg; - + background-color: var(--file-tree-item-hover-bg); outline: 0; } } diff --git a/services/web/locales/en.json b/services/web/locales/en.json index 9a37accd9b..fb8a2aa340 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -1375,6 +1375,7 @@ "only_group_admin_or_managers_can_delete_your_account_5": "For more information, see the \"Managed Accounts\" section in our terms of use, which you agree to by clicking Accept invitation", "only_importer_can_refresh": "Only the person who originally imported this __provider__ file can refresh it.", "open_a_file_on_the_left": "Open a file on the left", + "open_action_menu": "Open __name__ action menu", "open_advanced_reference_search": "Open advanced reference search", "open_as_template": "Open as Template", "open_file": "Edit file", diff --git a/services/web/test/frontend/features/file-tree/components/file-tree-item/file-tree-item-inner.spec.tsx b/services/web/test/frontend/features/file-tree/components/file-tree-item/file-tree-item-inner.spec.tsx index 85a6708b57..411a8b1aff 100644 --- a/services/web/test/frontend/features/file-tree/components/file-tree-item/file-tree-item-inner.spec.tsx +++ b/services/web/test/frontend/features/file-tree/components/file-tree-item/file-tree-item-inner.spec.tsx @@ -116,7 +116,7 @@ describe('', function () { ) - cy.findByRole('button', { name: 'Menu' }).click() + cy.findByRole('button', { name: 'Open bar.tex action menu' }).click() cy.findByRole('menuitem', { name: 'Rename' }).click() cy.findByRole('button', { name: 'bar.tex' }).should('not.exist') cy.findByRole('textbox') diff --git a/services/web/test/frontend/features/file-tree/components/file-tree-root.spec.tsx b/services/web/test/frontend/features/file-tree/components/file-tree-root.spec.tsx index d997e3abe3..b26dca9a49 100644 --- a/services/web/test/frontend/features/file-tree/components/file-tree-root.spec.tsx +++ b/services/web/test/frontend/features/file-tree/components/file-tree-root.spec.tsx @@ -90,7 +90,7 @@ describe('', function () { ctrlKey: true, cmdKey: true, }) - cy.findByRole('button', { name: 'Menu' }).click() + cy.findByRole('button', { name: 'Open main.tex action menu' }).click() cy.findByRole('menuitem', { name: 'Delete' }).click() cy.findByRole('button', { name: 'Cancel' }) }) @@ -256,7 +256,10 @@ describe('', function () { cy.findByRole('treeitem', { name: 'other.tex', selected: false }) // single item selected: menu button is visible - cy.findAllByRole('button', { name: 'Menu' }).should('have.length', 1) + cy.findAllByRole('button', { name: 'Open main.tex action menu' }).should( + 'have.length', + 1 + ) // select the other item cy.findByRole('treeitem', { name: 'other.tex' }).click() @@ -265,7 +268,10 @@ describe('', function () { cy.findByRole('treeitem', { name: 'other.tex', selected: true }) // single item selected: menu button is visible - cy.findAllByRole('button', { name: 'Menu' }).should('have.length', 1) + cy.findAllByRole('button', { name: 'Open other.tex action menu' }).should( + 'have.length', + 1 + ) // multi-select the main item cy.findByRole('treeitem', { name: 'main.tex' }).click({ @@ -277,7 +283,10 @@ describe('', function () { cy.findByRole('treeitem', { name: 'other.tex', selected: true }) // multiple items selected: no menu button is visible - cy.findAllByRole('button', { name: 'Menu' }).should('have.length', 0) + cy.findAllByRole('button', { name: 'Open main.tex action menu' }).should( + 'have.length', + 0 + ) }) describe('when deselecting files', function () { diff --git a/services/web/test/frontend/features/file-tree/flows/delete-entity.spec.tsx b/services/web/test/frontend/features/file-tree/flows/delete-entity.spec.tsx index ebf26a5aee..71ae26ab50 100644 --- a/services/web/test/frontend/features/file-tree/flows/delete-entity.spec.tsx +++ b/services/web/test/frontend/features/file-tree/flows/delete-entity.spec.tsx @@ -47,7 +47,7 @@ describe('FileTree Delete Entity Flow', function () { ) cy.findByRole('treeitem', { name: 'main.tex' }).click() - cy.findByRole('button', { name: 'Menu' }).click() + cy.findByRole('button', { name: 'Open main.tex action menu' }).click() cy.findByRole('menuitem', { name: 'Delete' }).click() }) @@ -195,7 +195,7 @@ describe('FileTree Delete Entity Flow', function () { // as a proxy to check that the child entity has been unselect we start // a delete and ensure the modal is displayed (the cancel button can be // selected) This is needed to make sure the test fail. - cy.findByRole('button', { name: 'Menu' }).click() + cy.findByRole('button', { name: 'Open main.tex action menu' }).click() cy.findByRole('menuitem', { name: 'Delete' }).click() cy.findByRole('button', { name: 'Cancel' }) }) diff --git a/services/web/test/frontend/features/file-tree/flows/rename-entity.spec.tsx b/services/web/test/frontend/features/file-tree/flows/rename-entity.spec.tsx index f69a669e4e..0da8f8bcb7 100644 --- a/services/web/test/frontend/features/file-tree/flows/rename-entity.spec.tsx +++ b/services/web/test/frontend/features/file-tree/flows/rename-entity.spec.tsx @@ -157,7 +157,7 @@ describe('FileTree Rename Entity Flow', function () { function renameItem(from: string, to: string) { cy.findByRole('treeitem', { name: from }).click() - cy.findByRole('button', { name: 'Menu' }).click() + cy.findByRole('button', { name: `Open ${from} action menu` }).click() cy.findByRole('menuitem', { name: 'Rename' }).click() cy.findByRole('textbox').clear() cy.findByRole('textbox').type(to + '{enter}')