mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #20298 from overleaf/rd-ide-filetree
[web] Migrate the file tree on the editor page to Bootstrap 5 GitOrigin-RevId: e2efec26242c8cdab37a54bc182b83bfb0f1eb3c
This commit is contained in:
parent
6f8a9a0f81
commit
abb59e4603
40 changed files with 1297 additions and 326 deletions
|
@ -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": "",
|
||||
|
|
|
@ -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<Dropdown>) {
|
||||
function handleKeyDown(event: React.KeyboardEvent<BS3Dropdown | Element>) {
|
||||
if (event.key === 'Tab') {
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
<Dropdown
|
||||
onClick={handleClick}
|
||||
open
|
||||
id="dropdown-file-tree-context-menu"
|
||||
onToggle={handleToggle}
|
||||
dropup={
|
||||
document.body.offsetHeight / contextMenuCoords.top < 2 &&
|
||||
document.body.offsetHeight - contextMenuCoords.top < 250
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={
|
||||
<BS3Dropdown
|
||||
onClick={handleClick}
|
||||
open
|
||||
id="dropdown-file-tree-context-menu"
|
||||
onToggle={handleToggle}
|
||||
dropup={
|
||||
document.body.offsetHeight / contextMenuCoords.top < 2 &&
|
||||
document.body.offsetHeight - contextMenuCoords.top < 250
|
||||
}
|
||||
className="context-menu"
|
||||
style={contextMenuCoords}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<FakeDropDownToggle bsRole="toggle" />
|
||||
<BS3Dropdown.Menu tabIndex={-1}>
|
||||
<FileTreeItemMenuItems />
|
||||
</BS3Dropdown.Menu>
|
||||
</BS3Dropdown>
|
||||
}
|
||||
className="context-menu"
|
||||
style={contextMenuCoords}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<FakeDropDownToggle bsRole="toggle" />
|
||||
<Dropdown.Menu tabIndex={-1}>
|
||||
<FileTreeItemMenuItems />
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>,
|
||||
bs5={
|
||||
<div style={contextMenuCoords} className="context-menu">
|
||||
<Dropdown
|
||||
show
|
||||
drop={
|
||||
document.body.offsetHeight / contextMenuCoords.top < 2 &&
|
||||
document.body.offsetHeight - contextMenuCoords.top < 250
|
||||
? 'up'
|
||||
: 'down'
|
||||
}
|
||||
focusFirstItemOnShow // A11y - Focus the first item in the context menu when it opens since the menu is rendered at the root level
|
||||
onKeyDown={handleKeyDown}
|
||||
onToggle={handleToggle}
|
||||
>
|
||||
<DropdownMenu
|
||||
className="dropdown-menu-sm-width"
|
||||
id="dropdown-file-tree-context-menu"
|
||||
>
|
||||
<FileTreeItemMenuItems />
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
}
|
||||
/>,
|
||||
document.body
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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 <Alert bsStyle="danger">{children}</Alert>
|
||||
return <OLNotification type="error" content={children} />
|
||||
}
|
||||
DangerMessage.propTypes = {
|
||||
children: PropTypes.any.isRequired,
|
||||
|
|
|
@ -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 (
|
||||
<FormGroup controlId="new-doc-name" className={classes.formGroup}>
|
||||
<ControlLabel>{label || t('file_name')}</ControlLabel>
|
||||
<OLFormGroup controlId="new-doc-name" className={classes.formGroup}>
|
||||
<OLFormLabel>{label || t('file_name')}</OLFormLabel>
|
||||
|
||||
<FormControl
|
||||
<OLFormControl
|
||||
type="text"
|
||||
placeholder={placeholder || t('file_name')}
|
||||
required
|
||||
value={name}
|
||||
onChange={event => setName(event.target.value)}
|
||||
inputRef={inputRef}
|
||||
ref={inputRef}
|
||||
disabled={inFlight}
|
||||
/>
|
||||
|
||||
<FormControl.Feedback />
|
||||
|
||||
{touchedName && !validName && (
|
||||
<Alert bsStyle="danger" className="row-spaced-small">
|
||||
{t('files_cannot_include_invalid_characters')}
|
||||
</Alert>
|
||||
<OLNotification
|
||||
type="error"
|
||||
className="row-spaced-small"
|
||||
content={t('files_cannot_include_invalid_characters')}
|
||||
/>
|
||||
)}
|
||||
|
||||
{error && <ErrorMessage error={error} />}
|
||||
</FormGroup>
|
||||
</OLFormGroup>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -89,23 +94,29 @@ function ErrorMessage({ error }) {
|
|||
switch (error.constructor) {
|
||||
case DuplicateFilenameError:
|
||||
return (
|
||||
<Alert bsStyle="danger" className="row-spaced-small">
|
||||
{t('file_already_exists')}
|
||||
</Alert>
|
||||
<OLNotification
|
||||
type="error"
|
||||
className="row-spaced-small"
|
||||
content={t('file_already_exists')}
|
||||
/>
|
||||
)
|
||||
|
||||
case InvalidFilenameError:
|
||||
return (
|
||||
<Alert bsStyle="danger" className="row-spaced-small">
|
||||
{t('files_cannot_include_invalid_characters')}
|
||||
</Alert>
|
||||
<OLNotification
|
||||
type="error"
|
||||
className="row-spaced-small"
|
||||
content={t('files_cannot_include_invalid_characters')}
|
||||
/>
|
||||
)
|
||||
|
||||
case BlockedFilenameError:
|
||||
return (
|
||||
<Alert bsStyle="danger" className="row-spaced-small">
|
||||
{t('blocked_filename')}
|
||||
</Alert>
|
||||
<OLNotification
|
||||
type="error"
|
||||
className="row-spaced-small"
|
||||
content={t('blocked_filename')}
|
||||
/>
|
||||
)
|
||||
|
||||
default:
|
||||
|
|
|
@ -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() {
|
|||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="modal-new-file--list">
|
||||
<td className="modal-new-file-list">
|
||||
<ul className="list-unstyled">
|
||||
<FileTreeModalCreateFileMode
|
||||
mode="doc"
|
||||
icon="file"
|
||||
icon={bsVersion({ bs5: 'description', bs3: 'file' })}
|
||||
label={t('new_file')}
|
||||
/>
|
||||
|
||||
|
@ -53,7 +54,7 @@ export default function FileTreeModalCreateFileBody() {
|
|||
hasLinkedProjectOutputFileFeature) && (
|
||||
<FileTreeModalCreateFileMode
|
||||
mode="project"
|
||||
icon="folder-open"
|
||||
icon={bsVersion({ bs5: 'folder_open', bs3: 'folder-open' })}
|
||||
label={t('from_another_project')}
|
||||
/>
|
||||
)}
|
||||
|
@ -75,7 +76,7 @@ export default function FileTreeModalCreateFileBody() {
|
|||
</td>
|
||||
|
||||
<td
|
||||
className={`modal-new-file--body modal-new-file--body-${newFileCreateMode}`}
|
||||
className={`modal-new-file-body modal-new-file-body-${newFileCreateMode}`}
|
||||
>
|
||||
{newFileCreateMode === 'doc' && (
|
||||
<FileTreeCreateNameProvider initialName="name.tex">
|
||||
|
|
|
@ -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' && (
|
||||
<Alert bsStyle="warning" className="at-file-limit">
|
||||
<OLNotification
|
||||
type="error"
|
||||
className="at-file-limit"
|
||||
content={t('project_has_too_many_files')}
|
||||
>
|
||||
{/* TODO: add parameter for fileCount.limit */}
|
||||
{t('project_has_too_many_files')}
|
||||
</Alert>
|
||||
</OLNotification>
|
||||
)}
|
||||
|
||||
<Button
|
||||
bsStyle={null}
|
||||
className="btn-secondary"
|
||||
<OLButton
|
||||
variant="secondary"
|
||||
type="button"
|
||||
disabled={inFlight}
|
||||
onClick={cancel}
|
||||
>
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
</OLButton>
|
||||
|
||||
{newFileCreateMode !== 'upload' && (
|
||||
<Button
|
||||
bsStyle="primary"
|
||||
<OLButton
|
||||
variant="primary"
|
||||
type="submit"
|
||||
form="create-file"
|
||||
disabled={inFlight || !valid}
|
||||
isLoading={inFlight}
|
||||
bs3Props={{ loading: inFlight ? `${t('creating')}…` : t('create') }}
|
||||
>
|
||||
<span>{inFlight ? `${t('creating')}…` : t('create')}</span>
|
||||
</Button>
|
||||
{t('create')}
|
||||
</OLButton>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
|
|
@ -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 (
|
||||
<li className={classnames({ active: newFileCreateMode === mode })}>
|
||||
<Button
|
||||
bsStyle="link"
|
||||
block
|
||||
<OLButton
|
||||
variant="link"
|
||||
onClick={handleClick}
|
||||
className="modal-new-file-mode"
|
||||
>
|
||||
<Icon type={icon} fw />
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<Icon type={icon} fw />}
|
||||
bs5={<MaterialIcon type={icon} />}
|
||||
/>
|
||||
|
||||
{label}
|
||||
</Button>
|
||||
</OLButton>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
<div className="small modal-new-file--body-conflict">
|
||||
<div className="small modal-new-file-body-conflict">
|
||||
{conflicts.length > 0 && (
|
||||
<>
|
||||
<p className="text-center mb-0">{getConflictText(conflicts, t)}</p>
|
||||
|
@ -70,14 +70,14 @@ export function FileUploadConflicts({
|
|||
)}
|
||||
|
||||
<p className="text-center">
|
||||
<Button bsStyle={null} className="btn-secondary" onClick={cancel}>
|
||||
<OLButton variant="secondary" onClick={cancel}>
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
</OLButton>
|
||||
|
||||
{!hasFolderConflict && (
|
||||
<Button bsStyle="danger" onClick={handleOverwrite}>
|
||||
<OLButton variant="danger" onClick={handleOverwrite}>
|
||||
{t('overwrite')}
|
||||
</Button>
|
||||
</OLButton>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
@ -118,7 +118,7 @@ export function FolderUploadConflicts({
|
|||
}, [setError, conflicts, handleOverwrite, projectId])
|
||||
|
||||
return (
|
||||
<div className="small modal-new-file--body-conflict">
|
||||
<div className="small modal-new-file-body-conflict">
|
||||
<p className="text-center mb-0">{getConflictText(conflicts, t)}</p>
|
||||
|
||||
<ul className="text-center list-unstyled row-spaced-small mt-1">
|
||||
|
@ -140,14 +140,14 @@ export function FolderUploadConflicts({
|
|||
)}
|
||||
|
||||
<p className="text-center">
|
||||
<Button bsStyle={null} className="btn-secondary" onClick={cancel}>
|
||||
<OLButton variant="secondary" onClick={cancel}>
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
</OLButton>
|
||||
|
||||
{!hasFileConflict && (
|
||||
<Button bsStyle="danger" onClick={deleteAndRetry}>
|
||||
<OLButton variant="danger" onClick={deleteAndRetry}>
|
||||
{t('overwrite')}
|
||||
</Button>
|
||||
</OLButton>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
@ -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 (
|
||||
<form className="form-controls" id="create-file" onSubmit={handleSubmit}>
|
||||
<OLForm id="create-file" onSubmit={handleSubmit}>
|
||||
<SelectProject
|
||||
selectedProject={selectedProject}
|
||||
setSelectedProject={setSelectedProject}
|
||||
|
@ -148,8 +154,8 @@ export default function FileTreeImportFromProject() {
|
|||
{canSwitchOutputFilesMode && (
|
||||
<div className="toggle-file-type-button">
|
||||
or
|
||||
<Button
|
||||
bsStyle="link"
|
||||
<OLButton
|
||||
variant="link"
|
||||
type="button"
|
||||
onClick={() => setOutputFilesMode(value => !value)}
|
||||
>
|
||||
|
@ -158,7 +164,7 @@ export default function FileTreeImportFromProject() {
|
|||
? t('select_from_source_files')
|
||||
: t('select_from_output_files')}
|
||||
</span>
|
||||
</Button>
|
||||
</OLButton>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
@ -173,7 +179,7 @@ export default function FileTreeImportFromProject() {
|
|||
/>
|
||||
|
||||
{error && <ErrorMessage error={error} />}
|
||||
</form>
|
||||
</OLForm>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -204,18 +210,27 @@ function SelectProject({
|
|||
}
|
||||
|
||||
return (
|
||||
<FormGroup className="form-controls" controlId="project-select">
|
||||
<ControlLabel>{t('select_a_project')}</ControlLabel>
|
||||
<OLFormGroup controlId="project-select">
|
||||
<OLFormLabel>{t('select_a_project')}</OLFormLabel>
|
||||
|
||||
{loading && (
|
||||
<span>
|
||||
|
||||
<Icon type="spinner" spin />
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<Icon type="spinner" spin />}
|
||||
bs5={
|
||||
<Spinner
|
||||
animation="border"
|
||||
aria-hidden="true"
|
||||
size="sm"
|
||||
role="status"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
|
||||
<FormControl
|
||||
componentClass="select"
|
||||
<OLFormSelect
|
||||
disabled={!data}
|
||||
value={selectedProject ? selectedProject._id : ''}
|
||||
onChange={event => {
|
||||
|
@ -234,12 +249,12 @@ function SelectProject({
|
|||
{project.name}
|
||||
</option>
|
||||
))}
|
||||
</FormControl>
|
||||
</OLFormSelect>
|
||||
|
||||
{filteredData && !filteredData.length && (
|
||||
<small>{t('no_other_projects_found')}</small>
|
||||
)}
|
||||
</FormGroup>
|
||||
</OLFormGroup>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -263,25 +278,34 @@ function SelectProjectOutputFile({
|
|||
}
|
||||
|
||||
return (
|
||||
<FormGroup
|
||||
className="form-controls row-spaced-small"
|
||||
<OLFormGroup
|
||||
className="row-spaced-small"
|
||||
controlId="project-output-file-select"
|
||||
>
|
||||
<ControlLabel>{t('select_an_output_file')}</ControlLabel>
|
||||
<OLFormLabel>{t('select_an_output_file')}</OLFormLabel>
|
||||
|
||||
{loading && (
|
||||
<span>
|
||||
|
||||
<Icon type="spinner" spin />
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<Icon type="spinner" spin />}
|
||||
bs5={
|
||||
<Spinner
|
||||
animation="border"
|
||||
aria-hidden="true"
|
||||
size="sm"
|
||||
role="status"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
|
||||
<FormControl
|
||||
componentClass="select"
|
||||
<OLFormSelect
|
||||
disabled={!data}
|
||||
value={selectedProjectOutputFile?.path || ''}
|
||||
onChange={event => {
|
||||
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}
|
||||
</option>
|
||||
))}
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
</OLFormSelect>
|
||||
</OLFormGroup>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -321,21 +345,27 @@ function SelectProjectEntity({
|
|||
}
|
||||
|
||||
return (
|
||||
<FormGroup
|
||||
className="form-controls row-spaced-small"
|
||||
controlId="project-entity-select"
|
||||
>
|
||||
<ControlLabel>{t('select_a_file')}</ControlLabel>
|
||||
<OLFormGroup className="row-spaced-small" controlId="project-entity-select">
|
||||
<OLFormLabel>{t('select_a_file')}</OLFormLabel>
|
||||
|
||||
{loading && (
|
||||
<span>
|
||||
|
||||
<Icon type="spinner" spin />
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<Icon type="spinner" spin />}
|
||||
bs5={
|
||||
<Spinner
|
||||
animation="border"
|
||||
aria-hidden="true"
|
||||
size="sm"
|
||||
role="status"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
|
||||
<FormControl
|
||||
componentClass="select"
|
||||
<OLFormSelect
|
||||
disabled={!data}
|
||||
value={selectedProjectEntity?.path || ''}
|
||||
onChange={event => {
|
||||
|
@ -354,7 +384,7 @@ function SelectProjectEntity({
|
|||
{entity.path.slice(1)}
|
||||
</option>
|
||||
))}
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
</OLFormSelect>
|
||||
</OLFormGroup>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
<FormGroup controlId="import-from-url">
|
||||
<ControlLabel>{t('url_to_fetch_the_file_from')}</ControlLabel>
|
||||
|
||||
<FormControl
|
||||
<OLFormGroup controlId="import-from-url">
|
||||
<OLFormLabel>{t('url_to_fetch_the_file_from')}</OLFormLabel>
|
||||
<OLFormControl
|
||||
type="url"
|
||||
placeholder="https://example.com/my-file.png"
|
||||
required
|
||||
value={url}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</OLFormGroup>
|
||||
|
||||
<FileTreeCreateNameInput
|
||||
label={t('file_name_in_this_project')}
|
||||
|
|
|
@ -7,6 +7,8 @@ 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, id, isFile, isLinkedFile }) {
|
||||
const type = isFile ? 'file' : 'doc'
|
||||
|
@ -46,25 +48,45 @@ FileTreeDoc.propTypes = {
|
|||
export const FileTreeIcon = ({ isLinkedFile, name }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const className = classnames('spaced', 'file-tree-icon', {
|
||||
const className = classnames('file-tree-icon', {
|
||||
'linked-file-icon': isLinkedFile,
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
<Icon type={iconTypeFromName(name)} fw className={className} />
|
||||
{isLinkedFile && (
|
||||
<Icon
|
||||
type="external-link-square"
|
||||
modifier="rotate-180"
|
||||
className="linked-file-highlight"
|
||||
accessibilityLabel={t('linked_file')}
|
||||
/>
|
||||
)}
|
||||
<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')}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
FileTreeIcon.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
isLinkedFile: PropTypes.bool,
|
||||
|
|
|
@ -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() {
|
|||
<div className="file-tree-error">
|
||||
<p>{t('generic_something_went_wrong')}</p>
|
||||
<p>{t('please_refresh')}</p>
|
||||
<Button bsStyle="primary" onClick={handleClick}>
|
||||
<OLButton variant="primary" onClick={handleClick}>
|
||||
{t('refresh')}
|
||||
</Button>
|
||||
</OLButton>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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 = (
|
||||
<>
|
||||
<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"
|
||||
/>
|
||||
</>
|
||||
<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 (
|
||||
|
|
|
@ -90,7 +90,7 @@ function FileTreeItemInner({
|
|||
isSelected={isSelected}
|
||||
setIsDraggable={setIsDraggable}
|
||||
/>
|
||||
{hasMenu ? <FileTreeItemMenu id={id} /> : null}
|
||||
{hasMenu ? <FileTreeItemMenu id={id} name={name} /> : null}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -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 ? (
|
||||
<MenuItem onClick={startRenaming}>{t('rename')}</MenuItem>
|
||||
) : null}
|
||||
{downloadPath ? (
|
||||
<MenuItem
|
||||
href={downloadPath}
|
||||
onClick={downloadWithAnalytics}
|
||||
download={selectedFileName}
|
||||
>
|
||||
{t('download')}
|
||||
</MenuItem>
|
||||
) : null}
|
||||
{canDelete ? (
|
||||
<MenuItem onClick={startDeleting}>{t('delete')}</MenuItem>
|
||||
) : null}
|
||||
{canCreate ? (
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={
|
||||
<>
|
||||
<li role="none" className="divider" />
|
||||
<MenuItem onClick={createWithAnalytics}>{t('new_file')}</MenuItem>
|
||||
<MenuItem onClick={startCreatingFolder}>{t('new_folder')}</MenuItem>
|
||||
<MenuItem onClick={uploadWithAnalytics}>{t('upload')}</MenuItem>
|
||||
{canRename ? (
|
||||
<MenuItem onClick={startRenaming}>{t('rename')}</MenuItem>
|
||||
) : null}
|
||||
{downloadPath ? (
|
||||
<MenuItem
|
||||
href={downloadPath}
|
||||
onClick={downloadWithAnalytics}
|
||||
download={selectedFileName}
|
||||
>
|
||||
{t('download')}
|
||||
</MenuItem>
|
||||
) : null}
|
||||
{canDelete ? (
|
||||
<MenuItem onClick={startDeleting}>{t('delete')}</MenuItem>
|
||||
) : null}
|
||||
{canCreate ? (
|
||||
<>
|
||||
<li role="none" className="divider" />
|
||||
<MenuItem onClick={createWithAnalytics}>{t('new_file')}</MenuItem>
|
||||
<MenuItem onClick={startCreatingFolder}>
|
||||
{t('new_folder')}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={uploadWithAnalytics}>{t('upload')}</MenuItem>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
}
|
||||
bs5={
|
||||
<>
|
||||
{canRename ? (
|
||||
<li role="none">
|
||||
<DropdownItem onClick={startRenaming}>{t('rename')}</DropdownItem>
|
||||
</li>
|
||||
) : null}
|
||||
{downloadPath ? (
|
||||
<li role="none">
|
||||
<DropdownItem
|
||||
href={downloadPath}
|
||||
onClick={downloadWithAnalytics}
|
||||
download={selectedFileName}
|
||||
>
|
||||
{t('download')}
|
||||
</DropdownItem>
|
||||
</li>
|
||||
) : null}
|
||||
{canDelete ? (
|
||||
<li role="none">
|
||||
<DropdownItem onClick={startDeleting}>{t('delete')}</DropdownItem>
|
||||
</li>
|
||||
) : null}
|
||||
{canCreate ? (
|
||||
<>
|
||||
<DropdownDivider role="none" />
|
||||
<li role="none">
|
||||
<DropdownItem onClick={createWithAnalytics}>
|
||||
{t('new_file')}
|
||||
</DropdownItem>
|
||||
</li>
|
||||
<li role="none">
|
||||
<DropdownItem onClick={startCreatingFolder}>
|
||||
{t('new_folder')}
|
||||
</DropdownItem>
|
||||
</li>
|
||||
<li role="none">
|
||||
<DropdownItem onClick={uploadWithAnalytics}>
|
||||
{t('upload')}
|
||||
</DropdownItem>
|
||||
</li>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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 })}
|
||||
>
|
||||
<Icon type="ellipsis-v" accessibilityLabel={t('menu')} />
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={<Icon type="ellipsis-v" accessibilityLabel={t('menu')} />}
|
||||
bs5={<MaterialIcon type="more_vert" accessibilityLabel={t('menu')} />}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
|
@ -40,6 +50,7 @@ function FileTreeItemMenu({ id }) {
|
|||
|
||||
FileTreeItemMenu.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
export default FileTreeItemMenu
|
||||
|
|
|
@ -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 (
|
||||
<FileTreeCreateFormProvider>
|
||||
<AccessibleModal bsSize="large" onHide={cancel} show>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>{t('add_files')}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<OLModal size="lg" onHide={cancel} show>
|
||||
<OLModalHeader closeButton>
|
||||
<OLModalTitle>{t('add_files')}</OLModalTitle>
|
||||
</OLModalHeader>
|
||||
|
||||
<Modal.Body className="modal-new-file">
|
||||
<OLModalBody className="modal-new-file">
|
||||
<FileTreeModalCreateFileBody />
|
||||
</Modal.Body>
|
||||
</OLModalBody>
|
||||
|
||||
<Modal.Footer>
|
||||
<OLModalFooter>
|
||||
<FileTreeModalCreateFileFooter />
|
||||
</Modal.Footer>
|
||||
</AccessibleModal>
|
||||
</OLModalFooter>
|
||||
</OLModal>
|
||||
</FileTreeCreateFormProvider>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
<AccessibleModal show onHide={handleHide}>
|
||||
<Modal.Header>
|
||||
<Modal.Title>{t('new_folder')}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<OLModal show onHide={handleHide}>
|
||||
<OLModalHeader>
|
||||
<OLModalTitle>{t('new_folder')}</OLModalTitle>
|
||||
</OLModalHeader>
|
||||
|
||||
<Modal.Body>
|
||||
<OLModalBody>
|
||||
<InputName
|
||||
name={name}
|
||||
setName={setName}
|
||||
|
@ -79,33 +79,32 @@ function FileTreeModalCreateFolder() {
|
|||
{errorMessage()}
|
||||
</div>
|
||||
) : null}
|
||||
</Modal.Body>
|
||||
</OLModalBody>
|
||||
|
||||
<Modal.Footer>
|
||||
<OLModalFooter>
|
||||
{inFlight ? (
|
||||
<Button bsStyle="primary" disabled>
|
||||
{t('creating')}…
|
||||
</Button>
|
||||
<OLButton
|
||||
variant="primary"
|
||||
disabled
|
||||
isLoading={inFlight}
|
||||
bs3Props={{ loading: `${t('creating')}…` }}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
bsStyle={null}
|
||||
className="btn-secondary"
|
||||
onClick={handleHide}
|
||||
>
|
||||
<OLButton variant="secondary" onClick={handleHide}>
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
bsStyle="primary"
|
||||
</OLButton>
|
||||
<OLButton
|
||||
variant="primary"
|
||||
onClick={handleCreateFolder}
|
||||
disabled={!validName}
|
||||
>
|
||||
{t('create')}
|
||||
</Button>
|
||||
</OLButton>
|
||||
</>
|
||||
)}
|
||||
</Modal.Footer>
|
||||
</AccessibleModal>
|
||||
</OLModalFooter>
|
||||
</OLModal>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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 (
|
||||
<AccessibleModal show onHide={handleHide}>
|
||||
<Modal.Header>
|
||||
<Modal.Title>{t('delete')}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<OLModal show onHide={handleHide}>
|
||||
<OLModalHeader>
|
||||
<OLModalTitle>{t('delete')}</OLModalTitle>
|
||||
</OLModalHeader>
|
||||
|
||||
<Modal.Body>
|
||||
<OLModalBody>
|
||||
<p>{t('sure_you_want_to_delete')}</p>
|
||||
<ul>
|
||||
{actionedEntities.map(entity => (
|
||||
|
@ -41,33 +46,33 @@ function FileTreeModalDelete() {
|
|||
))}
|
||||
</ul>
|
||||
{error && (
|
||||
<div className="alert alert-danger file-tree-modal-alert">
|
||||
{t('generic_something_went_wrong')}
|
||||
</div>
|
||||
<OLNotification
|
||||
type="error"
|
||||
content={t('generic_something_went_wrong')}
|
||||
/>
|
||||
)}
|
||||
</Modal.Body>
|
||||
</OLModalBody>
|
||||
|
||||
<Modal.Footer>
|
||||
<OLModalFooter>
|
||||
{inFlight ? (
|
||||
<Button bsStyle="danger" disabled>
|
||||
{t('deleting')}…
|
||||
</Button>
|
||||
<OLButton
|
||||
variant="danger"
|
||||
disabled
|
||||
isLoading
|
||||
bs3Props={{ loading: `${t('deleting')}…` }}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
bsStyle={null}
|
||||
className="btn-secondary"
|
||||
onClick={handleHide}
|
||||
>
|
||||
<OLButton className="secondary" onClick={handleHide}>
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
<Button bsStyle="danger" onClick={handleDelete}>
|
||||
</OLButton>
|
||||
<OLButton variant="danger" onClick={handleDelete}>
|
||||
{t('delete')}
|
||||
</Button>
|
||||
</OLButton>
|
||||
</>
|
||||
)}
|
||||
</Modal.Footer>
|
||||
</AccessibleModal>
|
||||
</OLModalFooter>
|
||||
</OLModal>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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 (
|
||||
<Modal show onHide={handleHide}>
|
||||
<Modal.Header>
|
||||
<Modal.Title>{errorTitle()}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<OLModal show onHide={handleHide}>
|
||||
<OLModalHeader>
|
||||
<OLModalTitle>{errorTitle()}</OLModalTitle>
|
||||
</OLModalHeader>
|
||||
|
||||
<Modal.Body>
|
||||
<OLModalBody>
|
||||
<div role="alert" aria-label={errorMessage()}>
|
||||
{errorMessage()}
|
||||
</div>
|
||||
</Modal.Body>
|
||||
</OLModalBody>
|
||||
|
||||
<Modal.Footer>
|
||||
<Button onClick={handleHide} bsStyle="primary">
|
||||
<OLModalFooter>
|
||||
<OLButton onClick={handleHide} variant="primary">
|
||||
{t('ok')}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
</OLButton>
|
||||
</OLModalFooter>
|
||||
</OLModal>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
|
|
|
@ -82,7 +82,7 @@ export default function TagsList() {
|
|||
<DropdownToggle id={`${tag._id}-dropdown-toggle`}>
|
||||
<span className="caret" />
|
||||
</DropdownToggle>
|
||||
<DropdownMenu className="sm">
|
||||
<DropdownMenu className="dropdown-menu-sm-width">
|
||||
<DropdownItem
|
||||
as="li"
|
||||
className="tag-action"
|
||||
|
|
|
@ -34,7 +34,10 @@ function LanguagePicker() {
|
|||
{translatedLanguages?.[currentLangCode]}
|
||||
</DropdownToggle>
|
||||
|
||||
<DropdownMenu className="sm" aria-labelledby="language-picker-toggle">
|
||||
<DropdownMenu
|
||||
className="dropdown-menu-sm-width"
|
||||
aria-labelledby="language-picker-toggle"
|
||||
>
|
||||
{subdomainLang &&
|
||||
Object.entries(subdomainLang).map(([subdomain, subdomainDetails]) => {
|
||||
if (
|
||||
|
|
|
@ -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<{
|
||||
|
|
|
@ -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 (
|
||||
<div className="loading">
|
||||
<Icon type="refresh" fw spin />
|
||||
|
||||
{loadingText || t('loading')}…
|
||||
</div>
|
||||
<BootstrapVersionSwitcher
|
||||
bs3={
|
||||
<div className="loading">
|
||||
<Icon type="refresh" fw spin />
|
||||
|
||||
{loadingText || t('loading')}…
|
||||
</div>
|
||||
}
|
||||
bs5={
|
||||
<div className="text-center mt-4">
|
||||
<Spinner
|
||||
animation="border"
|
||||
aria-hidden="true"
|
||||
role="status"
|
||||
className="align-bottom"
|
||||
/>
|
||||
|
||||
{loadingText || t('loading')}…
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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 (
|
||||
<>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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}'] {
|
||||
|
|
|
@ -54,3 +54,7 @@ hr {
|
|||
.row-spaced-extra-large {
|
||||
margin-top: calc(var(--line-height-03) * 4);
|
||||
}
|
||||
|
||||
.rotate-180 {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
|
|
@ -104,6 +104,7 @@
|
|||
text-decoration: underline;
|
||||
font-size: inherit;
|
||||
vertical-align: inherit;
|
||||
border-radius: 0;
|
||||
|
||||
&,
|
||||
&:active,
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
|
||||
min-width: 240px;
|
||||
|
||||
&.sm {
|
||||
&.dropdown-menu-sm-width {
|
||||
min-width: 160px;
|
||||
}
|
||||
|
||||
|
|
|
@ -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%;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -116,7 +116,7 @@ describe('<FileTreeitemInner />', function () {
|
|||
</EditorProviders>
|
||||
)
|
||||
|
||||
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')
|
||||
|
|
|
@ -90,7 +90,7 @@ describe('<FileTreeRoot/>', 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('<FileTreeRoot/>', 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('<FileTreeRoot/>', 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('<FileTreeRoot/>', 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 () {
|
||||
|
|
|
@ -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' })
|
||||
})
|
||||
|
|
|
@ -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}')
|
||||
|
|
Loading…
Reference in a new issue