mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-09 10:50:24 +00:00
feat(tag): display tags sidebar with tree (#7428)
This commit is contained in:
@@ -162,3 +162,26 @@ export const getAllSubTreeNodes = (nodeIndex, tree) => {
|
|||||||
}
|
}
|
||||||
return subNodes;
|
return subNodes;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getTreeChildNodes = (parentNode, tree) => {
|
||||||
|
const parentNodeKey = getTreeNodeKey(parentNode);
|
||||||
|
const parentNodeIndex = tree.findIndex((node) => getTreeNodeKey(node) === parentNodeKey);
|
||||||
|
if (parentNodeIndex < 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentNodeDepth = getTreeNodeDepth(parentNode);
|
||||||
|
const childNodeDepth = parentNodeDepth + 1;
|
||||||
|
let childNodes = [];
|
||||||
|
for (let i = parentNodeIndex + 1, len = tree.length; i < len; i++) {
|
||||||
|
const currentNode = tree[i];
|
||||||
|
if (!getTreeNodeKey(currentNode).includes(parentNodeKey)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getTreeNodeDepth(currentNode) === childNodeDepth) {
|
||||||
|
childNodes.push({ ...currentNode });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return childNodes;
|
||||||
|
};
|
||||||
|
1
frontend/src/tag/constants/sidebar-tree.js
Normal file
1
frontend/src/tag/constants/sidebar-tree.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const SIDEBAR_INIT_LEFT_INDENT = 40;
|
@@ -2,7 +2,7 @@ import React, { useCallback, useContext, useEffect, useState } from 'react';
|
|||||||
import { Utils } from '../../utils/utils';
|
import { Utils } from '../../utils/utils';
|
||||||
import tagsAPI from '../api';
|
import tagsAPI from '../api';
|
||||||
import { useTags } from './tags';
|
import { useTags } from './tags';
|
||||||
import { getTreeNodeByKey } from '../../components/sf-table/utils/tree';
|
import { getTreeNodeById, getTreeNodeByKey } from '../../components/sf-table/utils/tree';
|
||||||
import { getAllChildTagsIdsFromNode } from '../utils/tree';
|
import { getAllChildTagsIdsFromNode } from '../utils/tree';
|
||||||
|
|
||||||
// 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.
|
||||||
@@ -15,15 +15,20 @@ export const TagViewProvider = ({ repoID, tagID, nodeKey, children, ...params })
|
|||||||
|
|
||||||
const { tagsData } = useTags();
|
const { tagsData } = useTags();
|
||||||
|
|
||||||
const getChildTagsIds = useCallback((nodeKey) => {
|
const getChildTagsIds = useCallback((tagID, nodeKey) => {
|
||||||
if (!nodeKey) return [];
|
let displayNode = null;
|
||||||
const displayNode = getTreeNodeByKey(nodeKey, tagsData.key_tree_node_map);
|
if (nodeKey) {
|
||||||
|
displayNode = getTreeNodeByKey(nodeKey, tagsData.key_tree_node_map);
|
||||||
|
}
|
||||||
|
if (!displayNode) {
|
||||||
|
displayNode = getTreeNodeById(tagID, tagsData.rows_tree);
|
||||||
|
}
|
||||||
return getAllChildTagsIdsFromNode(displayNode);
|
return getAllChildTagsIdsFromNode(displayNode);
|
||||||
}, [tagsData]);
|
}, [tagsData]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const childTagsIds = getChildTagsIds(nodeKey);
|
const childTagsIds = getChildTagsIds(tagID, nodeKey);
|
||||||
let tagsIds = [tagID];
|
let tagsIds = [tagID];
|
||||||
if (Array.isArray(childTagsIds) && childTagsIds.length > 0) {
|
if (Array.isArray(childTagsIds) && childTagsIds.length > 0) {
|
||||||
tagsIds.push(...childTagsIds);
|
tagsIds.push(...childTagsIds);
|
||||||
|
@@ -21,6 +21,7 @@ export const TagsProvider = ({ repoID, currentPath, selectTagsView, children, ..
|
|||||||
const [isLoading, setLoading] = useState(true);
|
const [isLoading, setLoading] = useState(true);
|
||||||
const [isReloading, setReloading] = useState(false);
|
const [isReloading, setReloading] = useState(false);
|
||||||
const [tagsData, setTagsData] = useState(null);
|
const [tagsData, setTagsData] = useState(null);
|
||||||
|
const [displayNodeKey, setDisplayNodeKey] = useState('');
|
||||||
|
|
||||||
const storeRef = useRef(null);
|
const storeRef = useRef(null);
|
||||||
const contextRef = useRef(null);
|
const contextRef = useRef(null);
|
||||||
@@ -92,7 +93,7 @@ export const TagsProvider = ({ repoID, currentPath, selectTagsView, children, ..
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [enableMetadata, enableTags]);
|
}, [enableMetadata, enableTags]);
|
||||||
|
|
||||||
const handelSelectTag = useCallback((tag, isSelected) => {
|
const handleSelectTag = useCallback((tag, nodeKey, isSelected) => {
|
||||||
if (isSelected) return;
|
if (isSelected) return;
|
||||||
const id = getTagId(tag);
|
const id = getTagId(tag);
|
||||||
const node = {
|
const node = {
|
||||||
@@ -111,21 +112,22 @@ export const TagsProvider = ({ repoID, currentPath, selectTagsView, children, ..
|
|||||||
key: repoID,
|
key: repoID,
|
||||||
tag_id: id,
|
tag_id: id,
|
||||||
};
|
};
|
||||||
|
setDisplayNodeKey(nodeKey || '');
|
||||||
selectTagsView(node);
|
selectTagsView(node);
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [repoID, selectTagsView]);
|
}, [repoID, selectTagsView]);
|
||||||
|
|
||||||
const addTag = useCallback((row, callback) => {
|
const addTag = useCallback((row, callback) => {
|
||||||
return storeRef.current.addTags([row], callback);
|
return storeRef.current.addTags([row], callback);
|
||||||
}, []);
|
}, [storeRef]);
|
||||||
|
|
||||||
const addTags = useCallback((rows, callback) => {
|
const addTags = useCallback((rows, callback) => {
|
||||||
return storeRef.current.addTags(rows, callback);
|
return storeRef.current.addTags(rows, callback);
|
||||||
}, []);
|
}, [storeRef]);
|
||||||
|
|
||||||
const addChildTag = useCallback((tagData, parentTagId, callback = {}) => {
|
const addChildTag = useCallback((tagData, parentTagId, callback = {}) => {
|
||||||
return storeRef.current.addChildTag(tagData, parentTagId, callback);
|
return storeRef.current.addChildTag(tagData, parentTagId, callback);
|
||||||
}, []);
|
}, [storeRef]);
|
||||||
|
|
||||||
const modifyTags = useCallback((tagIds, idTagUpdates, idOriginalRowUpdates, idOldRowData, idOriginalOldRowData, { success_callback, fail_callback }) => {
|
const modifyTags = useCallback((tagIds, idTagUpdates, idOriginalRowUpdates, idOldRowData, idOriginalOldRowData, { success_callback, fail_callback }) => {
|
||||||
storeRef.current.modifyTags(tagIds, idTagUpdates, idOriginalRowUpdates, idOldRowData, idOriginalOldRowData, { success_callback, fail_callback });
|
storeRef.current.modifyTags(tagIds, idTagUpdates, idOriginalRowUpdates, idOldRowData, idOriginalOldRowData, { success_callback, fail_callback });
|
||||||
@@ -149,10 +151,10 @@ export const TagsProvider = ({ repoID, currentPath, selectTagsView, children, ..
|
|||||||
addTag(newTag, {
|
addTag(newTag, {
|
||||||
success_callback: (operation) => {
|
success_callback: (operation) => {
|
||||||
const copiedTag = operation.tags[0];
|
const copiedTag = operation.tags[0];
|
||||||
handelSelectTag(copiedTag);
|
handleSelectTag(copiedTag);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [tagsData, addTag, handelSelectTag]);
|
}, [tagsData, addTag, handleSelectTag]);
|
||||||
|
|
||||||
const updateTag = useCallback((tagId, update, { success_callback, fail_callback } = { }) => {
|
const updateTag = useCallback((tagId, update, { success_callback, fail_callback } = { }) => {
|
||||||
const tag = getRowById(tagsData, tagId);
|
const tag = getRowById(tagsData, tagId);
|
||||||
@@ -192,22 +194,22 @@ export const TagsProvider = ({ repoID, currentPath, selectTagsView, children, ..
|
|||||||
|
|
||||||
const addTagLinks = useCallback((columnKey, tagId, otherTagsIds, { success_callback, fail_callback } = {}) => {
|
const addTagLinks = useCallback((columnKey, tagId, otherTagsIds, { success_callback, fail_callback } = {}) => {
|
||||||
storeRef.current.addTagLinks(columnKey, tagId, otherTagsIds, success_callback, fail_callback);
|
storeRef.current.addTagLinks(columnKey, tagId, otherTagsIds, success_callback, fail_callback);
|
||||||
}, []);
|
}, [storeRef]);
|
||||||
|
|
||||||
const deleteTagLinks = useCallback((columnKey, tagId, otherTagsIds, { success_callback, fail_callback } = {}) => {
|
const deleteTagLinks = useCallback((columnKey, tagId, otherTagsIds, { success_callback, fail_callback } = {}) => {
|
||||||
storeRef.current.deleteTagLinks(columnKey, tagId, otherTagsIds, success_callback, fail_callback);
|
storeRef.current.deleteTagLinks(columnKey, tagId, otherTagsIds, success_callback, fail_callback);
|
||||||
}, []);
|
}, [storeRef]);
|
||||||
|
|
||||||
const mergeTags = useCallback((target_tag_id, merged_tags_ids, { success_callback, fail_callback } = {}) => {
|
const mergeTags = useCallback((target_tag_id, merged_tags_ids, { success_callback, fail_callback } = {}) => {
|
||||||
storeRef.current.mergeTags(target_tag_id, merged_tags_ids, success_callback, fail_callback);
|
storeRef.current.mergeTags(target_tag_id, merged_tags_ids, success_callback, fail_callback);
|
||||||
}, []);
|
}, [storeRef]);
|
||||||
|
|
||||||
const modifyColumnWidth = useCallback((columnKey, newWidth) => {
|
const modifyColumnWidth = useCallback((columnKey, newWidth) => {
|
||||||
storeRef.current.modifyColumnWidth(columnKey, newWidth);
|
storeRef.current.modifyColumnWidth(columnKey, newWidth);
|
||||||
}, [storeRef]);
|
}, [storeRef]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!handelSelectTag) return;
|
if (!handleSelectTag) return;
|
||||||
if (isLoading) return;
|
if (isLoading) return;
|
||||||
const { search } = window.location;
|
const { search } = window.location;
|
||||||
const urlParams = new URLSearchParams(search);
|
const urlParams = new URLSearchParams(search);
|
||||||
@@ -215,17 +217,17 @@ export const TagsProvider = ({ repoID, currentPath, selectTagsView, children, ..
|
|||||||
const tagId = urlParams.get('tag');
|
const tagId = urlParams.get('tag');
|
||||||
if (tagId) {
|
if (tagId) {
|
||||||
if (tagId === ALL_TAGS_ID) {
|
if (tagId === ALL_TAGS_ID) {
|
||||||
handelSelectTag({ [PRIVATE_COLUMN_KEY.ID]: ALL_TAGS_ID });
|
handleSelectTag({ [PRIVATE_COLUMN_KEY.ID]: ALL_TAGS_ID });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastOpenedTag = getRowById(tagsData, tagId);
|
const lastOpenedTag = getRowById(tagsData, tagId);
|
||||||
if (lastOpenedTag) {
|
if (lastOpenedTag) {
|
||||||
handelSelectTag(lastOpenedTag);
|
handleSelectTag(lastOpenedTag);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
handelSelectTag({ [PRIVATE_COLUMN_KEY.ID]: ALL_TAGS_ID });
|
handleSelectTag({ [PRIVATE_COLUMN_KEY.ID]: ALL_TAGS_ID });
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [isLoading]);
|
}, [isLoading]);
|
||||||
@@ -262,6 +264,7 @@ export const TagsProvider = ({ repoID, currentPath, selectTagsView, children, ..
|
|||||||
isLoading,
|
isLoading,
|
||||||
isReloading,
|
isReloading,
|
||||||
tagsData,
|
tagsData,
|
||||||
|
displayNodeKey,
|
||||||
currentPath,
|
currentPath,
|
||||||
store: storeRef.current,
|
store: storeRef.current,
|
||||||
context: contextRef.current,
|
context: contextRef.current,
|
||||||
@@ -279,7 +282,7 @@ export const TagsProvider = ({ repoID, currentPath, selectTagsView, children, ..
|
|||||||
deleteTagLinks,
|
deleteTagLinks,
|
||||||
mergeTags,
|
mergeTags,
|
||||||
updateLocalTag,
|
updateLocalTag,
|
||||||
selectTag: handelSelectTag,
|
selectTag: handleSelectTag,
|
||||||
modifyColumnWidth,
|
modifyColumnWidth,
|
||||||
}}>
|
}}>
|
||||||
{children}
|
{children}
|
||||||
|
@@ -2,23 +2,19 @@ import React, { useCallback, useMemo } from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { PRIVATE_FILE_TYPE } from '../../../constants';
|
import { PRIVATE_FILE_TYPE } from '../../../constants';
|
||||||
import { PRIVATE_COLUMN_KEY, ALL_TAGS_ID } from '../../constants';
|
import { ALL_TAGS_ID } from '../../constants';
|
||||||
import { useTags } from '../../hooks';
|
|
||||||
import { gettext } from '../../../utils/constants';
|
import { gettext } from '../../../utils/constants';
|
||||||
|
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
const AllTags = ({ currentPath }) => {
|
const AllTags = ({ currentPath, selectAllTags }) => {
|
||||||
const { selectTag } = useTags();
|
|
||||||
|
|
||||||
const path = useMemo(() => '/' + PRIVATE_FILE_TYPE.TAGS_PROPERTIES + '/' + ALL_TAGS_ID, []);
|
const path = useMemo(() => '/' + PRIVATE_FILE_TYPE.TAGS_PROPERTIES + '/' + ALL_TAGS_ID, []);
|
||||||
const isSelected = useMemo(() => currentPath === path, [currentPath, path]);
|
const isSelected = useMemo(() => currentPath === path, [currentPath, path]);
|
||||||
|
|
||||||
const handelClick = useCallback(() => {
|
const handelClick = useCallback(() => {
|
||||||
selectTag({
|
selectAllTags(isSelected);
|
||||||
[PRIVATE_COLUMN_KEY.ID]: ALL_TAGS_ID,
|
|
||||||
}, isSelected);
|
}, [isSelected, selectAllTags]);
|
||||||
}, [isSelected, selectTag]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@@ -1,17 +1,6 @@
|
|||||||
.metadata-tree-view-tag .tree-node-inner .left-icon {
|
|
||||||
top: 5px;
|
|
||||||
padding-left: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.metadata-tree-view-tag .tree-node-inner .tree-node-text {
|
|
||||||
padding-left: 28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.metadata-tree-view-tag .tree-node-icon {
|
.metadata-tree-view-tag .tree-node-icon {
|
||||||
height: 100%;
|
height: 26px;
|
||||||
line-height: 1.5;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
transform: translateY(1px);
|
|
||||||
}
|
}
|
||||||
|
@@ -1,39 +1,124 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import AllTags from './all-tags';
|
import AllTags from './all-tags';
|
||||||
import Tag from './tag';
|
import Tag from './tag';
|
||||||
import { useTags } from '../hooks';
|
import { useTags } from '../hooks';
|
||||||
import { getTagId } from '../utils/cell';
|
|
||||||
import { PRIVATE_FILE_TYPE } from '../../constants';
|
import { PRIVATE_FILE_TYPE } from '../../constants';
|
||||||
|
import { PRIVATE_COLUMN_KEY, ALL_TAGS_ID } from '../constants';
|
||||||
|
import { checkTreeNodeHasChildNodes, getTreeChildNodes, getTreeNodeDepth, getTreeNodeId, getTreeNodeKey } from '../../components/sf-table/utils/tree';
|
||||||
|
import { getRowById } from '../../metadata/utils/table';
|
||||||
|
import { SIDEBAR_INIT_LEFT_INDENT } from '../constants/sidebar-tree';
|
||||||
|
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
|
const LOCAL_KEY_TREE_NODE_EXPANDED = 'sidebar_key_tree_node_expanded_map';
|
||||||
|
|
||||||
const TagsTreeView = ({ currentPath }) => {
|
const TagsTreeView = ({ currentPath }) => {
|
||||||
const { tagsData, selectTag } = useTags();
|
const { tagsData, selectTag } = useTags();
|
||||||
|
const [currSelectedNodeKey, setCurrSelectedNodeKey] = useState('');
|
||||||
|
const [keyTreeNodeExpandedMap, setKeyTreeNodeExpandedMap] = useState({});
|
||||||
|
|
||||||
const tags = useMemo(() => {
|
const recordsTree = useMemo(() => {
|
||||||
if (!tagsData) return [];
|
return tagsData.rows_tree || [];
|
||||||
return tagsData.rows;
|
|
||||||
}, [tagsData]);
|
}, [tagsData]);
|
||||||
|
|
||||||
|
const buildTree = useCallback((roots, tree) => {
|
||||||
|
roots.forEach((node) => {
|
||||||
|
const childNodes = checkTreeNodeHasChildNodes(node) ? getTreeChildNodes(node, tree) : [];
|
||||||
|
if (childNodes.length > 0) {
|
||||||
|
node.children = childNodes;
|
||||||
|
buildTree(node.children, tree);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const visibleRoots = useMemo(() => {
|
||||||
|
let roots = recordsTree.filter((node) => getTreeNodeDepth(node) === 0);
|
||||||
|
roots = roots.slice(0, 20);
|
||||||
|
buildTree(roots, recordsTree);
|
||||||
|
return roots;
|
||||||
|
}, [recordsTree, buildTree]);
|
||||||
|
|
||||||
|
const getKeyTreeNodeExpandedMap = useCallback(() => {
|
||||||
|
const strKeyTreeNodeExpandedMap = window.sfTagsDataContext.localStorage.getItem(LOCAL_KEY_TREE_NODE_EXPANDED);
|
||||||
|
if (strKeyTreeNodeExpandedMap) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(strKeyTreeNodeExpandedMap);
|
||||||
|
} catch {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const storeKeyTreeNodeExpandedMap = useCallback((keyTreeNodeExpandedMap) => {
|
||||||
|
window.sfTagsDataContext.localStorage.setItem(LOCAL_KEY_TREE_NODE_EXPANDED, JSON.stringify(keyTreeNodeExpandedMap));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const checkNodeExpanded = useCallback((nodeKey) => {
|
||||||
|
return !!keyTreeNodeExpandedMap[nodeKey];
|
||||||
|
}, [keyTreeNodeExpandedMap]);
|
||||||
|
|
||||||
|
const toggleExpanded = useCallback((nodeKey, expanded) => {
|
||||||
|
let updatedKeyTreeNodeExpandedMap = { ...keyTreeNodeExpandedMap };
|
||||||
|
if (expanded) {
|
||||||
|
delete updatedKeyTreeNodeExpandedMap[nodeKey];
|
||||||
|
} else {
|
||||||
|
updatedKeyTreeNodeExpandedMap[nodeKey] = true;
|
||||||
|
}
|
||||||
|
storeKeyTreeNodeExpandedMap(updatedKeyTreeNodeExpandedMap);
|
||||||
|
setKeyTreeNodeExpandedMap(updatedKeyTreeNodeExpandedMap);
|
||||||
|
}, [keyTreeNodeExpandedMap, storeKeyTreeNodeExpandedMap]);
|
||||||
|
|
||||||
|
const selectNode = useCallback((node) => {
|
||||||
|
const tagId = getTreeNodeId(node);
|
||||||
|
const tag = getRowById(tagsData, tagId);
|
||||||
|
const nodeKey = getTreeNodeKey(node);
|
||||||
|
selectTag(tag, nodeKey);
|
||||||
|
setCurrSelectedNodeKey(nodeKey);
|
||||||
|
}, [tagsData, selectTag]);
|
||||||
|
|
||||||
|
const selectAllTags = useCallback((isSelected) => {
|
||||||
|
selectTag({ [PRIVATE_COLUMN_KEY.ID]: ALL_TAGS_ID }, isSelected);
|
||||||
|
setCurrSelectedNodeKey('');
|
||||||
|
}, [selectTag]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!currSelectedNodeKey) {
|
||||||
|
const selectedNode = recordsTree.find((node) => {
|
||||||
|
const nodePath = '/' + PRIVATE_FILE_TYPE.TAGS_PROPERTIES + '/' + getTreeNodeId(node);
|
||||||
|
return nodePath === currentPath;
|
||||||
|
});
|
||||||
|
const nextSelectedNodeKey = getTreeNodeKey(selectedNode);
|
||||||
|
setCurrSelectedNodeKey(nextSelectedNodeKey);
|
||||||
|
}
|
||||||
|
}, [currentPath, currSelectedNodeKey, recordsTree]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setKeyTreeNodeExpandedMap(getKeyTreeNodeExpandedMap());
|
||||||
|
}, [getKeyTreeNodeExpandedMap]);
|
||||||
|
|
||||||
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">
|
||||||
<div className="children">
|
<div className="children">
|
||||||
{tags.slice(0, 20).map(tag => {
|
{visibleRoots.map((node) => {
|
||||||
const id = getTagId(tag);
|
const nodeKey = getTreeNodeKey(node);
|
||||||
const tagPath = '/' + PRIVATE_FILE_TYPE.TAGS_PROPERTIES + '/' + id;
|
|
||||||
const isSelected = currentPath === tagPath;
|
|
||||||
return (
|
return (
|
||||||
<Tag
|
<Tag
|
||||||
key={id}
|
key={`sidebar-tree-node-${nodeKey}`}
|
||||||
tag={tag}
|
node={node}
|
||||||
isSelected={isSelected}
|
expanded={checkNodeExpanded(nodeKey)}
|
||||||
onClick={(tag) => selectTag(tag, isSelected)}
|
currentPath={currentPath}
|
||||||
|
leftIndent={SIDEBAR_INIT_LEFT_INDENT}
|
||||||
|
selectedNodeKey={currSelectedNodeKey}
|
||||||
|
checkNodeExpanded={checkNodeExpanded}
|
||||||
|
toggleExpanded={toggleExpanded}
|
||||||
|
selectNode={selectNode}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<AllTags currentPath={currentPath} />
|
<AllTags currentPath={currentPath} selectAllTags={selectAllTags} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -2,7 +2,6 @@
|
|||||||
height: 12px;
|
height: 12px;
|
||||||
width: 12px;
|
width: 12px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
transform: translateY(2px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-tree-node .tag-tree-node-text {
|
.tag-tree-node .tag-tree-node-text {
|
||||||
|
@@ -1,15 +1,51 @@
|
|||||||
import React, { useCallback, useMemo, useState } from 'react';
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { getTagColor, getTagName, getTagFilesCount } from '../../utils/cell';
|
import { getTagColor, getTagName, getTagFilesLinks } from '../../utils/cell';
|
||||||
|
import { checkTreeNodeHasChildNodes, getTreeNodeId, getTreeNodeKey } from '../../../components/sf-table/utils/tree';
|
||||||
|
import { getRowById } from '../../../metadata/utils/table';
|
||||||
|
import { useTags } from '../../hooks';
|
||||||
|
import { SIDEBAR_INIT_LEFT_INDENT } from '../../constants/sidebar-tree';
|
||||||
|
import { getAllChildTagsIdsFromNode } from '../../utils/tree';
|
||||||
|
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
const Tag = ({ isSelected, tag, onClick }) => {
|
const LEFT_INDENT_UNIT = 20;
|
||||||
|
const NODE_TEXT_LEFT_INDENT_UNIT = 5;
|
||||||
|
|
||||||
|
const Tag = ({ node, currentPath, leftIndent, selectedNodeKey, expanded, checkNodeExpanded, toggleExpanded, selectNode }) => {
|
||||||
|
const { tagsData } = useTags();
|
||||||
|
const [highlight, setHighlight] = useState(false);
|
||||||
|
|
||||||
|
const tagId = useMemo(() => {
|
||||||
|
return getTreeNodeId(node);
|
||||||
|
}, [node]);
|
||||||
|
|
||||||
|
const tag = useMemo(() => {
|
||||||
|
return getRowById(tagsData, tagId);
|
||||||
|
}, [tagsData, tagId]);
|
||||||
|
|
||||||
|
const hasChildren = useMemo(() => checkTreeNodeHasChildNodes(node), [node]);
|
||||||
|
const nodeKey = useMemo(() => getTreeNodeKey(node), [node]);
|
||||||
const tagName = useMemo(() => getTagName(tag), [tag]);
|
const tagName = useMemo(() => getTagName(tag), [tag]);
|
||||||
const tagColor = useMemo(() => getTagColor(tag), [tag]);
|
const tagColor = useMemo(() => getTagColor(tag), [tag]);
|
||||||
const tagCount = useMemo(() => getTagFilesCount(tag), [tag]);
|
const tagCount = useMemo(() => {
|
||||||
const [highlight, setHighlight] = useState(false);
|
const filesLinks = getTagFilesLinks(tag);
|
||||||
|
let allFilesLinks = [...filesLinks];
|
||||||
|
const childTagsIds = getAllChildTagsIdsFromNode(node);
|
||||||
|
childTagsIds.forEach((childTagId) => {
|
||||||
|
const childTag = getRowById(tagsData, childTagId);
|
||||||
|
const childFilesLinks = getTagFilesLinks(childTag);
|
||||||
|
if (childFilesLinks && childFilesLinks.length > 0) {
|
||||||
|
allFilesLinks.push(...childFilesLinks);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return allFilesLinks.length;
|
||||||
|
}, [node, tag, tagsData]);
|
||||||
|
|
||||||
|
const isSelected = useMemo(() => {
|
||||||
|
return nodeKey === selectedNodeKey;
|
||||||
|
}, [nodeKey, selectedNodeKey]);
|
||||||
|
|
||||||
const onMouseEnter = useCallback(() => {
|
const onMouseEnter = useCallback(() => {
|
||||||
setHighlight(true);
|
setHighlight(true);
|
||||||
@@ -23,30 +59,62 @@ const Tag = ({ isSelected, tag, onClick }) => {
|
|||||||
setHighlight(false);
|
setHighlight(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const onToggleExpanded = useCallback((event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
toggleExpanded(nodeKey, expanded);
|
||||||
|
}, [nodeKey, expanded, toggleExpanded]);
|
||||||
|
|
||||||
|
const renderChildren = useCallback(() => {
|
||||||
|
const { children } = node;
|
||||||
|
if (!expanded || !hasChildren || !Array.isArray(children) || children.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return children.map((childNode) => {
|
||||||
|
const childNodeKey = getTreeNodeKey(childNode);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Tag
|
||||||
|
key={`sidebar-tree-node-${childNodeKey}`}
|
||||||
|
node={childNode}
|
||||||
|
expanded={checkNodeExpanded(childNodeKey)}
|
||||||
|
selectedNodeKey={selectedNodeKey}
|
||||||
|
leftIndent={leftIndent + LEFT_INDENT_UNIT}
|
||||||
|
currentPath={currentPath}
|
||||||
|
checkNodeExpanded={checkNodeExpanded}
|
||||||
|
toggleExpanded={toggleExpanded}
|
||||||
|
selectNode={selectNode}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, [currentPath, node, selectedNodeKey, hasChildren, leftIndent, expanded, checkNodeExpanded, toggleExpanded, selectNode]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="tree-node">
|
||||||
<div
|
<div
|
||||||
className={classnames('tree-node-inner text-nowrap tag-tree-node', { 'tree-node-inner-hover': highlight, 'tree-node-hight-light': isSelected })}
|
className={classnames('tree-node-inner text-nowrap tag-tree-node', { 'tree-node-inner-hover': highlight, 'tree-node-hight-light': isSelected })}
|
||||||
title={`${tagName} (${tagCount})`}
|
title={`${tagName} (${tagCount})`}
|
||||||
onMouseEnter={onMouseEnter}
|
onMouseEnter={onMouseEnter}
|
||||||
onMouseOver={onMouseOver}
|
onMouseOver={onMouseOver}
|
||||||
onMouseLeave={onMouseLeave}
|
onMouseLeave={onMouseLeave}
|
||||||
onClick={() => onClick(tag)}
|
onClick={() => selectNode(node)}
|
||||||
>
|
>
|
||||||
<div className="tree-node-text tag-tree-node-text">
|
<div className="tree-node-text tag-tree-node-text" style={{ paddingLeft: leftIndent + NODE_TEXT_LEFT_INDENT_UNIT }}>
|
||||||
<div className="tag-tree-node-name">{tagName}</div>
|
<div className="tag-tree-node-name">{tagName}</div>
|
||||||
<div className="tag-tree-node-count">{tagCount}</div>
|
<div className="tag-tree-node-count">{tagCount}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="left-icon">
|
<div className="left-icon" style={{ left: leftIndent - SIDEBAR_INIT_LEFT_INDENT }}>
|
||||||
|
{hasChildren && <i className={classnames('folder-toggle-icon sf3-font sf3-font-down', { 'rotate-270': !expanded })} onClick={onToggleExpanded}></i>}
|
||||||
<div className="tree-node-icon">
|
<div className="tree-node-icon">
|
||||||
<div className="tag-tree-node-color" style={{ backgroundColor: tagColor }}></div>
|
<div className="tag-tree-node-color" style={{ backgroundColor: tagColor }}></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{hasChildren && renderChildren()}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
Tag.propTypes = {
|
Tag.propTypes = {
|
||||||
isSelected: PropTypes.bool,
|
|
||||||
tag: PropTypes.object,
|
tag: PropTypes.object,
|
||||||
onClick: PropTypes.func,
|
onClick: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
@@ -6,7 +6,7 @@ import AllTags from './all-tags';
|
|||||||
import { ALL_TAGS_ID } from '../constants';
|
import { ALL_TAGS_ID } from '../constants';
|
||||||
|
|
||||||
const Views = ({ ...params }) => {
|
const Views = ({ ...params }) => {
|
||||||
const { isLoading } = useTags();
|
const { isLoading, displayNodeKey } = useTags();
|
||||||
if (isLoading) return (<CenteredLoading />);
|
if (isLoading) return (<CenteredLoading />);
|
||||||
|
|
||||||
if (params.tagID === ALL_TAGS_ID) {
|
if (params.tagID === ALL_TAGS_ID) {
|
||||||
@@ -14,7 +14,7 @@ const Views = ({ ...params }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TagViewProvider { ...params }>
|
<TagViewProvider { ...params } nodeKey={displayNodeKey}>
|
||||||
<View />
|
<View />
|
||||||
</TagViewProvider>
|
</TagViewProvider>
|
||||||
);
|
);
|
||||||
|
Reference in New Issue
Block a user