mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-06 17:33:18 +00:00
feat(tag): support add child tag (#7374)
This commit is contained in:
@@ -2,7 +2,7 @@ export const TREE_NODE_KEY = {
|
|||||||
ID: '_id',
|
ID: '_id',
|
||||||
KEY: 'node_key',
|
KEY: 'node_key',
|
||||||
DEPTH: 'node_depth',
|
DEPTH: 'node_depth',
|
||||||
HAS_SUB_NODES: 'has_sub_nodes',
|
HAS_CHILD_NODES: 'has_child_nodes',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LOCAL_KEY_TREE_NODE_FOLDED = 'table_key_tree_node_folded_map';
|
export const LOCAL_KEY_TREE_NODE_FOLDED = 'table_key_tree_node_folded_map';
|
||||||
|
@@ -25,7 +25,7 @@ const Cell = React.memo(({
|
|||||||
height,
|
height,
|
||||||
showRecordAsTree,
|
showRecordAsTree,
|
||||||
nodeDepth,
|
nodeDepth,
|
||||||
hasSubNodes,
|
hasChildNodes,
|
||||||
isFoldedNode,
|
isFoldedNode,
|
||||||
checkCanModifyRecord,
|
checkCanModifyRecord,
|
||||||
toggleExpandNode,
|
toggleExpandNode,
|
||||||
@@ -172,7 +172,7 @@ const Cell = React.memo(({
|
|||||||
if (showRecordAsTree && isNameColumn) {
|
if (showRecordAsTree && isNameColumn) {
|
||||||
return (
|
return (
|
||||||
<div className="sf-table-cell-tree-node">
|
<div className="sf-table-cell-tree-node">
|
||||||
{hasSubNodes && <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: nodeDepth * NODE_ICON_LEFT_INDENT }} onClick={toggleExpandNode}><i className={classnames('sf3-font sf3-font-down', { 'rotate-270': isFoldedNode })}></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 + nodeDepth * NODE_ICON_LEFT_INDENT }}>
|
||||||
{columnFormatter}
|
{columnFormatter}
|
||||||
</div>
|
</div>
|
||||||
@@ -180,7 +180,7 @@ const Cell = React.memo(({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return columnFormatter;
|
return columnFormatter;
|
||||||
}, [isNameColumn, column, isCellSelected, cellValue, record, showRecordAsTree, nodeDepth, hasSubNodes, isFoldedNode, modifyRecord, toggleExpandNode]);
|
}, [isNameColumn, column, isCellSelected, cellValue, record, showRecordAsTree, nodeDepth, hasChildNodes, isFoldedNode, modifyRecord, toggleExpandNode]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={`${record._id}-${column.key}`} {...containerProps}>
|
<div key={`${record._id}-${column.key}`} {...containerProps}>
|
||||||
@@ -213,7 +213,7 @@ Cell.propTypes = {
|
|||||||
bgColor: PropTypes.string,
|
bgColor: PropTypes.string,
|
||||||
showRecordAsTree: PropTypes.bool,
|
showRecordAsTree: PropTypes.bool,
|
||||||
nodeDepth: PropTypes.number,
|
nodeDepth: PropTypes.number,
|
||||||
hasSubNodes: PropTypes.bool,
|
hasChildNodes: PropTypes.bool,
|
||||||
isFoldedNode: PropTypes.bool,
|
isFoldedNode: PropTypes.bool,
|
||||||
toggleExpandNode: PropTypes.func,
|
toggleExpandNode: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
@@ -38,7 +38,7 @@ class Record extends React.Component {
|
|||||||
nextProps.showRecordAsTree !== this.props.showRecordAsTree ||
|
nextProps.showRecordAsTree !== this.props.showRecordAsTree ||
|
||||||
nextProps.nodeKey !== this.props.nodeKey ||
|
nextProps.nodeKey !== this.props.nodeKey ||
|
||||||
nextProps.nodeDepth !== this.props.nodeDepth ||
|
nextProps.nodeDepth !== this.props.nodeDepth ||
|
||||||
nextProps.hasSubNodes !== this.props.hasSubNodes ||
|
nextProps.hasChildNodes !== this.props.hasChildNodes ||
|
||||||
nextProps.treeNodeDisplayIndex !== this.props.treeNodeDisplayIndex ||
|
nextProps.treeNodeDisplayIndex !== this.props.treeNodeDisplayIndex ||
|
||||||
nextProps.isFoldedNode !== this.props.isFoldedNode
|
nextProps.isFoldedNode !== this.props.isFoldedNode
|
||||||
);
|
);
|
||||||
@@ -117,7 +117,7 @@ class Record extends React.Component {
|
|||||||
bgColor={bgColor}
|
bgColor={bgColor}
|
||||||
showRecordAsTree={this.props.showRecordAsTree}
|
showRecordAsTree={this.props.showRecordAsTree}
|
||||||
nodeDepth={this.props.nodeDepth}
|
nodeDepth={this.props.nodeDepth}
|
||||||
hasSubNodes={this.props.hasSubNodes}
|
hasChildNodes={this.props.hasChildNodes}
|
||||||
isFoldedNode={this.props.isFoldedNode}
|
isFoldedNode={this.props.isFoldedNode}
|
||||||
toggleExpandNode={this.props.toggleExpandNode}
|
toggleExpandNode={this.props.toggleExpandNode}
|
||||||
/>
|
/>
|
||||||
@@ -184,7 +184,7 @@ class Record extends React.Component {
|
|||||||
bgColor={bgColor}
|
bgColor={bgColor}
|
||||||
showRecordAsTree={this.props.showRecordAsTree}
|
showRecordAsTree={this.props.showRecordAsTree}
|
||||||
nodeDepth={this.props.nodeDepth}
|
nodeDepth={this.props.nodeDepth}
|
||||||
hasSubNodes={this.props.hasSubNodes}
|
hasChildNodes={this.props.hasChildNodes}
|
||||||
isFoldedNode={this.props.isFoldedNode}
|
isFoldedNode={this.props.isFoldedNode}
|
||||||
toggleExpandNode={this.props.toggleExpandNode}
|
toggleExpandNode={this.props.toggleExpandNode}
|
||||||
/>
|
/>
|
||||||
@@ -318,7 +318,7 @@ Record.propTypes = {
|
|||||||
showRecordAsTree: PropTypes.bool,
|
showRecordAsTree: PropTypes.bool,
|
||||||
nodeKey: PropTypes.string,
|
nodeKey: PropTypes.string,
|
||||||
nodeDepth: PropTypes.number,
|
nodeDepth: PropTypes.number,
|
||||||
hasSubNodes: PropTypes.bool,
|
hasChildNodes: PropTypes.bool,
|
||||||
treeNodeDisplayIndex: PropTypes.number,
|
treeNodeDisplayIndex: PropTypes.number,
|
||||||
isFoldedNode: PropTypes.bool,
|
isFoldedNode: PropTypes.bool,
|
||||||
toggleExpandNode: PropTypes.func,
|
toggleExpandNode: PropTypes.func,
|
||||||
|
@@ -6,7 +6,7 @@ import InteractionMasks from '../../masks/interaction-masks';
|
|||||||
import Record from './record';
|
import Record from './record';
|
||||||
import EventBus from '../../../common/event-bus';
|
import EventBus from '../../../common/event-bus';
|
||||||
import { EVENT_BUS_TYPE } from '../../constants/event-bus-type';
|
import { EVENT_BUS_TYPE } from '../../constants/event-bus-type';
|
||||||
import { checkIsTreeNodeShown, getTreeNodeId, getTreeNodeKey, getValidKeyTreeNodeFoldedMap } from '../../utils/tree';
|
import { checkIsTreeNodeShown, checkTreeNodeHasChildNodes, getTreeNodeId, getTreeNodeKey, getValidKeyTreeNodeFoldedMap } from '../../utils/tree';
|
||||||
import { isShiftKeyDown } from '../../../../utils/keyboard-utils';
|
import { isShiftKeyDown } from '../../../../utils/keyboard-utils';
|
||||||
import { getColumnScrollPosition, getColVisibleStartIdx, getColVisibleEndIdx } from '../../utils/records-body-utils';
|
import { getColumnScrollPosition, getColVisibleStartIdx, getColVisibleEndIdx } from '../../utils/records-body-utils';
|
||||||
import { checkEditableViaClickCell, checkIsColumnSupportDirectEdit, getColumnByIndex, getColumnIndexByKey } from '../../utils/column';
|
import { checkEditableViaClickCell, checkIsColumnSupportDirectEdit, getColumnByIndex, getColumnIndexByKey } from '../../utils/column';
|
||||||
@@ -535,7 +535,8 @@ 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, has_sub_nodes, node_display_index } = node;
|
const { _id: recordId, node_key, node_depth, node_display_index } = 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);
|
||||||
const recordIndex = startRenderIndex + index;
|
const recordIndex = startRenderIndex + index;
|
||||||
@@ -568,7 +569,7 @@ class TreeBody extends Component {
|
|||||||
searchResult={this.props.searchResult}
|
searchResult={this.props.searchResult}
|
||||||
nodeKey={node_key}
|
nodeKey={node_key}
|
||||||
nodeDepth={node_depth}
|
nodeDepth={node_depth}
|
||||||
hasSubNodes={has_sub_nodes}
|
hasChildNodes={hasChildNodes}
|
||||||
isFoldedNode={isFoldedNode}
|
isFoldedNode={isFoldedNode}
|
||||||
checkCanModifyRecord={this.props.checkCanModifyRecord}
|
checkCanModifyRecord={this.props.checkCanModifyRecord}
|
||||||
checkCellValueChanged={this.props.checkCellValueChanged}
|
checkCellValueChanged={this.props.checkCellValueChanged}
|
||||||
|
@@ -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, hasSubNodes, isFoldedNode,
|
showRecordAsTree, nodeDepth, hasChildNodes, isFoldedNode,
|
||||||
} = props;
|
} = props;
|
||||||
const {
|
const {
|
||||||
record: newRecord, highlightClassName: newHighlightClassName, height: newHeight, column: newColumn, bgColor: newBgColor,
|
record: newRecord, highlightClassName: newHighlightClassName, height: newHeight, column: newColumn, bgColor: newBgColor,
|
||||||
@@ -50,7 +50,7 @@ export const cellCompare = (props, nextProps) => {
|
|||||||
bgColor !== newBgColor ||
|
bgColor !== newBgColor ||
|
||||||
showRecordAsTree !== nextProps.showRecordAsTree ||
|
showRecordAsTree !== nextProps.showRecordAsTree ||
|
||||||
nodeDepth !== nextProps.nodeDepth ||
|
nodeDepth !== nextProps.nodeDepth ||
|
||||||
hasSubNodes !== nextProps.hasSubNodes ||
|
hasChildNodes !== nextProps.hasChildNodes ||
|
||||||
isFoldedNode !== nextProps.isFoldedNode ||
|
isFoldedNode !== nextProps.isFoldedNode ||
|
||||||
props.groupRecordIndex !== nextProps.groupRecordIndex ||
|
props.groupRecordIndex !== nextProps.groupRecordIndex ||
|
||||||
props.recordIndex !== nextProps.recordIndex
|
props.recordIndex !== nextProps.recordIndex
|
||||||
|
@@ -1,11 +1,15 @@
|
|||||||
import { TREE_NODE_KEY } from '../constants/tree';
|
import { TREE_NODE_KEY } from '../constants/tree';
|
||||||
|
|
||||||
export const createTreeNode = (nodeId, nodeKey, depth, hasSubNodes) => {
|
export const generateNodeKey = (parentKey, currentNodeId) => {
|
||||||
|
return `${parentKey ? parentKey + '_' : ''}${currentNodeId}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createTreeNode = (nodeId, nodeKey, depth, hasChildNodes) => {
|
||||||
return {
|
return {
|
||||||
[TREE_NODE_KEY.ID]: nodeId,
|
[TREE_NODE_KEY.ID]: nodeId,
|
||||||
[TREE_NODE_KEY.KEY]: nodeKey,
|
[TREE_NODE_KEY.KEY]: nodeKey,
|
||||||
[TREE_NODE_KEY.DEPTH]: depth,
|
[TREE_NODE_KEY.DEPTH]: depth,
|
||||||
[TREE_NODE_KEY.HAS_SUB_NODES]: hasSubNodes,
|
[TREE_NODE_KEY.HAS_CHILD_NODES]: hasChildNodes,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -74,3 +78,56 @@ export const getTreeNodeId = (node) => {
|
|||||||
export const getTreeNodeKey = (node) => {
|
export const getTreeNodeKey = (node) => {
|
||||||
return node ? node[TREE_NODE_KEY.KEY] : '';
|
return node ? node[TREE_NODE_KEY.KEY] : '';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getTreeNodeDepth = (node) => {
|
||||||
|
return node ? node[TREE_NODE_KEY.DEPTH] : 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const checkTreeNodeHasChildNodes = (node) => {
|
||||||
|
return node ? node[TREE_NODE_KEY.HAS_CHILD_NODES] : false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resetTreeHasChildNodesStatus = (tree) => {
|
||||||
|
if (!Array.isArray(tree) || tree.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tree.forEach((node, index) => {
|
||||||
|
const nextNode = tree[index + 1];
|
||||||
|
const nextNodeKey = getTreeNodeKey(nextNode);
|
||||||
|
const currentNodeKey = getTreeNodeKey(node);
|
||||||
|
if (nextNode && checkTreeNodeHasChildNodes(node) && !nextNodeKey.includes(currentNodeKey)) {
|
||||||
|
tree[index][TREE_NODE_KEY.HAS_CHILD_NODES] = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addTreeChildNode = (newChildNode, parentNode, tree) => {
|
||||||
|
if (!parentNode || !Array.isArray(tree) || tree.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const parentNodeKey = getTreeNodeKey(parentNode);
|
||||||
|
const parentNodeDepth = getTreeNodeDepth(parentNode);
|
||||||
|
const parentNodeIndex = tree.findIndex((node) => getTreeNodeKey(node) === parentNodeKey);
|
||||||
|
if (parentNodeIndex < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!checkTreeNodeHasChildNodes(parentNode)) {
|
||||||
|
tree[parentNodeIndex] = { ...parentNode, [TREE_NODE_KEY.HAS_CHILD_NODES]: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const childNodeDepth = parentNodeDepth + 1;
|
||||||
|
let lastChildNodeIndex = parentNodeIndex;
|
||||||
|
for (let i = parentNodeIndex + 1, len = tree.length; i < len; i++) {
|
||||||
|
const currentNode = tree[i];
|
||||||
|
if (!getTreeNodeKey(currentNode).includes(parentNodeKey)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// insert new child tag behind the last child tag
|
||||||
|
if (getTreeNodeDepth(currentNode) === childNodeDepth) {
|
||||||
|
lastChildNodeIndex = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tree.splice(lastChildNodeIndex + 1, 0, newChildNode);
|
||||||
|
};
|
||||||
|
@@ -1,113 +0,0 @@
|
|||||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { SearchInput } from '@seafile/sf-metadata-ui-component';
|
|
||||||
import EmptyTip from '../../../../components/empty-tip';
|
|
||||||
import Tags from './tags';
|
|
||||||
import { KeyCodes } from '../../../../constants';
|
|
||||||
import { gettext } from '../../../../utils/constants';
|
|
||||||
import { getTagsByNameOrColor } from '../../../utils';
|
|
||||||
|
|
||||||
const getSelectableTags = (allTags, idTagSelectedMap) => {
|
|
||||||
if (!idTagSelectedMap) {
|
|
||||||
return allTags;
|
|
||||||
}
|
|
||||||
return allTags.filter((tag) => !idTagSelectedMap[tag._id]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const initIdTagSelectedMap = (linkedTags) => {
|
|
||||||
let idTagSelectedMap = {};
|
|
||||||
linkedTags.forEach((linkedTag) => {
|
|
||||||
idTagSelectedMap[linkedTag._id] = true;
|
|
||||||
});
|
|
||||||
return idTagSelectedMap;
|
|
||||||
};
|
|
||||||
|
|
||||||
const AddLinkedTags = ({ allTags, linkedTags, switchToLinkedTagsPage, addLinkedTag, deleteLinedTag }) => {
|
|
||||||
const initialIdTagSelectedMap = initIdTagSelectedMap(linkedTags);
|
|
||||||
const [idTagSelectedMap, setIdSelectedMap] = useState(initialIdTagSelectedMap);
|
|
||||||
const [selectableTags, setSelectableTags] = useState([]);
|
|
||||||
const [searchValue, setSearchValue] = useState('');
|
|
||||||
|
|
||||||
const initialSelectableTagsRef = useRef(getSelectableTags(allTags, initialIdTagSelectedMap));
|
|
||||||
|
|
||||||
const selectTag = useCallback((tag) => {
|
|
||||||
let updatedIdTagSelectedMap = { ...idTagSelectedMap };
|
|
||||||
if (updatedIdTagSelectedMap[tag._id]) {
|
|
||||||
delete updatedIdTagSelectedMap[tag._id];
|
|
||||||
setIdSelectedMap(updatedIdTagSelectedMap);
|
|
||||||
deleteLinedTag(tag._id);
|
|
||||||
} else {
|
|
||||||
updatedIdTagSelectedMap[tag._id] = true;
|
|
||||||
setIdSelectedMap(updatedIdTagSelectedMap);
|
|
||||||
addLinkedTag(tag);
|
|
||||||
}
|
|
||||||
|
|
||||||
}, [idTagSelectedMap, addLinkedTag, deleteLinedTag]);
|
|
||||||
|
|
||||||
const onKeyDown = useCallback((event) => {
|
|
||||||
if (
|
|
||||||
event.keyCode === KeyCodes.ChineseInputMethod ||
|
|
||||||
event.keyCode === KeyCodes.Enter ||
|
|
||||||
event.keyCode === KeyCodes.LeftArrow ||
|
|
||||||
event.keyCode === KeyCodes.RightArrow
|
|
||||||
) {
|
|
||||||
event.stopPropagation();
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const onChangeSearch = useCallback((newSearchValue) => {
|
|
||||||
if (searchValue === newSearchValue) return;
|
|
||||||
setSearchValue(newSearchValue);
|
|
||||||
}, [searchValue]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let searchedTags = [];
|
|
||||||
if (!searchValue) {
|
|
||||||
searchedTags = [...initialSelectableTagsRef.current];
|
|
||||||
} else {
|
|
||||||
searchedTags = getTagsByNameOrColor(initialSelectableTagsRef.current, searchValue);
|
|
||||||
}
|
|
||||||
setSelectableTags(searchedTags);
|
|
||||||
}, [searchValue, allTags]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="sf-metadata-set-linked-tags-popover-selector">
|
|
||||||
<div className="sf-metadata-set-linked-tags-popover-header">
|
|
||||||
<div className="sf-metadata-set-linked-tags-popover-title">
|
|
||||||
<span className="sf-metadata-set-linked-tags-popover-header-operation sf-metadata-set-linked-tags-popover-back" onClick={switchToLinkedTagsPage}><i className="sf3-font sf3-font-arrow sf-metadata-set-linked-tags-popover-back-icon"></i></span>
|
|
||||||
<span>{gettext('Link existing tags')}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="sf-metadata-set-linked-tags-popover-body">
|
|
||||||
<div className="sf-metadata-set-linked-tags-popover-search-container">
|
|
||||||
<SearchInput
|
|
||||||
autoFocus
|
|
||||||
className="sf-metadata-set-linked-tags-popover-search-tags"
|
|
||||||
placeholder={gettext('Search tag')}
|
|
||||||
onKeyDown={onKeyDown}
|
|
||||||
onChange={onChangeSearch}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{selectableTags.length === 0 && (
|
|
||||||
<EmptyTip text={gettext('No tags available')} />
|
|
||||||
)}
|
|
||||||
{selectableTags.length > 0 && (
|
|
||||||
<div className="sf-metadata-set-linked-tags-popover-selectable-tags-wrapper">
|
|
||||||
<Tags selectable tags={selectableTags} idTagSelectedMap={idTagSelectedMap} selectTag={selectTag} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
AddLinkedTags.propTypes = {
|
|
||||||
isParentTags: PropTypes.bool,
|
|
||||||
allTags: PropTypes.array,
|
|
||||||
linkedTags: PropTypes.array,
|
|
||||||
switchToLinkedTagsPage: PropTypes.func,
|
|
||||||
addLinkedTag: PropTypes.func,
|
|
||||||
deleteLinedTag: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AddLinkedTags;
|
|
@@ -1,134 +0,0 @@
|
|||||||
.sf-metadata-set-linked-tags-popover .popover {
|
|
||||||
max-width: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sf-metadata-set-linked-tags-popover-selected,
|
|
||||||
.sf-metadata-set-linked-tags-popover-selector {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sf-metadata-set-linked-tags-popover-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
height: 46px;
|
|
||||||
border-bottom: 1px solid #e9ecef;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sf-metadata-set-linked-tags-popover-title {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
height: 40px;
|
|
||||||
line-height: 40px;
|
|
||||||
padding: 0 20px;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sf-metadata-set-linked-tags-popover-selector .sf-metadata-set-linked-tags-popover-title {
|
|
||||||
margin-left: -3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sf-metadata-set-linked-tags-popover-body {
|
|
||||||
flex: 1;
|
|
||||||
height: calc(100% - 46px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sf-metadata-set-linked-tags-popover .empty-tip {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sf-metadata-set-linked-tags-popover-selectable-tags-wrapper {
|
|
||||||
height: calc(100% - 48px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sf-metadata-editing-tags-list {
|
|
||||||
height: 100%;
|
|
||||||
padding: 10px 20px;
|
|
||||||
overflow-y: scroll;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sf-metadata-editing-tags-list.selectable:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sf-metadata-editing-tag:hover {
|
|
||||||
background: #f5f5f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sf-metadata-editing-tag-container {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
height: 30px;
|
|
||||||
width: 100%;
|
|
||||||
border-radius: 2px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #212529;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sf-metadata-set-linked-tags-popover-search-container {
|
|
||||||
height: 48px;
|
|
||||||
padding: 10px 20px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sf-metadata-editing-tag-color-name {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sf-metadata-editing-tag-color-name .sf-metadata-tag-color {
|
|
||||||
height: 14px;
|
|
||||||
width: 14px;
|
|
||||||
border-radius: 50%;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sf-metadata-editing-tag-color-name .sf-metadata-tag-name {
|
|
||||||
flex: 1;
|
|
||||||
margin-left: 4px;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sf-metadata-editing-tag-operation {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sf-metadata-editing-tag-delete-icon {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sf-metadata-editing-tag-delete:hover .sf-metadata-editing-tag-delete-icon {
|
|
||||||
fill: #555;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sf-metadata-set-linked-tags-popover-header-operation {
|
|
||||||
display: inline-flex;
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
margin-right: 3px;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sf-metadata-set-linked-tags-popover-header-operation:hover {
|
|
||||||
border-radius: 3px;
|
|
||||||
background-color: #efefef;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sf-metadata-set-linked-tags-popover-back-icon {
|
|
||||||
display: inline-block;
|
|
||||||
transform: rotate(180deg);
|
|
||||||
font-size: 14px;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
@@ -1,135 +0,0 @@
|
|||||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { UncontrolledPopover } from 'reactstrap';
|
|
||||||
import isHotkey from 'is-hotkey';
|
|
||||||
import LinkedTags from './linked-tags';
|
|
||||||
import AddLinkedTags from './add-linked-tags';
|
|
||||||
import { getRowsByIds } from '../../../../metadata/utils/table';
|
|
||||||
import { useTags } from '../../../hooks';
|
|
||||||
import { getEventClassName } from '../../../../metadata/utils/common';
|
|
||||||
|
|
||||||
import './index.css';
|
|
||||||
|
|
||||||
const POPOVER_WIDTH = 560;
|
|
||||||
const POPOVER_WINDOW_SAFE_SPACE = 30;
|
|
||||||
const POPOVER_MAX_HEIGHT = 520;
|
|
||||||
const POPOVER_MIN_HEIGHT = 300;
|
|
||||||
|
|
||||||
const KEY_MODE_TYPE = {
|
|
||||||
LINKED_TAGS: 'linked_tags',
|
|
||||||
ADD_LINKED_TAGS: 'add_linked_tags',
|
|
||||||
};
|
|
||||||
|
|
||||||
const SetLinkedTagsPopover = ({ isParentTags, target, placement, tagLinks, allTags, hidePopover, addTagLinks, deleteTagLinks }) => {
|
|
||||||
const { tagsData } = useTags();
|
|
||||||
const linkedRowsIds = Array.isArray(tagLinks) ? tagLinks.map((link) => link.row_id) : [];
|
|
||||||
const initialLinkedTags = getRowsByIds(tagsData, linkedRowsIds);
|
|
||||||
const [mode, setMode] = useState(KEY_MODE_TYPE.LINKED_TAGS);
|
|
||||||
const [linkedTags, setLinkedTags] = useState(initialLinkedTags);
|
|
||||||
|
|
||||||
const popoverRef = useRef(null);
|
|
||||||
|
|
||||||
const getPopoverInnerStyle = () => {
|
|
||||||
let style = { width: POPOVER_WIDTH };
|
|
||||||
const windowHeight = window.innerHeight - POPOVER_WINDOW_SAFE_SPACE;
|
|
||||||
let maxHeight = POPOVER_MAX_HEIGHT;
|
|
||||||
if (windowHeight < maxHeight) {
|
|
||||||
maxHeight = windowHeight;
|
|
||||||
}
|
|
||||||
if (maxHeight < POPOVER_MIN_HEIGHT) {
|
|
||||||
maxHeight = POPOVER_MIN_HEIGHT;
|
|
||||||
}
|
|
||||||
style.height = maxHeight;
|
|
||||||
return style;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onHidePopover = useCallback((event) => {
|
|
||||||
if (popoverRef.current && !getEventClassName(event).includes('popover') && !popoverRef.current.contains(event.target)) {
|
|
||||||
hidePopover(event);
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}, [hidePopover]);
|
|
||||||
|
|
||||||
const onHotKey = useCallback((event) => {
|
|
||||||
if (isHotkey('esc', event)) {
|
|
||||||
event.preventDefault();
|
|
||||||
hidePopover();
|
|
||||||
}
|
|
||||||
}, [hidePopover]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
document.addEventListener('click', onHidePopover, true);
|
|
||||||
document.addEventListener('keydown', onHotKey);
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener('click', onHidePopover, true);
|
|
||||||
document.removeEventListener('keydown', onHotKey);
|
|
||||||
};
|
|
||||||
}, [onHidePopover, onHotKey]);
|
|
||||||
|
|
||||||
const deleteLinedTag = useCallback((tagId) => {
|
|
||||||
let updatedLinkedTags = [...linkedTags];
|
|
||||||
const deleteIndex = updatedLinkedTags.findIndex((tag) => tag._id === tagId);
|
|
||||||
if (deleteIndex < 0) return;
|
|
||||||
updatedLinkedTags.splice(deleteIndex, 1);
|
|
||||||
setLinkedTags(updatedLinkedTags);
|
|
||||||
deleteTagLinks(tagId);
|
|
||||||
}, [linkedTags, deleteTagLinks]);
|
|
||||||
|
|
||||||
const addLinkedTag = useCallback((tag) => {
|
|
||||||
let updatedLinkedTags = [...linkedTags];
|
|
||||||
updatedLinkedTags.push(tag);
|
|
||||||
setLinkedTags(updatedLinkedTags);
|
|
||||||
addTagLinks(tag);
|
|
||||||
}, [linkedTags, addTagLinks]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<UncontrolledPopover
|
|
||||||
isOpen
|
|
||||||
hideArrow
|
|
||||||
positionFixed
|
|
||||||
fade={false}
|
|
||||||
flip={false}
|
|
||||||
placement={placement}
|
|
||||||
target={target}
|
|
||||||
className="sf-metadata-set-linked-tags-popover"
|
|
||||||
boundariesElement={document.body}
|
|
||||||
>
|
|
||||||
<div ref={popoverRef} style={getPopoverInnerStyle()}>
|
|
||||||
{mode === KEY_MODE_TYPE.LINKED_TAGS ?
|
|
||||||
<LinkedTags
|
|
||||||
isParentTags={isParentTags}
|
|
||||||
linkedTags={linkedTags}
|
|
||||||
switchToAddTagsPage={() => setMode(KEY_MODE_TYPE.ADD_LINKED_TAGS)}
|
|
||||||
deleteLinedTag={deleteLinedTag}
|
|
||||||
/> :
|
|
||||||
<AddLinkedTags
|
|
||||||
allTags={allTags}
|
|
||||||
linkedTags={linkedTags}
|
|
||||||
switchToLinkedTagsPage={() => setMode(KEY_MODE_TYPE.LINKED_TAGS)}
|
|
||||||
addLinkedTag={addLinkedTag}
|
|
||||||
deleteLinedTag={deleteLinedTag}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</UncontrolledPopover>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
SetLinkedTagsPopover.propTypes = {
|
|
||||||
isParentTags: PropTypes.bool,
|
|
||||||
placement: PropTypes.string,
|
|
||||||
target: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
|
|
||||||
tagLinks: PropTypes.array,
|
|
||||||
allTags: PropTypes.array,
|
|
||||||
hidePopover: PropTypes.func,
|
|
||||||
addTagLinks: PropTypes.func,
|
|
||||||
deleteTagLinks: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
SetLinkedTagsPopover.defaultProps = {
|
|
||||||
placement: 'bottom-end',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SetLinkedTagsPopover;
|
|
@@ -1,36 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { Button } from 'reactstrap';
|
|
||||||
import EmptyTip from '../../../../components/empty-tip';
|
|
||||||
import Tags from './tags';
|
|
||||||
import { gettext } from '../../../../utils/constants';
|
|
||||||
|
|
||||||
const LinkedTags = ({ isParentTags, linkedTags, switchToAddTagsPage, deleteLinedTag }) => {
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="sf-metadata-set-linked-tags-popover-selected">
|
|
||||||
<div className="sf-metadata-set-linked-tags-popover-header">
|
|
||||||
<div className="sf-metadata-set-linked-tags-popover-title">
|
|
||||||
{isParentTags ? gettext('Parent tags') : gettext('Sub tags')}
|
|
||||||
</div>
|
|
||||||
<Button size="sm" color="primary" className="mr-2" onClick={switchToAddTagsPage}>{gettext('Link existing tags')}</Button>
|
|
||||||
</div>
|
|
||||||
<div className="sf-metadata-set-linked-tags-popover-body">
|
|
||||||
{linkedTags.length === 0 ?
|
|
||||||
<EmptyTip text={isParentTags ? gettext('No parent tag') : gettext('No sub tag')} />
|
|
||||||
:
|
|
||||||
<Tags deletable tags={linkedTags} deleteTag={deleteLinedTag} />
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
LinkedTags.propTypes = {
|
|
||||||
isParentTags: PropTypes.bool,
|
|
||||||
linkedTags: PropTypes.array,
|
|
||||||
switchToAddTagsPage: PropTypes.func,
|
|
||||||
deleteTag: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LinkedTags;
|
|
@@ -1,53 +0,0 @@
|
|||||||
import React, { useCallback } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import classnames from 'classnames';
|
|
||||||
import { Icon } from '@seafile/sf-metadata-ui-component';
|
|
||||||
import { getTagColor, getTagId, getTagName } from '../../../utils';
|
|
||||||
import { debounce } from '../../../../metadata/utils/common';
|
|
||||||
|
|
||||||
const Tags = ({ tags, deletable, selectable, idTagSelectedMap, selectTag, deleteTag }) => {
|
|
||||||
|
|
||||||
const clickTag = debounce((tag) => {
|
|
||||||
if (!selectable) return;
|
|
||||||
selectTag && selectTag(tag);
|
|
||||||
}, 200);
|
|
||||||
|
|
||||||
const remove = useCallback((tagId) => {
|
|
||||||
if (!deletable) return;
|
|
||||||
deleteTag && deleteTag(tagId);
|
|
||||||
}, [deletable, deleteTag]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classnames('sf-metadata-editing-tags-list', { 'selectable': selectable })}>
|
|
||||||
{tags.map((tag) => {
|
|
||||||
const tagId = getTagId(tag);
|
|
||||||
const tagName = getTagName(tag);
|
|
||||||
const tagColor = getTagColor(tag);
|
|
||||||
return (
|
|
||||||
<div className="sf-metadata-editing-tag" key={`sf-metadata-editing-tag-${tagId}`} onClick={() => clickTag(tag)}>
|
|
||||||
<div className="sf-metadata-editing-tag-container pl-2">
|
|
||||||
<div className="sf-metadata-editing-tag-color-name">
|
|
||||||
<div className="sf-metadata-tag-color" style={{ backgroundColor: tagColor }}></div>
|
|
||||||
<div className="sf-metadata-tag-name">{tagName}</div>
|
|
||||||
</div>
|
|
||||||
<div className="sf-metadata-editing-tag-operations">
|
|
||||||
{deletable && <div className="sf-metadata-editing-tag-operation sf-metadata-editing-tag-delete" onClick={() => remove(tagId)}><Icon iconName="close" className="sf-metadata-editing-tag-delete-icon" /></div>}
|
|
||||||
{(selectable && idTagSelectedMap && idTagSelectedMap[tagId]) && <div className="sf-metadata-editing-tag-operation sf-metadata-editing-tag-selected"><Icon iconName="check-mark" className="sf-metadata-editing-tag-selected-icon" /></div>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
Tags.propTypes = {
|
|
||||||
tags: PropTypes.array,
|
|
||||||
deletable: PropTypes.bool,
|
|
||||||
selectable: PropTypes.bool,
|
|
||||||
selectTag: PropTypes.func,
|
|
||||||
deleteTag: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Tags;
|
|
@@ -124,6 +124,10 @@ export const TagsProvider = ({ repoID, currentPath, selectTagsView, children, ..
|
|||||||
return storeRef.current.addTags(rows, callback);
|
return storeRef.current.addTags(rows, callback);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const addChildTag = useCallback((tagData, parentTagId, callback = {}) => {
|
||||||
|
return storeRef.current.addChildTag(tagData, parentTagId, callback);
|
||||||
|
}, []);
|
||||||
|
|
||||||
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 });
|
||||||
}, [storeRef]);
|
}, [storeRef]);
|
||||||
@@ -262,6 +266,7 @@ export const TagsProvider = ({ repoID, currentPath, selectTagsView, children, ..
|
|||||||
updateCurrentDirent: params.updateCurrentDirent,
|
updateCurrentDirent: params.updateCurrentDirent,
|
||||||
addTag,
|
addTag,
|
||||||
addTags,
|
addTags,
|
||||||
|
addChildTag,
|
||||||
modifyTags,
|
modifyTags,
|
||||||
deleteTags,
|
deleteTags,
|
||||||
duplicateTag,
|
duplicateTag,
|
||||||
|
@@ -6,7 +6,10 @@ import { OPERATION_TYPE } from './operations';
|
|||||||
import { buildTagsTree } from '../utils/tree';
|
import { buildTagsTree } 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 { TREE_NODE_KEY } from '../../components/sf-table/constants/tree';
|
||||||
import { createTreeNode } from '../../components/sf-table/utils/tree';
|
import {
|
||||||
|
addTreeChildNode, createTreeNode, generateNodeKey, getTreeNodeDepth, getTreeNodeId, getTreeNodeKey,
|
||||||
|
resetTreeHasChildNodesStatus,
|
||||||
|
} from '../../components/sf-table/utils/tree';
|
||||||
|
|
||||||
// const DEFAULT_COMPUTER_PROPERTIES_CONTROLLER = {
|
// const DEFAULT_COMPUTER_PROPERTIES_CONTROLLER = {
|
||||||
// isUpdateSummaries: true,
|
// isUpdateSummaries: true,
|
||||||
@@ -27,7 +30,7 @@ class DataProcessor {
|
|||||||
let updated_rows_tree = [...rows_tree];
|
let updated_rows_tree = [...rows_tree];
|
||||||
tags.forEach((tag) => {
|
tags.forEach((tag) => {
|
||||||
const tagId = getRecordIdFromRecord(tag);
|
const tagId = getRecordIdFromRecord(tag);
|
||||||
const nodeKey = 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);
|
||||||
});
|
});
|
||||||
@@ -38,20 +41,23 @@ 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_SUB_NODES]);
|
const hasDeletedParentNode = rows_tree.some((node) => idTagDeletedMap[node[TREE_NODE_KEY.ID]] && node[TREE_NODE_KEY.HAS_CHILD_NODES]);
|
||||||
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);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove the nodes which has no sub 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[node[TREE_NODE_KEY.ID]]) {
|
||||||
updated_rows_tree.push(node);
|
updated_rows_tree.push(node);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// update has_child_nodes status(all child nodes may be deleted)
|
||||||
|
resetTreeHasChildNodesStatus(updated_rows_tree);
|
||||||
table.rows_tree = updated_rows_tree;
|
table.rows_tree = updated_rows_tree;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,6 +204,23 @@ class DataProcessor {
|
|||||||
this.updateTagsTreeWithNewTags(tags, table);
|
this.updateTagsTreeWithNewTags(tags, table);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case OPERATION_TYPE.ADD_CHILD_TAG: {
|
||||||
|
const { tag, parent_tag_id } = operation;
|
||||||
|
const tagId = getRecordIdFromRecord(tag);
|
||||||
|
if (!tagId || !parent_tag_id) return;
|
||||||
|
const { rows_tree } = table;
|
||||||
|
rows_tree.forEach((node) => {
|
||||||
|
const nodeId = getTreeNodeId(node);
|
||||||
|
if (nodeId === parent_tag_id) {
|
||||||
|
const parentNodeKey = getTreeNodeKey(node);
|
||||||
|
const parentNodeDepth = getTreeNodeDepth(node);
|
||||||
|
const subNodeKey = generateNodeKey(parentNodeKey, tagId);
|
||||||
|
const childNode = createTreeNode(tagId, subNodeKey, parentNodeDepth + 1, false);
|
||||||
|
addTreeChildNode(childNode, node, rows_tree);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
case OPERATION_TYPE.ADD_TAG_LINKS:
|
case OPERATION_TYPE.ADD_TAG_LINKS:
|
||||||
case OPERATION_TYPE.DELETE_TAG_LINKS: {
|
case OPERATION_TYPE.DELETE_TAG_LINKS: {
|
||||||
this.buildTagsTree(table.rows, table);
|
this.buildTagsTree(table.rows, table);
|
||||||
|
@@ -250,6 +250,19 @@ class Store {
|
|||||||
this.applyOperation(operation);
|
this.applyOperation(operation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addChildTag(tagData, parentTagId, { fail_callback, success_callback } = {}) {
|
||||||
|
const type = OPERATION_TYPE.ADD_CHILD_TAG;
|
||||||
|
const operation = this.createOperation({
|
||||||
|
type,
|
||||||
|
repo_id: this.repoId,
|
||||||
|
tag_data: tagData,
|
||||||
|
parent_tag_id: parentTagId,
|
||||||
|
fail_callback,
|
||||||
|
success_callback,
|
||||||
|
});
|
||||||
|
this.applyOperation(operation);
|
||||||
|
}
|
||||||
|
|
||||||
modifyTags(row_ids, id_row_updates, id_original_row_updates, id_old_row_data, id_original_old_row_data, { fail_callback, success_callback }) {
|
modifyTags(row_ids, id_row_updates, id_original_row_updates, id_old_row_data, id_original_old_row_data, { fail_callback, success_callback }) {
|
||||||
const originalRows = getRowsByIds(this.data, row_ids);
|
const originalRows = getRowsByIds(this.data, row_ids);
|
||||||
let valid_row_ids = [];
|
let valid_row_ids = [];
|
||||||
|
@@ -5,6 +5,7 @@ import { OPERATION_TYPE } from './constants';
|
|||||||
import { PRIVATE_COLUMN_KEY } from '../../constants';
|
import { PRIVATE_COLUMN_KEY } from '../../constants';
|
||||||
import { username } from '../../../utils/constants';
|
import { username } from '../../../utils/constants';
|
||||||
import { addRowLinks, removeRowLinks } from '../../utils/link';
|
import { addRowLinks, removeRowLinks } from '../../utils/link';
|
||||||
|
import { getRecordIdFromRecord } from '../../../metadata/utils/cell';
|
||||||
|
|
||||||
dayjs.extend(utc);
|
dayjs.extend(utc);
|
||||||
|
|
||||||
@@ -30,6 +31,34 @@ export default function apply(data, operation) {
|
|||||||
data.rows = updatedRows;
|
data.rows = updatedRows;
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
case OPERATION_TYPE.ADD_CHILD_TAG: {
|
||||||
|
const { tag, parent_tag_id } = operation;
|
||||||
|
const tagId = getRecordIdFromRecord(tag);
|
||||||
|
if (!tagId || !parent_tag_id) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { rows } = data;
|
||||||
|
const updatedRows = [...rows];
|
||||||
|
|
||||||
|
// add parent link
|
||||||
|
const updatedTag = addRowLinks(tag, PRIVATE_COLUMN_KEY.PARENT_LINKS, [parent_tag_id]);
|
||||||
|
|
||||||
|
// add child link
|
||||||
|
const parentTagIndex = rows.findIndex((tag) => getRecordIdFromRecord(tag) === parent_tag_id);
|
||||||
|
if (parentTagIndex > -1) {
|
||||||
|
const parentTag = updatedRows[parentTagIndex];
|
||||||
|
const updatedParentTag = addRowLinks(parentTag, PRIVATE_COLUMN_KEY.SUB_LINKS, [tagId]);
|
||||||
|
updatedRows[parentTagIndex] = updatedParentTag;
|
||||||
|
data.id_row_map[parent_tag_id] = updatedParentTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedRows.push(updatedTag);
|
||||||
|
data.row_ids.push(tagId);
|
||||||
|
data.id_row_map[tagId] = updatedTag;
|
||||||
|
data.rows = updatedRows;
|
||||||
|
return data;
|
||||||
|
}
|
||||||
case OPERATION_TYPE.MODIFY_RECORDS:
|
case OPERATION_TYPE.MODIFY_RECORDS:
|
||||||
case OPERATION_TYPE.MODIFY_LOCAL_RECORDS: {
|
case OPERATION_TYPE.MODIFY_LOCAL_RECORDS: {
|
||||||
const { id_original_row_updates, id_row_updates } = operation;
|
const { id_original_row_updates, id_row_updates } = operation;
|
||||||
@@ -129,7 +158,7 @@ export default function apply(data, operation) {
|
|||||||
data.id_row_map[currentRowId] = updatedRow;
|
data.id_row_map[currentRowId] = updatedRow;
|
||||||
}
|
}
|
||||||
if (other_rows_ids.includes(currentRowId)) {
|
if (other_rows_ids.includes(currentRowId)) {
|
||||||
// add current tag as sub tag to related tags
|
// add current tag as child tag to related tags
|
||||||
updatedRow = addRowLinks(updatedRow, PRIVATE_COLUMN_KEY.SUB_LINKS, [row_id]);
|
updatedRow = addRowLinks(updatedRow, PRIVATE_COLUMN_KEY.SUB_LINKS, [row_id]);
|
||||||
data.rows[index] = updatedRow;
|
data.rows[index] = updatedRow;
|
||||||
data.id_row_map[currentRowId] = updatedRow;
|
data.id_row_map[currentRowId] = updatedRow;
|
||||||
@@ -140,7 +169,7 @@ export default function apply(data, operation) {
|
|||||||
const currentRowId = row._id;
|
const currentRowId = row._id;
|
||||||
let updatedRow = { ...row };
|
let updatedRow = { ...row };
|
||||||
if (currentRowId === row_id) {
|
if (currentRowId === row_id) {
|
||||||
// add sub tags to current tag
|
// add child tags to current tag
|
||||||
updatedRow = addRowLinks(updatedRow, PRIVATE_COLUMN_KEY.SUB_LINKS, other_rows_ids);
|
updatedRow = addRowLinks(updatedRow, PRIVATE_COLUMN_KEY.SUB_LINKS, other_rows_ids);
|
||||||
data.rows[index] = updatedRow;
|
data.rows[index] = updatedRow;
|
||||||
data.id_row_map[currentRowId] = updatedRow;
|
data.id_row_map[currentRowId] = updatedRow;
|
||||||
@@ -169,7 +198,7 @@ export default function apply(data, operation) {
|
|||||||
data.id_row_map[currentRowId] = updatedRow;
|
data.id_row_map[currentRowId] = updatedRow;
|
||||||
}
|
}
|
||||||
if (other_rows_ids.includes(currentRowId)) {
|
if (other_rows_ids.includes(currentRowId)) {
|
||||||
// remove current tag as sub tag from related tags
|
// remove current tag as child tag from related tags
|
||||||
updatedRow = removeRowLinks(updatedRow, PRIVATE_COLUMN_KEY.SUB_LINKS, [row_id]);
|
updatedRow = removeRowLinks(updatedRow, PRIVATE_COLUMN_KEY.SUB_LINKS, [row_id]);
|
||||||
data.rows[index] = updatedRow;
|
data.rows[index] = updatedRow;
|
||||||
data.id_row_map[currentRowId] = updatedRow;
|
data.id_row_map[currentRowId] = updatedRow;
|
||||||
@@ -180,7 +209,7 @@ export default function apply(data, operation) {
|
|||||||
const currentRowId = row._id;
|
const currentRowId = row._id;
|
||||||
let updatedRow = { ...row };
|
let updatedRow = { ...row };
|
||||||
if (currentRowId === row_id) {
|
if (currentRowId === row_id) {
|
||||||
// remove sub tags from current tag
|
// remove child tags from current tag
|
||||||
updatedRow = removeRowLinks(updatedRow, PRIVATE_COLUMN_KEY.SUB_LINKS, other_rows_ids);
|
updatedRow = removeRowLinks(updatedRow, PRIVATE_COLUMN_KEY.SUB_LINKS, other_rows_ids);
|
||||||
data.rows[index] = updatedRow;
|
data.rows[index] = updatedRow;
|
||||||
data.id_row_map[currentRowId] = updatedRow;
|
data.id_row_map[currentRowId] = updatedRow;
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
export const OPERATION_TYPE = {
|
export const OPERATION_TYPE = {
|
||||||
ADD_RECORDS: 'add_records',
|
ADD_RECORDS: 'add_records',
|
||||||
|
ADD_CHILD_TAG: 'add_child_tag',
|
||||||
MODIFY_RECORDS: 'modify_records',
|
MODIFY_RECORDS: 'modify_records',
|
||||||
DELETE_RECORDS: 'delete_records',
|
DELETE_RECORDS: 'delete_records',
|
||||||
RESTORE_RECORDS: 'restore_records',
|
RESTORE_RECORDS: 'restore_records',
|
||||||
@@ -14,6 +15,7 @@ export const OPERATION_TYPE = {
|
|||||||
|
|
||||||
export const OPERATION_ATTRIBUTES = {
|
export const OPERATION_ATTRIBUTES = {
|
||||||
[OPERATION_TYPE.ADD_RECORDS]: ['repo_id', 'rows', 'tags'],
|
[OPERATION_TYPE.ADD_RECORDS]: ['repo_id', 'rows', 'tags'],
|
||||||
|
[OPERATION_TYPE.ADD_CHILD_TAG]: ['repo_id', 'tag_data', 'parent_tag_id'],
|
||||||
[OPERATION_TYPE.MODIFY_RECORDS]: ['repo_id', 'row_ids', 'id_row_updates', 'id_original_row_updates', 'id_old_row_data', 'id_original_old_row_data', 'is_copy_paste', 'is_rename', 'id_obj_id'],
|
[OPERATION_TYPE.MODIFY_RECORDS]: ['repo_id', 'row_ids', 'id_row_updates', 'id_original_row_updates', 'id_old_row_data', 'id_original_old_row_data', 'is_copy_paste', 'is_rename', 'id_obj_id'],
|
||||||
[OPERATION_TYPE.DELETE_RECORDS]: ['repo_id', 'tag_ids', 'deleted_tags'],
|
[OPERATION_TYPE.DELETE_RECORDS]: ['repo_id', 'tag_ids', 'deleted_tags'],
|
||||||
[OPERATION_TYPE.RESTORE_RECORDS]: ['repo_id', 'rows_data', 'original_rows', 'link_infos', 'upper_row_ids'],
|
[OPERATION_TYPE.RESTORE_RECORDS]: ['repo_id', 'rows_data', 'original_rows', 'link_infos', 'upper_row_ids'],
|
||||||
@@ -38,6 +40,7 @@ export const LOCAL_APPLY_OPERATION_TYPE = [
|
|||||||
// apply operation after exec operation on the server
|
// apply operation after exec operation on the server
|
||||||
export const NEED_APPLY_AFTER_SERVER_OPERATION = [
|
export const NEED_APPLY_AFTER_SERVER_OPERATION = [
|
||||||
OPERATION_TYPE.ADD_RECORDS,
|
OPERATION_TYPE.ADD_RECORDS,
|
||||||
|
OPERATION_TYPE.ADD_CHILD_TAG,
|
||||||
];
|
];
|
||||||
|
|
||||||
export const VIEW_OPERATION = [
|
export const VIEW_OPERATION = [
|
||||||
|
@@ -2,6 +2,7 @@ import { gettext } from '../../utils/constants';
|
|||||||
import { OPERATION_TYPE } from './operations';
|
import { OPERATION_TYPE } from './operations';
|
||||||
import { getColumnByKey } from '../../metadata/utils/column';
|
import { getColumnByKey } from '../../metadata/utils/column';
|
||||||
import ObjectUtils from '../../metadata/utils/object-utils';
|
import ObjectUtils from '../../metadata/utils/object-utils';
|
||||||
|
import { PRIVATE_COLUMN_KEY } from '../constants';
|
||||||
|
|
||||||
const MAX_LOAD_RECORDS = 100;
|
const MAX_LOAD_RECORDS = 100;
|
||||||
|
|
||||||
@@ -26,6 +27,27 @@ class ServerOperator {
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case OPERATION_TYPE.ADD_CHILD_TAG: {
|
||||||
|
const { tag_data, parent_tag_id } = operation;
|
||||||
|
this.context.addTags([tag_data]).then(res => {
|
||||||
|
const tags = res?.data?.tags || [];
|
||||||
|
const childTag = tags[0];
|
||||||
|
if (!childTag) {
|
||||||
|
callback({ error: gettext('Failed to add tags') });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
operation.tag = childTag;
|
||||||
|
|
||||||
|
// set parent tag for new child tag
|
||||||
|
const id_linked_rows_ids_map = { [childTag._id]: [parent_tag_id] };
|
||||||
|
this.context.addTagLinks(PRIVATE_COLUMN_KEY.PARENT_LINKS, id_linked_rows_ids_map);
|
||||||
|
callback({ operation });
|
||||||
|
}).catch(error => {
|
||||||
|
callback({ error: gettext('Failed to add tags') });
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
case OPERATION_TYPE.MODIFY_RECORDS: {
|
case OPERATION_TYPE.MODIFY_RECORDS: {
|
||||||
const { row_ids, id_row_updates } = operation;
|
const { row_ids, id_row_updates } = operation;
|
||||||
const recordsData = row_ids.map(rowId => {
|
const recordsData = row_ids.map(rowId => {
|
||||||
|
@@ -28,15 +28,10 @@ export const getParentLinks = (tag) => {
|
|||||||
return (tag && tag[PRIVATE_COLUMN_KEY.PARENT_LINKS]) || [];
|
return (tag && tag[PRIVATE_COLUMN_KEY.PARENT_LINKS]) || [];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getSubLinks = (tag) => {
|
export const getChildLinks = (tag) => {
|
||||||
return (tag && tag[PRIVATE_COLUMN_KEY.SUB_LINKS]) || [];
|
return (tag && tag[PRIVATE_COLUMN_KEY.SUB_LINKS]) || [];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getSubTagsCount = (tag) => {
|
|
||||||
const subLinks = getSubLinks(tag);
|
|
||||||
return subLinks.length;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getTagFilesCount = (tag) => {
|
export const getTagFilesCount = (tag) => {
|
||||||
const links = tag ? tag[PRIVATE_COLUMN_KEY.TAG_FILE_LINKS] : [];
|
const links = tag ? tag[PRIVATE_COLUMN_KEY.TAG_FILE_LINKS] : [];
|
||||||
if (Array.isArray(links)) return links.length;
|
if (Array.isArray(links)) return links.length;
|
||||||
|
@@ -1,16 +1,16 @@
|
|||||||
import { createTreeNode } from '../../components/sf-table/utils/tree';
|
import { createTreeNode, generateNodeKey } 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, getSubLinks } from './cell';
|
import { getParentLinks, getChildLinks } from './cell';
|
||||||
|
|
||||||
const setSubNodes = (row, parentDepth, parentKey, idNodeInCurrentTreeMap, idNodeCreatedMap, tree, table) => {
|
const setChildNodes = (row, parentDepth, parentKey, idNodeInCurrentTreeMap, idNodeCreatedMap, tree, table) => {
|
||||||
const nodeId = getRecordIdFromRecord(row);
|
const nodeId = getRecordIdFromRecord(row);
|
||||||
|
|
||||||
idNodeCreatedMap[nodeId] = true;
|
idNodeCreatedMap[nodeId] = true;
|
||||||
idNodeInCurrentTreeMap[nodeId] = true; // for preventing circular dependencies
|
idNodeInCurrentTreeMap[nodeId] = true; // for preventing circular dependencies
|
||||||
|
|
||||||
const nodeKey = `${parentKey ? parentKey + '_' : ''}${nodeId}`; // the unique ID of each node
|
const nodeKey = generateNodeKey(parentKey, nodeId); // the unique ID of each node
|
||||||
const subLinks = getSubLinks(row);
|
const subLinks = getChildLinks(row);
|
||||||
const subRowsIds = subLinks.map((link) => link.row_id);
|
const subRowsIds = subLinks.map((link) => link.row_id);
|
||||||
const subRows = getRowsByIds(table, subRowsIds);
|
const subRows = getRowsByIds(table, subRowsIds);
|
||||||
const validSubRows = subRows.filter((row) => row && !idNodeInCurrentTreeMap[row._id]);
|
const validSubRows = subRows.filter((row) => row && !idNodeInCurrentTreeMap[row._id]);
|
||||||
@@ -21,7 +21,7 @@ const setSubNodes = (row, parentDepth, parentKey, idNodeInCurrentTreeMap, idNode
|
|||||||
if (validSubRows) {
|
if (validSubRows) {
|
||||||
const nextNodeDepth = parentDepth + 1;
|
const nextNodeDepth = parentDepth + 1;
|
||||||
validSubRows.forEach((subRow) => {
|
validSubRows.forEach((subRow) => {
|
||||||
setSubNodes(subRow, nextNodeDepth, nodeKey, { ...idNodeInCurrentTreeMap }, idNodeCreatedMap, tree, table);
|
setChildNodes(subRow, nextNodeDepth, nodeKey, { ...idNodeInCurrentTreeMap }, idNodeCreatedMap, tree, table);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ const setSubNodes = (row, parentDepth, parentKey, idNodeInCurrentTreeMap, idNode
|
|||||||
* @param {array} rows tags
|
* @param {array} rows tags
|
||||||
* @returns {array} tree
|
* @returns {array} tree
|
||||||
* tree: [
|
* tree: [
|
||||||
* { _id, node_depth, node_key, has_sub_nodes, ... }
|
* { _id, node_depth, node_key, has_child_nodes, ... }
|
||||||
* ...
|
* ...
|
||||||
* ]
|
* ]
|
||||||
*/
|
*/
|
||||||
@@ -44,7 +44,7 @@ export const buildTagsTree = (rows, table) => {
|
|||||||
const nodeId = getRecordIdFromRecord(row);
|
const nodeId = getRecordIdFromRecord(row);
|
||||||
const parentLinks = getParentLinks(row);
|
const parentLinks = getParentLinks(row);
|
||||||
if (parentLinks.length === 0 && !idNodeCreatedMap[nodeId]) {
|
if (parentLinks.length === 0 && !idNodeCreatedMap[nodeId]) {
|
||||||
setSubNodes(row, 0, '', {}, idNodeCreatedMap, tree, table);
|
setChildNodes(row, 0, '', {}, idNodeCreatedMap, tree, table);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ export const buildTagsTree = (rows, table) => {
|
|||||||
noneCreatedRows.forEach((row) => {
|
noneCreatedRows.forEach((row) => {
|
||||||
const nodeId = getRecordIdFromRecord(row);
|
const nodeId = getRecordIdFromRecord(row);
|
||||||
if (!idNodeCreatedMap[nodeId]) {
|
if (!idNodeCreatedMap[nodeId]) {
|
||||||
setSubNodes(row, 0, '', {}, idNodeCreatedMap, tree, table);
|
setChildNodes(row, 0, '', {}, idNodeCreatedMap, tree, table);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -13,7 +13,7 @@ const KEY_COLUMN_ICON_NAME = {
|
|||||||
const KEY_COLUMN_DISPLAY_NAME = {
|
const KEY_COLUMN_DISPLAY_NAME = {
|
||||||
[PRIVATE_COLUMN_KEY.TAG_NAME]: gettext('Tag'),
|
[PRIVATE_COLUMN_KEY.TAG_NAME]: gettext('Tag'),
|
||||||
[PRIVATE_COLUMN_KEY.PARENT_LINKS]: gettext('Parent tags'),
|
[PRIVATE_COLUMN_KEY.PARENT_LINKS]: gettext('Parent tags'),
|
||||||
[PRIVATE_COLUMN_KEY.SUB_LINKS]: gettext('Sub tags count'),
|
[PRIVATE_COLUMN_KEY.SUB_LINKS]: gettext('Child tags count'),
|
||||||
[PRIVATE_COLUMN_KEY.TAG_FILE_LINKS]: gettext('File count'),
|
[PRIVATE_COLUMN_KEY.TAG_FILE_LINKS]: gettext('File count'),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -12,6 +12,7 @@ const OPERATION = {
|
|||||||
SET_SUB_TAGS: 'set_sub_tags',
|
SET_SUB_TAGS: 'set_sub_tags',
|
||||||
DELETE_TAG: 'delete_tag',
|
DELETE_TAG: 'delete_tag',
|
||||||
DELETE_TAGS: 'delete_tags',
|
DELETE_TAGS: 'delete_tags',
|
||||||
|
NEW_SUB_TAG: 'new_sub_tag',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createContextMenuOptions = ({
|
export const createContextMenuOptions = ({
|
||||||
@@ -28,8 +29,10 @@ export const createContextMenuOptions = ({
|
|||||||
recordGetterByIndex,
|
recordGetterByIndex,
|
||||||
recordGetterById,
|
recordGetterById,
|
||||||
onDeleteTags,
|
onDeleteTags,
|
||||||
|
onNewSubTag,
|
||||||
}) => {
|
}) => {
|
||||||
const canDeleteTag = context.checkCanDeleteTag();
|
const canDeleteTag = context.checkCanDeleteTag();
|
||||||
|
const canAddTag = context.canAddTag();
|
||||||
const eventBus = EventBus.getInstance();
|
const eventBus = EventBus.getInstance();
|
||||||
|
|
||||||
const onClickMenuItem = (event, option) => {
|
const onClickMenuItem = (event, option) => {
|
||||||
@@ -48,7 +51,10 @@ export const createContextMenuOptions = ({
|
|||||||
onDeleteTags(option.tagsIds);
|
onDeleteTags(option.tagsIds);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case OPERATION.NEW_SUB_TAG: {
|
||||||
|
onNewSubTag(option.parentTagId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -118,7 +124,8 @@ export const createContextMenuOptions = ({
|
|||||||
const tag = recordGetterByIndex({ isGroupView, groupRecordIndex, recordIndex });
|
const tag = recordGetterByIndex({ isGroupView, groupRecordIndex, recordIndex });
|
||||||
const column = getColumnByIndex(idx, columns);
|
const column = getColumnByIndex(idx, columns);
|
||||||
if (!tag || !tag._id || !column) return options;
|
if (!tag || !tag._id || !column) return options;
|
||||||
if (checkIsNameColumn(column)) {
|
const isNameColumn = checkIsNameColumn(column);
|
||||||
|
if (isNameColumn) {
|
||||||
options.push({
|
options.push({
|
||||||
label: gettext('Edit tag'),
|
label: gettext('Edit tag'),
|
||||||
value: OPERATION.EDIT_TAG,
|
value: OPERATION.EDIT_TAG,
|
||||||
@@ -127,7 +134,7 @@ export const createContextMenuOptions = ({
|
|||||||
|
|
||||||
if (column.key === PRIVATE_COLUMN_KEY.SUB_LINKS) {
|
if (column.key === PRIVATE_COLUMN_KEY.SUB_LINKS) {
|
||||||
options.push({
|
options.push({
|
||||||
label: gettext('Set sub tags'),
|
label: gettext('Set child tags'),
|
||||||
value: OPERATION.SET_SUB_TAGS,
|
value: OPERATION.SET_SUB_TAGS,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -140,6 +147,13 @@ export const createContextMenuOptions = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isNameColumn && canAddTag) {
|
||||||
|
options.push({
|
||||||
|
label: gettext('New child tag'),
|
||||||
|
value: OPERATION.NEW_SUB_TAG,
|
||||||
|
parentTagId: tag._id,
|
||||||
|
});
|
||||||
|
}
|
||||||
return options;
|
return options;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -2,17 +2,17 @@ import React, { forwardRef, useCallback, useMemo } from 'react';
|
|||||||
import TagsEditor from '../../../../../components/sf-table/editors/tags-editor';
|
import TagsEditor from '../../../../../components/sf-table/editors/tags-editor';
|
||||||
import { useTags } from '../../../../hooks';
|
import { useTags } from '../../../../hooks';
|
||||||
import { getRowById } from '../../../../../components/sf-table/utils/table';
|
import { getRowById } from '../../../../../components/sf-table/utils/table';
|
||||||
import { getSubLinks } from '../../../../utils/cell';
|
import { getChildLinks } from '../../../../utils/cell';
|
||||||
|
|
||||||
const SubTagsEditor = forwardRef(({ editingRowId, column, addTagLinks, deleteTagLinks, ...editorProps }, ref) => {
|
const ChildTagsEditor = forwardRef(({ editingRowId, column, addTagLinks, deleteTagLinks, ...editorProps }, ref) => {
|
||||||
const { tagsData } = useTags();
|
const { tagsData } = useTags();
|
||||||
|
|
||||||
const tag = useMemo(() => {
|
const tag = useMemo(() => {
|
||||||
return getRowById(tagsData, editingRowId);
|
return getRowById(tagsData, editingRowId);
|
||||||
}, [tagsData, editingRowId]);
|
}, [tagsData, editingRowId]);
|
||||||
|
|
||||||
const subTags = useMemo(() => {
|
const childTags = useMemo(() => {
|
||||||
return getSubLinks(tag);
|
return getChildLinks(tag);
|
||||||
}, [tag]);
|
}, [tag]);
|
||||||
|
|
||||||
const selectTag = useCallback((tagId, recordId) => {
|
const selectTag = useCallback((tagId, recordId) => {
|
||||||
@@ -24,12 +24,12 @@ const SubTagsEditor = forwardRef(({ editingRowId, column, addTagLinks, deleteTag
|
|||||||
}, [column, deleteTagLinks]);
|
}, [column, deleteTagLinks]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="sf-metadata-tags-parent-links-editor">
|
<div className="sf-metadata-tags-child-links-editor">
|
||||||
<TagsEditor
|
<TagsEditor
|
||||||
{...editorProps}
|
{...editorProps}
|
||||||
record={tag}
|
record={tag}
|
||||||
column={column}
|
column={column}
|
||||||
value={subTags}
|
value={childTags}
|
||||||
tagsTable={tagsData}
|
tagsTable={tagsData}
|
||||||
selectTag={selectTag}
|
selectTag={selectTag}
|
||||||
deselectTag={deselectTag}
|
deselectTag={deselectTag}
|
||||||
@@ -38,4 +38,4 @@ const SubTagsEditor = forwardRef(({ editingRowId, column, addTagLinks, deleteTag
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export default SubTagsEditor;
|
export default ChildTagsEditor;
|
@@ -1,7 +1,7 @@
|
|||||||
import ParentTagsEditor from './parent-tags';
|
import ParentTagsEditor from './parent-tags';
|
||||||
import { PRIVATE_COLUMN_KEY } from '../../../../constants';
|
import ChildTagsEditor from './child-tags';
|
||||||
import TagNameEditor from './tag-name';
|
import TagNameEditor from './tag-name';
|
||||||
import SubTagsEditor from './sub-tags';
|
import { PRIVATE_COLUMN_KEY } from '../../../../constants';
|
||||||
|
|
||||||
export const createColumnEditor = ({ column, otherProps }) => {
|
export const createColumnEditor = ({ column, otherProps }) => {
|
||||||
switch (column.key) {
|
switch (column.key) {
|
||||||
@@ -15,7 +15,7 @@ export const createColumnEditor = ({ column, otherProps }) => {
|
|||||||
}
|
}
|
||||||
case PRIVATE_COLUMN_KEY.SUB_LINKS: {
|
case PRIVATE_COLUMN_KEY.SUB_LINKS: {
|
||||||
const { addTagLinks, deleteTagLinks } = otherProps;
|
const { addTagLinks, deleteTagLinks } = otherProps;
|
||||||
return <SubTagsEditor addTagLinks={addTagLinks} deleteTagLinks={deleteTagLinks} />;
|
return <ChildTagsEditor addTagLinks={addTagLinks} deleteTagLinks={deleteTagLinks} />;
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
return null;
|
return null;
|
||||||
|
@@ -0,0 +1,18 @@
|
|||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { NumberFormatter } from '@seafile/sf-metadata-ui-component';
|
||||||
|
|
||||||
|
const ChildTagsFormatter = ({ record, column }) => {
|
||||||
|
|
||||||
|
const childTagsLinksCount = useMemo(() => {
|
||||||
|
const subTagLinks = record[column.key];
|
||||||
|
return Array.isArray(subTagLinks) ? subTagLinks.length : 0;
|
||||||
|
}, [record, column]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="sf-table-child-tags-formatter sf-table-cell-formatter sf-metadata-ui cell-formatter-container">
|
||||||
|
<NumberFormatter value={childTagsLinksCount} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChildTagsFormatter;
|
@@ -1,6 +1,6 @@
|
|||||||
import ParentTagsFormatter from './parent-tags';
|
import ParentTagsFormatter from './parent-tags';
|
||||||
import TagNameFormatter from './tag-name';
|
import TagNameFormatter from './tag-name';
|
||||||
import SubTagsFormatter from './sub-tags';
|
import ChildTagsFormatter from './child-tags';
|
||||||
import TagFilesFormatter from './tag-files';
|
import TagFilesFormatter from './tag-files';
|
||||||
import { PRIVATE_COLUMN_KEY } from '../../../../constants';
|
import { PRIVATE_COLUMN_KEY } from '../../../../constants';
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ export const createColumnFormatter = ({ column }) => {
|
|||||||
return <ParentTagsFormatter />;
|
return <ParentTagsFormatter />;
|
||||||
}
|
}
|
||||||
case PRIVATE_COLUMN_KEY.SUB_LINKS: {
|
case PRIVATE_COLUMN_KEY.SUB_LINKS: {
|
||||||
return <SubTagsFormatter />;
|
return <ChildTagsFormatter />;
|
||||||
}
|
}
|
||||||
case PRIVATE_COLUMN_KEY.TAG_FILE_LINKS: {
|
case PRIVATE_COLUMN_KEY.TAG_FILE_LINKS: {
|
||||||
return <TagFilesFormatter />;
|
return <TagFilesFormatter />;
|
||||||
|
@@ -1,18 +0,0 @@
|
|||||||
import React, { useMemo } from 'react';
|
|
||||||
import { NumberFormatter } from '@seafile/sf-metadata-ui-component';
|
|
||||||
|
|
||||||
const SubTagsFormatter = ({ record, column }) => {
|
|
||||||
|
|
||||||
const subTagLinksCount = useMemo(() => {
|
|
||||||
const subTagLinks = record[column.key];
|
|
||||||
return Array.isArray(subTagLinks) ? subTagLinks.length : 0;
|
|
||||||
}, [record, column]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="sf-table-sub-tags-formatter sf-table-cell-formatter sf-metadata-ui cell-formatter-container">
|
|
||||||
<NumberFormatter value={subTagLinksCount} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SubTagsFormatter;
|
|
@@ -2,7 +2,7 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sf-table-sub-tags-formatter,
|
.sf-table-child-tags-formatter,
|
||||||
.sf-table-tag-files-formatter {
|
.sf-table-tag-files-formatter {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import React, { useCallback, useMemo } from 'react';
|
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import SFTable from '../../../../components/sf-table';
|
import SFTable from '../../../../components/sf-table';
|
||||||
|
import EditTagDialog from '../../../components/dialog/edit-tag-dialog';
|
||||||
import CellOperationBtn from './cell-operation';
|
import CellOperationBtn from './cell-operation';
|
||||||
import { createTableColumns } from './columns-factory';
|
import { createTableColumns } from './columns-factory';
|
||||||
import { createContextMenuOptions } from './context-menu-options';
|
import { createContextMenuOptions } from './context-menu-options';
|
||||||
@@ -34,15 +35,33 @@ const TagsTable = ({
|
|||||||
setDisplayTag,
|
setDisplayTag,
|
||||||
loadMore,
|
loadMore,
|
||||||
}) => {
|
}) => {
|
||||||
const { tagsData, updateTag, deleteTags, addTagLinks, deleteTagLinks } = useTags();
|
const { tagsData, updateTag, deleteTags, addTagLinks, deleteTagLinks, addChildTag } = useTags();
|
||||||
|
|
||||||
const handleDeleteTags = useCallback((tagsIds) => {
|
const [isShowNewSubTagDialog, setIsShowNewSubTagDialog] = useState(false);
|
||||||
|
|
||||||
|
const parentTagIdRef = useRef(null);
|
||||||
|
|
||||||
|
const onDeleteTags = useCallback((tagsIds) => {
|
||||||
deleteTags(tagsIds);
|
deleteTags(tagsIds);
|
||||||
|
|
||||||
const eventBus = EventBus.getInstance();
|
const eventBus = EventBus.getInstance();
|
||||||
eventBus.dispatch(EVENT_BUS_TYPE.SELECT_NONE);
|
eventBus.dispatch(EVENT_BUS_TYPE.SELECT_NONE);
|
||||||
}, [deleteTags]);
|
}, [deleteTags]);
|
||||||
|
|
||||||
|
const onNewSubTag = useCallback((parentTagId) => {
|
||||||
|
parentTagIdRef.current = parentTagId;
|
||||||
|
setIsShowNewSubTagDialog(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const closeNewSubTagDialog = useCallback(() => {
|
||||||
|
parentTagIdRef.current = null;
|
||||||
|
setIsShowNewSubTagDialog(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handelAddChildTag = useCallback((tagData, callback) => {
|
||||||
|
addChildTag(tagData, parentTagIdRef.current, callback);
|
||||||
|
}, [addChildTag]);
|
||||||
|
|
||||||
const table = useMemo(() => {
|
const table = useMemo(() => {
|
||||||
if (!tagsData) {
|
if (!tagsData) {
|
||||||
return {
|
return {
|
||||||
@@ -128,9 +147,10 @@ const TagsTable = ({
|
|||||||
return createContextMenuOptions({
|
return createContextMenuOptions({
|
||||||
...tableProps,
|
...tableProps,
|
||||||
context,
|
context,
|
||||||
onDeleteTags: handleDeleteTags,
|
onDeleteTags,
|
||||||
|
onNewSubTag,
|
||||||
});
|
});
|
||||||
}, [context, handleDeleteTags]);
|
}, [context, onDeleteTags, onNewSubTag]);
|
||||||
|
|
||||||
const checkCanModifyTag = useCallback((tag) => {
|
const checkCanModifyTag = useCallback((tag) => {
|
||||||
return context.canModifyTag(tag);
|
return context.canModifyTag(tag);
|
||||||
@@ -175,6 +195,9 @@ const TagsTable = ({
|
|||||||
modifyColumnWidth={modifyColumnWidth}
|
modifyColumnWidth={modifyColumnWidth}
|
||||||
loadMore={loadMore}
|
loadMore={loadMore}
|
||||||
/>
|
/>
|
||||||
|
{isShowNewSubTagDialog && (
|
||||||
|
<EditTagDialog tags={table.rows} title={gettext('New child tag')} onToggle={closeNewSubTagDialog} onSubmit={handelAddChildTag} />
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
Reference in New Issue
Block a user