mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-08 18:30:53 +00:00
Feature/refactor move dialog (#6990)
* update move dialog ui * create new folder in move dialog * optimize create new folder * optimize code * update ui * optimize ui, fix new folder bug * update new folder button * update create folder * optimize ui * optimize ui * optimize ui --------- Co-authored-by: zhouwenxuan <aries@Mac.local>
This commit is contained in:
@@ -1,9 +1,15 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { Modal, ModalHeader } from 'reactstrap';
|
import { Modal, ModalHeader } from 'reactstrap';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { IconBtn } from '@seafile/sf-metadata-ui-component';
|
||||||
import SelectDirentBody from './select-dirent-body';
|
import SelectDirentBody from './select-dirent-body';
|
||||||
import { gettext } from '../../utils/constants';
|
import { gettext, isPro } from '../../utils/constants';
|
||||||
import { Utils } from '../../utils/utils';
|
import { Utils } from '../../utils/utils';
|
||||||
|
import Searcher from '../file-chooser/searcher';
|
||||||
|
import { MODE_TYPE_MAP } from '../file-chooser/repo-list-wrapper';
|
||||||
|
import { RepoInfo } from '../../models';
|
||||||
|
import { seafileAPI } from '../../utils/seafile-api';
|
||||||
|
import toaster from '../toast';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
path: PropTypes.string.isRequired,
|
path: PropTypes.string.isRequired,
|
||||||
@@ -14,6 +20,7 @@ const propTypes = {
|
|||||||
onItemMove: PropTypes.func,
|
onItemMove: PropTypes.func,
|
||||||
onItemsMove: PropTypes.func,
|
onItemsMove: PropTypes.func,
|
||||||
onCancelMove: PropTypes.func.isRequired,
|
onCancelMove: PropTypes.func.isRequired,
|
||||||
|
onAddFolder: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
class MoveDirent extends React.Component {
|
class MoveDirent extends React.Component {
|
||||||
@@ -21,8 +28,14 @@ class MoveDirent extends React.Component {
|
|||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
|
mode: MODE_TYPE_MAP.ONLY_CURRENT_LIBRARY,
|
||||||
repo: { repo_id: this.props.repoID },
|
repo: { repo_id: this.props.repoID },
|
||||||
selectedPath: this.props.path,
|
selectedPath: this.props.path,
|
||||||
|
selectedSearchedItem: null,
|
||||||
|
selectedSearchedRepo: null,
|
||||||
|
searchStatus: '',
|
||||||
|
searchResults: [],
|
||||||
|
showSearchBar: false,
|
||||||
errMessage: '',
|
errMessage: '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -138,6 +151,100 @@ class MoveDirent extends React.Component {
|
|||||||
this.setState({ errMessage: message });
|
this.setState({ errMessage: message });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onUpdateMode = (mode) => {
|
||||||
|
if (mode === this.state.mode) return;
|
||||||
|
|
||||||
|
if (this.state.mode === MODE_TYPE_MAP.SEARCH_RESULTS) {
|
||||||
|
this.setState({
|
||||||
|
selectedSearchedRepo: null,
|
||||||
|
selectedSearchedItem: null,
|
||||||
|
searchResults: [],
|
||||||
|
showSearchBar: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state.selectedSearchedRepo) {
|
||||||
|
this.setState({
|
||||||
|
selectedSearchedRepo: null,
|
||||||
|
selectedSearchedItem: null,
|
||||||
|
searchResults: [],
|
||||||
|
showSearchBar: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
mode,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onUpdateSearchStatus = (status) => {
|
||||||
|
this.setState({ searchStatus: status });
|
||||||
|
};
|
||||||
|
|
||||||
|
onUpdateSearchResults = (results) => {
|
||||||
|
this.setState({
|
||||||
|
searchResults: results
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onDirentItemClick = (repo, selectedPath) => {
|
||||||
|
this.setState({
|
||||||
|
selectedPath: selectedPath,
|
||||||
|
repo,
|
||||||
|
errMessage: '',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onOpenSearchBar = () => {
|
||||||
|
this.setState({ showSearchBar: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
onCloseSearchBar = () => {
|
||||||
|
const { selectedSearchedRepo } = this.state;
|
||||||
|
const mode = (!selectedSearchedRepo || selectedSearchedRepo.repo_id === this.props.repoID) ? MODE_TYPE_MAP.ONLY_CURRENT_LIBRARY : MODE_TYPE_MAP.ONLY_OTHER_LIBRARIES;
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
mode,
|
||||||
|
searchStatus: '',
|
||||||
|
searchResults: [],
|
||||||
|
selectedSearchedRepo: null,
|
||||||
|
showSearchBar: false
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onSearchedItemClick = (item) => {
|
||||||
|
item['type'] = item.is_dir ? 'dir' : 'file';
|
||||||
|
let repo = new RepoInfo(item);
|
||||||
|
this.onDirentItemClick(repo, item.path, item);
|
||||||
|
};
|
||||||
|
|
||||||
|
onSearchedItemDoubleClick = (item) => {
|
||||||
|
if (item.type !== 'dir') return;
|
||||||
|
|
||||||
|
const selectedItemInfo = {
|
||||||
|
repoID: item.repo_id,
|
||||||
|
filePath: item.path,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.setState({ selectedSearchedItem: selectedItemInfo });
|
||||||
|
|
||||||
|
seafileAPI.getRepoInfo(item.repo_id).then(res => {
|
||||||
|
const repoInfo = new RepoInfo(res.data);
|
||||||
|
const path = item.path.substring(0, item.path.length - 1);
|
||||||
|
const mode = item.repo_id === this.props.repoID ? MODE_TYPE_MAP.ONLY_CURRENT_LIBRARY : MODE_TYPE_MAP.ONLY_OTHER_LIBRARIES;
|
||||||
|
this.setState({
|
||||||
|
mode,
|
||||||
|
searchResults: [],
|
||||||
|
selectedSearchedRepo: repoInfo,
|
||||||
|
selectedPath: path,
|
||||||
|
showSearchBar: mode === MODE_TYPE_MAP.ONLY_OTHER_LIBRARIES,
|
||||||
|
});
|
||||||
|
}).catch(err => {
|
||||||
|
const errMessage = Utils.getErrorMsg(err);
|
||||||
|
toaster.danger(errMessage);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
renderTitle = () => {
|
renderTitle = () => {
|
||||||
const { dirent, isMultipleOperation } = this.props;
|
const { dirent, isMultipleOperation } = this.props;
|
||||||
let title = gettext('Move {placeholder} to');
|
let title = gettext('Move {placeholder} to');
|
||||||
@@ -151,6 +258,7 @@ class MoveDirent extends React.Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { dirent, selectedDirentList, isMultipleOperation, path, repoID } = this.props;
|
const { dirent, selectedDirentList, isMultipleOperation, path, repoID } = this.props;
|
||||||
|
const { mode, selectedPath, showSearchBar, searchStatus, searchResults, selectedSearchedRepo, errMessage } = this.state;
|
||||||
const movedDirent = dirent || selectedDirentList[0];
|
const movedDirent = dirent || selectedDirentList[0];
|
||||||
const { permission } = movedDirent;
|
const { permission } = movedDirent;
|
||||||
const { isCustomPermission } = Utils.getUserPermission(permission);
|
const { isCustomPermission } = Utils.getUserPermission(permission);
|
||||||
@@ -158,19 +266,47 @@ class MoveDirent extends React.Component {
|
|||||||
return (
|
return (
|
||||||
<Modal className='custom-modal' isOpen={true} toggle={this.toggle}>
|
<Modal className='custom-modal' isOpen={true} toggle={this.toggle}>
|
||||||
<ModalHeader toggle={this.toggle}>
|
<ModalHeader toggle={this.toggle}>
|
||||||
{isMultipleOperation ? this.renderTitle() : <div dangerouslySetInnerHTML={{ __html: this.renderTitle() }} className='d-flex mw-100'></div>}
|
{isMultipleOperation ? this.renderTitle() : <div dangerouslySetInnerHTML={{ __html: this.renderTitle() }} className='d-flex'></div>}
|
||||||
|
{isPro && (
|
||||||
|
showSearchBar ? (
|
||||||
|
<Searcher
|
||||||
|
onUpdateMode={this.onUpdateMode}
|
||||||
|
onUpdateSearchStatus={this.onUpdateSearchStatus}
|
||||||
|
onUpdateSearchResults={this.onUpdateSearchResults}
|
||||||
|
onClose={this.onCloseSearchBar}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<IconBtn
|
||||||
|
iconName="search"
|
||||||
|
size={24}
|
||||||
|
className="search"
|
||||||
|
onClick={this.onOpenSearchBar}
|
||||||
|
role="button"
|
||||||
|
onKeyDown={() => {}}
|
||||||
|
tabIndex={0}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<SelectDirentBody
|
<SelectDirentBody
|
||||||
path={path}
|
path={path}
|
||||||
selectedPath={this.state.selectedPath}
|
selectedPath={selectedPath}
|
||||||
repoID={repoID}
|
repoID={repoID}
|
||||||
isSupportOtherLibraries={!isCustomPermission}
|
isSupportOtherLibraries={!isCustomPermission}
|
||||||
errMessage={this.state.errMessage}
|
errMessage={errMessage}
|
||||||
onCancel={this.toggle}
|
onCancel={this.toggle}
|
||||||
selectRepo={this.selectRepo}
|
selectRepo={this.selectRepo}
|
||||||
setSelectedPath={this.setSelectedPath}
|
setSelectedPath={this.setSelectedPath}
|
||||||
setErrMessage={this.setErrMessage}
|
setErrMessage={this.setErrMessage}
|
||||||
handleSubmit={this.handleSubmit}
|
handleSubmit={this.handleSubmit}
|
||||||
|
mode={mode}
|
||||||
|
onUpdateMode={this.onUpdateMode}
|
||||||
|
searchStatus={searchStatus}
|
||||||
|
searchResults={searchResults}
|
||||||
|
onSearchedItemClick={this.onSearchedItemClick}
|
||||||
|
onSearchedItemDoubleClick={this.onSearchedItemDoubleClick}
|
||||||
|
selectedSearchedRepo={selectedSearchedRepo}
|
||||||
|
onAddFolder={this.props.onAddFolder}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
@@ -2,16 +2,17 @@ import React from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Button, ModalFooter, ModalBody, Alert, Row, Col } from 'reactstrap';
|
import { Button, ModalFooter, ModalBody, Alert, Row, Col } from 'reactstrap';
|
||||||
import toaster from '../toast';
|
import toaster from '../toast';
|
||||||
import Searcher, { SearchStatus } from '../file-chooser/searcher';
|
|
||||||
import RepoListWrapper, { MODE_TYPE_MAP } from '../file-chooser/repo-list-wrapper';
|
import RepoListWrapper, { MODE_TYPE_MAP } from '../file-chooser/repo-list-wrapper';
|
||||||
import { seafileAPI } from '../../utils/seafile-api';
|
import { seafileAPI } from '../../utils/seafile-api';
|
||||||
import { gettext, isPro } from '../../utils/constants';
|
import { gettext } from '../../utils/constants';
|
||||||
import { Utils } from '../../utils/utils';
|
import { Utils } from '../../utils/utils';
|
||||||
import { RepoInfo } from '../../models';
|
import { RepoInfo } from '../../models';
|
||||||
|
import { ModalPortal } from '@seafile/sf-metadata-ui-component';
|
||||||
|
import CreateFolder from '../dialog/create-folder-dialog';
|
||||||
|
|
||||||
const LibraryOption = ({ mode, label, currentMode, selectedMode }) => {
|
const LibraryOption = ({ mode, label, currentMode, onUpdateMode }) => {
|
||||||
return (
|
return (
|
||||||
<div className={`repo-list-item ${mode === currentMode ? 'active' : ''}`} onClick={() => selectedMode(mode)}>
|
<div className={`repo-list-item ${mode === currentMode ? 'active' : ''}`} onClick={() => onUpdateMode(mode)}>
|
||||||
<span className='library'>{label}</span>
|
<span className='library'>{label}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -22,15 +23,13 @@ class SelectDirentBody extends React.Component {
|
|||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
mode: MODE_TYPE_MAP.ONLY_CURRENT_LIBRARY,
|
|
||||||
currentRepoInfo: null,
|
currentRepoInfo: null,
|
||||||
repoList: [],
|
repoList: [],
|
||||||
selectedSearchedItem: null,
|
|
||||||
selectedRepo: null,
|
selectedRepo: null,
|
||||||
browsingPath: '',
|
|
||||||
searchStatus: SearchStatus.IDLE,
|
|
||||||
errMessage: '',
|
errMessage: '',
|
||||||
|
showCreateFolderDialog: false,
|
||||||
};
|
};
|
||||||
|
this.newFolderName = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@@ -52,11 +51,32 @@ class SelectDirentBody extends React.Component {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
fetchSelectedRepoInfo = async (repoId) => {
|
||||||
|
try {
|
||||||
|
const res = await seafileAPI.getRepoInfo(repoId);
|
||||||
|
const repoInfo = new RepoInfo(res.data);
|
||||||
|
this.setState({
|
||||||
|
selectedRepo: repoInfo,
|
||||||
|
selectedSearchedRepo: repoInfo,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
const errMessage = Utils.getErrorMsg(err);
|
||||||
|
toaster.danger(errMessage);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
fetchRepoList = async () => {
|
fetchRepoList = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await seafileAPI.listRepos();
|
const res = await seafileAPI.listRepos();
|
||||||
const repos = res.data.repos;
|
const repos = res.data.repos;
|
||||||
const repoList = repos.filter((repo) => repo.permission === 'rw' && repo.repo_id !== this.props.repoID);
|
const repoList = [];
|
||||||
|
const uniqueRepoIds = new Set();
|
||||||
|
for (const repo of repos) {
|
||||||
|
if (repo.permission === 'rw' && repo.repo_id !== this.props.repoID && !uniqueRepoIds.has(repo.repo_id)) {
|
||||||
|
uniqueRepoIds.add(repo.repo_id);
|
||||||
|
repoList.push(repo);
|
||||||
|
}
|
||||||
|
}
|
||||||
const sortedRepoList = Utils.sortRepos(repoList, 'name', 'asc');
|
const sortedRepoList = Utils.sortRepos(repoList, 'name', 'asc');
|
||||||
const selectedRepo = sortedRepoList.find((repo) => repo.repo_id === this.props.repoID);
|
const selectedRepo = sortedRepoList.find((repo) => repo.repo_id === this.props.repoID);
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -69,29 +89,10 @@ class SelectDirentBody extends React.Component {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onUpdateSearchStatus = (status) => {
|
|
||||||
this.setState({ searchStatus: status });
|
|
||||||
};
|
|
||||||
|
|
||||||
onUpdateRepoList = (repoList) => {
|
onUpdateRepoList = (repoList) => {
|
||||||
this.setState({ repoList: repoList });
|
this.setState({ repoList: repoList });
|
||||||
};
|
};
|
||||||
|
|
||||||
selectSearchedItem = (item) => {
|
|
||||||
this.setState({ selectedSearchedItem: item });
|
|
||||||
};
|
|
||||||
|
|
||||||
onSelectSearchedRepo = (repo) => {
|
|
||||||
this.setState({
|
|
||||||
selectedRepo: repo,
|
|
||||||
mode: repo.repo_id === this.props.repoID ? MODE_TYPE_MAP.ONLY_CURRENT_LIBRARY : MODE_TYPE_MAP.ONLY_OTHER_LIBRARIES,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
setBrowsingPath = (path) => {
|
|
||||||
this.setState({ browsingPath: path });
|
|
||||||
};
|
|
||||||
|
|
||||||
handleSubmit = () => {
|
handleSubmit = () => {
|
||||||
if (this.props.handleSubmit) {
|
if (this.props.handleSubmit) {
|
||||||
this.props.handleSubmit();
|
this.props.handleSubmit();
|
||||||
@@ -118,23 +119,60 @@ class SelectDirentBody extends React.Component {
|
|||||||
this.setState({ selectedRepo: repo });
|
this.setState({ selectedRepo: repo });
|
||||||
};
|
};
|
||||||
|
|
||||||
selectedMode = (mode) => {
|
onUpdateMode = (mode) => {
|
||||||
const { repoID, path } = this.props;
|
const { path } = this.props;
|
||||||
|
const { repoList } = this.state;
|
||||||
|
if (mode === MODE_TYPE_MAP.ONLY_OTHER_LIBRARIES) {
|
||||||
|
this.setState({
|
||||||
|
selectedRepo: repoList[0],
|
||||||
|
});
|
||||||
|
this.props.setSelectedPath('/');
|
||||||
|
} else {
|
||||||
|
this.setState({ selectedRepo: this.state.currentRepoInfo });
|
||||||
|
this.props.setSelectedPath(path);
|
||||||
|
}
|
||||||
|
|
||||||
// reset selecting status
|
this.props.onUpdateMode(mode);
|
||||||
this.props.selectRepo({ repo_id: repoID });
|
};
|
||||||
this.props.setSelectedPath(path);
|
|
||||||
|
|
||||||
this.setState({
|
loadRepoDirentList = (repo) => {
|
||||||
mode,
|
try {
|
||||||
selectedSearchedItem: null,
|
const { data } = seafileAPI.listDir(repo.repo_id, '/');
|
||||||
searchStatus: SearchStatus.RESULTS,
|
return data.dirent_list.filter(item => item.type === 'dir');
|
||||||
});
|
} catch (error) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
createFolder = (fullPath) => {
|
||||||
|
this.newFolderName = fullPath.split('/').pop();
|
||||||
|
const selectedRepoId = this.state.selectedRepo.repo_id;
|
||||||
|
|
||||||
|
if (selectedRepoId === this.props.repoID) {
|
||||||
|
this.props.onAddFolder(fullPath, { successCallback: this.fetchRepoInfo });
|
||||||
|
} else {
|
||||||
|
seafileAPI.createDir(selectedRepoId, fullPath).then(() => {
|
||||||
|
this.fetchSelectedRepoInfo(selectedRepoId);
|
||||||
|
}).catch((error) => {
|
||||||
|
let errMessage = Utils.getErrorMsg(error);
|
||||||
|
this.props.setErrMessage(errMessage);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.setState({ showCreateFolderDialog: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
onToggleCreateFolder = () => {
|
||||||
|
this.setState({ showCreateFolderDialog: !this.state.showCreateFolderDialog });
|
||||||
|
};
|
||||||
|
|
||||||
|
checkDuplicatedName = (newName) => {
|
||||||
|
const folderList = this.loadRepoDirentList(this.state.selectedRepo);
|
||||||
|
return folderList.some(folder => folder.name === newName);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { path, selectedPath, isSupportOtherLibraries, errMessage } = this.props;
|
const { mode, path, selectedPath, isSupportOtherLibraries, errMessage, searchStatus, searchResults, selectedSearchedRepo } = this.props;
|
||||||
const { mode, searchStatus, selectedSearchedItem, selectedRepo, repoList, currentRepoInfo, browsingPath } = this.state;
|
const { selectedSearchedItem, selectedRepo, repoList, currentRepoInfo } = this.state;
|
||||||
let repoListWrapperKey = 'repo-list-wrapper';
|
let repoListWrapperKey = 'repo-list-wrapper';
|
||||||
if (selectedSearchedItem && selectedSearchedItem.repoID) {
|
if (selectedSearchedItem && selectedSearchedItem.repoID) {
|
||||||
repoListWrapperKey = `${repoListWrapperKey}-${selectedSearchedItem.repoID}`;
|
repoListWrapperKey = `${repoListWrapperKey}-${selectedSearchedItem.repoID}`;
|
||||||
@@ -143,63 +181,75 @@ class SelectDirentBody extends React.Component {
|
|||||||
return (
|
return (
|
||||||
<Row>
|
<Row>
|
||||||
<Col className='repo-list-col border-right'>
|
<Col className='repo-list-col border-right'>
|
||||||
{isPro && (
|
|
||||||
<Searcher
|
|
||||||
searchStatus={searchStatus}
|
|
||||||
onUpdateSearchStatus={this.onUpdateSearchStatus}
|
|
||||||
onDirentItemClick={this.onDirentItemClick}
|
|
||||||
selectSearchedItem={this.selectSearchedItem}
|
|
||||||
selectRepo={this.onSelectSearchedRepo}
|
|
||||||
setSelectedPath={this.props.setSelectedPath}
|
|
||||||
setBrowsingPath={this.setBrowsingPath}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<LibraryOption
|
<LibraryOption
|
||||||
mode={MODE_TYPE_MAP.ONLY_CURRENT_LIBRARY}
|
mode={MODE_TYPE_MAP.ONLY_CURRENT_LIBRARY}
|
||||||
label={gettext('Current Library')}
|
label={gettext('Current Library')}
|
||||||
currentMode={mode}
|
currentMode={mode}
|
||||||
selectedMode={this.selectedMode}
|
onUpdateMode={this.onUpdateMode}
|
||||||
/>
|
/>
|
||||||
{isSupportOtherLibraries && (
|
{isSupportOtherLibraries && (
|
||||||
<LibraryOption
|
<LibraryOption
|
||||||
mode={MODE_TYPE_MAP.ONLY_OTHER_LIBRARIES}
|
mode={MODE_TYPE_MAP.ONLY_OTHER_LIBRARIES}
|
||||||
label={gettext('Other Libraries')}
|
label={gettext('Other Libraries')}
|
||||||
currentMode={mode}
|
currentMode={mode}
|
||||||
selectedMode={this.selectedMode}
|
onUpdateMode={this.onUpdateMode}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<LibraryOption
|
<LibraryOption
|
||||||
mode={MODE_TYPE_MAP.RECENTLY_USED}
|
mode={MODE_TYPE_MAP.RECENTLY_USED}
|
||||||
label={gettext('Recently Used')}
|
label={gettext('Recently Used')}
|
||||||
currentMode={mode}
|
currentMode={mode}
|
||||||
selectedMode={this.selectedMode}
|
onUpdateMode={this.onUpdateMode}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col className='file-list-col'>
|
<Col className='file-list-col'>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
{currentRepoInfo && (
|
<RepoListWrapper
|
||||||
<RepoListWrapper
|
key={repoListWrapperKey}
|
||||||
key={repoListWrapperKey}
|
mode={mode}
|
||||||
mode={mode}
|
currentPath={path}
|
||||||
currentPath={path}
|
selectedItemInfo={selectedSearchedItem}
|
||||||
isBrowsing={searchStatus === SearchStatus.BROWSING}
|
currentRepoInfo={currentRepoInfo}
|
||||||
browsingPath={browsingPath}
|
selectedRepo={selectedRepo}
|
||||||
selectedItemInfo={selectedSearchedItem}
|
selectedPath={selectedPath}
|
||||||
currentRepoInfo={currentRepoInfo}
|
repoList={repoList}
|
||||||
selectedRepo={selectedRepo}
|
handleClickRepo={this.onRepoItemClick}
|
||||||
selectedPath={selectedPath}
|
handleClickDirent={this.onDirentItemClick}
|
||||||
repoList={repoList}
|
searchStatus={searchStatus}
|
||||||
handleClickRepo={this.onRepoItemClick}
|
searchResults={searchResults}
|
||||||
handleClickDirent={this.onDirentItemClick}
|
onSearchedItemClick={this.props.onSearchedItemClick}
|
||||||
/>
|
onSearchedItemDoubleClick={this.props.onSearchedItemDoubleClick}
|
||||||
)}
|
selectedSearchedRepo={selectedSearchedRepo}
|
||||||
|
newFolderName={this.newFolderName}
|
||||||
|
/>
|
||||||
{errMessage && <Alert color="danger" className="alert-message">{errMessage}</Alert>}
|
{errMessage && <Alert color="danger" className="alert-message">{errMessage}</Alert>}
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter className='move-dirent-dialog-footer'>
|
||||||
<Button color="secondary" onClick={this.onCancel}>{gettext('Cancel')}</Button>
|
<Button
|
||||||
<Button color="primary" onClick={this.handleSubmit}>{gettext('Submit')}</Button>
|
className="footer-left-btn"
|
||||||
|
color="secondary"
|
||||||
|
onClick={this.onToggleCreateFolder}
|
||||||
|
disabled={mode === MODE_TYPE_MAP.SEARCH_RESULTS}
|
||||||
|
>
|
||||||
|
<i className='sf3-font-new sf3-font mr-2'></i>
|
||||||
|
<span>{gettext('New folder')}</span>
|
||||||
|
</Button>
|
||||||
|
<div className='footer-right-btns'>
|
||||||
|
<Button color="secondary m-1" onClick={this.onCancel}>{gettext('Cancel')}</Button>
|
||||||
|
<Button color="primary m-1" onClick={this.handleSubmit}>{gettext('Submit')}</Button>
|
||||||
|
</div>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</Col>
|
</Col>
|
||||||
|
{this.state.showCreateFolderDialog && (
|
||||||
|
<ModalPortal>
|
||||||
|
<CreateFolder
|
||||||
|
parentPath={this.props.selectedPath}
|
||||||
|
onAddFolder={this.createFolder}
|
||||||
|
checkDuplicatedName={this.checkDuplicatedName}
|
||||||
|
addFolderCancel={this.onToggleCreateFolder}
|
||||||
|
/>
|
||||||
|
</ModalPortal>
|
||||||
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -215,6 +265,14 @@ SelectDirentBody.propTypes = {
|
|||||||
selectRepo: PropTypes.func,
|
selectRepo: PropTypes.func,
|
||||||
setSelectedPath: PropTypes.func,
|
setSelectedPath: PropTypes.func,
|
||||||
setErrMessage: PropTypes.func,
|
setErrMessage: PropTypes.func,
|
||||||
|
mode: PropTypes.string,
|
||||||
|
onUpdateMode: PropTypes.func,
|
||||||
|
searchStatus: PropTypes.string,
|
||||||
|
searchResults: PropTypes.array,
|
||||||
|
onSearchedItemClick: PropTypes.func,
|
||||||
|
onSearchedItemDoubleClick: PropTypes.func,
|
||||||
|
selectedSearchedRepo: PropTypes.object,
|
||||||
|
onAddFolder: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
SelectDirentBody.defaultProps = {
|
SelectDirentBody.defaultProps = {
|
||||||
|
@@ -935,6 +935,7 @@ class DirentGridView extends React.Component {
|
|||||||
onItemsMove={this.props.onItemsMove}
|
onItemsMove={this.props.onItemsMove}
|
||||||
onCancelMove={this.onMoveToggle}
|
onCancelMove={this.onMoveToggle}
|
||||||
dirent={this.state.activeDirent}
|
dirent={this.state.activeDirent}
|
||||||
|
onAddFolder={this.props.onAddFolder}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
{this.state.isZipDialogOpen &&
|
{this.state.isZipDialogOpen &&
|
||||||
|
@@ -968,6 +968,7 @@ class DirentListItem extends React.Component {
|
|||||||
onItemMove={this.props.onItemMove}
|
onItemMove={this.props.onItemMove}
|
||||||
onCancelMove={this.onItemMoveToggle}
|
onCancelMove={this.onItemMoveToggle}
|
||||||
repoEncrypted={this.props.repoEncrypted}
|
repoEncrypted={this.props.repoEncrypted}
|
||||||
|
onAddFolder={this.props.onAddFolder}
|
||||||
/>
|
/>
|
||||||
</ModalPortal>
|
</ModalPortal>
|
||||||
}
|
}
|
||||||
|
@@ -779,6 +779,7 @@ class DirentListView extends React.Component {
|
|||||||
onItemsMove={this.props.onItemsMove}
|
onItemsMove={this.props.onItemsMove}
|
||||||
onShowDirentsDraggablePreview={this.onShowDirentsDraggablePreview}
|
onShowDirentsDraggablePreview={this.onShowDirentsDraggablePreview}
|
||||||
loadDirentList={this.props.loadDirentList}
|
loadDirentList={this.props.loadDirentList}
|
||||||
|
onAddFolder={this.props.onAddFolder}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -859,6 +860,7 @@ class DirentListView extends React.Component {
|
|||||||
selectedDirentList={this.props.selectedDirentList}
|
selectedDirentList={this.props.selectedDirentList}
|
||||||
onItemsMove={this.props.onItemsMove}
|
onItemsMove={this.props.onItemsMove}
|
||||||
onCancelMove={this.onMoveToggle}
|
onCancelMove={this.onMoveToggle}
|
||||||
|
onAddFolder={this.props.onAddFolder}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
{this.state.isCopyDialogShow &&
|
{this.state.isCopyDialogShow &&
|
||||||
|
@@ -21,8 +21,7 @@ const propTypes = {
|
|||||||
fileSuffixes: PropTypes.array,
|
fileSuffixes: PropTypes.array,
|
||||||
selectedItemInfo: PropTypes.object,
|
selectedItemInfo: PropTypes.object,
|
||||||
hideLibraryName: PropTypes.bool,
|
hideLibraryName: PropTypes.bool,
|
||||||
isBrowsing: PropTypes.bool,
|
newFolderName: PropTypes.string,
|
||||||
browsingPath: PropTypes.string,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class RepoListItem extends React.Component {
|
class RepoListItem extends React.Component {
|
||||||
@@ -65,6 +64,26 @@ class RepoListItem extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
const { repo, selectedRepo, selectedPath, newFolderName } = this.props;
|
||||||
|
if (repo.repo_id === selectedRepo.repo_id && prevProps.selectedRepo !== selectedRepo) {
|
||||||
|
seafileAPI.listDir(repo.repo_id, selectedPath).then(res => {
|
||||||
|
if (!this.state.isMounted) return;
|
||||||
|
const direntData = res.data.dirent_list.find(item => item.type === 'dir' && item.name === newFolderName);
|
||||||
|
if (direntData) {
|
||||||
|
const object = new Dirent(direntData);
|
||||||
|
const direntNode = new TreeNode({ object });
|
||||||
|
const newTreeData = treeHelper.addNodeToParentByPath(this.state.treeData, direntNode, selectedPath);
|
||||||
|
this.setState({ treeData: newTreeData });
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
if (!this.state.isMounted) return;
|
||||||
|
const errMessage = Utils.getErrorMsg(error);
|
||||||
|
toaster.danger(errMessage);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.clearLoadRepoTimer();
|
this.clearLoadRepoTimer();
|
||||||
this.setState({ isMounted: false, hasLoaded: false });
|
this.setState({ isMounted: false, hasLoaded: false });
|
||||||
@@ -213,7 +232,7 @@ class RepoListItem extends React.Component {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<li>
|
<li>
|
||||||
{!this.props.hideLibraryName && !this.props.isBrowsing &&
|
{!this.props.hideLibraryName &&
|
||||||
<div className={`${repoActive ? 'item-active' : ''} item-info`} onClick={this.onRepoItemClick}>
|
<div className={`${repoActive ? 'item-active' : ''} item-info`} onClick={this.onRepoItemClick}>
|
||||||
<div className="item-left-icon">
|
<div className="item-left-icon">
|
||||||
<span className={`item-toggle icon sf3-font ${this.state.isShowChildren ? 'sf3-font-down' : 'sf3-font-down rotate-270 d-inline-block'}`} onClick={this.onToggleClick}></span>
|
<span className={`item-toggle icon sf3-font ${this.state.isShowChildren ? 'sf3-font-down' : 'sf3-font-down rotate-270 d-inline-block'}`} onClick={this.onToggleClick}></span>
|
||||||
@@ -236,8 +255,6 @@ class RepoListItem extends React.Component {
|
|||||||
treeData={this.state.treeData}
|
treeData={this.state.treeData}
|
||||||
onNodeCollapse={this.onNodeCollapse}
|
onNodeCollapse={this.onNodeCollapse}
|
||||||
onNodeExpanded={this.onNodeExpanded}
|
onNodeExpanded={this.onNodeExpanded}
|
||||||
isBrowsing={this.props.isBrowsing}
|
|
||||||
browsingPath={this.props.browsingPath}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
|
@@ -15,8 +15,8 @@ const propTypes = {
|
|||||||
fileSuffixes: PropTypes.array,
|
fileSuffixes: PropTypes.array,
|
||||||
selectedItemInfo: PropTypes.object,
|
selectedItemInfo: PropTypes.object,
|
||||||
currentPath: PropTypes.string,
|
currentPath: PropTypes.string,
|
||||||
isBrowsing: PropTypes.bool,
|
selectedSearchedRepo: PropTypes.object,
|
||||||
browsingPath: PropTypes.string,
|
newFolderName: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
@@ -29,25 +29,28 @@ const defaultProps = {
|
|||||||
fileSuffixes: [],
|
fileSuffixes: [],
|
||||||
selectedItemInfo: null,
|
selectedItemInfo: null,
|
||||||
currentPath: '',
|
currentPath: '',
|
||||||
isBrowsing: false,
|
|
||||||
browsingPath: '',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class RepoListView extends React.Component {
|
class RepoListView extends React.Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let { currentRepoInfo, currentPath, repoList } = this.props;
|
let { currentRepoInfo, currentPath, repoList, selectedSearchedRepo } = this.props;
|
||||||
if (currentRepoInfo) {
|
if (currentRepoInfo) {
|
||||||
repoList = [];
|
repoList = [];
|
||||||
repoList.push(currentRepoInfo);
|
repoList.push(currentRepoInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (selectedSearchedRepo) {
|
||||||
|
repoList = [];
|
||||||
|
repoList.push(selectedSearchedRepo);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul className="list-view-content file-chooser-item" >
|
<ul className="list-view-content file-chooser-item" >
|
||||||
{repoList.length > 0 && repoList.map((repoItem, index) => {
|
{repoList.length > 0 && repoList.map((repoItem, index) => {
|
||||||
return (
|
return (
|
||||||
<RepoListItem
|
<RepoListItem
|
||||||
key={index}
|
key={repoItem.repo_id}
|
||||||
isCurrentRepo={currentRepoInfo ? true : false}
|
isCurrentRepo={currentRepoInfo ? true : false}
|
||||||
currentPath={currentPath}
|
currentPath={currentPath}
|
||||||
repo={repoItem}
|
repo={repoItem}
|
||||||
@@ -59,8 +62,7 @@ class RepoListView extends React.Component {
|
|||||||
isShowFile={this.props.isShowFile}
|
isShowFile={this.props.isShowFile}
|
||||||
fileSuffixes={this.props.fileSuffixes}
|
fileSuffixes={this.props.fileSuffixes}
|
||||||
selectedItemInfo={this.props.selectedItemInfo}
|
selectedItemInfo={this.props.selectedItemInfo}
|
||||||
isBrowsing={this.props.isBrowsing}
|
newFolderName={this.props.newFolderName}
|
||||||
browsingPath={this.props.browsingPath}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
@@ -3,6 +3,9 @@ import PropTypes from 'prop-types';
|
|||||||
import RepoListView from './repo-list-view';
|
import RepoListView from './repo-list-view';
|
||||||
import RecentlyUsedListView from './recently-used-list-view';
|
import RecentlyUsedListView from './recently-used-list-view';
|
||||||
import { gettext } from '../../utils/constants';
|
import { gettext } from '../../utils/constants';
|
||||||
|
import SearchedListView from './searched-list-view';
|
||||||
|
import { SearchStatus } from './searcher';
|
||||||
|
import Loading from '../loading';
|
||||||
|
|
||||||
export const MODE_TYPE_MAP = {
|
export const MODE_TYPE_MAP = {
|
||||||
CURRENT_AND_OTHER_REPOS: 'current_repo_and_other_repos',
|
CURRENT_AND_OTHER_REPOS: 'current_repo_and_other_repos',
|
||||||
@@ -10,12 +13,14 @@ export const MODE_TYPE_MAP = {
|
|||||||
ONLY_ALL_REPOS: 'only_all_repos',
|
ONLY_ALL_REPOS: 'only_all_repos',
|
||||||
ONLY_OTHER_LIBRARIES: 'only_other_libraries',
|
ONLY_OTHER_LIBRARIES: 'only_other_libraries',
|
||||||
RECENTLY_USED: 'recently_used',
|
RECENTLY_USED: 'recently_used',
|
||||||
|
SEARCH_RESULTS: 'search_results',
|
||||||
};
|
};
|
||||||
|
|
||||||
const RepoListWrapper = (props) => {
|
const RepoListWrapper = (props) => {
|
||||||
const {
|
const {
|
||||||
mode, isShowFile, fileSuffixes, currentPath, isBrowsing, browsingPath, isCurrentRepoShow, currentRepoInfo, selectedRepo,
|
mode, isShowFile, fileSuffixes, currentPath, isCurrentRepoShow, currentRepoInfo, selectedRepo,
|
||||||
selectedPath, isOtherRepoShow, selectedItemInfo, repoList,
|
selectedPath, isOtherRepoShow, selectedItemInfo, repoList,
|
||||||
|
searchStatus, searchResults, onSearchedItemClick, onSearchedItemDoubleClick, selectedSearchedRepo, newFolderName
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const renderRecentlyUsed = () => {
|
const renderRecentlyUsed = () => {
|
||||||
@@ -34,6 +39,29 @@ const RepoListWrapper = (props) => {
|
|||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderSearchResults = () => {
|
||||||
|
switch (searchStatus) {
|
||||||
|
case SearchStatus.LOADING:
|
||||||
|
return <Loading />;
|
||||||
|
case SearchStatus.RESULTS:
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{searchResults.length === 0 ? (
|
||||||
|
<div className='search-results-none text-center'>{gettext('No results matching')}</div>
|
||||||
|
) : (
|
||||||
|
<SearchedListView
|
||||||
|
searchResults={searchResults}
|
||||||
|
onItemClick={onSearchedItemClick}
|
||||||
|
onSearchedItemDoubleClick={onSearchedItemDoubleClick}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='file-chooser-scroll-wrapper' onScroll={onScroll}>
|
<div className='file-chooser-scroll-wrapper' onScroll={onScroll}>
|
||||||
<div className="file-chooser-container user-select-none">
|
<div className="file-chooser-container user-select-none">
|
||||||
@@ -91,10 +119,10 @@ const RepoListWrapper = (props) => {
|
|||||||
isShowFile={isShowFile}
|
isShowFile={isShowFile}
|
||||||
fileSuffixes={fileSuffixes}
|
fileSuffixes={fileSuffixes}
|
||||||
selectedItemInfo={selectedItemInfo}
|
selectedItemInfo={selectedItemInfo}
|
||||||
isBrowsing={isBrowsing}
|
|
||||||
browsingPath={browsingPath}
|
|
||||||
onRepoItemClick={props.handleClickRepo}
|
onRepoItemClick={props.handleClickRepo}
|
||||||
onDirentItemClick={props.handleClickDirent}
|
onDirentItemClick={props.handleClickDirent}
|
||||||
|
selectedSearchedRepo={selectedSearchedRepo}
|
||||||
|
newFolderName={newFolderName}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -129,14 +157,19 @@ const RepoListWrapper = (props) => {
|
|||||||
isShowFile={isShowFile}
|
isShowFile={isShowFile}
|
||||||
fileSuffixes={fileSuffixes}
|
fileSuffixes={fileSuffixes}
|
||||||
selectedItemInfo={selectedItemInfo}
|
selectedItemInfo={selectedItemInfo}
|
||||||
isBrowsing={isBrowsing}
|
|
||||||
browsingPath={browsingPath}
|
|
||||||
onRepoItemClick={props.handleClickRepo}
|
onRepoItemClick={props.handleClickRepo}
|
||||||
onDirentItemClick={props.handleClickDirent}
|
onDirentItemClick={props.handleClickDirent}
|
||||||
|
selectedSearchedRepo={selectedSearchedRepo}
|
||||||
|
newFolderName={newFolderName}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{mode === MODE_TYPE_MAP.RECENTLY_USED && renderRecentlyUsed()}
|
{mode === MODE_TYPE_MAP.RECENTLY_USED && renderRecentlyUsed()}
|
||||||
|
{mode === MODE_TYPE_MAP.SEARCH_RESULTS && (
|
||||||
|
<div className="list-view">
|
||||||
|
{renderSearchResults()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -147,8 +180,6 @@ RepoListWrapper.propTypes = {
|
|||||||
currentPath: PropTypes.string,
|
currentPath: PropTypes.string,
|
||||||
isShowFile: PropTypes.bool,
|
isShowFile: PropTypes.bool,
|
||||||
fileSuffixes: PropTypes.array,
|
fileSuffixes: PropTypes.array,
|
||||||
isBrowsing: PropTypes.bool,
|
|
||||||
browsingPath: PropTypes.string,
|
|
||||||
selectedItemInfo: PropTypes.object,
|
selectedItemInfo: PropTypes.object,
|
||||||
currentRepoInfo: PropTypes.object,
|
currentRepoInfo: PropTypes.object,
|
||||||
selectedRepo: PropTypes.object,
|
selectedRepo: PropTypes.object,
|
||||||
@@ -160,6 +191,12 @@ RepoListWrapper.propTypes = {
|
|||||||
onOtherRepoToggle: PropTypes.func,
|
onOtherRepoToggle: PropTypes.func,
|
||||||
handleClickRepo: PropTypes.func,
|
handleClickRepo: PropTypes.func,
|
||||||
handleClickDirent: PropTypes.func,
|
handleClickDirent: PropTypes.func,
|
||||||
|
searchStatus: PropTypes.string,
|
||||||
|
searchResults: PropTypes.array,
|
||||||
|
onSearchedItemClick: PropTypes.func,
|
||||||
|
onSearchedItemDoubleClick: PropTypes.func,
|
||||||
|
selectedSearchedRepo: PropTypes.object,
|
||||||
|
newFolderName: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
RepoListWrapper.defaultProps = {
|
RepoListWrapper.defaultProps = {
|
||||||
|
@@ -1,8 +1,9 @@
|
|||||||
.file-chooser-searcher.search-container {
|
.file-chooser-searcher.search-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
position: relative;
|
position: absolute;
|
||||||
margin-bottom: 16px;
|
top: 14px;
|
||||||
|
right: 48px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-chooser-searcher .search-input-container {
|
.file-chooser-searcher .search-input-container {
|
||||||
@@ -15,14 +16,14 @@
|
|||||||
|
|
||||||
.file-chooser-searcher .search-input-container .search-input {
|
.file-chooser-searcher .search-input-container .search-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 2.375rem;
|
height: 1.875em;
|
||||||
padding-left: 2rem;
|
padding-left: 2rem;
|
||||||
padding-right: 1.5rem;
|
padding-right: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-chooser-searcher .search-input-container .search-control {
|
.file-chooser-searcher .search-input-container .search-control {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 12px;
|
top: 6px;
|
||||||
right: 8px;
|
right: 8px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
@@ -1,51 +1,65 @@
|
|||||||
import React, { useState, useRef, useCallback } from 'react';
|
import React, { useState, useRef, useCallback, useEffect } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Input, UncontrolledPopover } from 'reactstrap';
|
import { Input } from 'reactstrap';
|
||||||
import Loading from '../../loading';
|
|
||||||
import toaster from '../../toast';
|
|
||||||
import RepoInfo from '../../../models/repo-info';
|
|
||||||
import SearchedListView from '../searched-list-view';
|
|
||||||
import { gettext } from '../../../utils/constants';
|
import { gettext } from '../../../utils/constants';
|
||||||
import { seafileAPI } from '../../../utils/seafile-api';
|
import { seafileAPI } from '../../../utils/seafile-api';
|
||||||
import { Utils } from '../../../utils/utils';
|
|
||||||
import { SEARCH_CONTAINER } from '../../../constants/zIndexes';
|
import { SEARCH_CONTAINER } from '../../../constants/zIndexes';
|
||||||
|
import { MODE_TYPE_MAP } from '../repo-list-wrapper';
|
||||||
|
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
export const SearchStatus = {
|
export const SearchStatus = {
|
||||||
IDLE: 'idle',
|
|
||||||
LOADING: 'loading',
|
LOADING: 'loading',
|
||||||
RESULTS: 'results',
|
RESULTS: 'results',
|
||||||
NO_RESULTS: 'no_results',
|
|
||||||
BROWSING: 'browsing',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const Searcher = ({ searchStatus, onUpdateSearchStatus, onDirentItemClick, selectSearchedItem, selectRepo, setSelectedPath, setBrowsingPath }) => {
|
const Searcher = ({ onUpdateMode, onUpdateSearchStatus, onUpdateSearchResults, onClose }) => {
|
||||||
const [inputValue, setInputValue] = useState('');
|
const [inputValue, setInputValue] = useState('');
|
||||||
const [isResultsPopoverOpen, setIsResultsPopoverOpen] = useState(false);
|
|
||||||
const [searchResults, setSearchResults] = useState([]);
|
|
||||||
|
|
||||||
const inputRef = useRef(null);
|
const inputRef = useRef(null);
|
||||||
|
|
||||||
const searchTimer = useRef(null);
|
const searchTimer = useRef(null);
|
||||||
const source = useRef(null);
|
const source = useRef(null);
|
||||||
|
|
||||||
const onPopoverToggle = useCallback((show) => {
|
useEffect(() => {
|
||||||
setIsResultsPopoverOpen(show);
|
const handleClickOutside = (event) => {
|
||||||
}, []);
|
if (inputRef.current && !inputRef.current.contains(event.target) && inputValue === '') {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleSearchInputChange = (e) => {
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
};
|
||||||
|
}, [inputValue, onClose]);
|
||||||
|
|
||||||
|
const getSearchResult = useCallback((queryData) => {
|
||||||
|
if (source.current) {
|
||||||
|
source.current.cancel('prev request is cancelled');
|
||||||
|
}
|
||||||
|
|
||||||
|
source.current = seafileAPI.getSource();
|
||||||
|
seafileAPI.searchFiles(queryData, source.current.token).then(res => {
|
||||||
|
onUpdateSearchStatus(SearchStatus.RESULTS);
|
||||||
|
onUpdateSearchResults(res.data.total ? formatResultItems(res.data.results) : []);
|
||||||
|
source.current = null;
|
||||||
|
}).catch(err => {
|
||||||
|
source.current = null;
|
||||||
|
});
|
||||||
|
}, [onUpdateSearchStatus, onUpdateSearchResults]);
|
||||||
|
|
||||||
|
const handleSearchInputChange = useCallback((e) => {
|
||||||
const newValue = e.target.value.trim();
|
const newValue = e.target.value.trim();
|
||||||
setInputValue(newValue);
|
setInputValue(newValue);
|
||||||
|
|
||||||
if (newValue.length === 0) {
|
if (newValue.length === 0) {
|
||||||
onUpdateSearchStatus(SearchStatus.IDLE);
|
onUpdateSearchResults([]);
|
||||||
setSearchResults([]);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
onUpdateSearchStatus(SearchStatus.LOADING);
|
onUpdateSearchStatus(SearchStatus.LOADING);
|
||||||
onPopoverToggle(true);
|
|
||||||
|
|
||||||
const queryData = {
|
const queryData = {
|
||||||
q: newValue,
|
q: newValue,
|
||||||
@@ -61,23 +75,7 @@ const Searcher = ({ searchStatus, onUpdateSearchStatus, onDirentItemClick, selec
|
|||||||
searchTimer.current = setTimeout(() => {
|
searchTimer.current = setTimeout(() => {
|
||||||
getSearchResult(queryData);
|
getSearchResult(queryData);
|
||||||
}, 500);
|
}, 500);
|
||||||
};
|
}, [onUpdateSearchStatus, onUpdateSearchResults, getSearchResult]);
|
||||||
|
|
||||||
const getSearchResult = useCallback((queryData) => {
|
|
||||||
if (source.current) {
|
|
||||||
source.current.cancel('prev request is cancelled');
|
|
||||||
}
|
|
||||||
|
|
||||||
source.current = seafileAPI.getSource();
|
|
||||||
seafileAPI.searchFiles(queryData, source.current.token).then(res => {
|
|
||||||
setSearchResults(res.data.total ? formatResultItems(res.data.results) : []);
|
|
||||||
onUpdateSearchStatus(res.data.results.length > 0 ? SearchStatus.RESULTS : SearchStatus.NO_RESULTS);
|
|
||||||
source.current = null;
|
|
||||||
}).catch(err => {
|
|
||||||
onUpdateSearchStatus(SearchStatus.NO_RESULTS);
|
|
||||||
source.current = null;
|
|
||||||
});
|
|
||||||
}, [onUpdateSearchStatus]);
|
|
||||||
|
|
||||||
const formatResultItems = (data) => {
|
const formatResultItems = (data) => {
|
||||||
let items = [];
|
let items = [];
|
||||||
@@ -96,66 +94,18 @@ const Searcher = ({ searchStatus, onUpdateSearchStatus, onDirentItemClick, selec
|
|||||||
return items;
|
return items;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = useCallback((e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (e.key === 'Enter' && inputValue.trim().length > 0) {
|
||||||
|
onUpdateMode(MODE_TYPE_MAP.SEARCH_RESULTS);
|
||||||
|
}
|
||||||
|
}, [inputValue, onUpdateMode]);
|
||||||
|
|
||||||
const onCloseSearching = useCallback(() => {
|
const onCloseSearching = useCallback(() => {
|
||||||
setInputValue('');
|
setInputValue('');
|
||||||
setSearchResults([]);
|
onClose();
|
||||||
onUpdateSearchStatus(SearchStatus.IDLE);
|
}, [onClose]);
|
||||||
onPopoverToggle(false);
|
|
||||||
selectSearchedItem(null);
|
|
||||||
}, [onUpdateSearchStatus, selectSearchedItem, onPopoverToggle]);
|
|
||||||
|
|
||||||
const onSearchedItemClick = (item) => {
|
|
||||||
item['type'] = item.is_dir ? 'dir' : 'file';
|
|
||||||
let repo = new RepoInfo(item);
|
|
||||||
onDirentItemClick(repo, item.path, item);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSearchedItemDoubleClick = (item) => {
|
|
||||||
if (item.type !== 'dir') return;
|
|
||||||
|
|
||||||
const selectedItemInfo = {
|
|
||||||
repoID: item.repo_id,
|
|
||||||
filePath: item.path,
|
|
||||||
};
|
|
||||||
|
|
||||||
selectSearchedItem(selectedItemInfo);
|
|
||||||
onPopoverToggle(false);
|
|
||||||
|
|
||||||
seafileAPI.getRepoInfo(item.repo_id).then(res => {
|
|
||||||
const repoInfo = new RepoInfo(res.data);
|
|
||||||
const path = item.path.substring(0, item.path.length - 1);
|
|
||||||
selectRepo(repoInfo);
|
|
||||||
setSelectedPath(path);
|
|
||||||
setBrowsingPath(item.path.substring(0, item.path.length - 1));
|
|
||||||
}).catch(err => {
|
|
||||||
const errMessage = Utils.getErrorMsg(err);
|
|
||||||
toaster.danger(errMessage);
|
|
||||||
});
|
|
||||||
|
|
||||||
onUpdateSearchStatus(SearchStatus.BROWSING);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderSearchResults = () => {
|
|
||||||
switch (searchStatus) {
|
|
||||||
case SearchStatus.IDLE:
|
|
||||||
return null;
|
|
||||||
case SearchStatus.LOADING:
|
|
||||||
return <Loading />;
|
|
||||||
case SearchStatus.NO_RESULTS:
|
|
||||||
return (
|
|
||||||
<div className='search-results-none'>{gettext('No results matching')}</div>
|
|
||||||
);
|
|
||||||
case SearchStatus.BROWSING:
|
|
||||||
case SearchStatus.RESULTS:
|
|
||||||
return (
|
|
||||||
<SearchedListView
|
|
||||||
searchResults={searchResults}
|
|
||||||
onItemClick={onSearchedItemClick}
|
|
||||||
onSearchedItemDoubleClick={onSearchedItemDoubleClick}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='search-container file-chooser-searcher' style={{ zIndex: SEARCH_CONTAINER }}>
|
<div className='search-container file-chooser-searcher' style={{ zIndex: SEARCH_CONTAINER }}>
|
||||||
@@ -168,39 +118,22 @@ const Searcher = ({ searchStatus, onUpdateSearchStatus, onDirentItemClick, selec
|
|||||||
type='text'
|
type='text'
|
||||||
value={inputValue}
|
value={inputValue}
|
||||||
onChange={handleSearchInputChange}
|
onChange={handleSearchInputChange}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
autoFocus
|
||||||
/>
|
/>
|
||||||
{inputValue.length !== 0 && (
|
{inputValue.length !== 0 && (
|
||||||
<span className="search-control attr-action-icon sf3-font sf3-font-x-01" onClick={onCloseSearching}></span>
|
<span className="search-control attr-action-icon sf3-font sf3-font-x-01" onClick={onCloseSearching}></span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{searchStatus !== SearchStatus.IDLE &&
|
|
||||||
<UncontrolledPopover
|
|
||||||
className='file-chooser-search-results-popover'
|
|
||||||
isOpen={isResultsPopoverOpen}
|
|
||||||
toggle={() => onPopoverToggle(!isResultsPopoverOpen)}
|
|
||||||
target={inputRef.current}
|
|
||||||
placement='bottom-start'
|
|
||||||
hideArrow={true}
|
|
||||||
fade={false}
|
|
||||||
trigger="legacy"
|
|
||||||
>
|
|
||||||
<div className='search-results-popover-body'>
|
|
||||||
{renderSearchResults()}
|
|
||||||
</div>
|
|
||||||
</UncontrolledPopover>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
Searcher.propTypes = {
|
Searcher.propTypes = {
|
||||||
searchStatus: PropTypes.string,
|
onUpdateMode: PropTypes.func,
|
||||||
onUpdateSearchStatus: PropTypes.func,
|
onUpdateSearchStatus: PropTypes.func,
|
||||||
onDirentItemClick: PropTypes.func,
|
onUpdateSearchResults: PropTypes.func,
|
||||||
selectSearchedItem: PropTypes.func,
|
onClose: PropTypes.func,
|
||||||
selectRepo: PropTypes.func,
|
|
||||||
setSelectedPath: PropTypes.func,
|
|
||||||
setBrowsingPath: PropTypes.func,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Searcher;
|
export default Searcher;
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import TreeListItem from './tree-list-item';
|
import TreeListItem from './tree-list-item';
|
||||||
import treeHelper from '../tree-view/tree-helper';
|
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
selectedPath: PropTypes.string,
|
selectedPath: PropTypes.string,
|
||||||
@@ -12,16 +11,12 @@ const propTypes = {
|
|||||||
onNodeCollapse: PropTypes.func.isRequired,
|
onNodeCollapse: PropTypes.func.isRequired,
|
||||||
onNodeExpanded: PropTypes.func.isRequired,
|
onNodeExpanded: PropTypes.func.isRequired,
|
||||||
fileSuffixes: PropTypes.array,
|
fileSuffixes: PropTypes.array,
|
||||||
isBrowsing: PropTypes.bool,
|
|
||||||
browsingPath: PropTypes.string,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class TreeListView extends React.Component {
|
class TreeListView extends React.Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
isBrowsing,
|
|
||||||
browsingPath,
|
|
||||||
treeData,
|
treeData,
|
||||||
selectedPath,
|
selectedPath,
|
||||||
onNodeCollapse,
|
onNodeCollapse,
|
||||||
@@ -32,10 +27,7 @@ class TreeListView extends React.Component {
|
|||||||
fileSuffixes
|
fileSuffixes
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const browsingNode = treeHelper.findNodeByPath(treeData, browsingPath);
|
const node = treeData.root;
|
||||||
if (isBrowsing && !browsingNode) return null;
|
|
||||||
|
|
||||||
const node = isBrowsing ? browsingNode : treeData.root;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="list-view-content">
|
<div className="list-view-content">
|
||||||
|
@@ -40,6 +40,7 @@ const propTypes = {
|
|||||||
onItemRename: PropTypes.func.isRequired,
|
onItemRename: PropTypes.func.isRequired,
|
||||||
showDirentDetail: PropTypes.func.isRequired,
|
showDirentDetail: PropTypes.func.isRequired,
|
||||||
isGroupOwnedRepo: PropTypes.bool.isRequired,
|
isGroupOwnedRepo: PropTypes.bool.isRequired,
|
||||||
|
onAddFolder: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
class SelectedDirentsToolbar extends React.Component {
|
class SelectedDirentsToolbar extends React.Component {
|
||||||
@@ -417,6 +418,7 @@ class SelectedDirentsToolbar extends React.Component {
|
|||||||
selectedDirentList={this.props.selectedDirentList}
|
selectedDirentList={this.props.selectedDirentList}
|
||||||
onItemsMove={this.props.onItemsMove}
|
onItemsMove={this.props.onItemsMove}
|
||||||
onCancelMove={this.onMoveToggle}
|
onCancelMove={this.onMoveToggle}
|
||||||
|
onAddFolder={this.props.onAddFolder}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
{this.state.isCopyDialogShow &&
|
{this.state.isCopyDialogShow &&
|
||||||
|
@@ -301,6 +301,57 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.custom-modal .modal-header {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1rem;
|
||||||
|
border-bottom: 1px solid #e5e5e5;
|
||||||
|
position: relative;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-modal .modal-header .modal-title {
|
||||||
|
max-width: calc(100% - 260px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-modal .modal-header .search {
|
||||||
|
position: absolute;
|
||||||
|
top: 16px;
|
||||||
|
right: 48px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-modal .modal-header .close {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
position: absolute;
|
||||||
|
right: 16px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 12px;
|
||||||
|
color: #666 !important;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-modal .modal-header .search:hover,
|
||||||
|
.custom-modal .modal-header .close:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #666 !important;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-modal .modal-header .close span{
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.custom-modal .modal-content {
|
.custom-modal .modal-content {
|
||||||
min-height: 534px;
|
min-height: 534px;
|
||||||
border: 0;
|
border: 0;
|
||||||
@@ -310,8 +361,8 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
justify-content: flex-start;
|
||||||
padding: 1rem 0;
|
padding: 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -378,9 +429,41 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-modal .file-list-col .file-chooser-container,
|
.custom-modal .file-list-col .file-chooser-container {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.custom-modal .file-list-col .file-chooser-search-input {
|
.custom-modal .file-list-col .file-chooser-search-input {
|
||||||
padding: 0 1rem;
|
padding: 1rem 1rem 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-modal .modal-footer {
|
||||||
|
position: relative;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-modal .move-dirent-dialog-footer {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-modal .modal-footer .footer-left-btn {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: none;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-modal .modal-footer .footer-left-btn.disabled {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-modal .modal-footer .footer-left-btn:not(.disabled):hover {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cur-view-container .cur-view-path {
|
.cur-view-container .cur-view-path {
|
||||||
|
@@ -959,7 +959,8 @@ class LibContentView extends React.Component {
|
|||||||
localStorage.setItem('recently-used-list', JSON.stringify(updatedRecentlyUsed));
|
localStorage.setItem('recently-used-list', JSON.stringify(updatedRecentlyUsed));
|
||||||
};
|
};
|
||||||
|
|
||||||
onAddFolder = (dirPath) => {
|
onAddFolder = (dirPath, options = {}) => {
|
||||||
|
const { successCallback = () => {} } = options;
|
||||||
let repoID = this.props.repoID;
|
let repoID = this.props.repoID;
|
||||||
seafileAPI.createDir(repoID, dirPath).then(() => {
|
seafileAPI.createDir(repoID, dirPath).then(() => {
|
||||||
let name = Utils.getFileName(dirPath);
|
let name = Utils.getFileName(dirPath);
|
||||||
@@ -972,6 +973,8 @@ class LibContentView extends React.Component {
|
|||||||
if (parentPath === this.state.path && !this.state.isViewFile) {
|
if (parentPath === this.state.path && !this.state.isViewFile) {
|
||||||
this.addDirent(name, 'dir');
|
this.addDirent(name, 'dir');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
successCallback();
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
let errMessage = Utils.getErrorMsg(error);
|
let errMessage = Utils.getErrorMsg(error);
|
||||||
toaster.danger(errMessage);
|
toaster.danger(errMessage);
|
||||||
@@ -2280,6 +2283,7 @@ class LibContentView extends React.Component {
|
|||||||
currentMode={this.state.currentMode}
|
currentMode={this.state.currentMode}
|
||||||
switchViewMode={this.switchViewMode}
|
switchViewMode={this.switchViewMode}
|
||||||
onItemConvert={this.onConvertItem}
|
onItemConvert={this.onConvertItem}
|
||||||
|
onAddFolder={this.onAddFolder}
|
||||||
/>
|
/>
|
||||||
:
|
:
|
||||||
<CurDirPath
|
<CurDirPath
|
||||||
@@ -2312,6 +2316,7 @@ class LibContentView extends React.Component {
|
|||||||
onItemMove={this.onMoveItem}
|
onItemMove={this.onMoveItem}
|
||||||
isDesktop={isDesktop}
|
isDesktop={isDesktop}
|
||||||
loadDirentList={this.loadDirentList}
|
loadDirentList={this.loadDirentList}
|
||||||
|
onAddFolderNode={this.onAddFolder}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
Reference in New Issue
Block a user