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:
@@ -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;
|
||||
|
||||
|
47
frontend/src/components/search/wiki2-search-result.css
Normal file
47
frontend/src/components/search/wiki2-search-result.css
Normal 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;
|
||||
}
|
47
frontend/src/components/search/wiki2-search-result.js
Normal file
47
frontend/src/components/search/wiki2-search-result.js
Normal 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;
|
50
frontend/src/components/search/wiki2-search.css
Normal file
50
frontend/src/components/search/wiki2-search.css
Normal 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;
|
||||
}
|
||||
|
205
frontend/src/components/search/wiki2-search.js
Normal file
205
frontend/src/components/search/wiki2-search.js
Normal 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;
|
@@ -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()}
|
||||
|
@@ -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>
|
||||
|
Reference in New Issue
Block a user