mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-05 17:02:47 +00:00
feat: all tags (#7099)
* feat: all tags * feat: optimize code --------- Co-authored-by: 杨国璇 <ygx@Hello-word.local>
This commit is contained in:
@@ -157,7 +157,7 @@ class DirPath extends React.Component {
|
||||
return (
|
||||
<Fragment key={index}>
|
||||
<span className="path-split">/</span>
|
||||
<span className="path-item"><TagViewName id={item} /></span>
|
||||
<TagViewName id={item} />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
@@ -6,17 +6,21 @@ import { gettext } from '../../utils/constants';
|
||||
import { PRIVATE_FILE_TYPE } from '../../constants';
|
||||
import { FACE_RECOGNITION_VIEW_ID, VIEW_TYPE } from '../constants';
|
||||
import { useMetadataStatus } from '../../hooks';
|
||||
import { updateFavicon } from '../utils/favicon';
|
||||
|
||||
// This hook provides content related to seahub interaction, such as whether to enable extended attributes, views data, etc.
|
||||
const MetadataContext = React.createContext(null);
|
||||
|
||||
export const MetadataProvider = ({ repoID, repoInfo, hideMetadataView, selectMetadataView, children }) => {
|
||||
export const MetadataProvider = ({ repoID, currentPath, repoInfo, hideMetadataView, selectMetadataView, children }) => {
|
||||
const [isLoading, setLoading] = useState(true);
|
||||
const [enableFaceRecognition, setEnableFaceRecognition] = useState(false);
|
||||
const [showFirstView, setShowFirstView] = useState(false);
|
||||
const [navigation, setNavigation] = useState([]);
|
||||
const [staticView, setStaticView] = useState([]);
|
||||
const [, setCount] = useState(0);
|
||||
|
||||
const viewsMap = useRef({});
|
||||
const originalTitleRef = useRef(document.title);
|
||||
|
||||
const isEmptyRepo = useMemo(() => repoInfo.file_count === 0, [repoInfo]);
|
||||
|
||||
@@ -32,6 +36,7 @@ export const MetadataProvider = ({ repoID, repoInfo, hideMetadataView, selectMet
|
||||
|
||||
// views
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
if (enableMetadata) {
|
||||
metadataAPI.listViews(repoID).then(res => {
|
||||
const { navigation, views } = res.data;
|
||||
@@ -46,9 +51,11 @@ export const MetadataProvider = ({ repoID, repoInfo, hideMetadataView, selectMet
|
||||
type: VIEW_TYPE.FACE_RECOGNITION,
|
||||
};
|
||||
setNavigation(navigation);
|
||||
setLoading(false);
|
||||
}).catch(error => {
|
||||
const errorMsg = Utils.getErrorMsg(error);
|
||||
toaster.danger(errorMsg);
|
||||
setLoading(false);
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -57,8 +64,9 @@ export const MetadataProvider = ({ repoID, repoInfo, hideMetadataView, selectMet
|
||||
viewsMap.current = {};
|
||||
setStaticView([]);
|
||||
setNavigation([]);
|
||||
setLoading(false);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [repoID, enableMetadata, hideMetadataView]);
|
||||
}, [repoID, enableMetadata]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enableMetadata) {
|
||||
@@ -171,6 +179,43 @@ export const MetadataProvider = ({ repoID, repoInfo, hideMetadataView, selectMet
|
||||
});
|
||||
}, [repoID]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading) return;
|
||||
const { origin, pathname, search } = window.location;
|
||||
const urlParams = new URLSearchParams(search);
|
||||
if (!urlParams.has('view')) return;
|
||||
const viewID = urlParams.get('view');
|
||||
if (viewID) {
|
||||
const lastOpenedView = viewsMap.current[viewID] || '';
|
||||
if (lastOpenedView) {
|
||||
selectView(lastOpenedView);
|
||||
return;
|
||||
}
|
||||
const url = `${origin}${pathname}`;
|
||||
window.history.pushState({ url: url, path: '' }, '', url);
|
||||
}
|
||||
|
||||
const firstViewObject = navigation.find(item => item.type === 'view');
|
||||
const firstView = firstViewObject ? viewsMap.current[firstViewObject._id] : '';
|
||||
if (firstView) {
|
||||
selectView(firstView);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentPath.includes('/' + PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES + '/')) return;
|
||||
const currentViewId = currentPath.split('/').pop();
|
||||
const currentView = viewsMap.current[currentViewId];
|
||||
if (currentView) {
|
||||
document.title = `${currentView.name} - Seafile`;
|
||||
updateFavicon(currentView.type);
|
||||
return;
|
||||
}
|
||||
document.title = originalTitleRef.current;
|
||||
updateFavicon('default');
|
||||
}, [currentPath, viewsMap]);
|
||||
|
||||
return (
|
||||
<MetadataContext.Provider value={{
|
||||
isEmptyRepo,
|
||||
|
@@ -6,7 +6,7 @@ import toaster from '../../components/toast';
|
||||
import Icon from '../../components/icon';
|
||||
import ViewItem from './view-item';
|
||||
import { AddView } from '../components/popover/view-popover';
|
||||
import { gettext, mediaUrl } from '../../utils/constants';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import { useMetadata } from '../hooks';
|
||||
import { PRIVATE_FILE_TYPE } from '../../constants';
|
||||
import { VIEW_TYPE, VIEW_TYPE_ICON } from '../constants';
|
||||
@@ -15,32 +15,6 @@ import { isEnter } from '../utils/hotkey';
|
||||
|
||||
import './index.css';
|
||||
|
||||
const updateFavicon = (type) => {
|
||||
const favicon = document.getElementById('favicon');
|
||||
if (favicon) {
|
||||
switch (type) {
|
||||
case VIEW_TYPE.GALLERY:
|
||||
case 'image':
|
||||
favicon.href = `${mediaUrl}favicons/gallery.png`;
|
||||
break;
|
||||
case VIEW_TYPE.TABLE:
|
||||
favicon.href = `${mediaUrl}favicons/table.png`;
|
||||
break;
|
||||
case VIEW_TYPE.FACE_RECOGNITION:
|
||||
favicon.href = `${mediaUrl}favicons/face-recognition-view.png`;
|
||||
break;
|
||||
case VIEW_TYPE.KANBAN:
|
||||
favicon.href = `${mediaUrl}favicons/kanban.png`;
|
||||
break;
|
||||
case VIEW_TYPE.MAP:
|
||||
favicon.href = `${mediaUrl}favicons/map.png`;
|
||||
break;
|
||||
default:
|
||||
favicon.href = `${mediaUrl}favicons/favicon.png`;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const MetadataTreeView = ({ userPerm, currentPath }) => {
|
||||
const canAdd = useMemo(() => {
|
||||
if (userPerm !== 'rw' && userPerm !== 'admin') return false;
|
||||
@@ -49,7 +23,6 @@ const MetadataTreeView = ({ userPerm, currentPath }) => {
|
||||
const [, setState] = useState(0);
|
||||
const {
|
||||
enableFaceRecognition,
|
||||
showFirstView,
|
||||
navigation,
|
||||
staticView,
|
||||
viewsMap,
|
||||
@@ -64,55 +37,9 @@ const MetadataTreeView = ({ userPerm, currentPath }) => {
|
||||
const [showAddViewPopover, setShowAddViewPopover] = useState(false);
|
||||
const [showInput, setShowInput] = useState(false);
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [originalTitle, setOriginalTitle] = useState('');
|
||||
|
||||
const inputRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
setOriginalTitle(document.title);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const { origin, pathname, search } = window.location;
|
||||
const urlParams = new URLSearchParams(search);
|
||||
const viewID = urlParams.get('view');
|
||||
if (viewID) {
|
||||
const lastOpenedView = viewsMap[viewID] || '';
|
||||
if (lastOpenedView) {
|
||||
selectView(lastOpenedView);
|
||||
document.title = `${lastOpenedView.name} - Seafile`;
|
||||
updateFavicon(lastOpenedView.type);
|
||||
return;
|
||||
}
|
||||
const url = `${origin}${pathname}`;
|
||||
window.history.pushState({ url: url, path: '' }, '', url);
|
||||
}
|
||||
|
||||
const firstViewObject = navigation.find(item => item.type === 'view');
|
||||
const firstView = firstViewObject ? viewsMap[firstViewObject._id] : '';
|
||||
if (showFirstView && firstView) {
|
||||
selectView(firstView);
|
||||
document.title = `${firstView.name} - Seafile`;
|
||||
updateFavicon(firstView.type);
|
||||
} else {
|
||||
document.title = originalTitle;
|
||||
updateFavicon('default');
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentPath.includes('/' + PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES + '/')) return;
|
||||
const currentViewId = currentPath.split('/').pop();
|
||||
const currentView = viewsMap[currentViewId];
|
||||
if (currentView) {
|
||||
document.title = `${currentView.name} - Seafile`;
|
||||
updateFavicon(currentView.type);
|
||||
return;
|
||||
}
|
||||
document.title = originalTitle;
|
||||
updateFavicon('default');
|
||||
}, [currentPath, viewsMap, originalTitle]);
|
||||
|
||||
const onUpdateView = useCallback((viewId, update, successCallback, failCallback) => {
|
||||
updateView(viewId, update, () => {
|
||||
setState(n => n + 1);
|
||||
|
28
frontend/src/metadata/utils/favicon.js
Normal file
28
frontend/src/metadata/utils/favicon.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import { VIEW_TYPE } from '../constants';
|
||||
import { mediaUrl } from '../../utils/constants';
|
||||
|
||||
export const updateFavicon = (type) => {
|
||||
const favicon = document.getElementById('favicon');
|
||||
if (favicon) {
|
||||
switch (type) {
|
||||
case VIEW_TYPE.GALLERY:
|
||||
case 'image':
|
||||
favicon.href = `${mediaUrl}favicons/gallery.png`;
|
||||
break;
|
||||
case VIEW_TYPE.TABLE:
|
||||
favicon.href = `${mediaUrl}favicons/table.png`;
|
||||
break;
|
||||
case VIEW_TYPE.FACE_RECOGNITION:
|
||||
favicon.href = `${mediaUrl}favicons/face-recognition-view.png`;
|
||||
break;
|
||||
case VIEW_TYPE.KANBAN:
|
||||
favicon.href = `${mediaUrl}favicons/kanban.png`;
|
||||
break;
|
||||
case VIEW_TYPE.MAP:
|
||||
favicon.href = `${mediaUrl}favicons/map.png`;
|
||||
break;
|
||||
default:
|
||||
favicon.href = `${mediaUrl}favicons/favicon.png`;
|
||||
}
|
||||
}
|
||||
};
|
@@ -2197,8 +2197,8 @@ class LibContentView extends React.Component {
|
||||
|
||||
return (
|
||||
<MetadataStatusProvider repoID={repoID} currentRepoInfo={currentRepoInfo} hideMetadataView={this.hideMetadataView}>
|
||||
<TagsProvider repoID={repoID} repoInfo={currentRepoInfo} selectTagsView={this.onTreeNodeClick}>
|
||||
<MetadataProvider repoID={repoID} repoInfo={currentRepoInfo} selectMetadataView={this.onTreeNodeClick} hideMetadataView={this.hideMetadataView} >
|
||||
<TagsProvider repoID={repoID} currentPath={path} repoInfo={currentRepoInfo} selectTagsView={this.onTreeNodeClick}>
|
||||
<MetadataProvider repoID={repoID} currentPath={path} repoInfo={currentRepoInfo} selectMetadataView={this.onTreeNodeClick} hideMetadataView={this.hideMetadataView} >
|
||||
<CollaboratorsProvider repoID={repoID}>
|
||||
<div className="main-panel-center flex-row">
|
||||
<div className="cur-view-container">
|
||||
|
@@ -1,22 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTags } from '../hooks';
|
||||
import { getRowById } from '../../metadata/utils/table';
|
||||
import { getTagName } from '../utils';
|
||||
import { TAG_MANAGEMENT_ID } from '../constants';
|
||||
import { gettext } from '../../utils/constants';
|
||||
|
||||
const TagViewName = ({ id }) => {
|
||||
const { tagsData } = useTags();
|
||||
if (!id) return null;
|
||||
if (id === TAG_MANAGEMENT_ID) return gettext('Tags management');
|
||||
const tag = getRowById(tagsData, id);
|
||||
if (!tag) return null;
|
||||
return (<>{getTagName(tag)}</>);
|
||||
};
|
||||
|
||||
TagViewName.propTypes = {
|
||||
id: PropTypes.string,
|
||||
};
|
||||
|
||||
export default TagViewName;
|
@@ -0,0 +1,76 @@
|
||||
import React, { useCallback, useState, useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Dropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap';
|
||||
import { isEnter, isSpace } from '../../../metadata/utils/hotkey';
|
||||
import { gettext } from '../../../utils/constants';
|
||||
import { useTags } from '../../hooks';
|
||||
import EditTagDialog from '../dialog/edit-tag-dialog';
|
||||
|
||||
const AllTagsOperationToolbar = ({ children }) => {
|
||||
const [isMenuOpen, setMenuOpen] = useState(false);
|
||||
const [isShowEditTagDialog, setShowEditTagDialog] = useState(false);
|
||||
|
||||
const { tagsData, addTag } = useTags();
|
||||
|
||||
const tags = useMemo(() => {
|
||||
if (!tagsData) return [];
|
||||
return tagsData.rows;
|
||||
}, [tagsData]);
|
||||
|
||||
const toggleMenuOpen = useCallback(() => {
|
||||
setMenuOpen(!isMenuOpen);
|
||||
}, [isMenuOpen]);
|
||||
|
||||
const onDropdownKeyDown = useCallback((event) => {
|
||||
if (isEnter(event) || isSpace(event)) {
|
||||
toggleMenuOpen();
|
||||
}
|
||||
}, [toggleMenuOpen]);
|
||||
|
||||
const openAddTag = useCallback(() => {
|
||||
setShowEditTagDialog(true);
|
||||
}, []);
|
||||
|
||||
const closeAddTag = useCallback(() => {
|
||||
setShowEditTagDialog(false);
|
||||
}, []);
|
||||
|
||||
const handelAddTags = useCallback((tag, callback) => {
|
||||
addTag(tag, callback);
|
||||
}, [addTag]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="dir-operation">
|
||||
<Dropdown isOpen={isMenuOpen} toggle={toggleMenuOpen}>
|
||||
<DropdownToggle
|
||||
tag="div"
|
||||
role="button"
|
||||
className="path-item"
|
||||
onClick={toggleMenuOpen}
|
||||
onKeyDown={onDropdownKeyDown}
|
||||
data-toggle="dropdown"
|
||||
>
|
||||
{children}
|
||||
<i className="sf3-font-down sf3-font ml-1 path-item-dropdown-toggle"></i>
|
||||
</DropdownToggle>
|
||||
<DropdownMenu positionFixed={true}>
|
||||
<DropdownItem onClick={openAddTag}>
|
||||
<i className="sf3-font sf3-font-new mr-2 dropdown-item-icon"></i>
|
||||
{gettext('New tag')}
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
{isShowEditTagDialog && (
|
||||
<EditTagDialog tags={tags} title={gettext('New tag')} onToggle={closeAddTag} onSubmit={handelAddTags} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
AllTagsOperationToolbar.propTypes = {
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
export default AllTagsOperationToolbar;
|
29
frontend/src/tag/components/tag-view-name/index.js
Normal file
29
frontend/src/tag/components/tag-view-name/index.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTags } from '../../hooks';
|
||||
import { getRowById } from '../../../metadata/utils/table';
|
||||
import { getTagName } from '../../utils';
|
||||
import { ALL_TAGS_ID } from '../../constants';
|
||||
import { gettext } from '../../../utils/constants';
|
||||
import AllTagsOperationToolbar from './all-tags-operation-toolbar';
|
||||
|
||||
const TagViewName = ({ id }) => {
|
||||
const { tagsData, context } = useTags();
|
||||
if (!id) return null;
|
||||
if (id === ALL_TAGS_ID) {
|
||||
const canModify = context.canModify();
|
||||
if (!canModify) return (<span className="path-item">{gettext('All tags')}</span>);
|
||||
const canAddTag = context.canAddTag();
|
||||
if (!canAddTag) return (<span className="path-item">{gettext('All tags')}</span>);
|
||||
return (<AllTagsOperationToolbar>{gettext('All tags')}</AllTagsOperationToolbar>);
|
||||
}
|
||||
const tag = getRowById(tagsData, id);
|
||||
if (!tag) return null;
|
||||
return (<span className="path-item">{getTagName(tag)}</span>);
|
||||
};
|
||||
|
||||
TagViewName.propTypes = {
|
||||
id: PropTypes.string,
|
||||
};
|
||||
|
||||
export default TagViewName;
|
@@ -1 +1 @@
|
||||
export const TAG_MANAGEMENT_ID = '__tag_management';
|
||||
export const ALL_TAGS_ID = '__all_tags';
|
||||
|
@@ -3,25 +3,26 @@ import { Utils } from '../../utils/utils';
|
||||
import toaster from '../../components/toast';
|
||||
import { useMetadataStatus } from '../../hooks';
|
||||
import { PRIVATE_FILE_TYPE } from '../../constants';
|
||||
import { getTagColor, getTagId, getTagName, getCellValueByColumn } from '../utils/cell/core';
|
||||
import { getTagColor, getTagId, getTagName, getCellValueByColumn, updateFavicon } from '../utils';
|
||||
import Context from '../context';
|
||||
import Store from '../store';
|
||||
import { PER_LOAD_NUMBER, EVENT_BUS_TYPE } from '../../metadata/constants';
|
||||
import { getRowById } from '../../metadata/utils/table';
|
||||
import { gettext } from '../../utils/constants';
|
||||
import { PRIVATE_COLUMN_KEY } from '../constants';
|
||||
import { PRIVATE_COLUMN_KEY, ALL_TAGS_ID } from '../constants';
|
||||
import { getColumnOriginName } from '../../metadata/utils/column';
|
||||
|
||||
// This hook provides content related to seahub interaction, such as whether to enable extended attributes, views data, etc.
|
||||
const TagsContext = React.createContext(null);
|
||||
|
||||
export const TagsProvider = ({ repoID, selectTagsView, children, ...params }) => {
|
||||
export const TagsProvider = ({ repoID, currentPath, selectTagsView, children, ...params }) => {
|
||||
|
||||
const [isLoading, setLoading] = useState(true);
|
||||
const [tagsData, setTagsData] = useState(null);
|
||||
|
||||
const storeRef = useRef(null);
|
||||
const contextRef = useRef(null);
|
||||
const originalTitleRef = useRef(document.title);
|
||||
|
||||
const { enableMetadata, enableTags } = useMetadataStatus();
|
||||
|
||||
@@ -179,6 +180,48 @@ export const TagsProvider = ({ repoID, selectTagsView, children, ...params }) =>
|
||||
modifyLocalTags(tagIds, idTagUpdates, { [tagId]: originalRowUpdates }, { [tagId]: oldRowData }, { [tagId]: originalOldRowData }, { success_callback, fail_callback });
|
||||
}, [tagsData, modifyLocalTags]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading) return;
|
||||
const { search } = window.location;
|
||||
const urlParams = new URLSearchParams(search);
|
||||
if (!urlParams.has('tag')) return;
|
||||
const tagId = urlParams.get('tag');
|
||||
if (tagId) {
|
||||
if (tagId === ALL_TAGS_ID) {
|
||||
handelSelectTag({ [PRIVATE_COLUMN_KEY.ID]: ALL_TAGS_ID });
|
||||
return;
|
||||
}
|
||||
|
||||
const lastOpenedTag = getRowById(tagsData, tagId);
|
||||
if (lastOpenedTag) {
|
||||
handelSelectTag(lastOpenedTag);
|
||||
return;
|
||||
}
|
||||
|
||||
handelSelectTag({ [PRIVATE_COLUMN_KEY.ID]: ALL_TAGS_ID });
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentPath.includes('/' + PRIVATE_FILE_TYPE.TAGS_PROPERTIES + '/')) return;
|
||||
const currentTagId = currentPath.split('/').pop();
|
||||
if (currentTagId === ALL_TAGS_ID) {
|
||||
document.title = `${gettext('All tags')} - Seafile`;
|
||||
return;
|
||||
}
|
||||
const currentTag = getRowById(tagsData, currentTagId);
|
||||
if (currentTag) {
|
||||
const tagName = getTagName(currentTag);
|
||||
document.title = `${tagName} - Seafile`;
|
||||
updateFavicon('default');
|
||||
return;
|
||||
}
|
||||
document.title = originalTitleRef.current;
|
||||
updateFavicon('default');
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentPath, tagsData]);
|
||||
|
||||
return (
|
||||
<TagsContext.Provider value={{
|
||||
isLoading,
|
||||
|
@@ -1,12 +1,12 @@
|
||||
.tag-management-tree-node-inner:hover {
|
||||
.all-tags-tree-node-inner:hover {
|
||||
background-color: #f0f0f0;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.tag-management-tree-node-inner .sf3-font-tag {
|
||||
.all-tags-tree-node-inner .sf3-font-tag {
|
||||
font-size: 12px;
|
||||
color: #666666;
|
||||
line-height: 1.625;
|
||||
line-height: 1.5;
|
||||
width: 1.5rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
@@ -2,30 +2,30 @@ import React, { useCallback, useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { PRIVATE_FILE_TYPE } from '../../../constants';
|
||||
import { PRIVATE_COLUMN_KEY, TAG_MANAGEMENT_ID } from '../../constants';
|
||||
import { PRIVATE_COLUMN_KEY, ALL_TAGS_ID } from '../../constants';
|
||||
import { useTags } from '../../hooks';
|
||||
import { gettext } from '../../../utils/constants';
|
||||
|
||||
import './index.css';
|
||||
|
||||
const TagsManagement = ({ currentPath }) => {
|
||||
const AllTags = ({ currentPath }) => {
|
||||
const { selectTag } = useTags();
|
||||
|
||||
const path = useMemo(() => '/' + PRIVATE_FILE_TYPE.TAGS_PROPERTIES + '/' + TAG_MANAGEMENT_ID, []);
|
||||
const path = useMemo(() => '/' + PRIVATE_FILE_TYPE.TAGS_PROPERTIES + '/' + ALL_TAGS_ID, []);
|
||||
const isSelected = useMemo(() => currentPath === path, [currentPath, path]);
|
||||
|
||||
const selectTagManagement = useCallback(() => {
|
||||
const handelClick = useCallback(() => {
|
||||
selectTag({
|
||||
[PRIVATE_COLUMN_KEY.ID]: TAG_MANAGEMENT_ID,
|
||||
[PRIVATE_COLUMN_KEY.ID]: ALL_TAGS_ID,
|
||||
}, isSelected);
|
||||
}, [isSelected, selectTag]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames('tree-node-inner text-nowrap tag-management-tree-node-inner', { 'tree-node-hight-light': isSelected })}
|
||||
onClick={selectTagManagement}
|
||||
className={classnames('tree-node-inner text-nowrap all-tags-tree-node-inner', { 'tree-node-hight-light': isSelected })}
|
||||
onClick={handelClick}
|
||||
>
|
||||
<div className="tree-node-text">{gettext('Tags management')}</div>
|
||||
<div className="tree-node-text">{gettext('All tags')}</div>
|
||||
<div className="left-icon">
|
||||
<div className="tree-node-icon">
|
||||
<i className="sf3-font sf3-font-tag"></i>
|
||||
@@ -35,8 +35,8 @@ const TagsManagement = ({ currentPath }) => {
|
||||
);
|
||||
};
|
||||
|
||||
TagsManagement.propTypes = {
|
||||
AllTags.propTypes = {
|
||||
currentPath: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default TagsManagement;
|
||||
export default AllTags;
|
@@ -1,24 +1,12 @@
|
||||
import React, { useEffect, useMemo, useRef } from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTags } from '../hooks';
|
||||
import Tag from './tag';
|
||||
import { getTagId, getTagName } from '../utils';
|
||||
import { getTagId } from '../utils';
|
||||
import { PRIVATE_FILE_TYPE } from '../../constants';
|
||||
import { gettext, mediaUrl } from '../../utils/constants';
|
||||
import { getRowById } from '../../metadata/utils/table';
|
||||
import TagsManagement from './tags-management';
|
||||
import { PRIVATE_COLUMN_KEY, TAG_MANAGEMENT_ID } from '../constants';
|
||||
|
||||
const updateFavicon = () => {
|
||||
const favicon = document.getElementById('favicon');
|
||||
if (favicon) {
|
||||
favicon.href = `${mediaUrl}favicons/favicon.png`;
|
||||
}
|
||||
};
|
||||
|
||||
const TagsTreeView = ({ userPerm, currentPath }) => {
|
||||
const originalTitle = useRef('');
|
||||
import AllTags from './all-tags';
|
||||
|
||||
const TagsTreeView = ({ currentPath }) => {
|
||||
const { tagsData, selectTag } = useTags();
|
||||
|
||||
const tags = useMemo(() => {
|
||||
@@ -26,67 +14,11 @@ const TagsTreeView = ({ userPerm, currentPath }) => {
|
||||
return tagsData.rows;
|
||||
}, [tagsData]);
|
||||
|
||||
const canUpdate = useMemo(() => {
|
||||
if (userPerm !== 'rw' && userPerm !== 'admin') return false;
|
||||
return true;
|
||||
}, [userPerm]);
|
||||
|
||||
useEffect(() => {
|
||||
originalTitle.current = document.title;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const { origin, pathname, search } = window.location;
|
||||
const urlParams = new URLSearchParams(search);
|
||||
const tagId = urlParams.get('tag');
|
||||
if (tagId) {
|
||||
if (tagId === TAG_MANAGEMENT_ID) {
|
||||
if (!canUpdate) return;
|
||||
selectTag({ [PRIVATE_COLUMN_KEY.ID]: TAG_MANAGEMENT_ID });
|
||||
return;
|
||||
}
|
||||
|
||||
const lastOpenedTag = getRowById(tagsData, tagId);
|
||||
if (lastOpenedTag) {
|
||||
selectTag(lastOpenedTag);
|
||||
const lastOpenedTagName = getTagName(lastOpenedTag);
|
||||
document.title = `${lastOpenedTagName} - Seafile`;
|
||||
updateFavicon();
|
||||
return;
|
||||
}
|
||||
const url = `${origin}${pathname}`;
|
||||
window.history.pushState({ url: url, path: '' }, '', url);
|
||||
}
|
||||
updateFavicon();
|
||||
document.title = originalTitle.current;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentPath.includes('/' + PRIVATE_FILE_TYPE.TAGS_PROPERTIES + '/')) return;
|
||||
const currentTagId = currentPath.split('/').pop();
|
||||
if (currentTagId === TAG_MANAGEMENT_ID) {
|
||||
if (!canUpdate) return;
|
||||
document.title = `${gettext('Tags management')} - Seafile`;
|
||||
return;
|
||||
}
|
||||
const currentTag = getRowById(tagsData, currentTagId);
|
||||
if (currentTag) {
|
||||
const tagName = getTagName(currentTag);
|
||||
document.title = `${tagName} - Seafile`;
|
||||
updateFavicon('default');
|
||||
return;
|
||||
}
|
||||
document.title = originalTitle;
|
||||
updateFavicon('default');
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentPath, tagsData]);
|
||||
|
||||
return (
|
||||
<div className="tree-view tree metadata-tree-view">
|
||||
<div className="tree-node">
|
||||
<div className="children">
|
||||
{tags.map(tag => {
|
||||
{tags.slice(0, 20).map(tag => {
|
||||
const id = getTagId(tag);
|
||||
const tagPath = '/' + PRIVATE_FILE_TYPE.TAGS_PROPERTIES + '/' + id;
|
||||
const isSelected = currentPath === tagPath;
|
||||
@@ -99,16 +31,14 @@ const TagsTreeView = ({ userPerm, currentPath }) => {
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{canUpdate && (<TagsManagement currentPath={currentPath} />)}
|
||||
<AllTags currentPath={currentPath} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
TagsTreeView.propTypes = {
|
||||
userPerm: PropTypes.string,
|
||||
currentPath: PropTypes.string,
|
||||
};
|
||||
|
||||
|
8
frontend/src/tag/utils/favicon.js
Normal file
8
frontend/src/tag/utils/favicon.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import { mediaUrl } from '../../utils/constants';
|
||||
|
||||
export const updateFavicon = () => {
|
||||
const favicon = document.getElementById('favicon');
|
||||
if (favicon) {
|
||||
favicon.href = `${mediaUrl}favicons/favicon.png`;
|
||||
}
|
||||
};
|
@@ -1,2 +1,3 @@
|
||||
export * from './cell';
|
||||
export * from './validate';
|
||||
export * from './favicon';
|
||||
|
19
frontend/src/tag/views/all-tags/index.css
Normal file
19
frontend/src/tag/views/all-tags/index.css
Normal file
@@ -0,0 +1,19 @@
|
||||
.sf-metadata-tags-wrapper {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sf-metadata-tags-wrapper .sf-metadata-tags-main {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sf-metadata-all-tags-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
38
frontend/src/tag/views/all-tags/index.js
Normal file
38
frontend/src/tag/views/all-tags/index.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { CenteredLoading } from '@seafile/sf-metadata-ui-component';
|
||||
import { useTags } from '../../hooks';
|
||||
import Main from './main';
|
||||
import { EVENT_BUS_TYPE } from '../../../metadata/constants';
|
||||
|
||||
import './index.css';
|
||||
|
||||
const AllTags = () => {
|
||||
const { isLoading, tagsData, context } = useTags();
|
||||
|
||||
useEffect(() => {
|
||||
const eventBus = context.eventBus;
|
||||
eventBus.dispatch(EVENT_BUS_TYPE.RELOAD_DATA);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const tags = useMemo(() => {
|
||||
if (!tagsData) return [];
|
||||
return tagsData.rows;
|
||||
}, [tagsData]);
|
||||
|
||||
if (isLoading) return (<CenteredLoading />);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="sf-metadata-tags-wrapper sf-metadata-all-tags-wrapper">
|
||||
<div className="sf-metadata-tags-main">
|
||||
<div className="sf-metadata-all-tags-container">
|
||||
<Main tags={tags} context={context} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AllTags;
|
@@ -5,6 +5,7 @@
|
||||
flex-direction: column;
|
||||
overflow-y: scroll;
|
||||
width: 100%;
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
|
||||
.sf-metadata-tags-table .sf-metadata-tags-table-header {
|
@@ -19,7 +19,7 @@ const Main = ({ context, tags }) => {
|
||||
return (
|
||||
<div className="sf-metadata-tags-table">
|
||||
<div className="sf-metadata-tags-table-header sf-metadata-tags-table-row">
|
||||
<div className="sf-metadata-tags-table-cell">{gettext('tag')}</div>
|
||||
<div className="sf-metadata-tags-table-cell">{gettext('Tag')}</div>
|
||||
<div className="sf-metadata-tags-table-cell">{gettext('File count')}</div>
|
||||
<div className="sf-metadata-tags-table-cell"></div>
|
||||
</div>
|
@@ -65,7 +65,7 @@ const Tag = ({ tags, tag, context }) => {
|
||||
</div>
|
||||
</div>
|
||||
{isShowEditTagDialog && (
|
||||
<EditTagDialog tags={tags} title={gettext('Add tag')} tag={tag} onToggle={closeEditTagDialog} onSubmit={handelEditTag} />
|
||||
<EditTagDialog tags={tags} title={gettext('Edit tag')} tag={tag} onToggle={closeEditTagDialog} onSubmit={handelEditTag} />
|
||||
)}
|
||||
{isShowDeleteDialog && (
|
||||
<DeleteConfirmDialog title={gettext('Delete tag')} content={tagName} onToggle={closeDeleteConfirmDialog} onSubmit={handelDelete} />
|
@@ -1,12 +1,12 @@
|
||||
import React from 'react';
|
||||
import { TagViewProvider } from '../hooks';
|
||||
import View from './view';
|
||||
import TagsManagement from './tags-management';
|
||||
import { TAG_MANAGEMENT_ID } from '../constants';
|
||||
import AllTags from './all-tags';
|
||||
import { ALL_TAGS_ID } from '../constants';
|
||||
|
||||
const Views = ({ ...params }) => {
|
||||
if (params.tagID === TAG_MANAGEMENT_ID) {
|
||||
return (<TagsManagement { ...params } />);
|
||||
if (params.tagID === ALL_TAGS_ID) {
|
||||
return (<AllTags { ...params } />);
|
||||
}
|
||||
|
||||
return (
|
||||
|
@@ -1,37 +0,0 @@
|
||||
.sf-metadata-tags-wrapper {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sf-metadata-tags-wrapper .sf-metadata-tags-main {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sf-metadata-tags-management-container {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sf-metadata-tags-management-container .sf-metadata-container-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 6px;
|
||||
padding-top: 2px;
|
||||
border-bottom: 1px solid #eaeaea;
|
||||
}
|
||||
|
||||
.sf-metadata-tags-management-container .sf-metadata-container-header .sf-metadata-container-header-add-tag {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
height: 26px;
|
||||
padding-bottom: 0;
|
||||
padding-top: 0;
|
||||
}
|
@@ -1,67 +0,0 @@
|
||||
import React, { useCallback, useEffect, useState, useMemo } from 'react';
|
||||
import { CenteredLoading } from '@seafile/sf-metadata-ui-component';
|
||||
import { Button } from 'reactstrap';
|
||||
import { useTags } from '../../hooks';
|
||||
import Main from './main';
|
||||
import { gettext } from '../../../utils/constants';
|
||||
import EditTagDialog from '../../components/dialog/edit-tag-dialog';
|
||||
|
||||
import './index.css';
|
||||
import { EVENT_BUS_TYPE } from '../../../metadata/constants';
|
||||
|
||||
const TagsManagement = () => {
|
||||
const [isShowEditTagDialog, setShowEditTagDialog] = useState(false);
|
||||
|
||||
const { isLoading, tagsData, addTag, context } = useTags();
|
||||
|
||||
useEffect(() => {
|
||||
const eventBus = context.eventBus;
|
||||
eventBus.dispatch(EVENT_BUS_TYPE.RELOAD_DATA);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const tags = useMemo(() => {
|
||||
if (!tagsData) return [];
|
||||
return tagsData.rows;
|
||||
}, [tagsData]);
|
||||
|
||||
const openAddTag = useCallback(() => {
|
||||
setShowEditTagDialog(true);
|
||||
}, []);
|
||||
|
||||
const closeAddTag = useCallback(() => {
|
||||
setShowEditTagDialog(false);
|
||||
}, []);
|
||||
|
||||
const handelAddTags = useCallback((tag, callback) => {
|
||||
addTag(tag, callback);
|
||||
}, [addTag]);
|
||||
|
||||
if (isLoading) return (<CenteredLoading />);
|
||||
return (
|
||||
<>
|
||||
<div className="sf-metadata-tags-wrapper sf-metadata-tags-management-wrapper">
|
||||
<div className="sf-metadata-tags-main">
|
||||
<div className="sf-metadata-tags-management-container">
|
||||
<div className="sf-metadata-container-header">
|
||||
<div className="sf-metadata-container-header-title">{gettext('Tags management')}</div>
|
||||
<div className="sf-metadata-container-header-actions">
|
||||
{context.canAddTag() && (
|
||||
<Button color="primary" className="sf-metadata-container-header-add-tag" onClick={openAddTag}>
|
||||
{gettext('New Tag')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Main tags={tags} context={context} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{isShowEditTagDialog && (
|
||||
<EditTagDialog tags={tags} title={gettext('Add tag')} onToggle={closeAddTag} onSubmit={handelAddTags} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TagsManagement;
|
Reference in New Issue
Block a user