mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-28 16:17:02 +00:00
Feature/sidepanel in search result (#7844)
* show sidepanel in search result * show metadata details * show library details * optimize * optimize * optimize --------- Co-authored-by: zhouwenxuan <aries@Mac.local>
This commit is contained in:
90
frontend/src/components/search/details/details.js
Normal file
90
frontend/src/components/search/details/details.js
Normal file
@@ -0,0 +1,90 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { Body, Header } from '../../dirent-detail/detail';
|
||||
import { siteRoot, thumbnailSizeForGrid } from '../../../utils/constants';
|
||||
import { Utils } from '../../../utils/utils';
|
||||
import FileDetails from '../../dirent-detail/dirent-details/file-details';
|
||||
import DirDetails from '../../dirent-detail/dirent-details/dir-details';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useMetadataStatus } from '../../../hooks';
|
||||
import tagsAPI from '../../../tag/api';
|
||||
import { PER_LOAD_NUMBER } from '../../../metadata/constants';
|
||||
import { normalizeColumns } from '../../../tag/utils/column';
|
||||
import { TAGS_DEFAULT_SORT } from '../../../tag/constants/sort';
|
||||
import TagsData from '../../../tag/model/tagsData';
|
||||
import toaster from '../../toast';
|
||||
|
||||
const Details = ({ repoID, repoInfo, path, dirent, direntDetail }) => {
|
||||
const [tagsData, setTagsData] = useState(null);
|
||||
const { enableMetadata, enableTags } = useMetadataStatus();
|
||||
|
||||
useEffect(() => {
|
||||
if (enableMetadata && enableTags) {
|
||||
tagsAPI.getTags(repoID, 0, PER_LOAD_NUMBER).then(res => {
|
||||
const rows = res?.data?.results || [];
|
||||
const columns = normalizeColumns(res?.data?.metadata);
|
||||
const tagsData = new TagsData({ rows, columns, TAGS_DEFAULT_SORT });
|
||||
setTagsData(tagsData);
|
||||
}).catch(error => {
|
||||
const errorMsg = Utils.getErrorMsg(error);
|
||||
toaster.danger(errorMsg);
|
||||
});
|
||||
} else {
|
||||
setTagsData(null);
|
||||
}
|
||||
}, [repoID, enableMetadata, enableTags]);
|
||||
|
||||
let src = '';
|
||||
if (repoInfo.encrypted) {
|
||||
src = `${siteRoot}repo/${repoID}/raw` + Utils.encodePath(`${path === '/' ? '' : path}/${dirent.name}`);
|
||||
} else {
|
||||
src = `${siteRoot}thumbnail/${repoID}/${thumbnailSizeForGrid}` + Utils.encodePath(`${path === '/' ? '' : path}/${dirent.name}`) + '?mtime=' + direntDetail.mtime;
|
||||
}
|
||||
return (
|
||||
<div className="searched-item-details">
|
||||
<div
|
||||
className="cur-view-detail"
|
||||
style={{ width: 300 }}
|
||||
>
|
||||
<Header title={dirent?.name || ''} icon={Utils.getDirentIcon(dirent, true)}></Header>
|
||||
<Body>
|
||||
{Utils.imageCheck(dirent.name) && (
|
||||
<div className="detail-image">
|
||||
<img src={src} alt="" />
|
||||
</div>
|
||||
)}
|
||||
<div className="detail-content">
|
||||
{dirent.type !== 'file' ? (
|
||||
<DirDetails
|
||||
direntDetail={direntDetail}
|
||||
readOnly={true}
|
||||
tagsData={tagsData}
|
||||
/>
|
||||
) : (
|
||||
<FileDetails
|
||||
repoID={repoID}
|
||||
dirent={dirent}
|
||||
path={path}
|
||||
direntDetail={direntDetail}
|
||||
repoTags={[]}
|
||||
fileTagList={dirent ? dirent.file_tags : []}
|
||||
readOnly={true}
|
||||
tagsData={tagsData}
|
||||
onFileTagChanged={() => {}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Body>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Details.propTypes = {
|
||||
repoID: PropTypes.string.isRequired,
|
||||
repoInfo: PropTypes.object.isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
dirent: PropTypes.object.isRequired,
|
||||
direntDetail: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default Details;
|
4
frontend/src/components/search/details/index.css
Normal file
4
frontend/src/components/search/details/index.css
Normal file
@@ -0,0 +1,4 @@
|
||||
.searched-item-details .detail-header {
|
||||
border: none;
|
||||
padding: 8px;
|
||||
}
|
103
frontend/src/components/search/details/index.js
Normal file
103
frontend/src/components/search/details/index.js
Normal file
@@ -0,0 +1,103 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Utils } from '../../../utils/utils';
|
||||
import { seafileAPI } from '../../../utils/seafile-api';
|
||||
import toaster from '../../toast';
|
||||
import { MetadataDetailsProvider } from '../../../metadata';
|
||||
import { Repo } from '../../../models';
|
||||
import { MetadataStatusProvider } from '../../../hooks';
|
||||
import Details from './details';
|
||||
import LibDetail from '../../dirent-detail/lib-details';
|
||||
|
||||
import './index.css';
|
||||
|
||||
const SearchedItemDetails = ({ repoID, path, dirent }) => {
|
||||
const [repoInfo, setRepoInfo] = useState(null);
|
||||
const [direntDetail, setDirentDetail] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
seafileAPI.getRepoInfo(repoID).then(res => {
|
||||
const repo = new Repo(res.data);
|
||||
setRepoInfo(repo);
|
||||
}).catch(error => {
|
||||
const errMessage = Utils.getErrorMsg(error);
|
||||
toaster.danger(errMessage);
|
||||
});
|
||||
}, [repoID]);
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
|
||||
const fetchData = async () => {
|
||||
if (!repoID || !path || !dirent || dirent.isLib) {
|
||||
setDirentDetail(null);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await seafileAPI[dirent.type === 'file' ? 'getFileInfo' : 'getDirInfo'](
|
||||
repoID,
|
||||
path,
|
||||
{ signal: controller.signal }
|
||||
);
|
||||
setDirentDetail(res.data);
|
||||
} catch (error) {
|
||||
if (error.name !== 'AbortError') {
|
||||
const errMessage = Utils.getErrorMsg(error);
|
||||
toaster.danger(errMessage);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const timer = setTimeout(fetchData, 200);
|
||||
|
||||
return () => {
|
||||
controller.abort();
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [repoID, repoInfo, path, dirent]);
|
||||
|
||||
if (!repoInfo) return;
|
||||
|
||||
if (dirent.isLib) {
|
||||
return (
|
||||
<div className="searched-item-details">
|
||||
<LibDetail currentRepoInfo={repoInfo} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!direntDetail) return null;
|
||||
|
||||
let parentDir = path !== '/' && path.endsWith('/') ? path.slice(0, -1) : path; // deal with folder path comes from search results, eg: /folder/
|
||||
parentDir = Utils.getDirName(parentDir);
|
||||
return (
|
||||
<MetadataStatusProvider key={repoID} repoID={repoID} repoInfo={repoInfo}>
|
||||
<MetadataDetailsProvider
|
||||
repoID={repoID}
|
||||
repoInfo={repoInfo}
|
||||
path={path !== '/' && path.endsWith('/') ? path.slice(0, -1) : path}
|
||||
dirent={dirent}
|
||||
direntDetail={direntDetail}
|
||||
direntType={dirent?.type !== 'file' ? 'dir' : 'file'}
|
||||
>
|
||||
<Details
|
||||
repoID={repoID}
|
||||
repoInfo={repoInfo}
|
||||
path={parentDir}
|
||||
dirent={dirent}
|
||||
direntDetail={direntDetail}
|
||||
/>
|
||||
</MetadataDetailsProvider>
|
||||
</MetadataStatusProvider>
|
||||
);
|
||||
};
|
||||
|
||||
SearchedItemDetails.propTypes = {
|
||||
repoID: PropTypes.string.isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
dirent: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default SearchedItemDetails;
|
||||
|
@@ -5,17 +5,38 @@ import { Utils } from '../../utils/utils';
|
||||
|
||||
const propTypes = {
|
||||
item: PropTypes.object.isRequired,
|
||||
idx: PropTypes.number.isRequired,
|
||||
onItemClickHandler: PropTypes.func.isRequired,
|
||||
isHighlight: PropTypes.bool,
|
||||
setRef: PropTypes.func,
|
||||
onHighlightIndex: PropTypes.func,
|
||||
timer: PropTypes.number,
|
||||
onSetTimer: PropTypes.func,
|
||||
};
|
||||
|
||||
class SearchResultItem extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.controller = null;
|
||||
}
|
||||
|
||||
onClickHandler = () => {
|
||||
this.props.onItemClickHandler(this.props.item);
|
||||
};
|
||||
|
||||
onMouseEnter = () => {
|
||||
if (this.props.isHighlight) return;
|
||||
if (this.controller) {
|
||||
this.controller.abort();
|
||||
}
|
||||
this.controller = new AbortController();
|
||||
|
||||
if (this.props.onHighlightIndex) {
|
||||
this.props.onHighlightIndex(this.props.idx);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { item, setRef = (() => {}) } = this.props;
|
||||
let folderIconUrl = item.link_content ? Utils.getFolderIconUrl(false, 192) : Utils.getDefaultLibIconUrl();
|
||||
@@ -32,6 +53,7 @@ class SearchResultItem extends React.Component {
|
||||
className={classnames('search-result-item', { 'search-result-item-highlight': this.props.isHighlight })}
|
||||
onClick={this.onClickHandler}
|
||||
ref={ref => setRef(ref)}
|
||||
onMouseEnter={this.onMouseEnter}
|
||||
>
|
||||
<img className={item.link_content ? 'item-img' : 'lib-item-img'} src={fileIconUrl} alt="" />
|
||||
<div className="item-content">
|
||||
|
@@ -8,7 +8,7 @@ import searchAPI from '../../utils/search-api';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import SearchResultItem from './search-result-item';
|
||||
import SearchResultLibrary from './search-result-library';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import { debounce, Utils } from '../../utils/utils';
|
||||
import toaster from '../toast';
|
||||
import Loading from '../loading';
|
||||
import { SEARCH_MASK, SEARCH_CONTAINER } from '../../constants/zIndexes';
|
||||
@@ -16,6 +16,8 @@ import { PRIVATE_FILE_TYPE, SEARCH_FILTER_BY_DATE_OPTION_KEY, SEARCH_FILTER_BY_D
|
||||
import SearchFilters from './search-filters';
|
||||
import SearchTags from './search-tags';
|
||||
import IconBtn from '../icon-btn';
|
||||
import SearchedItemDetails from './details';
|
||||
import { CollaboratorsProvider } from '../../metadata';
|
||||
|
||||
const propTypes = {
|
||||
repoID: PropTypes.string,
|
||||
@@ -75,6 +77,7 @@ class Search extends Component {
|
||||
this.isChineseInput = false;
|
||||
this.searchResultListContainerRef = React.createRef();
|
||||
this.calculateStoreKey(props);
|
||||
this.timer = null;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -138,7 +141,7 @@ class Search extends Component {
|
||||
};
|
||||
|
||||
onFocusHandler = () => {
|
||||
this.setState({ width: '570px', isMaskShow: true });
|
||||
this.setState({ width: '100%', isMaskShow: true });
|
||||
this.calculateHighlightType();
|
||||
};
|
||||
|
||||
@@ -700,22 +703,49 @@ class Search extends Component {
|
||||
this.getSearchResult(this.buildSearchParams(queryData));
|
||||
};
|
||||
|
||||
renderDetails = (results) => {
|
||||
const { repoID: currentRepoID } = this.props;
|
||||
const { highlightIndex } = this.state;
|
||||
const item = results[highlightIndex];
|
||||
if (!item) return null;
|
||||
const repoID = item.repo_id;
|
||||
const isLib = !currentRepoID && item.path === '/';
|
||||
const dirent = { name: item.name, type: item.is_dir ? 'dir' : 'file', isLib, file_tags: [], path: item.path };
|
||||
return (
|
||||
<CollaboratorsProvider repoID={repoID}>
|
||||
<SearchedItemDetails repoID={repoID} path={item.path} dirent={dirent} />
|
||||
</CollaboratorsProvider>
|
||||
);
|
||||
};
|
||||
|
||||
debounceHighlight = debounce((index) => {
|
||||
this.setState({ highlightIndex: index });
|
||||
}, 200);
|
||||
|
||||
renderResults = (resultItems, isVisited) => {
|
||||
const { highlightIndex } = this.state;
|
||||
|
||||
const results = (
|
||||
<>
|
||||
{isVisited && <h4 className="visited-search-results-title">{gettext('Search results visited recently')}</h4>}
|
||||
{isVisited ? (
|
||||
<h4 className="visited-search-results-title">{gettext('Search results visited recently')}</h4>
|
||||
) : (
|
||||
<h4 className="search-results-title">{gettext('Files')}</h4>
|
||||
)}
|
||||
<ul className="search-result-list" ref={this.searchResultListRef}>
|
||||
{resultItems.map((item, index) => {
|
||||
const isHighlight = index === highlightIndex;
|
||||
return (
|
||||
<SearchResultItem
|
||||
key={index}
|
||||
idx={index}
|
||||
item={item}
|
||||
onItemClickHandler={this.onItemClickHandler}
|
||||
isHighlight={isHighlight}
|
||||
setRef={isHighlight ? (ref) => {this.highlightRef = ref;} : () => {}}
|
||||
onHighlightIndex={this.debounceHighlight}
|
||||
timer={this.timer}
|
||||
onSetTimer={(timer) => {this.timer = timer;}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
@@ -726,8 +756,12 @@ class Search extends Component {
|
||||
return (
|
||||
<>
|
||||
<MediaQuery query="(min-width: 768px)">
|
||||
{!isVisited && <h4 className="search-results-title">{gettext('Files')}</h4>}
|
||||
<div className="search-result-list-container" ref={this.searchResultListContainerRef}>{results}</div>
|
||||
<div className="search-result-sidepanel-wrapper d-flex">
|
||||
<div className="search-result-list-container" ref={this.searchResultListContainerRef}>{results}</div>
|
||||
<div className="search-result-container-sidepanel d-flex flex-column flex-grow-1">
|
||||
{this.renderDetails(resultItems)}
|
||||
</div>
|
||||
</div>
|
||||
</MediaQuery>
|
||||
<MediaQuery query="(max-width: 767.8px)">
|
||||
{results}
|
||||
|
Reference in New Issue
Block a user