diff --git a/frontend/src/components/sf-table/constants/tree.js b/frontend/src/components/sf-table/constants/tree.js index 118ae6a25f..d4747fde8c 100644 --- a/frontend/src/components/sf-table/constants/tree.js +++ b/frontend/src/components/sf-table/constants/tree.js @@ -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'; diff --git a/frontend/src/components/sf-table/table-main/records/record/cell/index.js b/frontend/src/components/sf-table/table-main/records/record/cell/index.js index ee4c879efb..7a39187b34 100644 --- a/frontend/src/components/sf-table/table-main/records/record/cell/index.js +++ b/frontend/src/components/sf-table/table-main/records/record/cell/index.js @@ -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 (
- {hasSubNodes && } + {hasChildNodes && }
{columnFormatter}
@@ -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 (
@@ -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, }; diff --git a/frontend/src/components/sf-table/table-main/records/record/index.js b/frontend/src/components/sf-table/table-main/records/record/index.js index 795ce6e975..dc85fa0ff2 100644 --- a/frontend/src/components/sf-table/table-main/records/record/index.js +++ b/frontend/src/components/sf-table/table-main/records/record/index.js @@ -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, diff --git a/frontend/src/components/sf-table/table-main/records/tree-body.js b/frontend/src/components/sf-table/table-main/records/tree-body.js index 95d39a4dc2..1bc1927ba8 100644 --- a/frontend/src/components/sf-table/table-main/records/tree-body.js +++ b/frontend/src/components/sf-table/table-main/records/tree-body.js @@ -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} diff --git a/frontend/src/components/sf-table/utils/cell-comparer.js b/frontend/src/components/sf-table/utils/cell-comparer.js index 64dcaa37a6..5c9d08b123 100644 --- a/frontend/src/components/sf-table/utils/cell-comparer.js +++ b/frontend/src/components/sf-table/utils/cell-comparer.js @@ -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 diff --git a/frontend/src/components/sf-table/utils/tree.js b/frontend/src/components/sf-table/utils/tree.js index 3b3ff4f802..0cdc1c8b14 100644 --- a/frontend/src/components/sf-table/utils/tree.js +++ b/frontend/src/components/sf-table/utils/tree.js @@ -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); +}; diff --git a/frontend/src/tag/components/popover/set-linked-tags-popover/add-linked-tags.js b/frontend/src/tag/components/popover/set-linked-tags-popover/add-linked-tags.js deleted file mode 100644 index 1e5ceb2045..0000000000 --- a/frontend/src/tag/components/popover/set-linked-tags-popover/add-linked-tags.js +++ /dev/null @@ -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 ( -
-
-
- - {gettext('Link existing tags')} -
-
-
-
- -
- {selectableTags.length === 0 && ( - - )} - {selectableTags.length > 0 && ( -
- -
- )} -
-
- ); -}; - -AddLinkedTags.propTypes = { - isParentTags: PropTypes.bool, - allTags: PropTypes.array, - linkedTags: PropTypes.array, - switchToLinkedTagsPage: PropTypes.func, - addLinkedTag: PropTypes.func, - deleteLinedTag: PropTypes.func, -}; - -export default AddLinkedTags; diff --git a/frontend/src/tag/components/popover/set-linked-tags-popover/index.css b/frontend/src/tag/components/popover/set-linked-tags-popover/index.css deleted file mode 100644 index 80198fec0c..0000000000 --- a/frontend/src/tag/components/popover/set-linked-tags-popover/index.css +++ /dev/null @@ -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; -} diff --git a/frontend/src/tag/components/popover/set-linked-tags-popover/index.js b/frontend/src/tag/components/popover/set-linked-tags-popover/index.js deleted file mode 100644 index f2f41a1c91..0000000000 --- a/frontend/src/tag/components/popover/set-linked-tags-popover/index.js +++ /dev/null @@ -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 ( - -
- {mode === KEY_MODE_TYPE.LINKED_TAGS ? - setMode(KEY_MODE_TYPE.ADD_LINKED_TAGS)} - deleteLinedTag={deleteLinedTag} - /> : - setMode(KEY_MODE_TYPE.LINKED_TAGS)} - addLinkedTag={addLinkedTag} - deleteLinedTag={deleteLinedTag} - /> - } -
-
- ); -}; - -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; diff --git a/frontend/src/tag/components/popover/set-linked-tags-popover/linked-tags.js b/frontend/src/tag/components/popover/set-linked-tags-popover/linked-tags.js deleted file mode 100644 index e1aedcf889..0000000000 --- a/frontend/src/tag/components/popover/set-linked-tags-popover/linked-tags.js +++ /dev/null @@ -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 ( -
-
-
- {isParentTags ? gettext('Parent tags') : gettext('Sub tags')} -
- -
-
- {linkedTags.length === 0 ? - - : - - } -
-
- ); -}; - -LinkedTags.propTypes = { - isParentTags: PropTypes.bool, - linkedTags: PropTypes.array, - switchToAddTagsPage: PropTypes.func, - deleteTag: PropTypes.func, -}; - -export default LinkedTags; diff --git a/frontend/src/tag/components/popover/set-linked-tags-popover/tags.js b/frontend/src/tag/components/popover/set-linked-tags-popover/tags.js deleted file mode 100644 index a3314406c3..0000000000 --- a/frontend/src/tag/components/popover/set-linked-tags-popover/tags.js +++ /dev/null @@ -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 ( -
- {tags.map((tag) => { - const tagId = getTagId(tag); - const tagName = getTagName(tag); - const tagColor = getTagColor(tag); - return ( -
clickTag(tag)}> -
-
-
-
{tagName}
-
-
- {deletable &&
remove(tagId)}>
} - {(selectable && idTagSelectedMap && idTagSelectedMap[tagId]) &&
} -
-
-
- ); - })} -
- ); -}; - -Tags.propTypes = { - tags: PropTypes.array, - deletable: PropTypes.bool, - selectable: PropTypes.bool, - selectTag: PropTypes.func, - deleteTag: PropTypes.func, -}; - -export default Tags; diff --git a/frontend/src/tag/hooks/tags.js b/frontend/src/tag/hooks/tags.js index f7c9bc0224..5509287585 100644 --- a/frontend/src/tag/hooks/tags.js +++ b/frontend/src/tag/hooks/tags.js @@ -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, diff --git a/frontend/src/tag/store/data-processor.js b/frontend/src/tag/store/data-processor.js index 9cdfbfbe50..e8d187fd07 100644 --- a/frontend/src/tag/store/data-processor.js +++ b/frontend/src/tag/store/data-processor.js @@ -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); diff --git a/frontend/src/tag/store/index.js b/frontend/src/tag/store/index.js index 2bb00c29c3..0cf703fcbe 100644 --- a/frontend/src/tag/store/index.js +++ b/frontend/src/tag/store/index.js @@ -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 = []; diff --git a/frontend/src/tag/store/operations/apply.js b/frontend/src/tag/store/operations/apply.js index 0e58b443b2..4da72ce5fd 100644 --- a/frontend/src/tag/store/operations/apply.js +++ b/frontend/src/tag/store/operations/apply.js @@ -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; diff --git a/frontend/src/tag/store/operations/constants.js b/frontend/src/tag/store/operations/constants.js index 26ef311a96..672afd9e78 100644 --- a/frontend/src/tag/store/operations/constants.js +++ b/frontend/src/tag/store/operations/constants.js @@ -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 = [ diff --git a/frontend/src/tag/store/server-operator.js b/frontend/src/tag/store/server-operator.js index 9559cee74a..879a013eb0 100644 --- a/frontend/src/tag/store/server-operator.js +++ b/frontend/src/tag/store/server-operator.js @@ -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 => { diff --git a/frontend/src/tag/utils/cell.js b/frontend/src/tag/utils/cell.js index de1d604eb4..a6232767ad 100644 --- a/frontend/src/tag/utils/cell.js +++ b/frontend/src/tag/utils/cell.js @@ -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; diff --git a/frontend/src/tag/utils/tree.js b/frontend/src/tag/utils/tree.js index db8ff042f5..07406289ac 100644 --- a/frontend/src/tag/utils/tree.js +++ b/frontend/src/tag/utils/tree.js @@ -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); } }); diff --git a/frontend/src/tag/views/all-tags/tags-table/columns-factory.js b/frontend/src/tag/views/all-tags/tags-table/columns-factory.js index f7727a6d63..cf77cd6d7a 100644 --- a/frontend/src/tag/views/all-tags/tags-table/columns-factory.js +++ b/frontend/src/tag/views/all-tags/tags-table/columns-factory.js @@ -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'), }; diff --git a/frontend/src/tag/views/all-tags/tags-table/context-menu-options.js b/frontend/src/tag/views/all-tags/tags-table/context-menu-options.js index 32e71828a8..981058d735 100644 --- a/frontend/src/tag/views/all-tags/tags-table/context-menu-options.js +++ b/frontend/src/tag/views/all-tags/tags-table/context-menu-options.js @@ -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; }; diff --git a/frontend/src/tag/views/all-tags/tags-table/editors/sub-tags.js b/frontend/src/tag/views/all-tags/tags-table/editors/child-tags.js similarity index 72% rename from frontend/src/tag/views/all-tags/tags-table/editors/sub-tags.js rename to frontend/src/tag/views/all-tags/tags-table/editors/child-tags.js index 9ec6648d48..ebe051a654 100644 --- a/frontend/src/tag/views/all-tags/tags-table/editors/sub-tags.js +++ b/frontend/src/tag/views/all-tags/tags-table/editors/child-tags.js @@ -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 ( -
+
{ switch (column.key) { @@ -15,7 +15,7 @@ export const createColumnEditor = ({ column, otherProps }) => { } case PRIVATE_COLUMN_KEY.SUB_LINKS: { const { addTagLinks, deleteTagLinks } = otherProps; - return ; + return ; } default: { return null; diff --git a/frontend/src/tag/views/all-tags/tags-table/formatter/child-tags.js b/frontend/src/tag/views/all-tags/tags-table/formatter/child-tags.js new file mode 100644 index 0000000000..d27cbd527c --- /dev/null +++ b/frontend/src/tag/views/all-tags/tags-table/formatter/child-tags.js @@ -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 ( +
+ +
+ ); +}; + +export default ChildTagsFormatter; diff --git a/frontend/src/tag/views/all-tags/tags-table/formatter/formatter-factory.js b/frontend/src/tag/views/all-tags/tags-table/formatter/formatter-factory.js index f16fde96d2..8a58374d96 100644 --- a/frontend/src/tag/views/all-tags/tags-table/formatter/formatter-factory.js +++ b/frontend/src/tag/views/all-tags/tags-table/formatter/formatter-factory.js @@ -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 ; } case PRIVATE_COLUMN_KEY.SUB_LINKS: { - return ; + return ; } case PRIVATE_COLUMN_KEY.TAG_FILE_LINKS: { return ; diff --git a/frontend/src/tag/views/all-tags/tags-table/formatter/sub-tags.js b/frontend/src/tag/views/all-tags/tags-table/formatter/sub-tags.js deleted file mode 100644 index 9fe8bac52f..0000000000 --- a/frontend/src/tag/views/all-tags/tags-table/formatter/sub-tags.js +++ /dev/null @@ -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 ( -
- -
- ); -}; - -export default SubTagsFormatter; diff --git a/frontend/src/tag/views/all-tags/tags-table/index.css b/frontend/src/tag/views/all-tags/tags-table/index.css index ed0e86cfb0..6db83dfeba 100644 --- a/frontend/src/tag/views/all-tags/tags-table/index.css +++ b/frontend/src/tag/views/all-tags/tags-table/index.css @@ -2,7 +2,7 @@ width: 100%; } -.sf-table-sub-tags-formatter, +.sf-table-child-tags-formatter, .sf-table-tag-files-formatter { text-align: right; } diff --git a/frontend/src/tag/views/all-tags/tags-table/index.js b/frontend/src/tag/views/all-tags/tags-table/index.js index 1890668175..ad4f8576c0 100644 --- a/frontend/src/tag/views/all-tags/tags-table/index.js +++ b/frontend/src/tag/views/all-tags/tags-table/index.js @@ -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 && ( + + )} ); };