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

feat(tag): support add child tag (#7374)

This commit is contained in:
Jerry Ren
2025-01-17 10:10:45 +08:00
committed by GitHub
parent fdf720ffd5
commit 3250ca151c
28 changed files with 264 additions and 550 deletions

View File

@@ -2,7 +2,7 @@ export const TREE_NODE_KEY = {
ID: '_id',
KEY: 'node_key',
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';

View File

@@ -25,7 +25,7 @@ const Cell = React.memo(({
height,
showRecordAsTree,
nodeDepth,
hasSubNodes,
hasChildNodes,
isFoldedNode,
checkCanModifyRecord,
toggleExpandNode,
@@ -172,7 +172,7 @@ const Cell = React.memo(({
if (showRecordAsTree && isNameColumn) {
return (
<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 }}>
{columnFormatter}
</div>
@@ -180,7 +180,7 @@ const Cell = React.memo(({
);
}
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 (
<div key={`${record._id}-${column.key}`} {...containerProps}>
@@ -213,7 +213,7 @@ Cell.propTypes = {
bgColor: PropTypes.string,
showRecordAsTree: PropTypes.bool,
nodeDepth: PropTypes.number,
hasSubNodes: PropTypes.bool,
hasChildNodes: PropTypes.bool,
isFoldedNode: PropTypes.bool,
toggleExpandNode: PropTypes.func,
};

View File

@@ -38,7 +38,7 @@ class Record extends React.Component {
nextProps.showRecordAsTree !== this.props.showRecordAsTree ||
nextProps.nodeKey !== this.props.nodeKey ||
nextProps.nodeDepth !== this.props.nodeDepth ||
nextProps.hasSubNodes !== this.props.hasSubNodes ||
nextProps.hasChildNodes !== this.props.hasChildNodes ||
nextProps.treeNodeDisplayIndex !== this.props.treeNodeDisplayIndex ||
nextProps.isFoldedNode !== this.props.isFoldedNode
);
@@ -117,7 +117,7 @@ class Record extends React.Component {
bgColor={bgColor}
showRecordAsTree={this.props.showRecordAsTree}
nodeDepth={this.props.nodeDepth}
hasSubNodes={this.props.hasSubNodes}
hasChildNodes={this.props.hasChildNodes}
isFoldedNode={this.props.isFoldedNode}
toggleExpandNode={this.props.toggleExpandNode}
/>
@@ -184,7 +184,7 @@ class Record extends React.Component {
bgColor={bgColor}
showRecordAsTree={this.props.showRecordAsTree}
nodeDepth={this.props.nodeDepth}
hasSubNodes={this.props.hasSubNodes}
hasChildNodes={this.props.hasChildNodes}
isFoldedNode={this.props.isFoldedNode}
toggleExpandNode={this.props.toggleExpandNode}
/>
@@ -318,7 +318,7 @@ Record.propTypes = {
showRecordAsTree: PropTypes.bool,
nodeKey: PropTypes.string,
nodeDepth: PropTypes.number,
hasSubNodes: PropTypes.bool,
hasChildNodes: PropTypes.bool,
treeNodeDisplayIndex: PropTypes.number,
isFoldedNode: PropTypes.bool,
toggleExpandNode: PropTypes.func,

View File

@@ -6,7 +6,7 @@ import InteractionMasks from '../../masks/interaction-masks';
import Record from './record';
import EventBus from '../../../common/event-bus';
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 { getColumnScrollPosition, getColVisibleStartIdx, getColVisibleEndIdx } from '../../utils/records-body-utils';
import { checkEditableViaClickCell, checkIsColumnSupportDirectEdit, getColumnByIndex, getColumnIndexByKey } from '../../utils/column';
@@ -535,7 +535,8 @@ class TreeBody extends Component {
const rowHeight = this.getRowHeight();
const cellMetaData = this.getCellMetaData();
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 isSelected = TreeMetrics.checkIsTreeNodeSelected(node_key, treeMetrics);
const recordIndex = startRenderIndex + index;
@@ -568,7 +569,7 @@ class TreeBody extends Component {
searchResult={this.props.searchResult}
nodeKey={node_key}
nodeDepth={node_depth}
hasSubNodes={has_sub_nodes}
hasChildNodes={hasChildNodes}
isFoldedNode={isFoldedNode}
checkCanModifyRecord={this.props.checkCanModifyRecord}
checkCellValueChanged={this.props.checkCellValueChanged}

View File

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

View File

@@ -1,11 +1,15 @@
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 {
[TREE_NODE_KEY.ID]: nodeId,
[TREE_NODE_KEY.KEY]: nodeKey,
[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) => {
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);
};

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -124,6 +124,10 @@ export const TagsProvider = ({ repoID, currentPath, selectTagsView, children, ..
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 }) => {
storeRef.current.modifyTags(tagIds, idTagUpdates, idOriginalRowUpdates, idOldRowData, idOriginalOldRowData, { success_callback, fail_callback });
}, [storeRef]);
@@ -262,6 +266,7 @@ export const TagsProvider = ({ repoID, currentPath, selectTagsView, children, ..
updateCurrentDirent: params.updateCurrentDirent,
addTag,
addTags,
addChildTag,
modifyTags,
deleteTags,
duplicateTag,

View File

@@ -6,7 +6,10 @@ import { OPERATION_TYPE } from './operations';
import { buildTagsTree } from '../utils/tree';
import { getRecordIdFromRecord } from '../../metadata/utils/cell';
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 = {
// isUpdateSummaries: true,
@@ -27,7 +30,7 @@ class DataProcessor {
let updated_rows_tree = [...rows_tree];
tags.forEach((tag) => {
const tagId = getRecordIdFromRecord(tag);
const nodeKey = tagId;
const nodeKey = generateNodeKey('', tagId);
const node = createTreeNode(tagId, nodeKey, 0, false);
updated_rows_tree.push(node);
});
@@ -38,20 +41,23 @@ class DataProcessor {
if (!Array.isArray(deletedTagsIds) || deletedTagsIds.length === 0) return;
const { rows_tree } = table;
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) {
// need re-build tree if some parent nodes deleted
this.buildTagsTree(table.rows, table);
return;
}
// remove the nodes which has no sub nodes directly
// remove the nodes which has no child nodes directly
let updated_rows_tree = [];
rows_tree.forEach((node) => {
if (!idTagDeletedMap[node[TREE_NODE_KEY.ID]]) {
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;
}
@@ -198,6 +204,23 @@ class DataProcessor {
this.updateTagsTreeWithNewTags(tags, table);
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.DELETE_TAG_LINKS: {
this.buildTagsTree(table.rows, table);

View File

@@ -250,6 +250,19 @@ class Store {
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 }) {
const originalRows = getRowsByIds(this.data, row_ids);
let valid_row_ids = [];

View File

@@ -5,6 +5,7 @@ import { OPERATION_TYPE } from './constants';
import { PRIVATE_COLUMN_KEY } from '../../constants';
import { username } from '../../../utils/constants';
import { addRowLinks, removeRowLinks } from '../../utils/link';
import { getRecordIdFromRecord } from '../../../metadata/utils/cell';
dayjs.extend(utc);
@@ -30,6 +31,34 @@ export default function apply(data, operation) {
data.rows = updatedRows;
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_LOCAL_RECORDS: {
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;
}
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]);
data.rows[index] = updatedRow;
data.id_row_map[currentRowId] = updatedRow;
@@ -140,7 +169,7 @@ export default function apply(data, operation) {
const currentRowId = row._id;
let updatedRow = { ...row };
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);
data.rows[index] = updatedRow;
data.id_row_map[currentRowId] = updatedRow;
@@ -169,7 +198,7 @@ export default function apply(data, operation) {
data.id_row_map[currentRowId] = updatedRow;
}
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]);
data.rows[index] = updatedRow;
data.id_row_map[currentRowId] = updatedRow;
@@ -180,7 +209,7 @@ export default function apply(data, operation) {
const currentRowId = row._id;
let updatedRow = { ...row };
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);
data.rows[index] = updatedRow;
data.id_row_map[currentRowId] = updatedRow;

View File

@@ -1,5 +1,6 @@
export const OPERATION_TYPE = {
ADD_RECORDS: 'add_records',
ADD_CHILD_TAG: 'add_child_tag',
MODIFY_RECORDS: 'modify_records',
DELETE_RECORDS: 'delete_records',
RESTORE_RECORDS: 'restore_records',
@@ -14,6 +15,7 @@ export const OPERATION_TYPE = {
export const OPERATION_ATTRIBUTES = {
[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.DELETE_RECORDS]: ['repo_id', 'tag_ids', 'deleted_tags'],
[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
export const NEED_APPLY_AFTER_SERVER_OPERATION = [
OPERATION_TYPE.ADD_RECORDS,
OPERATION_TYPE.ADD_CHILD_TAG,
];
export const VIEW_OPERATION = [

View File

@@ -2,6 +2,7 @@ import { gettext } from '../../utils/constants';
import { OPERATION_TYPE } from './operations';
import { getColumnByKey } from '../../metadata/utils/column';
import ObjectUtils from '../../metadata/utils/object-utils';
import { PRIVATE_COLUMN_KEY } from '../constants';
const MAX_LOAD_RECORDS = 100;
@@ -26,6 +27,27 @@ class ServerOperator {
});
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: {
const { row_ids, id_row_updates } = operation;
const recordsData = row_ids.map(rowId => {

View File

@@ -28,15 +28,10 @@ export const getParentLinks = (tag) => {
return (tag && tag[PRIVATE_COLUMN_KEY.PARENT_LINKS]) || [];
};
export const getSubLinks = (tag) => {
export const getChildLinks = (tag) => {
return (tag && tag[PRIVATE_COLUMN_KEY.SUB_LINKS]) || [];
};
export const getSubTagsCount = (tag) => {
const subLinks = getSubLinks(tag);
return subLinks.length;
};
export const getTagFilesCount = (tag) => {
const links = tag ? tag[PRIVATE_COLUMN_KEY.TAG_FILE_LINKS] : [];
if (Array.isArray(links)) return links.length;

View File

@@ -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 { 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);
idNodeCreatedMap[nodeId] = true;
idNodeInCurrentTreeMap[nodeId] = true; // for preventing circular dependencies
const nodeKey = `${parentKey ? parentKey + '_' : ''}${nodeId}`; // the unique ID of each node
const subLinks = getSubLinks(row);
const nodeKey = generateNodeKey(parentKey, nodeId); // the unique ID of each node
const subLinks = getChildLinks(row);
const subRowsIds = subLinks.map((link) => link.row_id);
const subRows = getRowsByIds(table, subRowsIds);
const validSubRows = subRows.filter((row) => row && !idNodeInCurrentTreeMap[row._id]);
@@ -21,7 +21,7 @@ const setSubNodes = (row, parentDepth, parentKey, idNodeInCurrentTreeMap, idNode
if (validSubRows) {
const nextNodeDepth = parentDepth + 1;
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
* @returns {array} 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 parentLinks = getParentLinks(row);
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) => {
const nodeId = getRecordIdFromRecord(row);
if (!idNodeCreatedMap[nodeId]) {
setSubNodes(row, 0, '', {}, idNodeCreatedMap, tree, table);
setChildNodes(row, 0, '', {}, idNodeCreatedMap, tree, table);
}
});

View File

@@ -13,7 +13,7 @@ const KEY_COLUMN_ICON_NAME = {
const KEY_COLUMN_DISPLAY_NAME = {
[PRIVATE_COLUMN_KEY.TAG_NAME]: gettext('Tag'),
[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'),
};

View File

@@ -12,6 +12,7 @@ const OPERATION = {
SET_SUB_TAGS: 'set_sub_tags',
DELETE_TAG: 'delete_tag',
DELETE_TAGS: 'delete_tags',
NEW_SUB_TAG: 'new_sub_tag',
};
export const createContextMenuOptions = ({
@@ -28,8 +29,10 @@ export const createContextMenuOptions = ({
recordGetterByIndex,
recordGetterById,
onDeleteTags,
onNewSubTag,
}) => {
const canDeleteTag = context.checkCanDeleteTag();
const canAddTag = context.canAddTag();
const eventBus = EventBus.getInstance();
const onClickMenuItem = (event, option) => {
@@ -48,7 +51,10 @@ export const createContextMenuOptions = ({
onDeleteTags(option.tagsIds);
break;
}
case OPERATION.NEW_SUB_TAG: {
onNewSubTag(option.parentTagId);
break;
}
default: {
break;
}
@@ -118,7 +124,8 @@ export const createContextMenuOptions = ({
const tag = recordGetterByIndex({ isGroupView, groupRecordIndex, recordIndex });
const column = getColumnByIndex(idx, columns);
if (!tag || !tag._id || !column) return options;
if (checkIsNameColumn(column)) {
const isNameColumn = checkIsNameColumn(column);
if (isNameColumn) {
options.push({
label: gettext('Edit tag'),
value: OPERATION.EDIT_TAG,
@@ -127,7 +134,7 @@ export const createContextMenuOptions = ({
if (column.key === PRIVATE_COLUMN_KEY.SUB_LINKS) {
options.push({
label: gettext('Set sub tags'),
label: gettext('Set child 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;
};

View File

@@ -2,17 +2,17 @@ import React, { forwardRef, useCallback, useMemo } from 'react';
import TagsEditor from '../../../../../components/sf-table/editors/tags-editor';
import { useTags } from '../../../../hooks';
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 tag = useMemo(() => {
return getRowById(tagsData, editingRowId);
}, [tagsData, editingRowId]);
const subTags = useMemo(() => {
return getSubLinks(tag);
const childTags = useMemo(() => {
return getChildLinks(tag);
}, [tag]);
const selectTag = useCallback((tagId, recordId) => {
@@ -24,12 +24,12 @@ const SubTagsEditor = forwardRef(({ editingRowId, column, addTagLinks, deleteTag
}, [column, deleteTagLinks]);
return (
<div className="sf-metadata-tags-parent-links-editor">
<div className="sf-metadata-tags-child-links-editor">
<TagsEditor
{...editorProps}
record={tag}
column={column}
value={subTags}
value={childTags}
tagsTable={tagsData}
selectTag={selectTag}
deselectTag={deselectTag}
@@ -38,4 +38,4 @@ const SubTagsEditor = forwardRef(({ editingRowId, column, addTagLinks, deleteTag
);
});
export default SubTagsEditor;
export default ChildTagsEditor;

View File

@@ -1,7 +1,7 @@
import ParentTagsEditor from './parent-tags';
import { PRIVATE_COLUMN_KEY } from '../../../../constants';
import ChildTagsEditor from './child-tags';
import TagNameEditor from './tag-name';
import SubTagsEditor from './sub-tags';
import { PRIVATE_COLUMN_KEY } from '../../../../constants';
export const createColumnEditor = ({ column, otherProps }) => {
switch (column.key) {
@@ -15,7 +15,7 @@ export const createColumnEditor = ({ column, otherProps }) => {
}
case PRIVATE_COLUMN_KEY.SUB_LINKS: {
const { addTagLinks, deleteTagLinks } = otherProps;
return <SubTagsEditor addTagLinks={addTagLinks} deleteTagLinks={deleteTagLinks} />;
return <ChildTagsEditor addTagLinks={addTagLinks} deleteTagLinks={deleteTagLinks} />;
}
default: {
return null;

View File

@@ -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;

View File

@@ -1,6 +1,6 @@
import ParentTagsFormatter from './parent-tags';
import TagNameFormatter from './tag-name';
import SubTagsFormatter from './sub-tags';
import ChildTagsFormatter from './child-tags';
import TagFilesFormatter from './tag-files';
import { PRIVATE_COLUMN_KEY } from '../../../../constants';
@@ -13,7 +13,7 @@ export const createColumnFormatter = ({ column }) => {
return <ParentTagsFormatter />;
}
case PRIVATE_COLUMN_KEY.SUB_LINKS: {
return <SubTagsFormatter />;
return <ChildTagsFormatter />;
}
case PRIVATE_COLUMN_KEY.TAG_FILE_LINKS: {
return <TagFilesFormatter />;

View File

@@ -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;

View File

@@ -2,7 +2,7 @@
width: 100%;
}
.sf-table-sub-tags-formatter,
.sf-table-child-tags-formatter,
.sf-table-tag-files-formatter {
text-align: right;
}

View File

@@ -1,6 +1,7 @@
import React, { useCallback, useMemo } from 'react';
import React, { useCallback, useMemo, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import SFTable from '../../../../components/sf-table';
import EditTagDialog from '../../../components/dialog/edit-tag-dialog';
import CellOperationBtn from './cell-operation';
import { createTableColumns } from './columns-factory';
import { createContextMenuOptions } from './context-menu-options';
@@ -34,15 +35,33 @@ const TagsTable = ({
setDisplayTag,
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);
const eventBus = EventBus.getInstance();
eventBus.dispatch(EVENT_BUS_TYPE.SELECT_NONE);
}, [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(() => {
if (!tagsData) {
return {
@@ -128,9 +147,10 @@ const TagsTable = ({
return createContextMenuOptions({
...tableProps,
context,
onDeleteTags: handleDeleteTags,
onDeleteTags,
onNewSubTag,
});
}, [context, handleDeleteTags]);
}, [context, onDeleteTags, onNewSubTag]);
const checkCanModifyTag = useCallback((tag) => {
return context.canModifyTag(tag);
@@ -175,6 +195,9 @@ const TagsTable = ({
modifyColumnWidth={modifyColumnWidth}
loadMore={loadMore}
/>
{isShowNewSubTagDialog && (
<EditTagDialog tags={table.rows} title={gettext('New child tag')} onToggle={closeNewSubTagDialog} onSubmit={handelAddChildTag} />
)}
</>
);
};