1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-14 14:21:23 +00:00

refactor(tag): parent tag files count includes children (#7420)

This commit is contained in:
Jerry Ren
2025-01-23 14:37:57 +08:00
committed by GitHub
parent 076d147d4e
commit b7d9f781fd
17 changed files with 294 additions and 84 deletions

View File

@@ -52,10 +52,7 @@ class ActionsCell extends Component {
}; };
getRecordNo = () => { getRecordNo = () => {
if (this.props.showRecordAsTree) { return (this.props.showRecordAsTree ? this.props.treeNodeIndex : this.props.index) + 1;
return this.props.treeNodeDisplayIndex;
}
return this.props.index + 1;
}; };
render() { render() {

View File

@@ -24,11 +24,12 @@ const Cell = React.memo(({
frozen, frozen,
height, height,
showRecordAsTree, showRecordAsTree,
nodeDepth, treeNodeIndex,
treeNodeDepth,
hasChildNodes, hasChildNodes,
isFoldedNode, isFoldedTreeNode,
checkCanModifyRecord, checkCanModifyRecord,
toggleExpandNode, toggleExpandTreeNode,
}) => { }) => {
const cellEditable = useMemo(() => { const cellEditable = useMemo(() => {
return checkIsColumnEditable(column) && checkCanModifyRecord && checkCanModifyRecord(record); return checkIsColumnEditable(column) && checkCanModifyRecord && checkCanModifyRecord(record);
@@ -168,19 +169,19 @@ const Cell = React.memo(({
}; };
const renderCellContent = useCallback(() => { const renderCellContent = useCallback(() => {
const columnFormatter = isValidElement(column.formatter) && cloneElement(column.formatter, { isCellSelected, value: cellValue, column, record, onChange: modifyRecord }); const columnFormatter = isValidElement(column.formatter) && cloneElement(column.formatter, { isCellSelected, value: cellValue, column, record, treeNodeIndex, onChange: modifyRecord });
if (showRecordAsTree && isNameColumn) { if (showRecordAsTree && isNameColumn) {
return ( return (
<div className="sf-table-cell-tree-node"> <div className="sf-table-cell-tree-node">
{hasChildNodes && <span className="sf-table-record-tree-expand-icon" style={{ left: nodeDepth * NODE_ICON_LEFT_INDENT }} onClick={toggleExpandNode}><i className={classnames('sf3-font sf3-font-down', { 'rotate-270': isFoldedNode })}></i></span>} {hasChildNodes && <span className="sf-table-record-tree-expand-icon" style={{ left: treeNodeDepth * NODE_ICON_LEFT_INDENT }} onClick={toggleExpandTreeNode}><i className={classnames('sf3-font sf3-font-down', { 'rotate-270': isFoldedTreeNode })}></i></span>}
<div className="sf-table-cell-tree-node-content" style={{ paddingLeft: NODE_CONTENT_LEFT_INDENT + nodeDepth * NODE_ICON_LEFT_INDENT }}> <div className="sf-table-cell-tree-node-content" style={{ paddingLeft: NODE_CONTENT_LEFT_INDENT + treeNodeDepth * NODE_ICON_LEFT_INDENT }}>
{columnFormatter} {columnFormatter}
</div> </div>
</div> </div>
); );
} }
return columnFormatter; return columnFormatter;
}, [isNameColumn, column, isCellSelected, cellValue, record, showRecordAsTree, nodeDepth, hasChildNodes, isFoldedNode, modifyRecord, toggleExpandNode]); }, [isNameColumn, column, isCellSelected, cellValue, record, showRecordAsTree, treeNodeIndex, treeNodeDepth, hasChildNodes, isFoldedTreeNode, modifyRecord, toggleExpandTreeNode]);
return ( return (
<div key={`${record._id}-${column.key}`} {...containerProps}> <div key={`${record._id}-${column.key}`} {...containerProps}>
@@ -208,14 +209,15 @@ Cell.propTypes = {
column: PropTypes.object.isRequired, column: PropTypes.object.isRequired,
height: PropTypes.number, height: PropTypes.number,
needBindEvents: PropTypes.bool, needBindEvents: PropTypes.bool,
modifyRecord: PropTypes.func,
highlightClassName: PropTypes.string, highlightClassName: PropTypes.string,
bgColor: PropTypes.string, bgColor: PropTypes.string,
showRecordAsTree: PropTypes.bool, showRecordAsTree: PropTypes.bool,
nodeDepth: PropTypes.number, treeNodeIndex: PropTypes.number,
treeNodeDepth: PropTypes.number,
hasChildNodes: PropTypes.bool, hasChildNodes: PropTypes.bool,
isFoldedNode: PropTypes.bool, isFoldedTreeNode: PropTypes.bool,
toggleExpandNode: PropTypes.func, modifyRecord: PropTypes.func,
toggleExpandTreeNode: PropTypes.func,
}; };
export default Cell; export default Cell;

View File

@@ -36,11 +36,11 @@ class Record extends React.Component {
nextProps.searchResult !== this.props.searchResult || nextProps.searchResult !== this.props.searchResult ||
nextProps.columnColor !== this.props.columnColor || nextProps.columnColor !== this.props.columnColor ||
nextProps.showRecordAsTree !== this.props.showRecordAsTree || nextProps.showRecordAsTree !== this.props.showRecordAsTree ||
nextProps.nodeKey !== this.props.nodeKey || nextProps.treeNodeIndex !== this.props.treeNodeIndex ||
nextProps.nodeDepth !== this.props.nodeDepth || nextProps.treeNodeKey !== this.props.treeNodeKey ||
nextProps.treeNodeDepth !== this.props.treeNodeDepth ||
nextProps.hasChildNodes !== this.props.hasChildNodes || nextProps.hasChildNodes !== this.props.hasChildNodes ||
nextProps.treeNodeDisplayIndex !== this.props.treeNodeDisplayIndex || nextProps.isFoldedTreeNode !== this.props.isFoldedTreeNode
nextProps.isFoldedNode !== this.props.isFoldedNode
); );
} }
@@ -116,10 +116,11 @@ class Record extends React.Component {
highlightClassName={highlightClassName} highlightClassName={highlightClassName}
bgColor={bgColor} bgColor={bgColor}
showRecordAsTree={this.props.showRecordAsTree} showRecordAsTree={this.props.showRecordAsTree}
nodeDepth={this.props.nodeDepth} treeNodeIndex={this.props.treeNodeIndex}
treeNodeDepth={this.props.treeNodeDepth}
hasChildNodes={this.props.hasChildNodes} hasChildNodes={this.props.hasChildNodes}
isFoldedNode={this.props.isFoldedNode} isFoldedTreeNode={this.props.isFoldedTreeNode}
toggleExpandNode={this.props.toggleExpandNode} toggleExpandTreeNode={this.props.toggleExpandTreeNode}
/> />
); );
}); });
@@ -183,10 +184,11 @@ class Record extends React.Component {
highlightClassName={highlightClassName} highlightClassName={highlightClassName}
bgColor={bgColor} bgColor={bgColor}
showRecordAsTree={this.props.showRecordAsTree} showRecordAsTree={this.props.showRecordAsTree}
nodeDepth={this.props.nodeDepth} treeNodeIndex={this.props.treeNodeIndex}
treeNodeDepth={this.props.treeNodeDepth}
hasChildNodes={this.props.hasChildNodes} hasChildNodes={this.props.hasChildNodes}
isFoldedNode={this.props.isFoldedNode} isFoldedTreeNode={this.props.isFoldedTreeNode}
toggleExpandNode={this.props.toggleExpandNode} toggleExpandTreeNode={this.props.toggleExpandTreeNode}
/> />
); );
}); });
@@ -273,7 +275,7 @@ class Record extends React.Component {
recordId={record._id} recordId={record._id}
index={index} index={index}
showRecordAsTree={this.props.showRecordAsTree} showRecordAsTree={this.props.showRecordAsTree}
treeNodeDisplayIndex={this.props.treeNodeDisplayIndex} treeNodeIndex={this.props.treeNodeIndex}
onSelectRecord={this.onSelectRecord} onSelectRecord={this.onSelectRecord}
isLastFrozenCell={!lastFrozenColumnKey} isLastFrozenCell={!lastFrozenColumnKey}
height={cellHeight} height={cellHeight}
@@ -316,12 +318,12 @@ Record.propTypes = {
searchResult: PropTypes.object, searchResult: PropTypes.object,
columnColor: PropTypes.object, columnColor: PropTypes.object,
showRecordAsTree: PropTypes.bool, showRecordAsTree: PropTypes.bool,
nodeKey: PropTypes.string, treeNodeIndex: PropTypes.number,
nodeDepth: PropTypes.number, treeNodeKey: PropTypes.string,
treeNodeDepth: PropTypes.number,
hasChildNodes: PropTypes.bool, hasChildNodes: PropTypes.bool,
treeNodeDisplayIndex: PropTypes.number, isFoldedTreeNode: PropTypes.bool,
isFoldedNode: PropTypes.bool, toggleExpandTreeNode: PropTypes.func,
toggleExpandNode: PropTypes.func,
}; };
export default Record; export default Record;

View File

@@ -99,7 +99,7 @@ class TreeBody extends Component {
if (row && checkIsTreeNodeShown(nodeKey, keyNodeFoldedMap)) { if (row && checkIsTreeNodeShown(nodeKey, keyNodeFoldedMap)) {
shownNodes.push({ shownNodes.push({
...node, ...node,
node_display_index: index + 1, node_index: index,
}); });
} }
}); });
@@ -535,7 +535,7 @@ class TreeBody extends Component {
const rowHeight = this.getRowHeight(); const rowHeight = this.getRowHeight();
const cellMetaData = this.getCellMetaData(); const cellMetaData = this.getCellMetaData();
let shownNodes = visibleNodes.map((node, index) => { let shownNodes = visibleNodes.map((node, index) => {
const { _id: recordId, node_key, node_depth, node_display_index } = node; const { _id: recordId, node_key, node_depth, node_index } = node;
const hasChildNodes = checkTreeNodeHasChildNodes(node); const hasChildNodes = checkTreeNodeHasChildNodes(node);
const record = this.props.recordGetterById(recordId); const record = this.props.recordGetterById(recordId);
const isSelected = TreeMetrics.checkIsTreeNodeSelected(node_key, treeMetrics); const isSelected = TreeMetrics.checkIsTreeNodeSelected(node_key, treeMetrics);
@@ -553,7 +553,6 @@ class TreeBody extends Component {
}} }}
isSelected={isSelected} isSelected={isSelected}
index={recordIndex} index={recordIndex}
treeNodeDisplayIndex={node_display_index}
isLastRecord={isLastRecord} isLastRecord={isLastRecord}
showSequenceColumn={this.props.showSequenceColumn} showSequenceColumn={this.props.showSequenceColumn}
record={record} record={record}
@@ -567,17 +566,18 @@ class TreeBody extends Component {
cellMetaData={cellMetaData} cellMetaData={cellMetaData}
columnColor={columnColor} columnColor={columnColor}
searchResult={this.props.searchResult} searchResult={this.props.searchResult}
nodeKey={node_key} treeNodeIndex={node_index}
nodeDepth={node_depth} treeNodeKey={node_key}
treeNodeDepth={node_depth}
hasChildNodes={hasChildNodes} hasChildNodes={hasChildNodes}
isFoldedNode={isFoldedNode} isFoldedTreeNode={isFoldedNode}
checkCanModifyRecord={this.props.checkCanModifyRecord} checkCanModifyRecord={this.props.checkCanModifyRecord}
checkCellValueChanged={this.props.checkCellValueChanged} checkCellValueChanged={this.props.checkCellValueChanged}
hasSelectedCell={hasSelectedCell} hasSelectedCell={hasSelectedCell}
selectedPosition={selectedPosition} selectedPosition={selectedPosition}
selectNoneCells={this.selectNoneCells} selectNoneCells={this.selectNoneCells}
onSelectRecord={this.props.onSelectRecord} onSelectRecord={this.props.onSelectRecord}
toggleExpandNode={() => this.toggleExpandNode(node_key)} toggleExpandTreeNode={() => this.toggleExpandNode(node_key)}
/> />
); );
}); });

View File

@@ -22,7 +22,7 @@ export const checkCellValueChanged = (oldVal, newVal) => {
export const cellCompare = (props, nextProps) => { export const cellCompare = (props, nextProps) => {
const { const {
record: oldRecord, column, isCellSelected, isLastCell, highlightClassName, height, bgColor, record: oldRecord, column, isCellSelected, isLastCell, highlightClassName, height, bgColor,
showRecordAsTree, nodeDepth, hasChildNodes, isFoldedNode, showRecordAsTree, treeNodeIndex, treeNodeDepth, hasChildNodes, isFoldedTreeNode,
} = props; } = props;
const { const {
record: newRecord, highlightClassName: newHighlightClassName, height: newHeight, column: newColumn, bgColor: newBgColor, record: newRecord, highlightClassName: newHighlightClassName, height: newHeight, column: newColumn, bgColor: newBgColor,
@@ -49,9 +49,10 @@ export const cellCompare = (props, nextProps) => {
!ObjectUtils.isSameObject(column.data, newColumn.data) || !ObjectUtils.isSameObject(column.data, newColumn.data) ||
bgColor !== newBgColor || bgColor !== newBgColor ||
showRecordAsTree !== nextProps.showRecordAsTree || showRecordAsTree !== nextProps.showRecordAsTree ||
nodeDepth !== nextProps.nodeDepth || treeNodeIndex !== nextProps.treeNodeIndex ||
treeNodeDepth !== nextProps.treeNodeDepth ||
hasChildNodes !== nextProps.hasChildNodes || hasChildNodes !== nextProps.hasChildNodes ||
isFoldedNode !== nextProps.isFoldedNode || isFoldedTreeNode !== nextProps.isFoldedTreeNode ||
props.groupRecordIndex !== nextProps.groupRecordIndex || props.groupRecordIndex !== nextProps.groupRecordIndex ||
props.recordIndex !== nextProps.recordIndex props.recordIndex !== nextProps.recordIndex
); );

View File

@@ -27,7 +27,6 @@ export const generateKeyTreeNodeRowIdMap = (tree) => {
return tree_node_key_row_id_map; return tree_node_key_row_id_map;
}; };
export const getValidKeyTreeNodeFoldedMap = (keyTreeNodeFoldedMap, treeNodeKeyRecordIdMap) => { export const getValidKeyTreeNodeFoldedMap = (keyTreeNodeFoldedMap, treeNodeKeyRecordIdMap) => {
if (!keyTreeNodeFoldedMap) return {}; if (!keyTreeNodeFoldedMap) return {};
@@ -71,6 +70,21 @@ export const checkIsTreeNodeShown = (nodeKey, keyFoldedNodeMap) => {
return !foldedNodeKeys.some((foldedNodeKey) => nodeKey !== foldedNodeKey && nodeKey.includes(foldedNodeKey)); return !foldedNodeKeys.some((foldedNodeKey) => nodeKey !== foldedNodeKey && nodeKey.includes(foldedNodeKey));
}; };
export const updatedKeyTreeNodeMap = (nodeKey, node, keyTreeNodeMap) => {
if (!nodeKey || !node || !keyTreeNodeMap) return;
keyTreeNodeMap[nodeKey] = node;
};
export const getTreeNodeByKey = (nodeKey, keyTreeNodeMap) => {
if (!nodeKey || !keyTreeNodeMap) return null;
return keyTreeNodeMap[nodeKey];
};
export const getTreeNodeById = (nodeId, tree) => {
if (!nodeId || !Array.isArray(tree) || tree.length === 0) return null;
return tree.find((node) => getTreeNodeId(node) === nodeId);
};
export const getTreeNodeId = (node) => { export const getTreeNodeId = (node) => {
return node ? node[TREE_NODE_KEY.ID] : ''; return node ? node[TREE_NODE_KEY.ID] : '';
}; };
@@ -95,7 +109,7 @@ export const resetTreeHasChildNodesStatus = (tree) => {
const nextNode = tree[index + 1]; const nextNode = tree[index + 1];
const nextNodeKey = getTreeNodeKey(nextNode); const nextNodeKey = getTreeNodeKey(nextNode);
const currentNodeKey = getTreeNodeKey(node); const currentNodeKey = getTreeNodeKey(node);
if (nextNode && checkTreeNodeHasChildNodes(node) && !nextNodeKey.includes(currentNodeKey)) { if (checkTreeNodeHasChildNodes(node) && (!nextNode || !nextNodeKey.includes(currentNodeKey))) {
tree[index][TREE_NODE_KEY.HAS_CHILD_NODES] = false; tree[index][TREE_NODE_KEY.HAS_CHILD_NODES] = false;
} }
}); });
@@ -131,3 +145,20 @@ export const addTreeChildNode = (newChildNode, parentNode, tree) => {
} }
tree.splice(lastChildNodeIndex + 1, 0, newChildNode); tree.splice(lastChildNodeIndex + 1, 0, newChildNode);
}; };
export const getAllSubTreeNodes = (nodeIndex, tree) => {
const treeLen = Array.isArray(tree) ? tree.length : 0;
const parentNode = tree[nodeIndex];
const parentNodeKey = getTreeNodeKey(parentNode);
if (!parentNodeKey || nodeIndex === treeLen - 1) return [];
let subNodes = [];
for (let i = nodeIndex + 1, len = treeLen; i < len; i++) {
const currNodeKey = getTreeNodeKey(tree[i]);
if (!currNodeKey || !currNodeKey.includes(parentNodeKey)) {
break;
}
subNodes.push(tree[i]);
}
return subNodes;
};

View File

@@ -96,6 +96,12 @@ class TagsManagerAPI {
return this.req.get(url); return this.req.get(url);
}; };
getTagsFiles = (repoID, tags_ids) => {
const url = this.server + '/api/v2.1/repos/' + repoID + '/metadata/tags-files/';
const params = { tags_ids };
return this.req.post(url, params);
};
// file tags // file tags
updateFileTags = (repoID, data) => { updateFileTags = (repoID, data) => {
const url = this.server + '/api/v2.1/repos/' + repoID + '/metadata/file-tags/'; const url = this.server + '/api/v2.1/repos/' + repoID + '/metadata/file-tags/';

View File

@@ -1,34 +1,36 @@
import React, { useContext, useEffect, useState } from 'react'; 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 { PRIVATE_COLUMN_KEY } from '../constants'; import { getTreeNodeByKey } from '../../components/sf-table/utils/tree';
import { getRecordIdFromRecord } from '../../metadata/utils/cell'; 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.
const TagViewContext = React.createContext(null); const TagViewContext = React.createContext(null);
export const TagViewProvider = ({ repoID, tagID, children, ...params }) => { export const TagViewProvider = ({ repoID, tagID, nodeKey, children, ...params }) => {
const [isLoading, setLoading] = useState(true); const [isLoading, setLoading] = useState(true);
const [tagFiles, setTagFiles] = useState(null); const [tagFiles, setTagFiles] = useState(null);
const [errorMessage, setErrorMessage] = useState(null); const [errorMessage, setErrorMessage] = useState(null);
const { updateLocalTag } = useTags(); const { tagsData } = useTags();
const getChildTagsIds = useCallback((nodeKey) => {
if (!nodeKey) return [];
const displayNode = getTreeNodeByKey(nodeKey, tagsData.key_tree_node_map);
return getAllChildTagsIdsFromNode(displayNode);
}, [tagsData]);
useEffect(() => { useEffect(() => {
setLoading(true); setLoading(true);
tagsAPI.getTagFiles(repoID, tagID).then(res => { const childTagsIds = getChildTagsIds(nodeKey);
let tagsIds = [tagID];
if (Array.isArray(childTagsIds) && childTagsIds.length > 0) {
tagsIds.push(...childTagsIds);
}
tagsAPI.getTagsFiles(repoID, tagsIds).then(res => {
const rows = res.data?.results || []; const rows = res.data?.results || [];
setTagFiles({ columns: res.data?.metadata || [], rows: res.data?.results || [] }); setTagFiles({ columns: res.data?.metadata || [], rows });
updateLocalTag(tagID, {
[PRIVATE_COLUMN_KEY.TAG_FILE_LINKS]: rows.map(r => {
const recordId = getRecordIdFromRecord(r);
return {
row_id: recordId,
display_value: recordId
};
})
});
setLoading(false); setLoading(false);
}).catch(error => { }).catch(error => {
const errorMessage = Utils.getErrorMsg(error); const errorMessage = Utils.getErrorMsg(error);
@@ -36,7 +38,7 @@ export const TagViewProvider = ({ repoID, tagID, children, ...params }) => {
setLoading(false); setLoading(false);
}); });
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [repoID, tagID]); }, [repoID, tagID, nodeKey]);
return ( return (
<TagViewContext.Provider value={{ <TagViewContext.Provider value={{

View File

@@ -3,12 +3,12 @@ import { getColumnByKey } from '../../metadata/utils/column';
import { getGroupRows } from '../../metadata/utils/group'; import { getGroupRows } from '../../metadata/utils/group';
import { getRowsByIds } from '../../metadata/utils/table'; import { getRowsByIds } from '../../metadata/utils/table';
import { OPERATION_TYPE } from './operations'; import { OPERATION_TYPE } from './operations';
import { buildTagsTree } from '../utils/tree'; import { buildTagsTree, setNodeAllChildTagsIds } from '../utils/tree';
import { getRecordIdFromRecord } from '../../metadata/utils/cell'; import { getRecordIdFromRecord } from '../../metadata/utils/cell';
import { TREE_NODE_KEY } from '../../components/sf-table/constants/tree';
import { import {
addTreeChildNode, createTreeNode, generateNodeKey, getTreeNodeDepth, getTreeNodeId, getTreeNodeKey, addTreeChildNode, checkTreeNodeHasChildNodes, createTreeNode, generateNodeKey, getTreeNodeDepth, getTreeNodeId, getTreeNodeKey,
resetTreeHasChildNodesStatus, resetTreeHasChildNodesStatus,
updatedKeyTreeNodeMap,
} from '../../components/sf-table/utils/tree'; } from '../../components/sf-table/utils/tree';
// const DEFAULT_COMPUTER_PROPERTIES_CONTROLLER = { // const DEFAULT_COMPUTER_PROPERTIES_CONTROLLER = {
@@ -21,7 +21,9 @@ import {
class DataProcessor { class DataProcessor {
static buildTagsTree(rows, table) { static buildTagsTree(rows, table) {
table.rows_tree = buildTagsTree(rows, table); const { tree, key_tree_node_map } = buildTagsTree(rows, table);
table.rows_tree = tree;
table.key_tree_node_map = key_tree_node_map;
} }
static updateTagsTreeWithNewTags(tags, table) { static updateTagsTreeWithNewTags(tags, table) {
@@ -33,6 +35,7 @@ class DataProcessor {
const nodeKey = generateNodeKey('', tagId); const nodeKey = generateNodeKey('', tagId);
const node = createTreeNode(tagId, nodeKey, 0, false); const node = createTreeNode(tagId, nodeKey, 0, false);
updated_rows_tree.push(node); updated_rows_tree.push(node);
updatedKeyTreeNodeMap(nodeKey, node, table.key_tree_node_map);
}); });
table.rows_tree = updated_rows_tree; table.rows_tree = updated_rows_tree;
} }
@@ -41,7 +44,7 @@ class DataProcessor {
if (!Array.isArray(deletedTagsIds) || deletedTagsIds.length === 0) return; if (!Array.isArray(deletedTagsIds) || deletedTagsIds.length === 0) return;
const { rows_tree } = table; const { rows_tree } = table;
const idTagDeletedMap = deletedTagsIds.reduce((currIdTagDeletedMap, tagId) => ({ ...currIdTagDeletedMap, [tagId]: true }), {}); const idTagDeletedMap = deletedTagsIds.reduce((currIdTagDeletedMap, tagId) => ({ ...currIdTagDeletedMap, [tagId]: true }), {});
const hasDeletedParentNode = rows_tree.some((node) => idTagDeletedMap[node[TREE_NODE_KEY.ID]] && node[TREE_NODE_KEY.HAS_CHILD_NODES]); const hasDeletedParentNode = rows_tree.some((node) => idTagDeletedMap[getTreeNodeId(node)] && checkTreeNodeHasChildNodes(node));
if (hasDeletedParentNode) { if (hasDeletedParentNode) {
// need re-build tree if some parent nodes deleted // need re-build tree if some parent nodes deleted
this.buildTagsTree(table.rows, table); this.buildTagsTree(table.rows, table);
@@ -51,13 +54,21 @@ class DataProcessor {
// remove the nodes which has no child nodes directly // remove the nodes which has no child nodes directly
let updated_rows_tree = []; let updated_rows_tree = [];
rows_tree.forEach((node) => { rows_tree.forEach((node) => {
if (!idTagDeletedMap[node[TREE_NODE_KEY.ID]]) { if (!idTagDeletedMap[getTreeNodeId(node)]) {
updated_rows_tree.push(node); updated_rows_tree.push(node);
} }
}); });
// update has_child_nodes status(all child nodes may be deleted) // update has_child_nodes status(all child nodes may be deleted)
resetTreeHasChildNodesStatus(updated_rows_tree); resetTreeHasChildNodesStatus(updated_rows_tree);
// update tag all files links
setNodeAllChildTagsIds(updated_rows_tree);
table.key_tree_node_map = {};
updated_rows_tree.forEach((node) => {
updatedKeyTreeNodeMap(getTreeNodeKey(node), node, table.key_tree_node_map);
});
table.rows_tree = updated_rows_tree; table.rows_tree = updated_rows_tree;
} }
@@ -131,6 +142,7 @@ class DataProcessor {
} }
}); });
this.buildTagsTree(table.rows, table);
this.updateDataWithModifyRecords(); this.updateDataWithModifyRecords();
this.updateSummaries(); this.updateSummaries();
} }
@@ -149,6 +161,7 @@ class DataProcessor {
}); });
table.rows = table.rows.filter((record) => !idRecordNotExistMap[record._id]); table.rows = table.rows.filter((record) => !idRecordNotExistMap[record._id]);
this.buildTagsTree(table.rows, table);
this.updateSummaries(); this.updateSummaries();
} }
@@ -217,6 +230,7 @@ class DataProcessor {
const subNodeKey = generateNodeKey(parentNodeKey, tagId); const subNodeKey = generateNodeKey(parentNodeKey, tagId);
const childNode = createTreeNode(tagId, subNodeKey, parentNodeDepth + 1, false); const childNode = createTreeNode(tagId, subNodeKey, parentNodeDepth + 1, false);
addTreeChildNode(childNode, node, rows_tree); addTreeChildNode(childNode, node, rows_tree);
updatedKeyTreeNodeMap(subNodeKey, childNode, table.key_tree_node_map);
} }
}); });
break; break;

View File

@@ -7,7 +7,7 @@ import { username } from '../../../utils/constants';
import { addRowLinks, removeRowLinks } from '../../utils/link'; import { addRowLinks, removeRowLinks } from '../../utils/link';
import { getRecordIdFromRecord } from '../../../metadata/utils/cell'; import { getRecordIdFromRecord } from '../../../metadata/utils/cell';
import { getRowById, getRowsByIds } from '../../../metadata/utils/table'; import { getRowById, getRowsByIds } from '../../../metadata/utils/table';
import { getChildLinks, getParentLinks, getTagFileLinks } from '../../utils/cell'; import { getChildLinks, getParentLinks, getTagFilesLinks } from '../../utils/cell';
dayjs.extend(utc); dayjs.extend(utc);
@@ -229,7 +229,7 @@ export default function apply(data, operation) {
const opTagsIds = [target_tag_id, ...merged_tags_ids]; const opTagsIds = [target_tag_id, ...merged_tags_ids];
const parentLinks = getParentLinks(targetTag); const parentLinks = getParentLinks(targetTag);
const childLinks = getChildLinks(targetTag); const childLinks = getChildLinks(targetTag);
const fileLinks = getTagFileLinks(targetTag); const fileLinks = getTagFilesLinks(targetTag);
const idParentLinkExistMap = parentLinks.reduce((currIdParentLinkExist, link) => ({ ...currIdParentLinkExist, [link.row_id]: true }), {}); const idParentLinkExistMap = parentLinks.reduce((currIdParentLinkExist, link) => ({ ...currIdParentLinkExist, [link.row_id]: true }), {});
const idChildLinkExistMap = childLinks.reduce((currIdChildLinkExist, link) => ({ ...currIdChildLinkExist, [link.row_id]: true }), {}); const idChildLinkExistMap = childLinks.reduce((currIdChildLinkExist, link) => ({ ...currIdChildLinkExist, [link.row_id]: true }), {});
const idFileLinkExistMap = fileLinks.reduce((currIdFileLinkExistMap, link) => ({ ...currIdFileLinkExistMap, [link.row_id]: true }), {}); const idFileLinkExistMap = fileLinks.reduce((currIdFileLinkExistMap, link) => ({ ...currIdFileLinkExistMap, [link.row_id]: true }), {});
@@ -241,7 +241,7 @@ export default function apply(data, operation) {
mergedTags.forEach((mergedTag) => { mergedTags.forEach((mergedTag) => {
const currParentLinks = getParentLinks(mergedTag); const currParentLinks = getParentLinks(mergedTag);
const currChildLinks = getChildLinks(mergedTag); const currChildLinks = getChildLinks(mergedTag);
const currFileLinks = getTagFileLinks(mergedTag); const currFileLinks = getTagFilesLinks(mergedTag);
currParentLinks.forEach((parentLink) => { currParentLinks.forEach((parentLink) => {
const parentLinkedTagId = parentLink.row_id; const parentLinkedTagId = parentLink.row_id;
if (!opTagsIds.includes(parentLinkedTagId) && !idParentLinkExistMap[parentLinkedTagId]) { if (!opTagsIds.includes(parentLinkedTagId) && !idParentLinkExistMap[parentLinkedTagId]) {

View File

@@ -32,14 +32,15 @@ export const getChildLinks = (tag) => {
return (tag && tag[PRIVATE_COLUMN_KEY.SUB_LINKS]) || []; return (tag && tag[PRIVATE_COLUMN_KEY.SUB_LINKS]) || [];
}; };
export const getTagFileLinks = (tag) => { export const getTagFilesLinks = (tag) => {
return (tag && tag[PRIVATE_COLUMN_KEY.TAG_FILE_LINKS]) || []; return (tag && tag[PRIVATE_COLUMN_KEY.TAG_FILE_LINKS]) || [];
}; };
export const getTagFilesCount = (tag) => { export const getTagFilesCount = (tag) => {
const links = getTagFileLinks(tag); const links = getTagFilesLinks(tag);
return Array.isArray(links) ? links.length : 0; return Array.isArray(links) ? links.length : 0;
}; };
export const getTagsByNameOrColor = (tags, nameOrColor) => { export const getTagsByNameOrColor = (tags, nameOrColor) => {
if (!Array.isArray(tags) || tags.length === 0) return []; if (!Array.isArray(tags) || tags.length === 0) return [];
if (!nameOrColor) return tags; if (!nameOrColor) return tags;

View File

@@ -1,8 +1,27 @@
import { createTreeNode, generateNodeKey } from '../../components/sf-table/utils/tree'; import { checkTreeNodeHasChildNodes, createTreeNode, generateNodeKey, getAllSubTreeNodes, getTreeNodeId, getTreeNodeKey } from '../../components/sf-table/utils/tree';
import { getRecordIdFromRecord } from '../../metadata/utils/cell'; import { getRecordIdFromRecord } from '../../metadata/utils/cell';
import { getRowsByIds } from '../../metadata/utils/table'; import { getRowsByIds } from '../../metadata/utils/table';
import { getParentLinks, getChildLinks } from './cell'; import { getParentLinks, getChildLinks } from './cell';
const KEY_ALL_CHILD_TAGS_IDS = 'all_child_tags_ids';
const findAllChildTagIds = (nodeIndex, tree) => {
const targetNode = tree[nodeIndex];
if (!checkTreeNodeHasChildNodes(targetNode)) {
return [];
}
let allChildTagsIds = [];
const allSubNodes = getAllSubTreeNodes(nodeIndex, tree);
allSubNodes.forEach((subNode) => {
const nodeId = getTreeNodeId(subNode);
if (!allChildTagsIds.includes(nodeId)) {
allChildTagsIds.push(nodeId);
}
});
return allChildTagsIds;
};
const setChildNodes = (row, parentDepth, parentKey, idNodeInCurrentTreeMap, idNodeCreatedMap, tree, table) => { const setChildNodes = (row, parentDepth, parentKey, idNodeInCurrentTreeMap, idNodeCreatedMap, tree, table) => {
const nodeId = getRecordIdFromRecord(row); const nodeId = getRecordIdFromRecord(row);
@@ -28,6 +47,12 @@ const setChildNodes = (row, parentDepth, parentKey, idNodeInCurrentTreeMap, idNo
delete idNodeInCurrentTreeMap[nodeId]; delete idNodeInCurrentTreeMap[nodeId];
}; };
export const setNodeAllChildTagsIds = (tree) => {
tree.forEach((node, nodeIndex) => {
node[KEY_ALL_CHILD_TAGS_IDS] = findAllChildTagIds(nodeIndex, tree);
});
};
/** /**
* generate tree for display in table * generate tree for display in table
* @param {array} rows tags * @param {array} rows tags
@@ -57,5 +82,17 @@ export const buildTagsTree = (rows, table) => {
} }
}); });
return tree; // set node all file links
setNodeAllChildTagsIds(tree);
let key_tree_node_map = {};
tree.forEach((node) => {
key_tree_node_map[getTreeNodeKey(node)] = node;
});
return { tree, key_tree_node_map };
};
export const getAllChildTagsIdsFromNode = (node) => {
return (node && node[KEY_ALL_CHILD_TAGS_IDS]) || [];
}; };

View File

@@ -21,6 +21,8 @@ const AllTags = ({ updateCurrentPath, ...params }) => {
const { isLoading, isReloading, tagsData, store, context, currentPath } = useTags(); const { isLoading, isReloading, tagsData, store, context, currentPath } = useTags();
const displayNodeKey = useRef('');
useEffect(() => { useEffect(() => {
const eventBus = context.eventBus; const eventBus = context.eventBus;
eventBus.dispatch(EVENT_BUS_TYPE.RELOAD_DATA); eventBus.dispatch(EVENT_BUS_TYPE.RELOAD_DATA);
@@ -34,11 +36,11 @@ const AllTags = ({ updateCurrentPath, ...params }) => {
const pathList = currentPath.split('/'); const pathList = currentPath.split('/');
const [, , currentTagId, children] = pathList; const [, , currentTagId, children] = pathList;
if (currentTagId === ALL_TAGS_ID && !children) { if (currentTagId === ALL_TAGS_ID && !children) {
setDisplayTag(''); setDisplayTag();
} }
}, [currentPath]); }, [currentPath]);
const onChangeDisplayTag = useCallback((tagID = '') => { const onChangeDisplayTag = useCallback((tagID = '', nodeKey = '') => {
if (displayTag === tagID) return; if (displayTag === tagID) return;
const tag = tagID && getRowById(tagsData, tagID); const tag = tagID && getRowById(tagsData, tagID);
@@ -46,6 +48,8 @@ const AllTags = ({ updateCurrentPath, ...params }) => {
if (tag) { if (tag) {
path += `/${getTagName(tag)}`; path += `/${getTagName(tag)}`;
} }
displayNodeKey.current = nodeKey || '';
updateCurrentPath(path); updateCurrentPath(path);
setDisplayTag(tagID); setDisplayTag(tagID);
@@ -84,7 +88,7 @@ const AllTags = ({ updateCurrentPath, ...params }) => {
if (displayTag) { if (displayTag) {
return ( return (
<div className="sf-metadata-all-tags-tag-files"> <div className="sf-metadata-all-tags-tag-files">
<TagViewProvider { ...params } tagID={displayTag} updateCurrentPath={updateCurrentPath} > <TagViewProvider { ...params } tagID={displayTag} nodeKey={displayNodeKey.current} updateCurrentPath={updateCurrentPath} >
<View /> <View />
</TagViewProvider> </TagViewProvider>
</div> </div>

View File

@@ -1,12 +1,41 @@
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import PropTypes from 'prop-types';
import { NumberFormatter } from '@seafile/sf-metadata-ui-component'; import { NumberFormatter } from '@seafile/sf-metadata-ui-component';
import { useTags } from '../../../../hooks';
import { getTagFilesLinks } from '../../../../utils/cell';
import { getAllChildTagsIdsFromNode } from '../../../../utils/tree';
import { getRowById } from '../../../../../metadata/utils/table';
import { getTreeNodeId } from '../../../../../components/sf-table/utils/tree';
const TagFilesFormatter = ({ record, column }) => { const TagFilesFormatter = ({ treeNodeIndex }) => {
const { tagsData } = useTags();
const tree = useMemo(() => {
return tagsData.rows_tree || [];
}, [tagsData]);
const currentNode = useMemo(() => {
return tree[treeNodeIndex];
}, [tree, treeNodeIndex]);
const currentTag = useMemo(() => {
const nodeId = getTreeNodeId(currentNode);
return getRowById(tagsData, nodeId);
}, [currentNode, tagsData]);
const tagFileLinksCount = useMemo(() => { const tagFileLinksCount = useMemo(() => {
const tagFileLinks = record[column.key]; const filesLinks = getTagFilesLinks(currentTag);
return Array.isArray(tagFileLinks) ? tagFileLinks.length : 0; let allFilesLinks = [...filesLinks];
}, [record, column]); const childTagsIds = getAllChildTagsIdsFromNode(currentNode);
childTagsIds.forEach((childTagId) => {
const childTag = getRowById(tagsData, childTagId);
const childFilesLinks = getTagFilesLinks(childTag);
if (childFilesLinks && childFilesLinks.length > 0) {
allFilesLinks.push(...childFilesLinks);
}
});
return allFilesLinks.length;
}, [currentNode, currentTag, tagsData]);
return ( return (
<div className="sf-table-tag-files-formatter sf-table-cell-formatter sf-metadata-ui cell-formatter-container"> <div className="sf-table-tag-files-formatter sf-table-cell-formatter sf-metadata-ui cell-formatter-container">
@@ -15,4 +44,8 @@ const TagFilesFormatter = ({ record, column }) => {
); );
}; };
TagFilesFormatter.propTypes = {
treeNodeIndex: PropTypes.number,
};
export default TagFilesFormatter; export default TagFilesFormatter;

View File

@@ -1,9 +1,21 @@
import React, { useCallback, useMemo } from 'react'; import React, { useCallback, useMemo } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useTags } from '../../../../hooks';
import { PRIVATE_COLUMN_KEY } from '../../../../constants'; import { PRIVATE_COLUMN_KEY } from '../../../../constants';
import { getRecordIdFromRecord } from '../../../../../metadata/utils/cell'; import { getRecordIdFromRecord } from '../../../../../metadata/utils/cell';
import { getTreeNodeKey } from '../../../../../components/sf-table/utils/tree';
const TagNameFormatter = ({ record, isCellSelected, setDisplayTag, treeNodeIndex }) => {
const { tagsData } = useTags();
const tree = useMemo(() => {
return tagsData.rows_tree || [];
}, [tagsData]);
const currentNode = useMemo(() => {
return tree[treeNodeIndex];
}, [tree, treeNodeIndex]);
const TagNameFormatter = ({ record, isCellSelected, setDisplayTag }) => {
const tagColor = useMemo(() => { const tagColor = useMemo(() => {
return record[PRIVATE_COLUMN_KEY.TAG_COLOR]; return record[PRIVATE_COLUMN_KEY.TAG_COLOR];
}, [record]); }, [record]);
@@ -15,8 +27,9 @@ const TagNameFormatter = ({ record, isCellSelected, setDisplayTag }) => {
const onClickName = useCallback(() => { const onClickName = useCallback(() => {
if (!isCellSelected) return; if (!isCellSelected) return;
const tagId = getRecordIdFromRecord(record); const tagId = getRecordIdFromRecord(record);
setDisplayTag(tagId); const nodeKey = getTreeNodeKey(currentNode);
}, [isCellSelected, record, setDisplayTag]); setDisplayTag(tagId, nodeKey);
}, [isCellSelected, record, currentNode, setDisplayTag]);
return ( return (
<div className="sf-table-tag-name-formatter sf-table-cell-formatter sf-metadata-ui cell-formatter-container"> <div className="sf-table-tag-name-formatter sf-table-cell-formatter sf-metadata-ui cell-formatter-container">
@@ -29,6 +42,7 @@ const TagNameFormatter = ({ record, isCellSelected, setDisplayTag }) => {
TagNameFormatter.propTypes = { TagNameFormatter.propTypes = {
record: PropTypes.object, record: PropTypes.object,
isCellSelected: PropTypes.bool, isCellSelected: PropTypes.bool,
treeNodeIndex: PropTypes.number,
setDisplayTag: PropTypes.func, setDisplayTag: PropTypes.func,
}; };

View File

@@ -2324,6 +2324,70 @@ class MetadataTagFiles(APIView):
return Response(tag_files_query) return Response(tag_files_query)
class MetadataTagsFiles(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication)
permission_classes = (IsAuthenticated,)
throttle_classes = (UserRateThrottle,)
def post(self, request, repo_id):
tags_ids = request.data.get('tags_ids', None)
if not tags_ids:
return api_error(status.HTTP_400_BAD_REQUEST, 'tags_ids is invalid.')
metadata = RepoMetadata.objects.filter(repo_id=repo_id).first()
if not metadata or not metadata.enabled or not metadata.tags_enabled:
error_msg = f'The tags is disabled for repo {repo_id}.'
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
repo = seafile_api.get_repo(repo_id)
if not repo:
error_msg = 'Library %s not found.' % repo_id
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
if not can_read_metadata(request, repo_id):
error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
metadata_server_api = MetadataServerAPI(repo_id, request.user.username)
from seafevents.repo_metadata.constants import TAGS_TABLE, METADATA_TABLE
tags_ids_str = ', '.join([f'"{id}"' for id in tags_ids])
sql = f'SELECT * FROM {TAGS_TABLE.name} WHERE `{TAGS_TABLE.columns.id.name}` in ({tags_ids_str})'
try:
query_new_rows = metadata_server_api.query_rows(sql)
found_tags = query_new_rows.get('results', [])
except Exception as e:
logger.error(e)
error_msg = 'Internal Server Error'
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error')
if not found_tags:
return Response([])
tags_files_ids = []
for tag in found_tags:
tags_files_ids.extend(tag.get(TAGS_TABLE.columns.file_links.name, []))
if not tags_files_ids:
return Response([])
tags_files_sql = 'SELECT `%s`, `%s`, `%s`, `%s`, `%s`, `%s` FROM %s WHERE `%s` IN (%s)' % (METADATA_TABLE.columns.id.name, METADATA_TABLE.columns.file_name.name, \
METADATA_TABLE.columns.parent_dir.name, METADATA_TABLE.columns.size.name, \
METADATA_TABLE.columns.file_mtime.name, METADATA_TABLE.columns.tags.name, \
METADATA_TABLE.name, METADATA_TABLE.columns.id.name, \
', '.join(["'%s'" % id.get('row_id') for id in tags_files_ids]))
try:
tags_files_query = metadata_server_api.query_rows(tags_files_sql)
except Exception as e:
logger.error(e)
error_msg = 'Internal Server Error'
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
return Response(tags_files_query)
class MetadataMergeTags(APIView): class MetadataMergeTags(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication) authentication_classes = (TokenAuthentication, SessionAuthentication)
permission_classes = (IsAuthenticated,) permission_classes = (IsAuthenticated,)

View File

@@ -2,7 +2,8 @@ from django.urls import re_path
from .apis import MetadataRecords, MetadataManage, MetadataColumns, MetadataRecord, \ from .apis import MetadataRecords, MetadataManage, MetadataColumns, MetadataRecord, \
MetadataFolders, MetadataViews, MetadataViewsMoveView, MetadataViewsDetailView, MetadataViewsDuplicateView, FacesRecords, \ MetadataFolders, MetadataViews, MetadataViewsMoveView, MetadataViewsDetailView, MetadataViewsDuplicateView, FacesRecords, \
FaceRecognitionManage, FacesRecord, MetadataExtractFileDetails, PeoplePhotos, MetadataTagsStatusManage, MetadataTags, \ FaceRecognitionManage, FacesRecord, MetadataExtractFileDetails, PeoplePhotos, MetadataTagsStatusManage, MetadataTags, \
MetadataTagsLinks, MetadataFileTags, MetadataTagFiles, MetadataMergeTags, MetadataDetailsSettingsView, MetadataOCRManageView MetadataTagsLinks, MetadataFileTags, MetadataTagFiles, MetadataMergeTags, MetadataTagsFiles, MetadataDetailsSettingsView, \
MetadataOCRManageView
urlpatterns = [ urlpatterns = [
re_path(r'^$', MetadataManage.as_view(), name='api-v2.1-metadata'), re_path(r'^$', MetadataManage.as_view(), name='api-v2.1-metadata'),
@@ -38,4 +39,5 @@ urlpatterns = [
re_path(r'^file-tags/$', MetadataFileTags.as_view(), name='api-v2.1-metadata-file-tags'), re_path(r'^file-tags/$', MetadataFileTags.as_view(), name='api-v2.1-metadata-file-tags'),
re_path(r'^tag-files/(?P<tag_id>.+)/$', MetadataTagFiles.as_view(), name='api-v2.1-metadata-tag-files'), re_path(r'^tag-files/(?P<tag_id>.+)/$', MetadataTagFiles.as_view(), name='api-v2.1-metadata-tag-files'),
re_path(r'^merge-tags/$', MetadataMergeTags.as_view(), name='api-v2.1-metadata-merge-tags'), re_path(r'^merge-tags/$', MetadataMergeTags.as_view(), name='api-v2.1-metadata-merge-tags'),
re_path(r'^tags-files/$', MetadataTagsFiles.as_view(), name='api-v2.1-metadata-tags-files'),
] ]