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:
@@ -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';
|
||||
|
@@ -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,
|
||||
};
|
||||
|
@@ -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,
|
||||
|
@@ -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}
|
||||
|
@@ -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
|
||||
|
@@ -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);
|
||||
};
|
||||
|
@@ -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);
|
||||
}, []);
|
||||
|
||||
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,
|
||||
|
@@ -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);
|
||||
|
@@ -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 = [];
|
||||
|
@@ -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;
|
||||
|
@@ -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 = [
|
||||
|
@@ -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 => {
|
||||
|
@@ -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;
|
||||
|
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
@@ -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'),
|
||||
};
|
||||
|
||||
|
@@ -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;
|
||||
};
|
||||
|
||||
|
@@ -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;
|
@@ -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;
|
||||
|
@@ -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 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 />;
|
||||
|
@@ -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%;
|
||||
}
|
||||
|
||||
.sf-table-sub-tags-formatter,
|
||||
.sf-table-child-tags-formatter,
|
||||
.sf-table-tag-files-formatter {
|
||||
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 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} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
Reference in New Issue
Block a user