1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-05 00:43:53 +00:00

change Wiki2 search UI (#7000)

* change Wiki2 search UI

* change style when no pages
This commit is contained in:
Michael An
2024-11-06 20:13:12 +08:00
committed by GitHub
parent b04cc17872
commit 57caaddd75
7 changed files with 361 additions and 93 deletions

View File

@@ -22,7 +22,6 @@ const propTypes = {
onSearchedClick: PropTypes.func.isRequired,
isPublic: PropTypes.bool,
isViewFile: PropTypes.bool,
isWiki2: PropTypes.bool,
};
const PER_PAGE = 20;
@@ -552,23 +551,6 @@ class Search extends Component {
renderSearchTypes = (inputValue) => {
const highlightIndex = this.state.highlightSearchTypesIndex;
const { resultItems } = this.state;
if (this.props.isWiki2) {
return (
<div className="search-types">
<div
className="search-types-wiki search-types-highlight"
onClick={this.searchWiki}
tabIndex={0}
>
<i className="search-icon-left input-icon-addon sf3-font sf3-font-search"></i>
{inputValue}
<span className="search-types-text">{gettext('in this wiki')}</span>
<i className="sf3-font sf3-font-enter"></i>
</div>
</div>
);
}
if (!this.props.repoID) {
return (
<div className="search-result-list-container" ref={this.searchResultListContainerRef}>
@@ -687,69 +669,6 @@ class Search extends Component {
this.getSearchResult(queryData);
};
getWikiSearchResult = (queryData) => {
if (this.source) {
this.source.cancel('prev request is cancelled');
}
this.setState({
isLoading: true,
isResultGetted: false,
resultItems: [],
highlightIndex: 0,
});
this.source = seafileAPI.getSource();
const query = queryData.q;
const search_wiki = this.props.repoID;
searchAPI.searchWiki(query, search_wiki, this.source.token).then(res => {
this.source = null;
if (res.data.total > 0) {
this.setState({
resultItems: this.formatWikiResultItems(res.data.results),
isResultGetted: true,
isLoading: false,
page: 1,
hasMore: res.data.has_more,
});
} else {
this.setState({
highlightIndex: 0,
resultItems: [],
isLoading: false,
isResultGetted: true,
hasMore: false,
});
}
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
this.setState({ isLoading: false });
});
};
formatWikiResultItems(data) {
return data.map((item, index) => ({
index: [index],
name: item.name,
path: item.path,
repo_id: item.repo_id,
repo_name: item.repo_name,
is_dir: false,
link_content: item.path,
content: item.content_highlight,
}));
}
searchWiki = () => {
const { value } = this.state;
const queryData = {
q: value,
search_repo: this.props.repoID,
};
this.getWikiSearchResult(queryData);
};
renderResults = (resultItems, isVisited) => {
const { highlightIndex, hasMore, searchPageUrl } = this.state;

View File

@@ -0,0 +1,47 @@
.wiki2-search-result {
width: 100%;
padding-top: 8px;
border-radius: 3px;
}
.wiki2-search-result-current {
height: 20px;
line-height: 20px;
color: #757575;
background-color: #eaeaea;
padding: 0 4px;
margin: 0 4px;
border-radius: 3px;
font-size: 12px;
}
.wiki2-search-result-top .wiki2-search-result-page-name {
font-size: 14px;
}
.wiki2-search-result-bottom {
padding-left: 36px;
padding-right: 36px;
overflow: hidden;
}
.wiki2-search-result-bottom mark {
padding: 0;
background: yellow;
}
.wiki2-search-result:hover,
.wiki2-search-result.wiki2-search-result-highlight {
cursor: pointer;
background-color: #F7F7F5;
}
.wiki2-search-result-enter.sf3-font-enter {
opacity: 0;
color: #999;
font-size: 12px;
}
.wiki2-search-result:hover .wiki2-search-result-enter.sf3-font-enter {
opacity: 1;
}

View File

@@ -0,0 +1,47 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import NavItemIcon from '../../pages/wiki2/common/nav-item-icon';
import CustomIcon from '../../pages/wiki2/custom-icon';
import { gettext } from '../../utils/constants';
import './wiki2-search-result.css';
function Wiki2SearchResult({ result, currentPageId, setCurrentPage, resetToDefault, setRef, isHighlight }) {
const { content, page } = result;
const isCurrentPage = currentPageId === page.id;
return (
<div
className={classNames('wiki2-search-result', { 'wiki2-search-result-highlight': isHighlight })}
onClick={() => { setCurrentPage(page.id); resetToDefault(); }}
ref={ref => setRef(ref)}
>
<div className='wiki2-search-result-top d-flex align-items-center'>
{page.icon ? <CustomIcon icon={page.icon} /> : <NavItemIcon symbol={'file'} disable={true} />}
<span className='wiki2-search-result-page-name text-truncate' title={page.name} style={isCurrentPage ? { width: 'auto' } : { width: 700 }}>{page.name}</span>
{currentPageId === page.id ?
<span className='wiki2-search-result-current'>{gettext('Current page')}</span> :
<span className='wiki2-search-result-enter sf3-font sf3-font-enter' style={isHighlight ? { opacity: 1 } : {}}></span>
}
</div>
{content ?
<div className='wiki2-search-result-bottom'>
<p dangerouslySetInnerHTML={{ __html: content }} ></p>
</div>
:
<div className='py-1'></div>
}
</div>
);
}
Wiki2SearchResult.propTypes = {
result: PropTypes.object.isRequired,
currentPageId: PropTypes.string.isRequired,
setCurrentPage: PropTypes.func.isRequired,
resetToDefault: PropTypes.func.isRequired,
setRef: PropTypes.func.isRequired,
isHighlight: PropTypes.bool.isRequired,
};
export default Wiki2SearchResult;

View File

@@ -0,0 +1,50 @@
.wiki2-search {
margin: 10px 8px 10px;
height: 32px;
display: flex;
align-items: center;
border-radius: 3px;
}
.wiki2-search:hover {
cursor: pointer;
background-color: #EDEDEA;
}
.wiki2-search .sf3-font-search {
margin: 0 8px;
color: #666;
}
.wiki2-search-input {
position: relative;
}
.wiki2-search-input .sf3-font-search {
position: absolute;
top: 0;
bottom: 0;
left: 0;
color: #9aa0ac;
display: flex;
align-items: center;
justify-content: center;
min-width: 2.5rem;
pointer-events: none;
display: flex;
}
.wiki2-search-input .form-control {
padding-left: 2.5rem;
height: 38px;
font-size: 0.875rem;
width: 100%;
}
.wiki2-search-result-container {
min-height: 300px;
overflow-y: auto;
margin-right: -16px;
padding-right: 16px;
}

View File

@@ -0,0 +1,205 @@
import React, { useCallback, useState, useRef, useEffect } from 'react';
import PropTypes from 'prop-types';
import { Modal, ModalBody, Input } from 'reactstrap';
import isHotkey from 'is-hotkey';
import searchAPI from '../../utils/search-api';
import { gettext } from '../../utils/constants';
import { Utils } from '../../utils/utils';
import toaster from '../toast';
import Loading from '../loading';
import Wiki2SearchResult from './wiki2-search-result';
import { isModF } from '../../metadata/utils/hotkey';
import './wiki2-search.css';
const isEnter = isHotkey('enter');
const isUp = isHotkey('up');
const isDown = isHotkey('down');
function Wiki2Search({ setCurrentPage, config, currentPageId, wikiId }) {
const [isModalOpen, setIsModalOpen] = useState(false);
const [value, setValue] = useState('');
const [results, setResults] = useState([]);
const [highlightIndex, setHighlightIndex] = useState(-1);
const [isLoading, setIsLoading] = useState(false);
const [isResultGetted, setIsResultGetted] = useState(false);
let searchResultListContainerRef = useRef(null);
let highlightRef = useRef(null);
const onDocumentKeyDown = useCallback((e) => {
if (!isModalOpen && isModF(e)) {
e.preventDefault();
e.stopPropagation();
setIsModalOpen(true);
return;
}
}, [isModalOpen]);
useEffect(() => {
document.addEventListener('keydown', onDocumentKeyDown);
return () => {
document.removeEventListener('keydown', onDocumentKeyDown);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const resetToDefault = useCallback(() => {
setValue('');
setHighlightIndex(0);
setResults([]);
setIsResultGetted(false);
setIsModalOpen(false);
}, []);
const onKeyDown = useCallback((e) => {
if (isHotkey('esc', e)) {
resetToDefault();
return;
}
if (isResultGetted) {
if (isEnter(e)) {
const hightlightResult = results[highlightIndex];
if (hightlightResult && hightlightResult.page.id !== currentPageId) {
setCurrentPage(hightlightResult.page.id);
resetToDefault();
}
} else if (isUp(e)) {
onUp(e, highlightIndex);
} else if (isDown(e)) {
onDown(e, results, highlightIndex);
}
} else {
if (isEnter(e)) {
getWikiSearchResult(value);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isResultGetted, value, results, highlightIndex, currentPageId]);
const onUp = useCallback((e, highlightIndex) => {
e.preventDefault();
e.stopPropagation();
if (highlightIndex > 0) {
setHighlightIndex(highlightIndex - 1);
setTimeout(() => {
if (highlightRef.current) {
const { top, height } = highlightRef.current.getBoundingClientRect();
if (top - height < 0) {
searchResultListContainerRef.current.scrollTop -= height;
}
}
}, 1);
}
}, []);
const onDown = useCallback((e, results, highlightIndex) => {
e.preventDefault();
e.stopPropagation();
if (highlightIndex < results.length - 1) {
setHighlightIndex(highlightIndex + 1);
setTimeout(() => {
if (highlightRef.current) {
const { top, height } = highlightRef.current.getBoundingClientRect();
const outerHeight = 300;
if (top > outerHeight) {
const newScrollTop = searchResultListContainerRef.current.scrollTop + height;
searchResultListContainerRef.current.scrollTop = newScrollTop;
}
}
}, 1);
}
}, []);
const getWikiSearchResult = useCallback((searchValue) => {
if (!searchValue.trim()) return;
setIsLoading(true);
setHighlightIndex(-1);
setIsResultGetted(false);
setResults([]);
searchAPI.searchWiki(searchValue.trim(), wikiId).then(res => {
if (res.data.results) {
let newResults = [];
res.data.results.forEach(result => {
const page = config.pages.find(page => page.docUuid === result.doc_uuid);
if (page) {
result.page = Object.assign({}, page);
newResults.push(result);
}
});
setResults(newResults);
setHighlightIndex(0);
setIsLoading(false);
setIsResultGetted(true);
} else {
setResults([]);
setHighlightIndex(-1);
setIsLoading(false);
setIsResultGetted(true);
}
}).catch(error => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
setIsLoading(false);
});
}, [config, wikiId]);
return (
<>
<div className="wiki2-search" onClick={() => setIsModalOpen(true)}>
<i className="sf3-font sf3-font-search"></i>
<span>{gettext('Search')}</span>
</div>
{isModalOpen &&
<Modal className="wiki2-search-modal" isOpen={isModalOpen} toggle={resetToDefault} autoFocus={false} size='lg'>
<ModalBody>
<div className="wiki2-search-input mb-4">
<i className="sf3-font sf3-font-search"></i>
<Input
type="text"
className="form-control search-input"
name="query"
placeholder={gettext('Search')}
autoComplete="off"
value={value}
onChange={(e) => { setValue(e.target.value); setIsResultGetted(false); }}
onKeyDown={onKeyDown}
autoFocus={true}
/>
<button type="button" className="search-icon-right input-icon-addon sf3-font sf3-font-x-01 border-0 bg-transparent mr-1" onClick={resetToDefault}></button>
</div>
<div className="wiki2-search-result-container" style={{ maxHeight: (window.innerHeight - 200) + 'px' }} ref={searchResultListContainerRef}>
{isLoading && <Loading />}
{(value === '' && !isResultGetted) &&
<p className='sf-tip-default d-flex justify-content-center'>{gettext('Type characters to start search')}</p>}
{(value !== '' && isResultGetted && results.length === 0) &&
<p className='sf-tip-default d-flex justify-content-center'>{gettext('No result')}</p>}
{results.map((result, index) => {
return (
<Wiki2SearchResult
result={result}
key={result._id}
currentPageId={currentPageId}
setCurrentPage={setCurrentPage}
resetToDefault={resetToDefault}
isHighlight={highlightIndex === index}
setRef={highlightIndex === index ? (ref) => {highlightRef.current = ref;} : () => {}}
/>
);
})}
</div>
</ModalBody>
</Modal>
}
</>
);
}
Wiki2Search.propTypes = {
wikiId: PropTypes.string.isRequired,
setCurrentPage: PropTypes.func.isRequired,
config: PropTypes.object.isRequired,
currentPageId: PropTypes.string.isRequired,
};
export default Wiki2Search;

View File

@@ -14,7 +14,7 @@ import { Utils } from '../../utils/utils';
import WikiExternalOperations from './wiki-external-operations';
import WikiTrashDialog from './wiki-trash-dialog';
import { DEFAULT_PAGE_NAME } from './constant';
import Search from '../../components/search/search';
import Wiki2Search from '../../components/search/wiki2-search';
import './side-panel.css';
@@ -38,6 +38,7 @@ class SidePanel extends Component {
isShowTrashDialog: false,
};
}
confirmDeletePage = (pageId) => {
const config = deepCopy(this.props.config);
const { pages } = config;
@@ -157,7 +158,7 @@ class SidePanel extends Component {
};
render() {
const { isLoading } = this.props;
const { isLoading, config, currentPageId } = this.props;
const isDesktop = Utils.isDesktop();
return (
<div className={`wiki2-side-panel${this.props.closeSideBar ? '' : ' left-zero'}`}>
@@ -177,11 +178,11 @@ class SidePanel extends Component {
</div>
}
</div>
<Search
repoID={wikiId}
onSearchedClick={this.onSearchedClick}
placeholder={gettext('Search')}
isWiki2={true}
<Wiki2Search
wikiId={wikiId}
config={config}
currentPageId={currentPageId}
setCurrentPage={this.props.setCurrentPage}
/>
<div className="wiki2-side-nav">
{isLoading ? <Loading/> : this.renderWikiNav()}

View File

@@ -1,5 +1,6 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { DropTarget, DragLayer } from 'react-dnd';
import html5DragDropContext from './html5DragDropContext';
import DraggedPageItem from './pages/dragged-page-item';
@@ -97,10 +98,8 @@ class WikiNav extends Component {
// eslint-disable-next-line
renderStructureBody = React.forwardRef((layerDragProps, ref) => {
const { navigation, pages } = this.props;
let isOnlyOnePage = false;
if (pages.length === 1) {
isOnlyOnePage = true;
}
const pagesLen = pages.length;
const isOnlyOnePage = pagesLen === 1;
let id_page_map = {};
pages.forEach(page => id_page_map[page.id] = page);
return (
@@ -109,7 +108,7 @@ class WikiNav extends Component {
return this.renderPage(item, index, pages.length, isOnlyOnePage, id_page_map, layerDragProps);
})}
{wikiPermission !== 'public' &&
<div className='wiki2-trash' onClick={this.props.toggelTrashDialog}>
<div className={classNames('wiki2-trash', { 'mt-0': !pagesLen })} onClick={this.props.toggelTrashDialog}>
<span className="sf3-font-trash sf3-font mr-2"></span>
<span>{gettext('Trash')}</span>
</div>