1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-08-27 03:01:26 +00:00

Improve move and copy dialog ui components (#6467)

* implement catalog view in move dialog

* update move dialog ui components

* improve move dialog ui

* remove debug info

* improve copy dialog ui

* improve search view ui in move and copy dialog, refactor part of file-chooser.js and file-chooser.css

* handle cases where repo_name is too long, truncate text with an ellipsis, remove search container border

* handle cases where repo_name too long in search result item

* update move and dialog ui details

* add radius to repo list item
This commit is contained in:
Aries 2024-08-05 10:48:16 +08:00 committed by GitHub
parent 044124e2d8
commit 8623e01e99
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 679 additions and 413 deletions

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Button, Modal, ModalHeader, ModalFooter, ModalBody, Alert } from 'reactstrap'; import { Button, Modal, ModalHeader, ModalFooter, ModalBody, Alert, Row, Col } from 'reactstrap';
import { gettext } from '../../utils/constants'; import { gettext } from '../../utils/constants';
import { Utils } from '../../utils/utils'; import { Utils } from '../../utils/utils';
import FileChooser from '../file-chooser/file-chooser'; import FileChooser from '../file-chooser/file-chooser';
@ -25,17 +25,11 @@ class CopyDirent extends React.Component {
this.state = { this.state = {
repo: { repo_id: this.props.repoID }, repo: { repo_id: this.props.repoID },
selectedPath: this.props.path, selectedPath: this.props.path,
errMessage: '' errMessage: '',
mode: 'only_current_library',
}; };
} }
shouldComponentUpdate(nextProps, nextState) {
if (this.state.errMessage === nextState.errMessage) {
return false;
}
return true;
}
handleSubmit = () => { handleSubmit = () => {
if (this.props.isMultipleOperation) { if (this.props.isMultipleOperation) {
this.copyItems(); this.copyItems();
@ -137,34 +131,64 @@ class CopyDirent extends React.Component {
}); });
}; };
render() { onSelectedMode = (mode) => {
this.setState({ mode: mode });
};
renderTitle = () => {
const { dirent, isMultipleOperation } = this.props;
let title = gettext('Copy {placeholder} to'); let title = gettext('Copy {placeholder} to');
if (!this.props.isMultipleOperation) {
title = title.replace('{placeholder}', '<span class="op-target text-truncate mx-1">' + Utils.HTMLescape(this.props.dirent.name) + '</span>'); if (isMultipleOperation) {
return gettext('Copy selected item(s) to:');
} else { } else {
title = gettext('Copy selected item(s) to:'); return title.replace('{placeholder}', `<span class="op-target text-truncate mx-1">${Utils.HTMLescape(dirent.name)}</span>`);
} }
let mode = 'current_repo_and_other_repos'; };
const { isMultipleOperation } = this.props;
render() {
const { dirent, selectedDirentList, isMultipleOperation, repoID, path } = this.props;
const { mode, errMessage } = this.state;
const copiedDirent = dirent || selectedDirentList[0];
const { permission } = copiedDirent;
const { isCustomPermission } = Utils.getUserPermission(permission);
const LibraryOption = ({ mode, label }) => (
<div className={`repo-list-item ${this.state.mode === mode ? 'active' : ''}`} onClick={() => this.onSelectedMode(mode)}>
<span className='library'>{label}</span>
</div>
);
return ( return (
<Modal isOpen={true} toggle={this.toggle}> <Modal className='custom-modal' isOpen={true} toggle={this.toggle}>
<ModalHeader toggle={this.toggle}> <ModalHeader toggle={this.toggle}>
{isMultipleOperation ? title : <div dangerouslySetInnerHTML={{ __html: title }} className="d-flex mw-100"></div>} {isMultipleOperation ? this.renderTitle() : <div dangerouslySetInnerHTML={{ __html: this.renderTitle() }} className="d-flex mw-100"></div>}
</ModalHeader> </ModalHeader>
<ModalBody> <Row>
<FileChooser <Col className='repo-list-col border-right'>
repoID={this.props.repoID} <LibraryOption mode='only_current_library' label={gettext('Current Library')} />
currentPath={this.props.path} {!isCustomPermission && <LibraryOption mode='only_other_libraries' label={gettext('Other Libraries')} />}
onDirentItemClick={this.onDirentItemClick} <LibraryOption mode='recently_used' label={gettext('Recently Used')} />
onRepoItemClick={this.onRepoItemClick} </Col>
mode={mode} <Col className='file-list-col'>
/> <ModalBody>
{this.state.errMessage && <Alert color="danger" className="mt-2">{this.state.errMessage}</Alert>} <FileChooser
</ModalBody> repoID={repoID}
<ModalFooter> currentPath={path}
<Button color="secondary" onClick={this.toggle}>{gettext('Cancel')}</Button> onDirentItemClick={this.onDirentItemClick}
<Button color="primary" onClick={this.handleSubmit}>{gettext('Submit')}</Button> onRepoItemClick={this.onRepoItemClick}
</ModalFooter> mode={mode}
hideLibraryName={false}
/>
{errMessage && <Alert color="danger" className="mt-2">{errMessage}</Alert>}
</ModalBody>
<ModalFooter>
<Button color="secondary" onClick={this.toggle}>{gettext('Cancel')}</Button>
<Button color="primary" onClick={this.handleSubmit}>{gettext('Submit')}</Button>
</ModalFooter>
</Col>
</Row>
</Modal> </Modal>
); );
} }

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Button, Modal, ModalHeader, ModalFooter, ModalBody, Alert } from 'reactstrap'; import { Button, Modal, ModalHeader, ModalFooter, ModalBody, Alert, Row, Col } from 'reactstrap';
import { gettext } from '../../utils/constants'; import { gettext } from '../../utils/constants';
import { Utils } from '../../utils/utils'; import { Utils } from '../../utils/utils';
import FileChooser from '../file-chooser/file-chooser'; import FileChooser from '../file-chooser/file-chooser';
@ -25,17 +25,11 @@ class MoveDirent extends React.Component {
this.state = { this.state = {
repo: { repo_id: this.props.repoID }, repo: { repo_id: this.props.repoID },
selectedPath: this.props.path, selectedPath: this.props.path,
errMessage: '' errMessage: '',
mode: 'only_current_library',
}; };
} }
shouldComponentUpdate(nextProps, nextState) {
if (this.state.errMessage === nextState.errMessage) {
return false;
}
return true;
}
handleSubmit = () => { handleSubmit = () => {
if (this.props.isMultipleOperation) { if (this.props.isMultipleOperation) {
this.moveItems(); this.moveItems();
@ -151,40 +145,64 @@ class MoveDirent extends React.Component {
}); });
}; };
render() { onSelectedMode = (mode) => {
this.setState({ mode: mode });
};
renderTitle = () => {
const { dirent, isMultipleOperation } = this.props;
let title = gettext('Move {placeholder} to'); let title = gettext('Move {placeholder} to');
if (!this.props.isMultipleOperation) {
title = title.replace('{placeholder}', '<span class="op-target text-truncate mx-1">' + Utils.HTMLescape(this.props.dirent.name) + '</span>'); if (isMultipleOperation) {
return gettext('Move selected item(s) to:');
} else { } else {
title = gettext('Move selected item(s) to:'); return title.replace('{placeholder}', `<span class="op-target text-truncate mx-1">${Utils.HTMLescape(dirent.name)}</span>`);
} }
let mode = 'current_repo_and_other_repos'; };
const { dirent, selectedDirentList, isMultipleOperation } = this.props;
const movedDirent = dirent ? dirent : selectedDirentList[0]; render() {
const { dirent, selectedDirentList, isMultipleOperation, repoID, path } = this.props;
const { mode, errMessage } = this.state;
const movedDirent = dirent || selectedDirentList[0];
const { permission } = movedDirent; const { permission } = movedDirent;
const { isCustomPermission } = Utils.getUserPermission(permission); const { isCustomPermission } = Utils.getUserPermission(permission);
if (isCustomPermission) {
mode = 'only_current_library'; const LibraryOption = ({ mode, label }) => (
} <div className={`repo-list-item ${this.state.mode === mode ? 'active' : ''}`} onClick={() => this.onSelectedMode(mode)}>
<span className='library'>{label}</span>
</div>
);
return ( return (
<Modal isOpen={true} toggle={this.toggle}> <Modal className='custom-modal' isOpen={true} toggle={this.toggle}>
<ModalHeader toggle={this.toggle}> <ModalHeader toggle={this.toggle}>
{isMultipleOperation ? title : <div dangerouslySetInnerHTML={{ __html: title }} className="d-flex mw-100"></div>} {isMultipleOperation ? this.renderTitle() : <div dangerouslySetInnerHTML={{ __html: this.renderTitle() }} className='d-flex mw-100'></div>}
</ModalHeader> </ModalHeader>
<ModalBody> <Row>
<FileChooser <Col className='repo-list-col border-right' >
repoID={this.props.repoID} <LibraryOption mode='only_current_library' label={gettext('Current Library')} />
currentPath={this.props.path} {!isCustomPermission && <LibraryOption mode='only_other_libraries' label={gettext('Other Libraries')} />}
onDirentItemClick={this.onDirentItemClick} <LibraryOption mode='recently_used' label={gettext('Recently Used')} />
onRepoItemClick={this.onRepoItemClick} </Col>
mode={mode} <Col className='file-list-col'>
/> <ModalBody>
{this.state.errMessage && <Alert color="danger" className="mt-2">{this.state.errMessage}</Alert>} <FileChooser
</ModalBody> repoID={repoID}
<ModalFooter> currentPath={path}
<Button color="secondary" onClick={this.toggle}>{gettext('Cancel')}</Button> onDirentItemClick={this.onDirentItemClick}
<Button color="primary" onClick={this.handleSubmit}>{gettext('Submit')}</Button> onRepoItemClick={this.onRepoItemClick}
</ModalFooter> mode={mode}
hideLibraryName={false}
/>
{errMessage && <Alert color="danger" className="alert-message">{errMessage}</Alert>}
</ModalBody>
<ModalFooter>
<Button color="secondary" onClick={this.toggle}>{gettext('Cancel')}</Button>
<Button color="primary" onClick={this.handleSubmit}>{gettext('Submit')}</Button>
</ModalFooter>
</Col>
</Row>
</Modal> </Modal>
); );
} }

View File

@ -821,6 +821,7 @@ class DirentGridView extends React.Component {
repoEncrypted={this.props.currentRepoInfo.encrypted} repoEncrypted={this.props.currentRepoInfo.encrypted}
isMultipleOperation={this.state.isMultipleOperation} isMultipleOperation={this.state.isMultipleOperation}
selectedDirentList={selectedDirentList} selectedDirentList={selectedDirentList}
onItemMove={this.props.onItemMove}
onItemsMove={this.props.onItemsMove} onItemsMove={this.props.onItemsMove}
onCancelMove={this.onMoveToggle} onCancelMove={this.onMoveToggle}
dirent={this.state.activeDirent} dirent={this.state.activeDirent}

View File

@ -18,11 +18,27 @@ const propTypes = {
repoID: PropTypes.string, repoID: PropTypes.string,
onDirentItemClick: PropTypes.func, onDirentItemClick: PropTypes.func,
onRepoItemClick: PropTypes.func, onRepoItemClick: PropTypes.func,
mode: PropTypes.oneOf(['current_repo_and_other_repos', 'only_all_repos', 'only_current_library']), mode: PropTypes.oneOf([
fileSuffixes: PropTypes.array, 'current_repo_and_other_repos',
'only_all_repos',
'only_current_library',
'only_other_libraries',
'recently_used'
]).isRequired,
fileSuffixes: PropTypes.arrayOf(PropTypes.string),
currentPath: PropTypes.string, currentPath: PropTypes.string,
}; };
const defaultProps = {
isShowFile: false,
hideLibraryName: false,
repoID: '',
onDirentItemClick: () => {},
onRepoItemClick: () => {},
fileSuffixes: [],
currentPath: '',
};
class FileChooser extends React.Component { class FileChooser extends React.Component {
constructor(props) { constructor(props) {
@ -46,71 +62,95 @@ class FileChooser extends React.Component {
this.source = null; this.source = null;
} }
componentDidMount() { async componentDidMount() {
if (this.props.repoID) { // current_repo_and_other_repos, only_current_library const { repoID } = this.props;
let repoID = this.props.repoID;
seafileAPI.getRepoInfo(repoID).then(res => { const fetchRepoInfo = async (repoID) => {
// need to optimized try {
let repoInfo = new RepoInfo(res.data); const res = await seafileAPI.getRepoInfo(repoID);
const repoInfo = new RepoInfo(res.data);
this.setState({ this.setState({
currentRepoInfo: repoInfo, currentRepoInfo: repoInfo,
selectedRepo: repoInfo selectedRepo: repoInfo,
}); });
}).catch(error => { } catch (error) {
let errMessage = Utils.getErrorMsg(error); const errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage); toaster.danger(errMessage);
}); }
} else { // only_all_repos };
seafileAPI.listRepos().then(res => {
let repos = res.data.repos; const fetchRepoList = async () => {
let repoList = []; try {
let repoIdList = []; const res = await seafileAPI.listRepos();
for (let i = 0; i < repos.length; i++) { const repos = res.data.repos;
if (repos[i].permission !== 'rw') { const repoList = [];
continue; const repoIdList = [];
repos.forEach(repo => {
if (repo.permission !== 'rw' || repoIdList.includes(repo.repo_id)) {
return;
} }
if (repoIdList.indexOf(repos[i].repo_id) > -1) { repoList.push(repo);
continue; repoIdList.push(repo.repo_id);
} });
repoList.push(repos[i]);
repoIdList.push(repos[i].repo_id); const sortedRepoList = Utils.sortRepos(repoList, 'name', 'asc');
} this.setState({ repoList: sortedRepoList });
repoList = Utils.sortRepos(repoList, 'name', 'asc'); } catch (error) {
this.setState({ repoList: repoList }); const errMessage = Utils.getErrorMsg(error);
}); toaster.danger(errMessage);
}
};
if (repoID) {
await fetchRepoInfo(repoID);
} else {
await fetchRepoList();
} }
} }
onOtherRepoToggle = () => { componentDidUpdate(prevProps, prevState) {
if (!this.state.hasRequest) { if (prevProps.mode !== this.props.mode) {
let that = this; this.setState({
seafileAPI.listRepos().then(res => { isSearching: false,
// todo optimized code isResultGot: false,
let repos = res.data.repos; searchInfo: '',
let repoList = []; searchResults: [],
let repoIdList = [];
for (let i = 0; i < repos.length; i++) {
if (repos[i].permission !== 'rw') {
continue;
}
if (that.props.repoID && (repos[i].repo_name === that.state.currentRepoInfo.repo_name)) {
continue;
}
if (repoIdList.indexOf(repos[i].repo_id) > -1) {
continue;
}
repoList.push(repos[i]);
repoIdList.push(repos[i].repo_id);
}
repoList = Utils.sortRepos(repoList, 'name', 'asc');
this.setState({
repoList: repoList,
isOtherRepoShow: !this.state.isOtherRepoShow,
selectedItemInfo: {}
});
}); });
if (this.props.mode === 'only_other_libraries') {
this.onOtherRepoToggle();
}
} }
else { }
onOtherRepoToggle = async () => {
if (!this.state.hasRequest) {
try {
const res = await seafileAPI.listRepos();
const repos = res.data.repos;
const repoList = [];
const repoIdList = [];
repos.forEach(repo => {
if (repo.permission !== 'rw') return;
if (this.props.repoID && repo.repo_name === this.state.currentRepoInfo.repo_name) return;
if (repoIdList.includes(repo.repo_id)) return;
repoList.push(repo);
repoIdList.push(repo.repo_id);
});
const sortedRepoList = Utils.sortRepos(repoList, 'name', 'asc');
this.setState({
repoList: sortedRepoList,
isOtherRepoShow: !this.state.isOtherRepoShow,
selectedItemInfo: {},
});
} catch (error) {
const errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
}
} else {
this.setState({ isOtherRepoShow: !this.state.isOtherRepoShow }); this.setState({ isOtherRepoShow: !this.state.isOtherRepoShow });
} }
}; };
@ -151,32 +191,22 @@ class FileChooser extends React.Component {
onSearchInfoChanged = (event) => { onSearchInfoChanged = (event) => {
let searchInfo = event.target.value.trim(); let searchInfo = event.target.value.trim();
this.setState({ searchInfo: searchInfo });
if (this.inputValue === searchInfo) {
return false;
}
this.inputValue = searchInfo;
if (searchInfo.length === 0) {
this.setState({
isSearching: false,
searchResults: [],
});
return false;
}
if (!this.state.searchResults.length && searchInfo.length > 0) { if (!this.state.searchResults.length && searchInfo.length > 0) {
this.setState({ this.setState({
isSearching: true, isSearching: true,
isResultGot: false, isResultGot: false,
}); });
} }
this.setState({ searchInfo: searchInfo });
if (this.inputValue === searchInfo) {
return false;
}
this.inputValue = searchInfo;
if (this.inputValue === '' || this.getValueLength(this.inputValue) < 3) { if (this.inputValue === '' || this.getValueLength(this.inputValue) < 3) {
this.setState({ isResultGot: false }); this.setState({
isResultGot: false,
});
return false; return false;
} }
@ -297,205 +327,248 @@ class FileChooser extends React.Component {
} }
}; };
onSearchedItemDoubleClick = (item) => { onSearchedItemDoubleClick = async (item) => {
if (item.type !== 'dir') { if (item.type !== 'dir') {
return; return;
} }
let selectedItemInfo = { const { repoID } = this.props;
const { hasRequest, currentRepoInfo } = this.state;
const selectedItemInfo = {
repoID: item.repo_id, repoID: item.repo_id,
filePath: item.path, filePath: item.path,
}; };
this.setState({ this.setState({ selectedItemInfo });
selectedItemInfo: selectedItemInfo
});
if (this.props.repoID && item.repo_id === this.props.repoID) { const updateStateForRepo = (repoInfo, path) => {
seafileAPI.getRepoInfo(this.props.repoID).then(res => { this.setState({
// need to optimized selectedRepo: repoInfo,
let repoInfo = new RepoInfo(res.data); selectedPath: path,
let path = item.path.substring(0, (item.path.length - 1)); isCurrentRepoShow: true,
});
};
const handleError = (error) => {
const errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
};
const fetchRepoInfo = async () => {
try {
const res = await seafileAPI.getRepoInfo(repoID);
const repoInfo = new RepoInfo(res.data);
const path = item.path.substring(0, item.path.length - 1);
updateStateForRepo(repoInfo, path);
} catch (error) {
handleError(error);
}
};
const fetchRepoList = async () => {
try {
const res = await seafileAPI.listRepos();
const repos = res.data.repos;
const repoList = [];
const repoIdList = [];
repos.forEach(repo => {
if (repo.permission !== 'rw') return;
if (repoID && repo.repo_name === currentRepoInfo.repo_name) return;
if (repoIdList.includes(repo.repo_id)) return;
repoList.push(repo);
repoIdList.push(repo.repo_id);
});
const sortedRepoList = Utils.sortRepos(repoList, 'name', 'asc');
const selectedRepo = sortedRepoList.find(repo => repo.repo_id === item.repo_id);
const path = item.path.substring(0, item.path.length - 1);
this.setState({ this.setState({
selectedRepo: repoInfo, repoList: sortedRepoList,
isOtherRepoShow: true,
selectedPath: path, selectedPath: path,
isCurrentRepoShow: true, selectedRepo,
});
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
} else {
if (!this.state.hasRequest) {
let that = this;
seafileAPI.listRepos().then(res => {
// todo optimized code
let repos = res.data.repos;
let repoList = [];
let repoIdList = [];
for (let i = 0; i < repos.length; i++) {
if (repos[i].permission !== 'rw') {
continue;
}
if (that.props.repoID && (repos[i].repo_name === that.state.currentRepoInfo.repo_name)) {
continue;
}
if (repoIdList.indexOf(repos[i].repo_id) > -1) {
continue;
}
repoList.push(repos[i]);
repoIdList.push(repos[i].repo_id);
}
repoList = Utils.sortRepos(repoList, 'name', 'asc');
let repo = repoList.filter(repoItem => repoItem.repo_id === item.repo_id);
let path = item.path.substring(0, (item.path.length - 1));
let selectRepo = repo[0];
this.setState({
repoList: repoList,
isOtherRepoShow: true,
selectedPath: path,
selectedRepo: selectRepo,
});
}); });
} catch (error) {
handleError(error);
} }
else { };
this.setState({ isOtherRepoShow: !this.state.isOtherRepoShow });
if (repoID && item.repo_id === repoID) {
await fetchRepoInfo();
} else {
if (!hasRequest) {
await fetchRepoList();
} else {
this.setState(prevState => ({ isOtherRepoShow: !prevState.isOtherRepoShow }));
} }
} }
this.onCloseSearching(); this.onCloseSearching();
}; };
onScroll = (event) => { onScroll = (event) => {
event.stopPropagation(); event.stopPropagation();
}; };
renderRepoListView = () => { renderRepoListView = () => {
const { mode, currentPath, isShowFile, fileSuffixes } = this.props;
const { isCurrentRepoShow, isOtherRepoShow, currentRepoInfo, repoList, selectedRepo, selectedPath, selectedItemInfo } = this.state;
const recentlyUsedRepos = JSON.parse(localStorage.getItem('recently-used-repos')) || [];
return ( return (
<div className="file-chooser-container user-select-none" onScroll={this.onScroll}> <div className='scroll-wrapper' onScroll={this.onScroll}>
{this.props.mode === 'current_repo_and_other_repos' && ( <div className="file-chooser-container user-select-none" >
<Fragment> {mode === 'current_repo_and_other_repos' && (
<div className="list-view"> <Fragment>
<div className="list-view-header"> <div className="list-view">
<span className={`item-toggle sf3-font ${this.state.isCurrentRepoShow ? 'sf3-font-down' : 'sf3-font-down rotate-270 d-inline-block'}`} onClick={this.onCurrentRepoToggle}></span> <div className="list-view-header">
<span className="library">{gettext('Current Library')}</span> <span className={`item-toggle sf3-font ${isCurrentRepoShow ? 'sf3-font-down' : 'sf3-font-down rotate-270 d-inline-block'}`} onClick={this.onCurrentRepoToggle}></span>
<span className="library">{gettext('Current Library')}</span>
</div>
{
isCurrentRepoShow && currentRepoInfo &&
<RepoListView
initToShowChildren={true}
currentRepoInfo={currentRepoInfo}
currentPath={currentPath}
selectedRepo={selectedRepo}
selectedPath={selectedPath}
onRepoItemClick={this.onRepoItemClick}
onDirentItemClick={this.onDirentItemClick}
isShowFile={isShowFile}
fileSuffixes={fileSuffixes}
selectedItemInfo={selectedItemInfo}
/>
}
</div> </div>
{ <div className="list-view">
this.state.isCurrentRepoShow && this.state.currentRepoInfo && <div className="list-view-header">
<RepoListView <span className={`item-toggle sf3-font ${isOtherRepoShow ? 'sf3-font-down' : 'sf3-font-down rotate-270 d-inline-block'}`} onClick={this.onOtherRepoToggle}></span>
initToShowChildren={true} <span className="library">{gettext('Other Libraries')}</span>
currentRepoInfo={this.state.currentRepoInfo} </div>
currentPath={this.props.currentPath} {
selectedRepo={this.state.selectedRepo} isOtherRepoShow &&
selectedPath={this.state.selectedPath} <RepoListView
onRepoItemClick={this.onRepoItemClick} initToShowChildren={false}
onDirentItemClick={this.onDirentItemClick} repoList={repoList}
isShowFile={this.props.isShowFile} selectedRepo={selectedRepo}
fileSuffixes={this.props.fileSuffixes} selectedPath={selectedPath}
selectedItemInfo={this.state.selectedItemInfo} onRepoItemClick={this.onRepoItemClick}
/> onDirentItemClick={this.onDirentItemClick}
} isShowFile={isShowFile}
</div> fileSuffixes={fileSuffixes}
<div className="list-view"> selectedItemInfo={selectedItemInfo}
<div className="list-view-header"> />
<span className={`item-toggle sf3-font ${this.state.isOtherRepoShow ? 'sf3-font-down' : 'sf3-font-down rotate-270 d-inline-block'}`} onClick={this.onOtherRepoToggle}></span> }
<span className="library">{gettext('Other Libraries')}</span>
</div> </div>
{ </Fragment>
this.state.isOtherRepoShow && )}
<RepoListView {mode === 'only_current_library' && (
initToShowChildren={false} <div className="list-view">
repoList={this.state.repoList}
selectedRepo={this.state.selectedRepo}
selectedPath={this.state.selectedPath}
onRepoItemClick={this.onRepoItemClick}
onDirentItemClick={this.onDirentItemClick}
isShowFile={this.props.isShowFile}
fileSuffixes={this.props.fileSuffixes}
selectedItemInfo={this.state.selectedItemInfo}
/>
}
</div>
</Fragment>
)}
{this.props.mode === 'only_current_library' && (
<div className="list-view">
{!this.props.hideLibraryName &&
<div className="list-view-header">
<span className={`item-toggle sf3-font ${this.state.isCurrentRepoShow ? 'sf3-font-down' : 'sf3-font-down rotate-270 d-inline-block'}`} onClick={this.onCurrentRepoToggle}></span>
<span className="library">{gettext('Current Library')}</span>
</div>
}
{
this.state.isCurrentRepoShow && this.state.currentRepoInfo &&
<RepoListView <RepoListView
initToShowChildren={true} initToShowChildren={true}
currentRepoInfo={this.state.currentRepoInfo} currentRepoInfo={currentRepoInfo}
currentPath={this.props.currentPath} currentPath={currentPath}
selectedRepo={this.state.selectedRepo} selectedRepo={selectedRepo}
selectedPath={this.state.selectedPath} selectedPath={selectedPath}
onRepoItemClick={this.onRepoItemClick} onRepoItemClick={this.onRepoItemClick}
onDirentItemClick={this.onDirentItemClick} onDirentItemClick={this.onDirentItemClick}
isShowFile={this.props.isShowFile} isShowFile={isShowFile}
fileSuffixes={this.props.fileSuffixes} fileSuffixes={fileSuffixes}
selectedItemInfo={this.state.selectedItemInfo} selectedItemInfo={selectedItemInfo}
hideLibraryName={this.props.hideLibraryName}
/>
}
</div>
)}
{this.props.mode === 'only_all_repos' && (
<div className="file-chooser-container">
<div className="list-view">
<div className="list-view-header">
<span className="item-toggle sf3-font sf3-font-down"></span>
<span className="library">{gettext('Libraries')}</span>
</div>
<RepoListView
initToShowChildren={false}
repoList={this.state.repoList}
selectedRepo={this.state.selectedRepo}
selectedPath={this.state.selectedPath}
onRepoItemClick={this.onRepoItemClick}
onDirentItemClick={this.onDirentItemClick}
isShowFile={this.props.isShowFile}
fileSuffixes={this.props.fileSuffixes}
selectedItemInfo={this.state.selectedItemInfo}
/> />
</div> </div>
</div> )}
)} {mode === 'only_all_repos' && (
<div className="file-chooser-container">
<div className="list-view">
<div className="list-view-header">
<span className="item-toggle sf3-font sf3-font-down"></span>
<span className="library">{gettext('Libraries')}</span>
</div>
<RepoListView
initToShowChildren={false}
repoList={repoList}
selectedRepo={selectedRepo}
selectedPath={selectedPath}
onRepoItemClick={this.onRepoItemClick}
onDirentItemClick={this.onDirentItemClick}
isShowFile={isShowFile}
fileSuffixes={fileSuffixes}
selectedItemInfo={selectedItemInfo}
/>
</div>
</div>
)}
{mode === 'only_other_libraries' && (
<div className="list-view">
<RepoListView
initToShowChildren={false}
repoList={repoList}
selectedRepo={selectedRepo}
selectedPath={selectedPath}
onRepoItemClick={this.onRepoItemClick}
onDirentItemClick={this.onDirentItemClick}
isShowFile={isShowFile}
fileSuffixes={fileSuffixes}
selectedItemInfo={selectedItemInfo}
/>
</div>
)}
{mode === 'recently_used' && (
<div className="list-view">
<RepoListView
initToShowChildren={false}
repoList={recentlyUsedRepos}
selectedRepo={selectedRepo}
selectedPath={selectedPath}
onRepoItemClick={this.onRepoItemClick}
onDirentItemClick={this.onDirentItemClick}
isShowFile={isShowFile}
fileSuffixes={fileSuffixes}
selectedItemInfo={selectedItemInfo}
/>
</div>
)}
</div>
</div> </div>
); );
}; };
render() { render() {
if (!this.state.selectedRepo && this.props.repoID) { const { repoID } = this.props;
const { selectedRepo, searchInfo, isSearching } = this.state;
if (!selectedRepo && repoID) {
return ''; return '';
} }
return ( return (
<Fragment> <Fragment>
{isPro && ( {isPro && this.props.mode !== 'recently_used' && (
<div className="file-chooser-search-input"> <div className="file-chooser-search-input">
<Input className="search-input mb-2" placeholder={gettext('Search')} type='text' value={this.state.searchInfo} onChange={this.onSearchInfoChanged}></Input> <Input className="search-input" placeholder={gettext('Search')} type='text' value={searchInfo} onChange={this.onSearchInfoChanged}></Input>
{this.state.searchInfo.length !== 0 && ( {searchInfo.length !== 0 && (
<span className="search-control attr-action-icon sf3-font sf3-font-x-01" onClick={this.onCloseSearching}></span> <span className="search-control attr-action-icon sf3-font sf3-font-x-01" onClick={this.onCloseSearching}></span>
)} )}
</div> </div>
)} )}
{this.state.isSearching && ( {isSearching ? (
<div className="file-chooser-search-container"> <div className="file-chooser-search-container">
{this.renderSearchedView()} {this.renderSearchedView()}
</div> </div>
) : (
this.renderRepoListView()
)} )}
{!this.state.isSearching && this.renderRepoListView()}
</Fragment> </Fragment>
); );
} }
} }
FileChooser.propTypes = propTypes; FileChooser.propTypes = propTypes;
FileChooser.defaultProps = defaultProps;
export default FileChooser; export default FileChooser;

View File

@ -31,10 +31,12 @@ class RepoListItem extends React.Component {
isShowChildren: this.props.initToShowChildren, isShowChildren: this.props.initToShowChildren,
treeData: treeHelper.buildTree(), treeData: treeHelper.buildTree(),
hasLoaded: false, hasLoaded: false,
isMounted: false,
}; };
} }
componentDidMount() { componentDidMount() {
this.setState({ isMounted: true });
const { isCurrentRepo, currentPath, repo, selectedItemInfo } = this.props; const { isCurrentRepo, currentPath, repo, selectedItemInfo } = this.props;
// render search result // render search result
@ -61,27 +63,31 @@ class RepoListItem extends React.Component {
} }
} }
loadRepoDirentList = (repo) => { componentWillUnmount() {
this.setState({ isMounted: false });
}
loadRepoDirentList = async (repo) => {
const { hasLoaded } = this.state; const { hasLoaded } = this.state;
if (hasLoaded) return; if (hasLoaded) return;
const repoID = repo.repo_id; const repoID = repo.repo_id;
seafileAPI.listDir(repoID, '/').then(res => { try {
const res = await seafileAPI.listDir(repoID, '/');
if (!this.state.isMounted) return;
let tree = this.state.treeData.clone(); let tree = this.state.treeData.clone();
let direntList = []; let direntList = this.props.isShowFile ? res.data.dirent_list : res.data.dirent_list.filter(item => item.type === 'dir');
if (this.props.isShowFile === true) {
direntList = res.data.dirent_list;
} else {
direntList = res.data.dirent_list.filter(item => item.type === 'dir');
}
this.addResponseListToNode(direntList, tree.root); this.addResponseListToNode(direntList, tree.root);
this.setState({ treeData: tree, hasLoaded: true }); this.setState({ treeData: tree, hasLoaded: true });
}).catch(error => { } catch (error) {
if (!this.state.isMounted) return;
let errMessage = Utils.getErrorMsg(error); let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage); toaster.danger(errMessage);
}); }
}; };
addResponseListToNode = (list, node) => { addResponseListToNode = (list, node) => {
@ -201,15 +207,15 @@ class RepoListItem extends React.Component {
<li> <li>
{!this.props.hideLibraryName && {!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-text">
<span className="name user-select-none ellipsis" title={this.props.repo.repo_name}>{this.props.repo.repo_name}</span>
</div>
<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>
<i className="tree-node-icon"> <i className="tree-node-icon">
<span className="icon sf3-font sf3-font-folder tree-node-icon"></span> <span className="icon sf3-font sf3-font-folder tree-node-icon"></span>
</i> </i>
</div> </div>
<div className="item-text">
<span className="name user-select-none ellipsis" title={this.props.repo.repo_name}>{this.props.repo.repo_name}</span>
</div>
</div> </div>
} }
{this.state.isShowChildren && ( {this.state.isShowChildren && (

View File

@ -15,7 +15,6 @@ const propTypes = {
fileSuffixes: PropTypes.array, fileSuffixes: PropTypes.array,
selectedItemInfo: PropTypes.object, selectedItemInfo: PropTypes.object,
currentPath: PropTypes.string, currentPath: PropTypes.string,
hideLibraryName: PropTypes.bool,
}; };
class RepoListView extends React.Component { class RepoListView extends React.Component {
@ -26,12 +25,9 @@ class RepoListView extends React.Component {
repoList = []; repoList = [];
repoList.push(currentRepoInfo); repoList.push(currentRepoInfo);
} }
let style = {};
if (this.props.hideLibraryName) {
style = { marginLeft: '-44px' };
}
return ( return (
<ul className="list-view-content file-chooser-item" style={style}> <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
@ -47,7 +43,6 @@ 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}
hideLibraryName={this.props.hideLibraryName}
/> />
); );
})} })}

View File

@ -11,13 +11,22 @@ const propTypes = {
onNodeExpanded: PropTypes.func.isRequired, onNodeExpanded: PropTypes.func.isRequired,
filePath: PropTypes.string, filePath: PropTypes.string,
fileSuffixes: PropTypes.array, fileSuffixes: PropTypes.array,
level: PropTypes.number,
}; };
class TreeViewItem extends React.Component { class TreeViewItem extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
let filePath = this.props.filePath ? this.props.filePath + '/' + this.props.node.object.name : this.props.node.path; let filePath;
if (this.props.filePath === '/') {
filePath = '/' + this.props.node.object.name;
} else if (this.props.filePath) {
filePath = this.props.filePath + '/' + this.props.node.object.name;
} else {
filePath = this.props.node.path;
}
this.state = { this.state = {
filePath: filePath, filePath: filePath,
@ -73,6 +82,8 @@ class TreeViewItem extends React.Component {
selectedRepo={this.props.selectedRepo} selectedRepo={this.props.selectedRepo}
selectedPath={this.props.selectedPath} selectedPath={this.props.selectedPath}
fileSuffixes={this.props.fileSuffixes} fileSuffixes={this.props.fileSuffixes}
filePath={this.state.filePath}
level={(this.props.level || 0) + 1}
/>); />);
})} })}
</div> </div>
@ -97,13 +108,15 @@ class TreeViewItem extends React.Component {
} }
} }
const paddingLeft = `${this.props.level * 20}px`;
return ( return (
<div className="file-chooser-item"> <div className="file-chooser-item">
<div className={`${node.path === '/' ? 'hide' : ''}`}> <div className={`${node.path === '/' ? 'hide' : ''}`}>
<div className={`${(isCurrentRepo && isCurrentPath) ? 'item-active' : ''} item-info`} onClick={this.onItemClick}> <div
<div className="item-text"> className={`${(isCurrentRepo && isCurrentPath) ? 'item-active' : ''} item-info`}
<span className="name user-select-none ellipsis" title={node.object && node.object.name}>{node.object && node.object.name}</span> onClick={this.onItemClick}
</div> style={{ paddingLeft }}
>
<div className="item-left-icon"> <div className="item-left-icon">
{ {
node.object.type !== 'file' && node.object.type !== 'file' &&
@ -113,6 +126,9 @@ class TreeViewItem extends React.Component {
<span className={`icon sf3-font ${node.object.type === 'dir' ? 'sf3-font-folder' : 'sf3-font-file'}`}></span> <span className={`icon sf3-font ${node.object.type === 'dir' ? 'sf3-font-folder' : 'sf3-font-file'}`}></span>
</i> </i>
</div> </div>
<div className="item-text">
<span className="name user-select-none ellipsis" title={node.object && node.object.name}>{node.object && node.object.name}</span>
</div>
</div> </div>
</div> </div>
{node.isExpanded && this.renderChildren()} {node.isExpanded && this.renderChildren()}

View File

@ -17,7 +17,7 @@ class TreeListView extends React.Component {
render() { render() {
return ( return (
<div className="list-view-content" style={{ 'marginLeft': '-1.5rem' }}> <div className="list-view-content">
<TreeListItem <TreeListItem
node={this.props.treeData.root} node={this.props.treeData.root}
onNodeCollapse={this.props.onNodeCollapse} onNodeCollapse={this.props.onNodeCollapse}
@ -27,6 +27,7 @@ class TreeListView extends React.Component {
selectedRepo={this.props.selectedRepo} selectedRepo={this.props.selectedRepo}
selectedPath={this.props.selectedPath} selectedPath={this.props.selectedPath}
fileSuffixes={this.props.fileSuffixes} fileSuffixes={this.props.fileSuffixes}
level={0}
/> />
</div> </div>
); );

View File

@ -1,78 +1,97 @@
.file-chooser-container { .file-chooser-container {
padding: 0.5rem;
height: 20rem;
border: 1px solid rgba(0, 40, 100, 0.12);
border-radius: 3px;
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
overflow: auto;
font-size: 1rem;
}
.item-toggle{
position: absolute;
height: 1.5rem;
width: 1.5rem;
left: 0;
top: 0;
line-height: 1.5rem !important;
text-align: center;
cursor: pointer;
color: #c0c0c0;
}
.file-chooser-container .list-view {
margin-top: 0.25rem;
}
.list-view-header {
position: relative; position: relative;
padding-left: 1.5rem; height: 20rem;
} border-radius: 3px;
.list-view-header:hover { font-size: 1rem;
background-color: #FDEFB9; transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
.list-view-header .name {
color: #eb8205;
}
.list-view-content {
margin: 0;
padding: 0;
list-style: none;
} }
.file-chooser-item { .file-chooser-item {
position: relative; position: relative;
padding-left: 22px; width: 100%;
}
.file-chooser-item .item-info {
height: 1.5rem;
cursor: pointer;
position: relative;
line-height: 1.625;
} }
.file-chooser-item .item-active { .file-chooser-item .item-active {
background: #F3AF7D !important; background: #f2f4f6 !important;
border-radius: 3px;
}
.file-chooser-item .item-active::before {
content: '';
position: absolute;
top: 2px;
left: -8px;
width: 4px;
height: 24px;
background-color: #ff8000;
border-radius: 2px; border-radius: 2px;
box-shadow: inset 0 0 1px #999; display: block;
color: #fff; z-index: 10;
} }
.file-chooser-item .item-info:hover { .file-chooser-item .item-info {
background: #FDEFB9; position: relative;
border-radius: 2px; display: flex;
box-shadow: inset 0 0 1px #999; align-items: center;
height: 1.75rem;
line-height: 1.625;
cursor: pointer;
transition: background-color 0.3s ease, color 0.3s ease;
} }
.file-chooser-item .item-info .name { .file-chooser-item .item-info:hover {
flex: 1; background: #f5f5f5;
border-radius: 3px;
} }
.file-chooser-item .item-active .icon { .file-chooser-item .item-info .item-left-icon {
color: #fff !important; display: flex;
align-items: center;
height: 100%;
}
.file-chooser-item .item-info .item-text {
display: flex;
align-items: center;
height: 100%;
font-size: 16px;
line-height: 1.5;
padding-left: 0.25rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
transition: color 0.3s ease, background-color 0.3s ease;
}
.file-chooser-search-close {
position: absolute;
top: -0.5rem;
right: -0.5rem;
}
.file-chooser-search-container {
position: relative;
height: 20rem;
padding: 10px;
overflow: auto;
}
.file-chooser-search-container td {
height: 40px;
padding: 0;
}
.file-chooser-search-container td .span {
display: inline-block;
max-width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: middle;
}
.file-chooser-search-container .tr-highlight {
background-color: #f5f5f5;
} }
.file-chooser-search-input { .file-chooser-search-input {
@ -82,67 +101,64 @@
.file-chooser-search-input .search-control { .file-chooser-search-input .search-control {
position: absolute; position: absolute;
top: 0.5rem; top: 0.5rem;
right: 0.7rem; right: 1.5rem;
} }
.file-chooser-search-input .search-input { .file-chooser-search-input .search-input {
width: 100%; width: 100%;
} }
.file-chooser-search-container { .file-chooser-table td {
height: 20rem; border-bottom: 1px solid rgba(0, 0, 0, 0);
}
.item-toggle {
width: 1.5rem;
height: 1.5rem;
line-height: 1.5rem !important;
color: #c0c0c0;
text-align: center;
cursor: pointer;
}
.list-view-content {
margin: 0;
padding: 0;
list-style: none;
}
.list-view-header {
position: relative; position: relative;
border: 1px solid #eee; padding-left: 1.5rem;
padding: 10px; }
.list-view-header:hover {
background-color: #f5f5f5;
}
.list-view-header .name {
color: #eb8205;
}
.scroll-wrapper {
max-height: 100%;
overflow: auto; overflow: auto;
} }
.file-chooser-search-close {
position: absolute;
right: -0.5rem;
top: -0.5rem;
}
.searched-active { .searched-active {
background: #F3AF7D !important; background: #f3af7d !important;
border-radius: 2px; border-radius: 2px;
box-shadow: inset 0 0 1px #999; box-shadow: inset 0 0 1px #999;
} }
.searched-active td {
color: #fff;
}
.searched-active .icon { .searched-active .icon {
color: #fff !important; color: #fff !important;
} }
.searched-active td {
color: #fff;
}
.select-open-repo { .select-open-repo {
background: #FDEFB9; background: #fdefb9;
} }
.file-chooser-table td {
border-bottom: 1px solid rgba(0, 0, 0, 0);
}
.file-chooser-item .item-info .item-text {
padding-left: 2.8rem;
font-size: 15px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 24px;
}
.file-chooser-item .item-info .item-left-icon {
position: absolute;
display: flex;
align-items: center;
top: 0;
left: 0;
padding-left: 1.5rem;
}

View File

@ -289,3 +289,92 @@
.dir-view-path .path-split { .dir-view-path .path-split {
padding: 0 2px; padding: 0 2px;
} }
.custom-modal {
box-sizing: border-box;
font-size: 1rem;
max-width: 740px;
}
.custom-modal .row {
flex: 1;
margin: 0;
}
.custom-modal .modal-content {
min-height: 534px;
border: 0;
}
.custom-modal .modal-body {
flex: 1;
display: flex;
flex-direction: column;
gap: 1rem;
padding: 1rem 0;
position: relative;
}
.custom-modal .modal-body .alert-message {
position: absolute;
bottom: 0;
left: 0;
right: 0;
z-index: 10;
margin: 0;
padding: 0.5rem;
}
.custom-modal .repo-list-col {
max-width: 240px;
padding: 1rem;
line-height: 24px;
text-overflow: ellipsis;
white-space: nowrap;
}
.custom-modal .repo-list-item {
display: flex;
align-items: center;
position: relative;
height: 32px;
padding: 0 10px;
cursor: pointer;
transition: background-color 0.3s ease, font-weight 0.3s ease;
}
.custom-modal .repo-list-item.active {
background-color: #f5f5f5;
font-weight: 500;
border-radius: 3px;
}
.custom-modal .repo-list-item:hover{
background-color: #f0f0f0;
border-radius: 3px;
}
.custom-modal .repo-list-item.active::before {
content: '';
display: block;
position: absolute;
top: 2px;
left: -8px;
width: 4px;
height: 24px;
background-color: #ff8000;
border-radius: 2px;
z-index: 0;
}
.custom-modal .file-list-col {
max-width: 500px;
padding: 0;
display: flex;
flex-direction: column;
}
.custom-modal .file-list-col .file-chooser-container,
.custom-modal .file-list-col .file-chooser-search-input {
padding: 0 1rem;
}

View File

@ -78,7 +78,7 @@
} }
.search-input { .search-input {
height: 1.875rem; height: 2.375rem;
width: 15rem; width: 15rem;
font-size: .875rem; font-size: .875rem;
} }

View File

@ -715,6 +715,27 @@ class LibContentView extends React.Component {
}); });
}; };
updateRecentlyUsedRepos = (destRepo) => {
const recentlyUsed = JSON.parse(localStorage.getItem('recently-used-repos')) || [];
const updatedRecentlyUsed = [destRepo, ...recentlyUsed.filter(repo => repo.repo_id !== destRepo.repo_id)];
const seen = new Set();
const filteredRecentlyUsed = updatedRecentlyUsed.filter(repo => {
if (seen.has(repo.repo_id)) {
return false;
} else {
seen.add(repo.repo_id);
return true;
}
});
if (filteredRecentlyUsed.length > 10) {
updatedRecentlyUsed.pop(); // Limit to 10 recent directories
}
localStorage.setItem('recently-used-repos', JSON.stringify(filteredRecentlyUsed));
};
// toolbar operations // toolbar operations
onMoveItems = (destRepo, destDirentPath) => { onMoveItems = (destRepo, destDirentPath) => {
let repoID = this.props.repoID; let repoID = this.props.repoID;
@ -722,6 +743,7 @@ class LibContentView extends React.Component {
let dirNames = this.getSelectedDirentNames(); let dirNames = this.getSelectedDirentNames();
let direntPaths = this.getSelectedDirentPaths(); let direntPaths = this.getSelectedDirentPaths();
seafileAPI.moveDir(repoID, destRepo.repo_id, destDirentPath, this.state.path, dirNames).then(res => { seafileAPI.moveDir(repoID, destRepo.repo_id, destDirentPath, this.state.path, dirNames).then(res => {
if (repoID !== destRepo.repo_id) { if (repoID !== destRepo.repo_id) {
this.setState({ this.setState({
@ -753,6 +775,8 @@ class LibContentView extends React.Component {
toaster.success(message); toaster.success(message);
} }
this.updateRecentlyUsedRepos(destRepo);
}).catch((error) => { }).catch((error) => {
if (!error.response.data.lib_need_decrypt) { if (!error.response.data.lib_need_decrypt) {
let errMessage = Utils.getErrorMsg(error); let errMessage = Utils.getErrorMsg(error);
@ -1222,6 +1246,9 @@ class LibContentView extends React.Component {
message = message.replace('{name}', dirName); message = message.replace('{name}', dirName);
toaster.success(message); toaster.success(message);
} }
this.updateRecentlyUsedRepos(destRepo);
}).catch((error) => { }).catch((error) => {
if (!error.response.data.lib_need_decrypt) { if (!error.response.data.lib_need_decrypt) {
let errMessage = Utils.getErrorMsg(error); let errMessage = Utils.getErrorMsg(error);