mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-01 15:09:14 +00:00
feat(tag): support self link (#7225)
This commit is contained in:
@@ -105,6 +105,24 @@ class TagsManagerAPI {
|
||||
return this.req.put(url, params);
|
||||
};
|
||||
|
||||
addTagLinks = (repoID, link_column_key, row_id_map) => {
|
||||
const url = this.server + '/api/v2.1/repos/' + repoID + '/metadata/tags-links/';
|
||||
const params = {
|
||||
link_column_key,
|
||||
row_id_map,
|
||||
};
|
||||
return this.req.post(url, params);
|
||||
};
|
||||
|
||||
deleteTagLinks = (repoID, link_column_key, row_id_map) => {
|
||||
const url = this.server + '/api/v2.1/repos/' + repoID + '/metadata/tags-links/';
|
||||
const params = {
|
||||
link_column_key,
|
||||
row_id_map,
|
||||
};
|
||||
return this.req.delete(url, { data: params });
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
const tagsAPI = new TagsManagerAPI();
|
||||
|
@@ -0,0 +1,113 @@
|
||||
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;
|
@@ -0,0 +1,130 @@
|
||||
.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-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;
|
||||
}
|
@@ -0,0 +1,135 @@
|
||||
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;
|
@@ -0,0 +1,37 @@
|
||||
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={gettext(isParentTags ? 'No parent tag' : 'No sub tag')} />
|
||||
)}
|
||||
{linkedTags.length > 0 && (
|
||||
<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;
|
@@ -0,0 +1,53 @@
|
||||
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;
|
@@ -9,6 +9,8 @@ export const PRIVATE_COLUMN_KEY = {
|
||||
TAG_NAME: '_tag_name',
|
||||
TAG_COLOR: '_tag_color',
|
||||
TAG_FILE_LINKS: '_tag_file_links',
|
||||
PARENT_LINKS: '_tag_parent_links',
|
||||
SUB_LINKS: '_tag_sub_links',
|
||||
};
|
||||
|
||||
export const PRIVATE_COLUMN_KEYS = [
|
||||
|
@@ -109,6 +109,14 @@ class Context {
|
||||
return this.api.deleteTags(this.repoId, tags);
|
||||
};
|
||||
|
||||
addTagLinks = (link_column_key, row_id_map) => {
|
||||
return this.api.addTagLinks(this.repoId, link_column_key, row_id_map);
|
||||
};
|
||||
|
||||
deleteTagLinks = (link_column_key, row_id_map) => {
|
||||
return this.api.deleteTagLinks(this.repoId, link_column_key, row_id_map);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
export default Context;
|
||||
|
@@ -186,6 +186,14 @@ export const TagsProvider = ({ repoID, currentPath, selectTagsView, children, ..
|
||||
modifyLocalTags(tagIds, idTagUpdates, { [tagId]: originalRowUpdates }, { [tagId]: oldRowData }, { [tagId]: originalOldRowData }, { success_callback, fail_callback });
|
||||
}, [tagsData, modifyLocalTags]);
|
||||
|
||||
const addTagLinks = useCallback((columnKey, tagId, otherTagsIds, { success_callback, fail_callback } = {}) => {
|
||||
storeRef.current.addTagLinks(columnKey, tagId, otherTagsIds, success_callback, fail_callback);
|
||||
}, []);
|
||||
|
||||
const deleteTagLinks = useCallback((columnKey, tagId, otherTagsIds, { success_callback, fail_callback } = {}) => {
|
||||
storeRef.current.deleteTagLinks(columnKey, tagId, otherTagsIds, success_callback, fail_callback);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!handelSelectTag) return;
|
||||
if (isLoading) return;
|
||||
@@ -247,6 +255,8 @@ export const TagsProvider = ({ repoID, currentPath, selectTagsView, children, ..
|
||||
deleteTags,
|
||||
duplicateTag,
|
||||
updateTag,
|
||||
addTagLinks,
|
||||
deleteTagLinks,
|
||||
updateLocalTag,
|
||||
selectTag: handelSelectTag,
|
||||
}}>
|
||||
|
@@ -339,6 +339,33 @@ class Store {
|
||||
this.applyOperation(operation);
|
||||
}
|
||||
|
||||
addTagLinks(column_key, row_id, other_rows_ids, success_callback, fail_callback) {
|
||||
const type = OPERATION_TYPE.ADD_TAG_LINKS;
|
||||
const operation = this.createOperation({
|
||||
type,
|
||||
repo_id: this.repoId,
|
||||
column_key,
|
||||
row_id,
|
||||
other_rows_ids,
|
||||
success_callback,
|
||||
fail_callback,
|
||||
});
|
||||
this.applyOperation(operation);
|
||||
}
|
||||
|
||||
deleteTagLinks(column_key, row_id, other_rows_ids, success_callback, fail_callback) {
|
||||
const type = OPERATION_TYPE.DELETE_TAG_LINKS;
|
||||
const operation = this.createOperation({
|
||||
type,
|
||||
repo_id: this.repoId,
|
||||
column_key,
|
||||
row_id,
|
||||
other_rows_ids,
|
||||
success_callback,
|
||||
fail_callback,
|
||||
});
|
||||
this.applyOperation(operation);
|
||||
}
|
||||
}
|
||||
|
||||
export default Store;
|
||||
|
@@ -4,6 +4,7 @@ import { UTC_FORMAT_DEFAULT } from '../../../metadata/constants';
|
||||
import { OPERATION_TYPE } from './constants';
|
||||
import { PRIVATE_COLUMN_KEY } from '../../constants';
|
||||
import { username } from '../../../utils/constants';
|
||||
import { addRowLinks, removeRowLinks } from '../../utils/link';
|
||||
|
||||
dayjs.extend(utc);
|
||||
|
||||
@@ -84,6 +85,68 @@ export default function apply(data, operation) {
|
||||
data.rows.push(insertRows);
|
||||
return data;
|
||||
}
|
||||
case OPERATION_TYPE.ADD_TAG_LINKS: {
|
||||
const { column_key, row_id, other_rows_ids } = operation;
|
||||
if (column_key === PRIVATE_COLUMN_KEY.PARENT_LINKS) {
|
||||
data.rows = data.rows.map((row) => {
|
||||
const currentRowId = row._id;
|
||||
if (currentRowId === row_id) {
|
||||
// add parent tags to current tag
|
||||
row = addRowLinks(row, PRIVATE_COLUMN_KEY.PARENT_LINKS, other_rows_ids);
|
||||
}
|
||||
if (other_rows_ids.includes(currentRowId)) {
|
||||
// add current tag as sub tag to related tags
|
||||
row = addRowLinks(row, PRIVATE_COLUMN_KEY.SUB_LINKS, [row_id]);
|
||||
}
|
||||
return row;
|
||||
});
|
||||
} else if (column_key === PRIVATE_COLUMN_KEY.SUB_LINKS) {
|
||||
data.rows = data.rows.map((row) => {
|
||||
const currentRowId = row._id;
|
||||
if (currentRowId === row_id) {
|
||||
// add sub tags to current tag
|
||||
row = addRowLinks(row, PRIVATE_COLUMN_KEY.SUB_LINKS, other_rows_ids);
|
||||
}
|
||||
if (other_rows_ids.includes(currentRowId)) {
|
||||
// add current tag as parent tag to related tags
|
||||
row = addRowLinks(row, PRIVATE_COLUMN_KEY.PARENT_LINKS, [row_id]);
|
||||
}
|
||||
return row;
|
||||
});
|
||||
}
|
||||
return data;
|
||||
}
|
||||
case OPERATION_TYPE.DELETE_TAG_LINKS: {
|
||||
const { column_key, row_id, other_rows_ids } = operation;
|
||||
if (column_key === PRIVATE_COLUMN_KEY.PARENT_LINKS) {
|
||||
data.rows = data.rows.map((row) => {
|
||||
const currentRowId = row._id;
|
||||
if (currentRowId === row_id) {
|
||||
// remove parent tags from current tag
|
||||
row = removeRowLinks(row, PRIVATE_COLUMN_KEY.PARENT_LINKS, other_rows_ids);
|
||||
}
|
||||
if (other_rows_ids.includes(currentRowId)) {
|
||||
// remove current tag as sub tag from related tags
|
||||
row = removeRowLinks(row, PRIVATE_COLUMN_KEY.SUB_LINKS, [row_id]);
|
||||
}
|
||||
return row;
|
||||
});
|
||||
} else if (column_key === PRIVATE_COLUMN_KEY.SUB_LINKS) {
|
||||
data.rows = data.rows.map((row) => {
|
||||
const currentRowId = row._id;
|
||||
if (currentRowId === row_id) {
|
||||
// remove sub tags from current tag
|
||||
row = removeRowLinks(row, PRIVATE_COLUMN_KEY.SUB_LINKS, other_rows_ids);
|
||||
}
|
||||
if (other_rows_ids.includes(currentRowId)) {
|
||||
// remove current tag as parent tag from related tags
|
||||
row = removeRowLinks(row, PRIVATE_COLUMN_KEY.PARENT_LINKS, [row_id]);
|
||||
}
|
||||
return row;
|
||||
});
|
||||
}
|
||||
return data;
|
||||
}
|
||||
default: {
|
||||
return data;
|
||||
}
|
||||
|
@@ -4,6 +4,8 @@ export const OPERATION_TYPE = {
|
||||
DELETE_RECORDS: 'delete_records',
|
||||
RESTORE_RECORDS: 'restore_records',
|
||||
RELOAD_RECORDS: 'reload_records',
|
||||
ADD_TAG_LINKS: 'add_tag_links',
|
||||
DELETE_TAG_LINKS: 'delete_tag_links',
|
||||
|
||||
MODIFY_LOCAL_RECORDS: 'modify_local_records',
|
||||
};
|
||||
@@ -14,6 +16,8 @@ export const OPERATION_ATTRIBUTES = {
|
||||
[OPERATION_TYPE.DELETE_RECORDS]: ['repo_id', 'tag_ids', 'deleted_tags'],
|
||||
[OPERATION_TYPE.RESTORE_RECORDS]: ['repo_id', 'rows_data', 'original_rows', 'link_infos', 'upper_row_ids'],
|
||||
[OPERATION_TYPE.RELOAD_RECORDS]: ['repo_id', 'row_ids'],
|
||||
[OPERATION_TYPE.ADD_TAG_LINKS]: ['repo_id', 'column_key', 'row_id', 'other_rows_ids'],
|
||||
[OPERATION_TYPE.DELETE_TAG_LINKS]: ['repo_id', 'column_key', 'row_id', 'other_rows_ids'],
|
||||
[OPERATION_TYPE.MODIFY_LOCAL_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'],
|
||||
};
|
||||
|
||||
|
@@ -51,6 +51,30 @@ class ServerOperator {
|
||||
});
|
||||
break;
|
||||
}
|
||||
case OPERATION_TYPE.ADD_TAG_LINKS: {
|
||||
const { column_key, row_id, other_rows_ids } = operation;
|
||||
const id_linked_rows_ids_map = {
|
||||
[row_id]: other_rows_ids,
|
||||
};
|
||||
this.context.addTagLinks(column_key, id_linked_rows_ids_map).then(res => {
|
||||
callback({ operation });
|
||||
}).catch(error => {
|
||||
callback({ error: gettext('Failed to add linked tags') });
|
||||
});
|
||||
break;
|
||||
}
|
||||
case OPERATION_TYPE.DELETE_TAG_LINKS: {
|
||||
const { column_key, row_id, other_rows_ids } = operation;
|
||||
const id_linked_rows_ids_map = {
|
||||
[row_id]: other_rows_ids,
|
||||
};
|
||||
this.context.deleteTagLinks(column_key, id_linked_rows_ids_map).then(res => {
|
||||
callback({ operation });
|
||||
}).catch(error => {
|
||||
callback({ error: gettext('Failed to delete linked tags') });
|
||||
});
|
||||
break;
|
||||
}
|
||||
case OPERATION_TYPE.RESTORE_RECORDS: {
|
||||
const { repo_id, rows_data } = operation;
|
||||
if (!Array.isArray(rows_data) || rows_data.length === 0) {
|
||||
|
@@ -24,6 +24,19 @@ export const getTagId = (tag) => {
|
||||
return tag ? tag[PRIVATE_COLUMN_KEY.ID] : '';
|
||||
};
|
||||
|
||||
export const getParentLinks = (tag) => {
|
||||
return (tag && tag[PRIVATE_COLUMN_KEY.PARENT_LINKS]) || [];
|
||||
};
|
||||
|
||||
export const getSubLinks = (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;
|
||||
|
27
frontend/src/tag/utils/link.js
Normal file
27
frontend/src/tag/utils/link.js
Normal file
@@ -0,0 +1,27 @@
|
||||
export const addRowLinks = (row, key, other_rows_ids) => {
|
||||
let updatedRow = row;
|
||||
let updatedLinks = Array.isArray(updatedRow[key]) ? [...updatedRow[key]] : [];
|
||||
other_rows_ids.forEach((otherRowId) => {
|
||||
if (updatedLinks.findIndex((linked) => linked.row_id === otherRowId) < 0) {
|
||||
updatedLinks.push({ row_id: otherRowId, display_value: otherRowId });
|
||||
}
|
||||
});
|
||||
updatedRow[key] = updatedLinks;
|
||||
return updatedRow;
|
||||
};
|
||||
|
||||
export const removeRowLinks = (row, key, other_rows_ids) => {
|
||||
if (!Array.isArray(row[key]) || row[key].length === 0) {
|
||||
return row;
|
||||
}
|
||||
let updatedRow = row;
|
||||
let updatedLinks = [...updatedRow[key]];
|
||||
other_rows_ids.forEach((otherRowId) => {
|
||||
const deleteIndex = updatedLinks.findIndex((linked) => linked.row_id === otherRowId);
|
||||
if (deleteIndex > -1) {
|
||||
updatedLinks.splice(deleteIndex, 1);
|
||||
}
|
||||
});
|
||||
updatedRow[key] = updatedLinks;
|
||||
return updatedRow;
|
||||
};
|
@@ -30,30 +30,36 @@
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
|
||||
.sf-metadata-tags-table .sf-metadata-tags-table-row .sf-metadata-tags-table-cell:first-child {
|
||||
width: calc((100% - 64px) * 0.7);
|
||||
height: 100%;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.sf-metadata-tags-table .sf-metadata-tags-table-row .sf-metadata-tags-table-cell:nth-child(2) {
|
||||
width: calc((100% - 64px) * 0.3);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.sf-metadata-tags-table .sf-metadata-tags-table-row .sf-metadata-tags-table-cell:last-child {
|
||||
width: 64px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.sf-metadata-tags-table .sf-metadata-tags-table-row .sf-metadata-tags-table-cell {
|
||||
font-size: 14px;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
padding: 0 8px;
|
||||
font-size: 14px;
|
||||
line-height: 40px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.sf-metadata-tags-table .sf-metadata-tags-table-row .sf-metadata-tags-table-cell-tag {
|
||||
width: calc((100% - 96px) * 0.3);
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.sf-metadata-tags-table .sf-metadata-tags-table-row .sf-metadata-tags-table-cell-parent-tags,
|
||||
.sf-metadata-tags-table .sf-metadata-tags-table-row .sf-metadata-tags-table-cell-sub-tags-count,
|
||||
.sf-metadata-tags-table .sf-metadata-tags-table-row .sf-metadata-tags-table-cell-tag-files-count {
|
||||
width: calc((100% - 96px) * 0.2);
|
||||
}
|
||||
|
||||
.sf-metadata-tags-table .sf-metadata-tags-table-row .sf-metadata-tags-table-cell-operations-wrapper {
|
||||
width: 96px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.sf-metadata-tags-table .sf-metadata-tags-table-row .sf-metadata-tags-table-cell-parent-tags {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.sf-metadata-tags-table .sf-metadata-tags-table-header .sf-metadata-tags-table-cell {
|
||||
@@ -62,3 +68,11 @@
|
||||
line-height: 16px;
|
||||
padding: 10px 8px;
|
||||
}
|
||||
|
||||
.sf-metadata-tags-table .sf-metadata-tags-table-cell .sf-metadata-tags-operation-pop-handler {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
@@ -45,9 +45,11 @@ const Main = React.memo(({ context, tags, onChangeDisplayTag, onLoadMore }) => {
|
||||
return (
|
||||
<div className="sf-metadata-tags-table" ref={tableRef} onScroll={handelScroll}>
|
||||
<div className="sf-metadata-tags-table-header sf-metadata-tags-table-row">
|
||||
<div className="sf-metadata-tags-table-cell">{gettext('Tag')}</div>
|
||||
<div className="sf-metadata-tags-table-cell">{gettext('File count')}</div>
|
||||
<div className="sf-metadata-tags-table-cell"></div>
|
||||
<div className="sf-metadata-tags-table-cell sf-metadata-tags-table-cell-tag">{gettext('Tag')}</div>
|
||||
<div className="sf-metadata-tags-table-cell sf-metadata-tags-table-cell-parent-tags">{gettext('Parent tags')}</div>
|
||||
<div className="sf-metadata-tags-table-cell sf-metadata-tags-table-cell-sub-tags-count">{gettext('Sub tags count')}</div>
|
||||
<div className="sf-metadata-tags-table-cell sf-metadata-tags-table-cell-tag-files-count">{gettext('File count')}</div>
|
||||
<div className="sf-metadata-tags-table-cell sf-metadata-tags-table-cell-operations-wrapper"></div>
|
||||
</div>
|
||||
{tags.map(tag => {
|
||||
const id = getTagId(tag);
|
||||
|
@@ -4,23 +4,24 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sf-metadata-tags-table-row.freezed .sf-metadata-tags-table-cell .sf-metadata-tags-table-cell-actions,
|
||||
.sf-metadata-tags-table-row:hover .sf-metadata-tags-table-cell .sf-metadata-tags-table-cell-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.sf-metadata-tags-table-cell-tag {
|
||||
.sf-metadata-tags-table-row .sf-metadata-tags-table-cell-tag {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sf-metadata-tags-table-cell-tag span:hover {
|
||||
.sf-metadata-tags-table-row .sf-metadata-tags-table-cell-tag span:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sf-metadata-tags-table-cell-tag .sf-metadata-tag-color {
|
||||
.sf-metadata-tags-table-row .sf-metadata-tags-table-cell-tag .sf-metadata-tag-color {
|
||||
display: inline-block;
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
@@ -28,7 +29,7 @@
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.sf-metadata-tags-table-row:hover .sf-metadata-tags-table-cell .sf-metadata-tags-table-cell-action {
|
||||
.sf-metadata-tags-table-row .sf-metadata-tags-table-cell .sf-metadata-tags-table-cell-action {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
display: flex;
|
||||
@@ -36,6 +37,11 @@
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.sf-metadata-tags-table-row:hover .sf-metadata-tags-table-cell .sf-metadata-tags-table-cell-action .op-icon {
|
||||
.sf-metadata-tags-table-row .sf-metadata-tags-table-cell .sf-metadata-tags-table-cell-action .sf-dropdown-toggle,
|
||||
.sf-metadata-tags-table-row .sf-metadata-tags-table-cell .sf-metadata-tags-table-cell-action .op-icon {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.sf-metadata-tags-table-cell-parent-tags .sf-metadata-ui.tags-formatter .sf-metadata-ui-tags-container .sf-metadata-ui-tag {
|
||||
cursor: default;
|
||||
}
|
||||
|
@@ -1,22 +1,33 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { getTagName, getTagColor, getTagFilesCount, getTagId } from '../../../../utils/cell/core';
|
||||
import { gettext } from '../../../../../utils/constants';
|
||||
import classnames from 'classnames';
|
||||
import { FileTagsFormatter } from '@seafile/sf-metadata-ui-component';
|
||||
import EditTagDialog from '../../../../components/dialog/edit-tag-dialog';
|
||||
import DeleteConfirmDialog from '../../../../../metadata/components/dialog/delete-confirm-dialog';
|
||||
import TagMoreOperation from './tag-more-operation';
|
||||
import SetLinkedTagsPopover from '../../../../components/popover/set-linked-tags-popover';
|
||||
import { getTagName, getTagColor, getTagFilesCount, getTagId, getParentLinks, getSubTagsCount, getSubLinks } from '../../../../utils/cell/core';
|
||||
import { gettext } from '../../../../../utils/constants';
|
||||
import { useTags } from '../../../../hooks';
|
||||
import { PRIVATE_COLUMN_KEY } from '../../../../constants';
|
||||
|
||||
import './index.css';
|
||||
|
||||
const Tag = ({ tags, tag, context, onChangeDisplayTag }) => {
|
||||
const { tagsData, updateTag, deleteTags, addTagLinks, deleteTagLinks } = useTags();
|
||||
const tagId = getTagId(tag);
|
||||
const tagName = getTagName(tag);
|
||||
const tagColor = getTagColor(tag);
|
||||
const parentLinks = getParentLinks(tag);
|
||||
const subLinks = getSubLinks(tag);
|
||||
const subTagsCount = getSubTagsCount(tag);
|
||||
const fileCount = getTagFilesCount(tag);
|
||||
const [isShowEditTagDialog, setShowEditTagDialog] = useState(false);
|
||||
const [isShowDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||
const [freeze, setFreeze] = useState(false);
|
||||
const [editingColumnKey, setEditingColumnKey] = useState(null);
|
||||
|
||||
const { updateTag, deleteTags } = useTags();
|
||||
const operationsPopHandlerRef = useRef(null);
|
||||
|
||||
const openEditTagDialog = useCallback(() => {
|
||||
setShowEditTagDialog(true);
|
||||
@@ -48,23 +59,69 @@ const Tag = ({ tags, tag, context, onChangeDisplayTag }) => {
|
||||
}
|
||||
}, [tagId, onChangeDisplayTag]);
|
||||
|
||||
const freezeItem = useCallback(() => {
|
||||
setFreeze(true);
|
||||
}, []);
|
||||
|
||||
const unfreezeItem = useCallback(() => {
|
||||
setFreeze(false);
|
||||
}, []);
|
||||
|
||||
const hideSetLinkedTagsPopover = useCallback(() => {
|
||||
setEditingColumnKey(null);
|
||||
}, []);
|
||||
|
||||
const showParentTagsSetter = useCallback(() => {
|
||||
setEditingColumnKey(PRIVATE_COLUMN_KEY.PARENT_LINKS);
|
||||
}, []);
|
||||
|
||||
const showSubTagsSetter = useCallback(() => {
|
||||
setEditingColumnKey(PRIVATE_COLUMN_KEY.SUB_LINKS);
|
||||
}, []);
|
||||
|
||||
const getEditingTagLinks = useCallback(() => {
|
||||
return editingColumnKey === PRIVATE_COLUMN_KEY.PARENT_LINKS ? parentLinks : subLinks;
|
||||
}, [editingColumnKey, parentLinks, subLinks]);
|
||||
|
||||
const handleAddTagLinks = useCallback((linkedTag) => {
|
||||
const { _id: otherTagId } = linkedTag;
|
||||
addTagLinks(editingColumnKey, tagId, [otherTagId]);
|
||||
}, [editingColumnKey, tagId, addTagLinks]);
|
||||
|
||||
const handleDeleteTagLinks = useCallback((otherTagId) => {
|
||||
deleteTagLinks(editingColumnKey, tagId, [otherTagId]);
|
||||
}, [editingColumnKey, tagId, deleteTagLinks]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="sf-metadata-tags-table-row">
|
||||
<div className={classnames('sf-metadata-tags-table-row', { 'freezed': freeze })}>
|
||||
<div className="sf-metadata-tags-table-cell sf-metadata-tags-table-cell-tag" onClick={handleDisplayTag}>
|
||||
<span className="sf-metadata-tag-color" style={{ backgroundColor: tagColor }}></span>
|
||||
<span className="sf-metadata-tag-name">{tagName}</span>
|
||||
<span className="sf-metadata-tag-name" title={tagName}>{tagName}</span>
|
||||
</div>
|
||||
<div className="sf-metadata-tags-table-cell">{fileCount}</div>
|
||||
<div className="sf-metadata-tags-table-cell">
|
||||
<div className="sf-metadata-tags-table-cell sf-metadata-tags-table-cell-parent-tags">
|
||||
<FileTagsFormatter tagsData={tagsData} value={parentLinks} />
|
||||
</div>
|
||||
<div className="sf-metadata-tags-table-cell sf-metadata-tags-table-cell-sub-tags-count" title={subTagsCount}>{subTagsCount}</div>
|
||||
<div className="sf-metadata-tags-table-cell sf-metadata-tags-table-cell-tag-files-count" title={fileCount}>{fileCount}</div>
|
||||
<div className="sf-metadata-tags-table-cell sf-metadata-tags-table-cell-operations-wrapper">
|
||||
<div className="sf-metadata-tags-operation-pop-handler" ref={operationsPopHandlerRef}></div>
|
||||
<div className="sf-metadata-tags-table-cell-actions">
|
||||
{context.canModifyTag() && (
|
||||
<div className="sf-metadata-tags-table-cell-action" title={gettext('Edit')} onClick={openEditTagDialog}>
|
||||
<i className="op-icon sf3-font-rename sf3-font"></i>
|
||||
</div>
|
||||
<>
|
||||
<TagMoreOperation
|
||||
freezeItem={freezeItem}
|
||||
unfreezeItem={unfreezeItem}
|
||||
showParentTagsSetter={showParentTagsSetter}
|
||||
showSubTagsSetter={showSubTagsSetter}
|
||||
/>
|
||||
<div className="sf-metadata-tags-table-cell-action mr-2" title={gettext('Edit')} onClick={openEditTagDialog}>
|
||||
<i className="op-icon sf3-font-rename sf3-font"></i>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{context.checkCanDeleteTag() && (
|
||||
<div className="sf-metadata-tags-table-cell-action ml-2" title={gettext('Delete')} onClick={openDeleteConfirmDialog}>
|
||||
<div className="sf-metadata-tags-table-cell-action" title={gettext('Delete')} onClick={openDeleteConfirmDialog}>
|
||||
<i className="op-icon sf3-font-delete1 sf3-font"></i>
|
||||
</div>
|
||||
)}
|
||||
@@ -77,6 +134,17 @@ const Tag = ({ tags, tag, context, onChangeDisplayTag }) => {
|
||||
{isShowDeleteDialog && (
|
||||
<DeleteConfirmDialog title={gettext('Delete tag')} content={tagName} onToggle={closeDeleteConfirmDialog} onSubmit={handelDelete} />
|
||||
)}
|
||||
{editingColumnKey && (
|
||||
<SetLinkedTagsPopover
|
||||
target={operationsPopHandlerRef.current}
|
||||
isParentTags={editingColumnKey === PRIVATE_COLUMN_KEY.PARENT_LINKS}
|
||||
tagLinks={getEditingTagLinks()}
|
||||
allTags={tags}
|
||||
hidePopover={hideSetLinkedTagsPopover}
|
||||
addTagLinks={handleAddTagLinks}
|
||||
deleteTagLinks={handleDeleteTagLinks}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@@ -0,0 +1,62 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ItemDropdownMenu from '../../../../../components/dropdown-menu/item-dropdown-menu';
|
||||
import { gettext } from '../../../../../utils/constants';
|
||||
import { isMobile } from '../../../../../utils/utils';
|
||||
|
||||
const KEY_MORE_OPERATION = {
|
||||
SET_PARENT_TAGS: 'set_parent_tags',
|
||||
SET_SUB_TAGS: 'set_sub_tags',
|
||||
};
|
||||
|
||||
const TagMoreOperation = ({ freezeItem, unfreezeItem, showParentTagsSetter, showSubTagsSetter }) => {
|
||||
|
||||
const operationMenus = useMemo(() => {
|
||||
let menus = [];
|
||||
menus.push(
|
||||
{ key: KEY_MORE_OPERATION.SET_PARENT_TAGS, value: gettext('Set parent tags') },
|
||||
{ key: KEY_MORE_OPERATION.SET_SUB_TAGS, value: gettext('Set sub tags') },
|
||||
);
|
||||
return menus;
|
||||
}, []);
|
||||
|
||||
const clickMenu = useCallback((key) => {
|
||||
switch (key) {
|
||||
case KEY_MORE_OPERATION.SET_PARENT_TAGS: {
|
||||
showParentTagsSetter();
|
||||
return;
|
||||
}
|
||||
case KEY_MORE_OPERATION.SET_SUB_TAGS: {
|
||||
showSubTagsSetter();
|
||||
return;
|
||||
}
|
||||
default: {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, [showParentTagsSetter, showSubTagsSetter]);
|
||||
|
||||
return (
|
||||
<div className="sf-metadata-tags-table-cell-action mr-2" title={gettext('More')}>
|
||||
<ItemDropdownMenu
|
||||
item={{ name: 'metadata-tag' }}
|
||||
menuClassname="metadata-tag-dropdown-menu"
|
||||
toggleClass="sf3-font sf3-font-more"
|
||||
freezeItem={freezeItem}
|
||||
unfreezeItem={unfreezeItem}
|
||||
getMenuList={() => operationMenus}
|
||||
onMenuItemClick={clickMenu}
|
||||
menuStyle={isMobile ? { zIndex: 1050 } : {}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
TagMoreOperation.propTypes = {
|
||||
freezeItem: PropTypes.func,
|
||||
unfreezeItem: PropTypes.func,
|
||||
showParentTagsSetter: PropTypes.func,
|
||||
showSubTagsSetter: PropTypes.func,
|
||||
};
|
||||
|
||||
export default TagMoreOperation;
|
Reference in New Issue
Block a user