mirror of
https://github.com/haiwen/seahub.git
synced 2025-08-31 06:34:40 +00:00
Feature/show tags in search dialog (#7727)
* show related tags * optimize * fix eslint warning * change searched tags background --------- Co-authored-by: zhouwenxuan <aries@Mac.local> Co-authored-by: Michael An <1822852997@qq.com>
This commit is contained in:
@@ -14,4 +14,10 @@ export const EVENT_BUS_TYPE = {
|
|||||||
// migrate tags
|
// migrate tags
|
||||||
OPEN_TREE_PANEL: 'open_tree_panel',
|
OPEN_TREE_PANEL: 'open_tree_panel',
|
||||||
OPEN_LIBRARY_SETTINGS_TAGS: 'open_library_settings_tags',
|
OPEN_LIBRARY_SETTINGS_TAGS: 'open_library_settings_tags',
|
||||||
|
|
||||||
|
// tags
|
||||||
|
TAG_STATUS: 'tag_status',
|
||||||
|
TAGS_DATA: 'tags_data',
|
||||||
|
SELECT_TAG: 'select_tag',
|
||||||
|
UPDATE_SELECTED_TAG: 'update_selected_tag',
|
||||||
};
|
};
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { Dropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reactstrap';
|
import { Dropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reactstrap';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import { gettext } from '../../../utils/constants';
|
import { gettext } from '../../../utils/constants';
|
||||||
import { Utils } from '../../../utils/utils';
|
import { Utils } from '../../../utils/utils';
|
||||||
import UserItem from './user-item';
|
import UserItem from './user-item';
|
||||||
@@ -7,7 +8,7 @@ import { seafileAPI } from '../../../utils/seafile-api';
|
|||||||
import ModalPortal from '../../modal-portal';
|
import ModalPortal from '../../modal-portal';
|
||||||
import toaster from '../../toast';
|
import toaster from '../../toast';
|
||||||
|
|
||||||
const FilterByCreator = ({ repoID, onSelect }) => {
|
const FilterByCreator = ({ onSelect }) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [options, setOptions] = useState([]);
|
const [options, setOptions] = useState([]);
|
||||||
const [value, setValue] = useState([]);
|
const [value, setValue] = useState([]);
|
||||||
@@ -30,7 +31,7 @@ const FilterByCreator = ({ repoID, onSelect }) => {
|
|||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
const displayOptions = useMemo(() => {
|
const displayOptions = useMemo(() => {
|
||||||
if (!searchValue) return options;
|
if (!searchValue) return null;
|
||||||
return options.filter((option) => {
|
return options.filter((option) => {
|
||||||
return option.name.toLowerCase().includes(searchValue.toLowerCase());
|
return option.name.toLowerCase().includes(searchValue.toLowerCase());
|
||||||
});
|
});
|
||||||
@@ -48,8 +49,10 @@ const FilterByCreator = ({ repoID, onSelect }) => {
|
|||||||
}
|
}
|
||||||
setValue(updated);
|
setValue(updated);
|
||||||
onSelect('creator', updated);
|
onSelect('creator', updated);
|
||||||
setSearchValue('');
|
if (displayOptions.length === 1) {
|
||||||
}, [value, onSelect]);
|
setSearchValue('');
|
||||||
|
}
|
||||||
|
}, [value, displayOptions, onSelect]);
|
||||||
|
|
||||||
const handleCancel = useCallback((v) => {
|
const handleCancel = useCallback((v) => {
|
||||||
const updated = value.filter((item) => item !== v);
|
const updated = value.filter((item) => item !== v);
|
||||||
@@ -57,26 +60,39 @@ const FilterByCreator = ({ repoID, onSelect }) => {
|
|||||||
onSelect('creator', updated);
|
onSelect('creator', updated);
|
||||||
}, [value, onSelect]);
|
}, [value, onSelect]);
|
||||||
|
|
||||||
|
const handleInputChange = useCallback((e) => {
|
||||||
|
const v = e.target.value;
|
||||||
|
setSearchValue(v);
|
||||||
|
if (!value) {
|
||||||
|
setOptions([]);
|
||||||
|
}
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!searchValue) return;
|
||||||
|
|
||||||
const getUsers = async () => {
|
const getUsers = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await seafileAPI.listRepoRelatedUsers(repoID);
|
const res = await seafileAPI.searchUsers(searchValue);
|
||||||
const users = res.data.user_list;
|
const userList = res.data.users
|
||||||
const options = users.map((user) => {
|
.filter(user => user.name.toLowerCase().includes(searchValue.toLowerCase()))
|
||||||
return {
|
.map(user => ({
|
||||||
key: user.email,
|
key: user.email,
|
||||||
value: user.email,
|
value: user.email,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
label: <UserItem user={user} />,
|
label: <UserItem user={user} />,
|
||||||
};
|
}))
|
||||||
});
|
.filter(user => !options.some(option => option.key === user.key));
|
||||||
setOptions(options);
|
|
||||||
|
setOptions(prevOptions => [...prevOptions, ...userList]);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toaster.danger(Utils.getErrorMsg(err));
|
toaster.danger(Utils.getErrorMsg(err));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
getUsers();
|
getUsers();
|
||||||
}, [repoID]);
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [searchValue]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="search-filter filter-by-creator-container">
|
<div className="search-filter filter-by-creator-container">
|
||||||
@@ -104,11 +120,11 @@ const FilterByCreator = ({ repoID, onSelect }) => {
|
|||||||
type="text"
|
type="text"
|
||||||
placeholder={value.length ? '' : gettext('Search user')}
|
placeholder={value.length ? '' : gettext('Search user')}
|
||||||
value={searchValue}
|
value={searchValue}
|
||||||
onChange={(e) => setSearchValue(e.target.value)}
|
onChange={handleInputChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{displayOptions.map((option) => (
|
{displayOptions && displayOptions.map((option) => (
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
key={option.key}
|
key={option.key}
|
||||||
tag="div"
|
tag="div"
|
||||||
@@ -129,4 +145,8 @@ const FilterByCreator = ({ repoID, onSelect }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
FilterByCreator.propTypes = {
|
||||||
|
onSelect: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
export default FilterByCreator;
|
export default FilterByCreator;
|
||||||
|
@@ -17,10 +17,10 @@ const FilterByText = ({ onSelect }) => {
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
key: TEXT_FILTER_KEY.SEARCH_FILENAME_AND_CONTENT,
|
key: TEXT_FILTER_KEY.SEARCH_FILENAME_AND_CONTENT,
|
||||||
label: gettext('Search filename and content'),
|
label: gettext('File name and content'),
|
||||||
}, {
|
}, {
|
||||||
key: TEXT_FILTER_KEY.SEARCH_FILENAME_ONLY,
|
key: TEXT_FILTER_KEY.SEARCH_FILENAME_ONLY,
|
||||||
label: gettext('Search filename only'),
|
label: gettext('File name only'),
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}, []);
|
}, []);
|
||||||
|
@@ -3,11 +3,12 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: flex-start;
|
align-items: center;
|
||||||
margin-top: 4px;
|
margin-top: 12px;
|
||||||
padding: 0 16px;
|
padding: 0 16px 10px;
|
||||||
overflow: auto hidden;
|
overflow: auto hidden;
|
||||||
scrollbar-color: #C1C1C1 rgba(0, 0, 0, 0);
|
scrollbar-color: #C1C1C1 rgba(0, 0, 0, 0);
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-filters-container .search-filter {
|
.search-filters-container .search-filter {
|
||||||
|
@@ -7,13 +7,11 @@ import FilterBySuffix from './filter-by-suffix';
|
|||||||
|
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
const SCROLLABLE_CONTAINER_HEIGHT = 44;
|
const SearchFilters = ({ onChange }) => {
|
||||||
|
|
||||||
const SearchFilters = ({ repoID, onChange }) => {
|
|
||||||
return (
|
return (
|
||||||
<div className="search-filters-container" style={{ height: SCROLLABLE_CONTAINER_HEIGHT }}>
|
<div className="search-filters-container">
|
||||||
<FilterByText onSelect={onChange} />
|
<FilterByText onSelect={onChange} />
|
||||||
<FilterByCreator repoID={repoID} onSelect={onChange} />
|
<FilterByCreator onSelect={onChange} />
|
||||||
<FilterByDate onSelect={onChange} />
|
<FilterByDate onSelect={onChange} />
|
||||||
<FilterBySuffix onSelect={onChange} />
|
<FilterBySuffix onSelect={onChange} />
|
||||||
</div>
|
</div>
|
||||||
@@ -21,7 +19,6 @@ const SearchFilters = ({ repoID, onChange }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
SearchFilters.propTypes = {
|
SearchFilters.propTypes = {
|
||||||
repoID: PropTypes.string,
|
|
||||||
onChange: PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
58
frontend/src/components/search/search-tags/index.css
Normal file
58
frontend/src/components/search/search-tags/index.css
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
.search-tags-container {
|
||||||
|
width: 100%;
|
||||||
|
height: fit-content;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-tags-container .tags-title {
|
||||||
|
width: 100%;
|
||||||
|
height: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
padding: 0 16px;
|
||||||
|
margin: 10px 0 4px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-tags-container .tags-content {
|
||||||
|
width: 100%;
|
||||||
|
height: fit-content;
|
||||||
|
max-height: 180px;
|
||||||
|
overflow: auto;
|
||||||
|
scrollbar-color: #C1C1C1 rgba(0, 0, 0, 0);
|
||||||
|
padding: 0 16px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-tags-container .tags-content .tag-item {
|
||||||
|
width: 100%;
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 16px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-tags-container .tags-content .tag-item:hover {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-tags-container .tags-content .tag-item .tag-color {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-tags-container .search-tags-divider {
|
||||||
|
height: 0;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
margin: 0 16px;
|
||||||
|
}
|
70
frontend/src/components/search/search-tags/index.js
Normal file
70
frontend/src/components/search/search-tags/index.js
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { gettext } from '../../../utils/constants';
|
||||||
|
import { getTagColor, getTagId, getTagName } from '../../../tag/utils/cell';
|
||||||
|
import { PRIVATE_FILE_TYPE } from '../../../constants';
|
||||||
|
import { EVENT_BUS_TYPE } from '../../common/event-bus-type';
|
||||||
|
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
const SearchTags = ({ repoID, tagsData, keyword, onSelectTag }) => {
|
||||||
|
const [displayTags, setDisplayTags] = useState([]);
|
||||||
|
|
||||||
|
const handleClick = useCallback((e, tagId) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
const node = {
|
||||||
|
children: [],
|
||||||
|
path: '/' + PRIVATE_FILE_TYPE.TAGS_PROPERTIES + '/' + tagId,
|
||||||
|
isExpanded: false,
|
||||||
|
isLoaded: true,
|
||||||
|
isPreload: true,
|
||||||
|
object: {
|
||||||
|
file_tags: [],
|
||||||
|
id: tagId,
|
||||||
|
type: PRIVATE_FILE_TYPE.TAGS_PROPERTIES,
|
||||||
|
isDir: () => false,
|
||||||
|
},
|
||||||
|
parentNode: {},
|
||||||
|
key: repoID,
|
||||||
|
tag_id: tagId,
|
||||||
|
};
|
||||||
|
onSelectTag(node);
|
||||||
|
window.sfTagsDataContext?.eventBus?.dispatch(EVENT_BUS_TYPE.UPDATE_SELECTED_TAG, tagId);
|
||||||
|
}, [repoID, onSelectTag]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!tagsData || tagsData.length === 0 || !keyword) return;
|
||||||
|
const tags = tagsData?.filter((tag) => getTagName(tag).toLowerCase().includes(keyword.toLowerCase()));
|
||||||
|
setDisplayTags(tags);
|
||||||
|
}, [tagsData, keyword]);
|
||||||
|
|
||||||
|
if (!tagsData || tagsData.length === 0 || !keyword || displayTags.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="search-tags-container">
|
||||||
|
<div className="tags-title">{gettext('Tags')}</div>
|
||||||
|
<div className="tags-content">
|
||||||
|
{displayTags.map((tag) => {
|
||||||
|
const tagId = getTagId(tag);
|
||||||
|
const tagName = getTagName(tag);
|
||||||
|
const tagColor = getTagColor(tag);
|
||||||
|
return (
|
||||||
|
<div className="tag-item" key={tagId} onClick={(e) => handleClick(e, tagId)}>
|
||||||
|
<div className="tag-color" style={{ backgroundColor: tagColor }} />
|
||||||
|
<div className="tag-name">{tagName}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div className="search-tags-divider" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
SearchTags.propTypes = {
|
||||||
|
tagsData: PropTypes.array.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SearchTags;
|
@@ -14,6 +14,7 @@ import Loading from '../loading';
|
|||||||
import { SEARCH_MASK, SEARCH_CONTAINER } from '../../constants/zIndexes';
|
import { SEARCH_MASK, SEARCH_CONTAINER } from '../../constants/zIndexes';
|
||||||
import { PRIVATE_FILE_TYPE } from '../../constants';
|
import { PRIVATE_FILE_TYPE } from '../../constants';
|
||||||
import SearchFilters from './search-filters';
|
import SearchFilters from './search-filters';
|
||||||
|
import SearchTags from './search-tags';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
repoID: PropTypes.string,
|
repoID: PropTypes.string,
|
||||||
@@ -22,6 +23,7 @@ const propTypes = {
|
|||||||
onSearchedClick: PropTypes.func.isRequired,
|
onSearchedClick: PropTypes.func.isRequired,
|
||||||
isPublic: PropTypes.bool,
|
isPublic: PropTypes.bool,
|
||||||
isViewFile: PropTypes.bool,
|
isViewFile: PropTypes.bool,
|
||||||
|
onSelectTag: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
const PER_PAGE = 20;
|
const PER_PAGE = 20;
|
||||||
@@ -729,6 +731,7 @@ class Search extends Component {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MediaQuery query="(min-width: 768px)">
|
<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-list-container" ref={this.searchResultListContainerRef}>{results}</div>
|
||||||
</MediaQuery>
|
</MediaQuery>
|
||||||
<MediaQuery query="(max-width: 767.8px)">
|
<MediaQuery query="(max-width: 767.8px)">
|
||||||
@@ -759,12 +762,19 @@ class Search extends Component {
|
|||||||
this.setState({ filters: newFilters }, () => this.forceUpdate());
|
this.setState({ filters: newFilters }, () => this.forceUpdate());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleSelectTag = (tag) => {
|
||||||
|
this.props.onSelectTag(tag);
|
||||||
|
this.resetToDefault();
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let width = this.state.width !== 'default' ? this.state.width : '';
|
let width = this.state.width !== 'default' ? this.state.width : '';
|
||||||
let style = {'width': width};
|
let style = {'width': width};
|
||||||
const { isMaskShow } = this.state;
|
const { repoID, isTagEnabled, tagsData } = this.props;
|
||||||
|
const { isMaskShow, isResultGotten } = this.state;
|
||||||
const placeholder = `${this.props.placeholder}${isMaskShow ? '' : ` (${controlKey} + k)`}`;
|
const placeholder = `${this.props.placeholder}${isMaskShow ? '' : ` (${controlKey} + k)`}`;
|
||||||
const isFiltersShow = this.props.repoID && isMaskShow;
|
const isFiltersShow = isMaskShow && isResultGotten;
|
||||||
|
const isTagsShow = this.props.repoID && isTagEnabled && isMaskShow && isResultGotten;
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<MediaQuery query="(min-width: 768px)">
|
<MediaQuery query="(min-width: 768px)">
|
||||||
@@ -795,10 +805,10 @@ class Search extends Component {
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
{isFiltersShow &&
|
{isFiltersShow &&
|
||||||
<SearchFilters
|
<SearchFilters onChange={this.handleFiltersChange} />
|
||||||
repoID={this.props.repoID}
|
}
|
||||||
onChange={this.handleFiltersChange}
|
{isTagsShow &&
|
||||||
/>
|
<SearchTags repoID={repoID} tagsData={tagsData} keyword={this.state.value} onSelectTag={this.handleSelectTag} />
|
||||||
}
|
}
|
||||||
<div
|
<div
|
||||||
className="search-result-container dropdown-search-result-container"
|
className="search-result-container dropdown-search-result-container"
|
||||||
|
@@ -7,6 +7,7 @@ import Notification from '../common/notification';
|
|||||||
import Account from '../common/account';
|
import Account from '../common/account';
|
||||||
import Logout from '../common/logout';
|
import Logout from '../common/logout';
|
||||||
import { EVENT_BUS_TYPE } from '../common/event-bus-type';
|
import { EVENT_BUS_TYPE } from '../common/event-bus-type';
|
||||||
|
import tagsAPI from '../../tag/api';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
repoID: PropTypes.string,
|
repoID: PropTypes.string,
|
||||||
@@ -32,19 +33,39 @@ class CommonToolbar extends React.Component {
|
|||||||
path: props.path,
|
path: props.path,
|
||||||
isViewFile: props.isViewFile,
|
isViewFile: props.isViewFile,
|
||||||
currentRepoInfo: props.currentRepoInfo,
|
currentRepoInfo: props.currentRepoInfo,
|
||||||
|
isTagEnabled: false,
|
||||||
|
tagsData: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
if (this.props.eventBus) {
|
if (this.props.eventBus) {
|
||||||
this.unsubscribeLibChange = this.props.eventBus.subscribe(EVENT_BUS_TYPE.CURRENT_LIBRARY_CHANGED, this.onRepoChange);
|
this.unsubscribeLibChange = this.props.eventBus.subscribe(EVENT_BUS_TYPE.CURRENT_LIBRARY_CHANGED, this.onRepoChange);
|
||||||
|
this.unsubscribeTagStatus = this.props.eventBus.subscribe(EVENT_BUS_TYPE.TAG_STATUS, (status) => this.onTagStatus(status));
|
||||||
|
this.unsubscribeTagsChanged = this.props.eventBus.subscribe(EVENT_BUS_TYPE.TAGS_CHANGED, (tags) => this.setState({ tagsData: tags }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.unsubscribeLibChange && this.unsubscribeLibChange();
|
this.unsubscribeLibChange && this.unsubscribeLibChange();
|
||||||
|
this.unsubscribeMetadataStatus && this.unsubscribeMetadataStatus();
|
||||||
|
this.unsubscribeTagsChanged && this.unsubscribeTagsChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onTagStatus = (status) => {
|
||||||
|
this.setState({ isTagEnabled: status });
|
||||||
|
if (status) {
|
||||||
|
tagsAPI.getTags(this.state.repoID).then((res) => {
|
||||||
|
const tags = res?.data?.results || [];
|
||||||
|
this.setState({ tagsData: tags });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onSelectTag = (tag) => {
|
||||||
|
this.props.eventBus.dispatch(EVENT_BUS_TYPE.SELECT_TAG, tag);
|
||||||
|
};
|
||||||
|
|
||||||
onRepoChange = ({ repoID, repoName, isLibView, path, isViewFile, currentRepoInfo }) => {
|
onRepoChange = ({ repoID, repoName, isLibView, path, isViewFile, currentRepoInfo }) => {
|
||||||
this.setState({ repoID, repoName, isLibView, path, isViewFile, currentRepoInfo });
|
this.setState({ repoID, repoName, isLibView, path, isViewFile, currentRepoInfo });
|
||||||
};
|
};
|
||||||
@@ -59,7 +80,7 @@ class CommonToolbar extends React.Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
renderSearch = () => {
|
renderSearch = () => {
|
||||||
const { repoID, repoName, isLibView, path, isViewFile } = this.state;
|
const { repoID, repoName, isLibView, path, isViewFile, isTagEnabled, tagsData } = this.state;
|
||||||
const { searchPlaceholder } = this.props;
|
const { searchPlaceholder } = this.props;
|
||||||
const placeholder = searchPlaceholder || gettext('Search files');
|
const placeholder = searchPlaceholder || gettext('Search files');
|
||||||
|
|
||||||
@@ -72,6 +93,9 @@ class CommonToolbar extends React.Component {
|
|||||||
isViewFile={isViewFile}
|
isViewFile={isViewFile}
|
||||||
isPublic={false}
|
isPublic={false}
|
||||||
path={path}
|
path={path}
|
||||||
|
isTagEnabled={isTagEnabled}
|
||||||
|
tagsData={tagsData}
|
||||||
|
onSelectTag={this.onSelectTag}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
@@ -91,7 +91,7 @@
|
|||||||
border-radius: 0 0 3px 3px;
|
border-radius: 0 0 3px 3px;
|
||||||
box-shadow: 0 3px 8px 0 rgba(116, 129, 141, 0.1);
|
box-shadow: 0 3px 8px 0 rgba(116, 129, 141, 0.1);
|
||||||
top: 60px;
|
top: 60px;
|
||||||
padding: 0 16px;
|
padding-left: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-search-result-container {
|
.dropdown-search-result-container {
|
||||||
@@ -118,10 +118,12 @@
|
|||||||
|
|
||||||
.search-result-container .search-result-list-container {
|
.search-result-container .search-result-list-container {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
scrollbar-color: #C1C1C1 rgba(0, 0, 0, 0);
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-result-container .search-result-item {
|
.search-result-container .search-result-item {
|
||||||
|
height: 58px;
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 10px 0 10px 8px;
|
padding: 10px 0 10px 8px;
|
||||||
font-size: 0.8125rem;
|
font-size: 0.8125rem;
|
||||||
@@ -149,7 +151,7 @@
|
|||||||
.search-result-item .item-content {
|
.search-result-item .item-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
margin-left: 0.25rem;
|
margin-left: 0.25rem;
|
||||||
overflow-x: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-content .item-name a {
|
.item-content .item-name a {
|
||||||
@@ -405,11 +407,12 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.search-results-title,
|
||||||
.visited-search-results-title {
|
.visited-search-results-title {
|
||||||
color: #999;
|
color: #999;
|
||||||
font-size: .875rem;
|
font-size: .875rem;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
margin: 7px 0 10px;
|
margin: 10px 0 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-types {
|
.search-types {
|
||||||
|
@@ -160,6 +160,7 @@ class LibContentView extends React.Component {
|
|||||||
this.unsubscribeEvent = this.props.eventBus.subscribe(EVENT_BUS_TYPE.SEARCH_LIBRARY_CONTENT, this.onSearchedClick);
|
this.unsubscribeEvent = this.props.eventBus.subscribe(EVENT_BUS_TYPE.SEARCH_LIBRARY_CONTENT, this.onSearchedClick);
|
||||||
this.unsubscribeOpenTreePanel = eventBus.subscribe(EVENT_BUS_TYPE.OPEN_TREE_PANEL, this.openTreePanel);
|
this.unsubscribeOpenTreePanel = eventBus.subscribe(EVENT_BUS_TYPE.OPEN_TREE_PANEL, this.openTreePanel);
|
||||||
this.calculatePara(this.props);
|
this.calculatePara(this.props);
|
||||||
|
this.unsubscribeSelectSearchedTag = this.props.eventBus.subscribe(EVENT_BUS_TYPE.SELECT_TAG, this.onTreeNodeClick);
|
||||||
}
|
}
|
||||||
|
|
||||||
onMessageCallback = (noticeData) => {
|
onMessageCallback = (noticeData) => {
|
||||||
@@ -324,6 +325,7 @@ class LibContentView extends React.Component {
|
|||||||
this.unsubscribeEvent();
|
this.unsubscribeEvent();
|
||||||
this.unsubscribeOpenTreePanel();
|
this.unsubscribeOpenTreePanel();
|
||||||
this.unsubscribeEventBus && this.unsubscribeEventBus();
|
this.unsubscribeEventBus && this.unsubscribeEventBus();
|
||||||
|
this.unsubscribeSelectSearchedTag && this.unsubscribeSelectSearchedTag();
|
||||||
this.props.eventBus.dispatch(EVENT_BUS_TYPE.CURRENT_LIBRARY_CHANGED, {
|
this.props.eventBus.dispatch(EVENT_BUS_TYPE.CURRENT_LIBRARY_CHANGED, {
|
||||||
repoID: '',
|
repoID: '',
|
||||||
repoName: '',
|
repoName: '',
|
||||||
@@ -2263,6 +2265,7 @@ class LibContentView extends React.Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
metadataStatusCallback = ({ enableMetadata, enableTags }) => {
|
metadataStatusCallback = ({ enableMetadata, enableTags }) => {
|
||||||
|
this.props.eventBus.dispatch(EVENT_BUS_TYPE.TAG_STATUS, enableTags);
|
||||||
if (enableMetadata && enableTags) {
|
if (enableMetadata && enableTags) {
|
||||||
this.updateUsedRepoTags();
|
this.updateUsedRepoTags();
|
||||||
return;
|
return;
|
||||||
@@ -2270,6 +2273,10 @@ class LibContentView extends React.Component {
|
|||||||
this.clearRepoTags();
|
this.clearRepoTags();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
tagsChangedCallback = (tags) => {
|
||||||
|
this.props.eventBus.dispatch(EVENT_BUS_TYPE.TAGS_CHANGED, tags);
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { repoID } = this.props;
|
const { repoID } = this.props;
|
||||||
let { currentRepoInfo, userPerm, isCopyMoveProgressDialogShow, isDeleteFolderDialogOpen, errorMsg,
|
let { currentRepoInfo, userPerm, isCopyMoveProgressDialogShow, isDeleteFolderDialogOpen, errorMsg,
|
||||||
@@ -2346,7 +2353,7 @@ class LibContentView extends React.Component {
|
|||||||
const detailDirent = currentDirent || currentNode?.object || null;
|
const detailDirent = currentDirent || currentNode?.object || null;
|
||||||
return (
|
return (
|
||||||
<MetadataStatusProvider repoID={repoID} repoInfo={currentRepoInfo} hideMetadataView={this.hideMetadataView} statusCallback={this.metadataStatusCallback} >
|
<MetadataStatusProvider repoID={repoID} repoInfo={currentRepoInfo} hideMetadataView={this.hideMetadataView} statusCallback={this.metadataStatusCallback} >
|
||||||
<TagsProvider repoID={repoID} currentPath={path} repoInfo={currentRepoInfo} selectTagsView={this.onTreeNodeClick} >
|
<TagsProvider repoID={repoID} currentPath={path} repoInfo={currentRepoInfo} selectTagsView={this.onTreeNodeClick} tagsChangedCallback={this.tagsChangedCallback} >
|
||||||
<MetadataProvider repoID={repoID} currentPath={path} repoInfo={currentRepoInfo} selectMetadataView={this.onTreeNodeClick} >
|
<MetadataProvider repoID={repoID} currentPath={path} repoInfo={currentRepoInfo} selectMetadataView={this.onTreeNodeClick} >
|
||||||
<CollaboratorsProvider repoID={repoID}>
|
<CollaboratorsProvider repoID={repoID}>
|
||||||
<div className="main-panel-center flex-row">
|
<div className="main-panel-center flex-row">
|
||||||
|
@@ -16,7 +16,7 @@ import { getColumnOriginName } from '../../metadata/utils/column';
|
|||||||
// This hook provides content related to seahub interaction, such as whether to enable extended attributes, views data, etc.
|
// This hook provides content related to seahub interaction, such as whether to enable extended attributes, views data, etc.
|
||||||
const TagsContext = React.createContext(null);
|
const TagsContext = React.createContext(null);
|
||||||
|
|
||||||
export const TagsProvider = ({ repoID, currentPath, selectTagsView, children, ...params }) => {
|
export const TagsProvider = ({ repoID, currentPath, selectTagsView, tagsChangedCallback, children, ...params }) => {
|
||||||
|
|
||||||
const [isLoading, setLoading] = useState(true);
|
const [isLoading, setLoading] = useState(true);
|
||||||
const [isReloading, setReloading] = useState(false);
|
const [isReloading, setReloading] = useState(false);
|
||||||
@@ -31,7 +31,8 @@ export const TagsProvider = ({ repoID, currentPath, selectTagsView, children, ..
|
|||||||
|
|
||||||
const tagsChanged = useCallback(() => {
|
const tagsChanged = useCallback(() => {
|
||||||
setTagsData(storeRef.current.data);
|
setTagsData(storeRef.current.data);
|
||||||
}, []);
|
tagsChangedCallback && tagsChangedCallback(storeRef.current.data.rows);
|
||||||
|
}, [tagsChangedCallback]);
|
||||||
|
|
||||||
const handleTableError = useCallback((error) => {
|
const handleTableError = useCallback((error) => {
|
||||||
toaster.danger(error.error);
|
toaster.danger(error.error);
|
||||||
|
@@ -9,6 +9,7 @@ import { checkTreeNodeHasChildNodes, getTreeChildNodes, getTreeNodeDepth, getTre
|
|||||||
import { getRowById } from '../../components/sf-table/utils/table';
|
import { getRowById } from '../../components/sf-table/utils/table';
|
||||||
import { SIDEBAR_INIT_LEFT_INDENT } from '../constants/sidebar-tree';
|
import { SIDEBAR_INIT_LEFT_INDENT } from '../constants/sidebar-tree';
|
||||||
import { EVENT_BUS_TYPE } from '../../metadata/constants';
|
import { EVENT_BUS_TYPE } from '../../metadata/constants';
|
||||||
|
import { EVENT_BUS_TYPE as COMMON_EVENT_BUS_TYPE } from '../../components/common/event-bus-type';
|
||||||
|
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
@@ -104,6 +105,21 @@ const TagsTreeView = ({ currentPath }) => {
|
|||||||
setKeyTreeNodeExpandedMap(getKeyTreeNodeExpandedMap());
|
setKeyTreeNodeExpandedMap(getKeyTreeNodeExpandedMap());
|
||||||
}, [getKeyTreeNodeExpandedMap]);
|
}, [getKeyTreeNodeExpandedMap]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubscribeUpdateSelectedTag = window.sfTagsDataContext?.eventBus?.subscribe(COMMON_EVENT_BUS_TYPE.UPDATE_SELECTED_TAG, (tagId) => {
|
||||||
|
if (tagId) {
|
||||||
|
const node = recordsTree.find((node) => getTreeNodeId(node) === tagId);
|
||||||
|
const nodeKey = getTreeNodeKey(node);
|
||||||
|
if (!nodeKey) return;
|
||||||
|
setCurrSelectedNodeKey(nodeKey);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribeUpdateSelectedTag && unsubscribeUpdateSelectedTag();
|
||||||
|
};
|
||||||
|
}, [recordsTree]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="tree-view tree metadata-tree-view metadata-tree-view-tag">
|
<div className="tree-view tree metadata-tree-view metadata-tree-view-tag">
|
||||||
<div className="tree-node">
|
<div className="tree-node">
|
||||||
|
Reference in New Issue
Block a user