1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-13 13:50:07 +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:
Aries
2024-11-08 18:04:48 +08:00
committed by GitHub
parent 89760c7114
commit 9d4c9b8f4b
15 changed files with 503 additions and 233 deletions

View File

@@ -1,9 +1,15 @@
import React from 'react';
import PropTypes from 'prop-types';
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 { gettext } from '../../utils/constants';
import { gettext, isPro } from '../../utils/constants';
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 = {
path: PropTypes.string.isRequired,
@@ -14,6 +20,7 @@ const propTypes = {
onItemMove: PropTypes.func,
onItemsMove: PropTypes.func,
onCancelMove: PropTypes.func.isRequired,
onAddFolder: PropTypes.func,
};
class MoveDirent extends React.Component {
@@ -21,8 +28,14 @@ class MoveDirent extends React.Component {
constructor(props) {
super(props);
this.state = {
mode: MODE_TYPE_MAP.ONLY_CURRENT_LIBRARY,
repo: { repo_id: this.props.repoID },
selectedPath: this.props.path,
selectedSearchedItem: null,
selectedSearchedRepo: null,
searchStatus: '',
searchResults: [],
showSearchBar: false,
errMessage: '',
};
}
@@ -138,6 +151,100 @@ class MoveDirent extends React.Component {
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 = () => {
const { dirent, isMultipleOperation } = this.props;
let title = gettext('Move {placeholder} to');
@@ -151,6 +258,7 @@ class MoveDirent extends React.Component {
render() {
const { dirent, selectedDirentList, isMultipleOperation, path, repoID } = this.props;
const { mode, selectedPath, showSearchBar, searchStatus, searchResults, selectedSearchedRepo, errMessage } = this.state;
const movedDirent = dirent || selectedDirentList[0];
const { permission } = movedDirent;
const { isCustomPermission } = Utils.getUserPermission(permission);
@@ -158,19 +266,47 @@ class MoveDirent extends React.Component {
return (
<Modal className='custom-modal' isOpen={true} 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>
<SelectDirentBody
path={path}
selectedPath={this.state.selectedPath}
selectedPath={selectedPath}
repoID={repoID}
isSupportOtherLibraries={!isCustomPermission}
errMessage={this.state.errMessage}
errMessage={errMessage}
onCancel={this.toggle}
selectRepo={this.selectRepo}
setSelectedPath={this.setSelectedPath}
setErrMessage={this.setErrMessage}
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>
);

View File

@@ -2,16 +2,17 @@ import React from 'react';
import PropTypes from 'prop-types';
import { Button, ModalFooter, ModalBody, Alert, Row, Col } from 'reactstrap';
import toaster from '../toast';
import Searcher, { SearchStatus } from '../file-chooser/searcher';
import RepoListWrapper, { MODE_TYPE_MAP } from '../file-chooser/repo-list-wrapper';
import { seafileAPI } from '../../utils/seafile-api';
import { gettext, isPro } from '../../utils/constants';
import { gettext } from '../../utils/constants';
import { Utils } from '../../utils/utils';
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 (
<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>
</div>
);
@@ -22,15 +23,13 @@ class SelectDirentBody extends React.Component {
constructor(props) {
super(props);
this.state = {
mode: MODE_TYPE_MAP.ONLY_CURRENT_LIBRARY,
currentRepoInfo: null,
repoList: [],
selectedSearchedItem: null,
selectedRepo: null,
browsingPath: '',
searchStatus: SearchStatus.IDLE,
errMessage: '',
showCreateFolderDialog: false,
};
this.newFolderName = '';
}
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 () => {
try {
const res = await seafileAPI.listRepos();
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 selectedRepo = sortedRepoList.find((repo) => repo.repo_id === this.props.repoID);
this.setState({
@@ -69,29 +89,10 @@ class SelectDirentBody extends React.Component {
}
};
onUpdateSearchStatus = (status) => {
this.setState({ searchStatus: status });
};
onUpdateRepoList = (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 = () => {
if (this.props.handleSubmit) {
this.props.handleSubmit();
@@ -118,23 +119,60 @@ class SelectDirentBody extends React.Component {
this.setState({ selectedRepo: repo });
};
selectedMode = (mode) => {
const { repoID, path } = this.props;
// reset selecting status
this.props.selectRepo({ repo_id: repoID });
this.props.setSelectedPath(path);
onUpdateMode = (mode) => {
const { path } = this.props;
const { repoList } = this.state;
if (mode === MODE_TYPE_MAP.ONLY_OTHER_LIBRARIES) {
this.setState({
mode,
selectedSearchedItem: null,
searchStatus: SearchStatus.RESULTS,
selectedRepo: repoList[0],
});
this.props.setSelectedPath('/');
} else {
this.setState({ selectedRepo: this.state.currentRepoInfo });
this.props.setSelectedPath(path);
}
this.props.onUpdateMode(mode);
};
loadRepoDirentList = (repo) => {
try {
const { data } = seafileAPI.listDir(repo.repo_id, '/');
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() {
const { path, selectedPath, isSupportOtherLibraries, errMessage } = this.props;
const { mode, searchStatus, selectedSearchedItem, selectedRepo, repoList, currentRepoInfo, browsingPath } = this.state;
const { mode, path, selectedPath, isSupportOtherLibraries, errMessage, searchStatus, searchResults, selectedSearchedRepo } = this.props;
const { selectedSearchedItem, selectedRepo, repoList, currentRepoInfo } = this.state;
let repoListWrapperKey = 'repo-list-wrapper';
if (selectedSearchedItem && selectedSearchedItem.repoID) {
repoListWrapperKey = `${repoListWrapperKey}-${selectedSearchedItem.repoID}`;
@@ -143,47 +181,33 @@ class SelectDirentBody extends React.Component {
return (
<Row>
<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
mode={MODE_TYPE_MAP.ONLY_CURRENT_LIBRARY}
label={gettext('Current Library')}
currentMode={mode}
selectedMode={this.selectedMode}
onUpdateMode={this.onUpdateMode}
/>
{isSupportOtherLibraries && (
<LibraryOption
mode={MODE_TYPE_MAP.ONLY_OTHER_LIBRARIES}
label={gettext('Other Libraries')}
currentMode={mode}
selectedMode={this.selectedMode}
onUpdateMode={this.onUpdateMode}
/>
)}
<LibraryOption
mode={MODE_TYPE_MAP.RECENTLY_USED}
label={gettext('Recently Used')}
currentMode={mode}
selectedMode={this.selectedMode}
onUpdateMode={this.onUpdateMode}
/>
</Col>
<Col className='file-list-col'>
<ModalBody>
{currentRepoInfo && (
<RepoListWrapper
key={repoListWrapperKey}
mode={mode}
currentPath={path}
isBrowsing={searchStatus === SearchStatus.BROWSING}
browsingPath={browsingPath}
selectedItemInfo={selectedSearchedItem}
currentRepoInfo={currentRepoInfo}
selectedRepo={selectedRepo}
@@ -191,15 +215,41 @@ class SelectDirentBody extends React.Component {
repoList={repoList}
handleClickRepo={this.onRepoItemClick}
handleClickDirent={this.onDirentItemClick}
searchStatus={searchStatus}
searchResults={searchResults}
onSearchedItemClick={this.props.onSearchedItemClick}
onSearchedItemDoubleClick={this.props.onSearchedItemDoubleClick}
selectedSearchedRepo={selectedSearchedRepo}
newFolderName={this.newFolderName}
/>
)}
{errMessage && <Alert color="danger" className="alert-message">{errMessage}</Alert>}
</ModalBody>
<ModalFooter>
<Button color="secondary" onClick={this.onCancel}>{gettext('Cancel')}</Button>
<Button color="primary" onClick={this.handleSubmit}>{gettext('Submit')}</Button>
<ModalFooter className='move-dirent-dialog-footer'>
<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>
</Col>
{this.state.showCreateFolderDialog && (
<ModalPortal>
<CreateFolder
parentPath={this.props.selectedPath}
onAddFolder={this.createFolder}
checkDuplicatedName={this.checkDuplicatedName}
addFolderCancel={this.onToggleCreateFolder}
/>
</ModalPortal>
)}
</Row>
);
}
@@ -215,6 +265,14 @@ SelectDirentBody.propTypes = {
selectRepo: PropTypes.func,
setSelectedPath: 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 = {

View File

@@ -935,6 +935,7 @@ class DirentGridView extends React.Component {
onItemsMove={this.props.onItemsMove}
onCancelMove={this.onMoveToggle}
dirent={this.state.activeDirent}
onAddFolder={this.props.onAddFolder}
/>
}
{this.state.isZipDialogOpen &&

View File

@@ -968,6 +968,7 @@ class DirentListItem extends React.Component {
onItemMove={this.props.onItemMove}
onCancelMove={this.onItemMoveToggle}
repoEncrypted={this.props.repoEncrypted}
onAddFolder={this.props.onAddFolder}
/>
</ModalPortal>
}

View File

@@ -779,6 +779,7 @@ class DirentListView extends React.Component {
onItemsMove={this.props.onItemsMove}
onShowDirentsDraggablePreview={this.onShowDirentsDraggablePreview}
loadDirentList={this.props.loadDirentList}
onAddFolder={this.props.onAddFolder}
/>
);
})}
@@ -859,6 +860,7 @@ class DirentListView extends React.Component {
selectedDirentList={this.props.selectedDirentList}
onItemsMove={this.props.onItemsMove}
onCancelMove={this.onMoveToggle}
onAddFolder={this.props.onAddFolder}
/>
}
{this.state.isCopyDialogShow &&

View File

@@ -21,8 +21,7 @@ const propTypes = {
fileSuffixes: PropTypes.array,
selectedItemInfo: PropTypes.object,
hideLibraryName: PropTypes.bool,
isBrowsing: PropTypes.bool,
browsingPath: PropTypes.string,
newFolderName: PropTypes.string,
};
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() {
this.clearLoadRepoTimer();
this.setState({ isMounted: false, hasLoaded: false });
@@ -213,7 +232,7 @@ class RepoListItem extends React.Component {
return (
<li>
{!this.props.hideLibraryName && !this.props.isBrowsing &&
{!this.props.hideLibraryName &&
<div className={`${repoActive ? 'item-active' : ''} item-info`} onClick={this.onRepoItemClick}>
<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>
@@ -236,8 +255,6 @@ class RepoListItem extends React.Component {
treeData={this.state.treeData}
onNodeCollapse={this.onNodeCollapse}
onNodeExpanded={this.onNodeExpanded}
isBrowsing={this.props.isBrowsing}
browsingPath={this.props.browsingPath}
/>
)}
</li>

View File

@@ -15,8 +15,8 @@ const propTypes = {
fileSuffixes: PropTypes.array,
selectedItemInfo: PropTypes.object,
currentPath: PropTypes.string,
isBrowsing: PropTypes.bool,
browsingPath: PropTypes.string,
selectedSearchedRepo: PropTypes.object,
newFolderName: PropTypes.string,
};
const defaultProps = {
@@ -29,25 +29,28 @@ const defaultProps = {
fileSuffixes: [],
selectedItemInfo: null,
currentPath: '',
isBrowsing: false,
browsingPath: '',
};
class RepoListView extends React.Component {
render() {
let { currentRepoInfo, currentPath, repoList } = this.props;
let { currentRepoInfo, currentPath, repoList, selectedSearchedRepo } = this.props;
if (currentRepoInfo) {
repoList = [];
repoList.push(currentRepoInfo);
}
if (selectedSearchedRepo) {
repoList = [];
repoList.push(selectedSearchedRepo);
}
return (
<ul className="list-view-content file-chooser-item" >
{repoList.length > 0 && repoList.map((repoItem, index) => {
return (
<RepoListItem
key={index}
key={repoItem.repo_id}
isCurrentRepo={currentRepoInfo ? true : false}
currentPath={currentPath}
repo={repoItem}
@@ -59,8 +62,7 @@ class RepoListView extends React.Component {
isShowFile={this.props.isShowFile}
fileSuffixes={this.props.fileSuffixes}
selectedItemInfo={this.props.selectedItemInfo}
isBrowsing={this.props.isBrowsing}
browsingPath={this.props.browsingPath}
newFolderName={this.props.newFolderName}
/>
);
})}

View File

@@ -3,6 +3,9 @@ import PropTypes from 'prop-types';
import RepoListView from './repo-list-view';
import RecentlyUsedListView from './recently-used-list-view';
import { gettext } from '../../utils/constants';
import SearchedListView from './searched-list-view';
import { SearchStatus } from './searcher';
import Loading from '../loading';
export const MODE_TYPE_MAP = {
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_OTHER_LIBRARIES: 'only_other_libraries',
RECENTLY_USED: 'recently_used',
SEARCH_RESULTS: 'search_results',
};
const RepoListWrapper = (props) => {
const {
mode, isShowFile, fileSuffixes, currentPath, isBrowsing, browsingPath, isCurrentRepoShow, currentRepoInfo, selectedRepo,
mode, isShowFile, fileSuffixes, currentPath, isCurrentRepoShow, currentRepoInfo, selectedRepo,
selectedPath, isOtherRepoShow, selectedItemInfo, repoList,
searchStatus, searchResults, onSearchedItemClick, onSearchedItemDoubleClick, selectedSearchedRepo, newFolderName
} = props;
const renderRecentlyUsed = () => {
@@ -34,6 +39,29 @@ const RepoListWrapper = (props) => {
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 (
<div className='file-chooser-scroll-wrapper' onScroll={onScroll}>
<div className="file-chooser-container user-select-none">
@@ -91,10 +119,10 @@ const RepoListWrapper = (props) => {
isShowFile={isShowFile}
fileSuffixes={fileSuffixes}
selectedItemInfo={selectedItemInfo}
isBrowsing={isBrowsing}
browsingPath={browsingPath}
onRepoItemClick={props.handleClickRepo}
onDirentItemClick={props.handleClickDirent}
selectedSearchedRepo={selectedSearchedRepo}
newFolderName={newFolderName}
/>
</div>
)}
@@ -129,14 +157,19 @@ const RepoListWrapper = (props) => {
isShowFile={isShowFile}
fileSuffixes={fileSuffixes}
selectedItemInfo={selectedItemInfo}
isBrowsing={isBrowsing}
browsingPath={browsingPath}
onRepoItemClick={props.handleClickRepo}
onDirentItemClick={props.handleClickDirent}
selectedSearchedRepo={selectedSearchedRepo}
newFolderName={newFolderName}
/>
</div>
)}
{mode === MODE_TYPE_MAP.RECENTLY_USED && renderRecentlyUsed()}
{mode === MODE_TYPE_MAP.SEARCH_RESULTS && (
<div className="list-view">
{renderSearchResults()}
</div>
)}
</div>
</div>
);
@@ -147,8 +180,6 @@ RepoListWrapper.propTypes = {
currentPath: PropTypes.string,
isShowFile: PropTypes.bool,
fileSuffixes: PropTypes.array,
isBrowsing: PropTypes.bool,
browsingPath: PropTypes.string,
selectedItemInfo: PropTypes.object,
currentRepoInfo: PropTypes.object,
selectedRepo: PropTypes.object,
@@ -160,6 +191,12 @@ RepoListWrapper.propTypes = {
onOtherRepoToggle: PropTypes.func,
handleClickRepo: 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 = {

View File

@@ -1,8 +1,9 @@
.file-chooser-searcher.search-container {
display: flex;
flex-direction: column;
position: relative;
margin-bottom: 16px;
position: absolute;
top: 14px;
right: 48px;
}
.file-chooser-searcher .search-input-container {
@@ -15,14 +16,14 @@
.file-chooser-searcher .search-input-container .search-input {
width: 100%;
height: 2.375rem;
height: 1.875em;
padding-left: 2rem;
padding-right: 1.5rem;
}
.file-chooser-searcher .search-input-container .search-control {
position: absolute;
top: 12px;
top: 6px;
right: 8px;
font-size: 16px;
}

View File

@@ -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 { Input, UncontrolledPopover } from 'reactstrap';
import Loading from '../../loading';
import toaster from '../../toast';
import RepoInfo from '../../../models/repo-info';
import SearchedListView from '../searched-list-view';
import { Input } from 'reactstrap';
import { gettext } from '../../../utils/constants';
import { seafileAPI } from '../../../utils/seafile-api';
import { Utils } from '../../../utils/utils';
import { SEARCH_CONTAINER } from '../../../constants/zIndexes';
import { MODE_TYPE_MAP } from '../repo-list-wrapper';
import './index.css';
export const SearchStatus = {
IDLE: 'idle',
LOADING: 'loading',
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 [isResultsPopoverOpen, setIsResultsPopoverOpen] = useState(false);
const [searchResults, setSearchResults] = useState([]);
const inputRef = useRef(null);
const searchTimer = useRef(null);
const source = useRef(null);
const onPopoverToggle = useCallback((show) => {
setIsResultsPopoverOpen(show);
}, []);
useEffect(() => {
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();
setInputValue(newValue);
if (newValue.length === 0) {
onUpdateSearchStatus(SearchStatus.IDLE);
setSearchResults([]);
onUpdateSearchResults([]);
return;
}
onUpdateSearchStatus(SearchStatus.LOADING);
onPopoverToggle(true);
const queryData = {
q: newValue,
@@ -61,23 +75,7 @@ const Searcher = ({ searchStatus, onUpdateSearchStatus, onDirentItemClick, selec
searchTimer.current = setTimeout(() => {
getSearchResult(queryData);
}, 500);
};
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]);
}, [onUpdateSearchStatus, onUpdateSearchResults, getSearchResult]);
const formatResultItems = (data) => {
let items = [];
@@ -96,66 +94,18 @@ const Searcher = ({ searchStatus, onUpdateSearchStatus, onDirentItemClick, selec
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(() => {
setInputValue('');
setSearchResults([]);
onUpdateSearchStatus(SearchStatus.IDLE);
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}
/>
);
}
};
onClose();
}, [onClose]);
return (
<div className='search-container file-chooser-searcher' style={{ zIndex: SEARCH_CONTAINER }}>
@@ -168,39 +118,22 @@ const Searcher = ({ searchStatus, onUpdateSearchStatus, onDirentItemClick, selec
type='text'
value={inputValue}
onChange={handleSearchInputChange}
onKeyDown={handleKeyDown}
autoFocus
/>
{inputValue.length !== 0 && (
<span className="search-control attr-action-icon sf3-font sf3-font-x-01" onClick={onCloseSearching}></span>
)}
</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>
);
};
Searcher.propTypes = {
searchStatus: PropTypes.string,
onUpdateMode: PropTypes.func,
onUpdateSearchStatus: PropTypes.func,
onDirentItemClick: PropTypes.func,
selectSearchedItem: PropTypes.func,
selectRepo: PropTypes.func,
setSelectedPath: PropTypes.func,
setBrowsingPath: PropTypes.func,
onUpdateSearchResults: PropTypes.func,
onClose: PropTypes.func,
};
export default Searcher;

View File

@@ -1,7 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import TreeListItem from './tree-list-item';
import treeHelper from '../tree-view/tree-helper';
const propTypes = {
selectedPath: PropTypes.string,
@@ -12,16 +11,12 @@ const propTypes = {
onNodeCollapse: PropTypes.func.isRequired,
onNodeExpanded: PropTypes.func.isRequired,
fileSuffixes: PropTypes.array,
isBrowsing: PropTypes.bool,
browsingPath: PropTypes.string,
};
class TreeListView extends React.Component {
render() {
const {
isBrowsing,
browsingPath,
treeData,
selectedPath,
onNodeCollapse,
@@ -32,10 +27,7 @@ class TreeListView extends React.Component {
fileSuffixes
} = this.props;
const browsingNode = treeHelper.findNodeByPath(treeData, browsingPath);
if (isBrowsing && !browsingNode) return null;
const node = isBrowsing ? browsingNode : treeData.root;
const node = treeData.root;
return (
<div className="list-view-content">

View File

@@ -40,6 +40,7 @@ const propTypes = {
onItemRename: PropTypes.func.isRequired,
showDirentDetail: PropTypes.func.isRequired,
isGroupOwnedRepo: PropTypes.bool.isRequired,
onAddFolder: PropTypes.func.isRequired,
};
class SelectedDirentsToolbar extends React.Component {
@@ -417,6 +418,7 @@ class SelectedDirentsToolbar extends React.Component {
selectedDirentList={this.props.selectedDirentList}
onItemsMove={this.props.onItemsMove}
onCancelMove={this.onMoveToggle}
onAddFolder={this.props.onAddFolder}
/>
}
{this.state.isCopyDialogShow &&

View File

@@ -301,6 +301,57 @@
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 {
min-height: 534px;
border: 0;
@@ -310,8 +361,8 @@
height: 100%;
display: flex;
flex-direction: column;
gap: 1rem;
padding: 1rem 0;
justify-content: flex-start;
padding: 0;
position: relative;
}
@@ -378,9 +429,41 @@
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 {
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 {

View File

@@ -959,7 +959,8 @@ class LibContentView extends React.Component {
localStorage.setItem('recently-used-list', JSON.stringify(updatedRecentlyUsed));
};
onAddFolder = (dirPath) => {
onAddFolder = (dirPath, options = {}) => {
const { successCallback = () => {} } = options;
let repoID = this.props.repoID;
seafileAPI.createDir(repoID, dirPath).then(() => {
let name = Utils.getFileName(dirPath);
@@ -972,6 +973,8 @@ class LibContentView extends React.Component {
if (parentPath === this.state.path && !this.state.isViewFile) {
this.addDirent(name, 'dir');
}
successCallback();
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
@@ -2280,6 +2283,7 @@ class LibContentView extends React.Component {
currentMode={this.state.currentMode}
switchViewMode={this.switchViewMode}
onItemConvert={this.onConvertItem}
onAddFolder={this.onAddFolder}
/>
:
<CurDirPath
@@ -2312,6 +2316,7 @@ class LibContentView extends React.Component {
onItemMove={this.onMoveItem}
isDesktop={isDesktop}
loadDirentList={this.loadDirentList}
onAddFolderNode={this.onAddFolder}
/>
}
</div>