1
0
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:
Aries
2025-05-28 21:29:46 +08:00
committed by GitHub
parent 1ef68b2a9c
commit 9f64c3ab8f
24 changed files with 364 additions and 39 deletions

View 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;

View File

@@ -0,0 +1,4 @@
.searched-item-details .detail-header {
border: none;
padding: 8px;
}

View 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;

View File

@@ -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">

View File

@@ -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}