diff --git a/frontend/src/components/sf-table/context-menu/index.js b/frontend/src/components/sf-table/context-menu/index.js
index 9c375b15797..b5b0205c3c5 100644
--- a/frontend/src/components/sf-table/context-menu/index.js
+++ b/frontend/src/components/sf-table/context-menu/index.js
@@ -83,8 +83,8 @@ const ContextMenu = ({
const options = useMemo(() => {
if (!visible || !createContextMenuOptions) return [];
- return createContextMenuOptions({ ...customProps, hideMenu: setVisible });
- }, [customProps, visible, createContextMenuOptions]);
+ return createContextMenuOptions({ ...customProps, hideMenu: setVisible, menuPosition: position });
+ }, [customProps, visible, createContextMenuOptions, position]);
if (!Array.isArray(options) || options.length === 0) return null;
diff --git a/frontend/src/tag/api.js b/frontend/src/tag/api.js
index 4d5897016e5..f8f57b523c3 100644
--- a/frontend/src/tag/api.js
+++ b/frontend/src/tag/api.js
@@ -123,6 +123,15 @@ class TagsManagerAPI {
return this.req.delete(url, { data: params });
};
+ mergeTags = (repoID, target_tag_id, merged_tags_ids) => {
+ const url = this.server + '/api/v2.1/repos/' + repoID + '/metadata/merge-tags/';
+ const params = {
+ target_tag_id,
+ merged_tags_ids,
+ };
+ return this.req.post(url, params);
+ };
+
}
const tagsAPI = new TagsManagerAPI();
diff --git a/frontend/src/tag/components/merge-tags-selector/index.css b/frontend/src/tag/components/merge-tags-selector/index.css
new file mode 100644
index 00000000000..2a2da506878
--- /dev/null
+++ b/frontend/src/tag/components/merge-tags-selector/index.css
@@ -0,0 +1,71 @@
+.sf-metadata-merge-tags-selector {
+ left: 0;
+ min-height: 160px;
+ width: 300px;
+ padding: 0;
+ opacity: 1;
+ overflow: hidden;
+ position: fixed;
+ background-color: #fff;
+ border: 1px solid #dedede;
+ border-radius: 4px;
+ box-shadow: 0 2px 10px 0 #dedede;
+}
+
+.sf-metadata-merge-tags-selector .sf-metadata-search-tags-container {
+ padding: 10px 10px 0;
+}
+
+.sf-metadata-merge-tags-selector .sf-metadata-search-tags-container .sf-metadata-search-tags {
+ font-size: 14px;
+ max-height: 30px;
+}
+
+.sf-metadata-merge-tags-selector .sf-metadata-merge-tags-selector-container {
+ max-height: 200px;
+ min-height: 100px;
+ overflow: auto;
+ padding: 10px;
+}
+
+.sf-metadata-merge-tags-selector .sf-metadata-merge-tags-selector-container .none-search-result {
+ font-size: 14px;
+ opacity: 0.5;
+ display: inline-block;
+}
+
+.sf-metadata-merge-tags-selector .sf-metadata-tags-editor-tag-container {
+ align-items: center;
+ border-radius: 2px;
+ color: #212529;
+ display: flex;
+ font-size: 13px;
+ height: 30px;
+ width: 100%;
+}
+
+.sf-metadata-merge-tags-selector .sf-metadata-tags-editor-tag-container-highlight {
+ background: #f5f5f5;
+ cursor: pointer;
+}
+
+.sf-metadata-tag-color-and-name {
+ display: flex;
+ align-items: center;
+ flex: 1;
+}
+
+.sf-metadata-tag-color-and-name .sf-metadata-tag-color {
+ height: 12px;
+ width: 12px;
+ border-radius: 50%;
+ flex-shrink: 0;
+}
+
+.sf-metadata-tag-color-and-name .sf-metadata-tag-name {
+ flex: 1;
+ margin-left: 8px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
diff --git a/frontend/src/tag/components/merge-tags-selector/index.js b/frontend/src/tag/components/merge-tags-selector/index.js
new file mode 100644
index 00000000000..1f7e134323b
--- /dev/null
+++ b/frontend/src/tag/components/merge-tags-selector/index.js
@@ -0,0 +1,202 @@
+import React, { useMemo, useCallback, useState, useRef, useEffect } from 'react';
+import PropTypes from 'prop-types';
+import classnames from 'classnames';
+import { SearchInput, ClickOutside, ModalPortal } from '@seafile/sf-metadata-ui-component';
+import { KeyCodes } from '../../../constants';
+import { gettext } from '../../../utils/constants';
+import { getRowsByIds } from '../../../components/sf-table/utils/table';
+import { getTagColor, getTagId, getTagName, getTagsByNameOrColor } from '../../utils/cell';
+import { EDITOR_CONTAINER as Z_INDEX_EDITOR_CONTAINER } from '../../../components/sf-table/constants/z-index';
+import { useTags } from '../../hooks';
+
+import './index.css';
+
+const getInitTags = (mergeTagsIds, tagsData) => {
+ if (!Array.isArray(mergeTagsIds) || mergeTagsIds.length === 0 || !tagsData || !Array.isArray(tagsData.row_ids)) return [];
+ const sortedTagsIds = tagsData.row_ids.filter((tagId) => mergeTagsIds.includes(tagId));
+ if (sortedTagsIds.length === 0) return [];
+ return getRowsByIds(tagsData, sortedTagsIds);
+};
+
+const MergeTagsSelector = ({
+ mergeTagsIds,
+ position = { left: 0, top: 0 },
+ closeSelector,
+ mergeTags,
+}) => {
+ const { tagsData } = useTags();
+ const [searchValue, setSearchValue] = useState('');
+ const [highlightIndex, setHighlightIndex] = useState(-1);
+ const [maxItemNum, setMaxItemNum] = useState(0);
+ const itemHeight = 30;
+ const allTagsRef = useRef(getInitTags(mergeTagsIds, tagsData));
+ const editorContainerRef = useRef(null);
+ const editorRef = useRef(null);
+ const selectItemRef = useRef(null);
+
+ const displayTags = useMemo(() => getTagsByNameOrColor(allTagsRef.current, searchValue), [searchValue, allTagsRef]);
+
+ const onChangeSearch = useCallback((newSearchValue) => {
+ if (searchValue === newSearchValue) return;
+ setSearchValue(newSearchValue);
+ }, [searchValue]);
+
+ const onSelectTag = useCallback((targetTagId) => {
+ const mergedTagsIds = mergeTagsIds.filter((tagId) => tagId !== targetTagId);
+ mergeTags(targetTagId, mergedTagsIds);
+ closeSelector();
+ }, [mergeTagsIds, closeSelector, mergeTags]);
+
+ const onMenuMouseEnter = useCallback((highlightIndex) => {
+ setHighlightIndex(highlightIndex);
+ }, []);
+
+ const onMenuMouseLeave = useCallback((index) => {
+ setHighlightIndex(-1);
+ }, []);
+
+ const getMaxItemNum = useCallback(() => {
+ let selectContainerStyle = getComputedStyle(editorContainerRef.current, null);
+ let selectItemStyle = getComputedStyle(selectItemRef.current, null);
+ let maxSelectItemNum = Math.floor(parseInt(selectContainerStyle.maxHeight) / parseInt(selectItemStyle.height));
+ return maxSelectItemNum - 1;
+ }, [editorContainerRef, selectItemRef]);
+
+ const onEnter = useCallback((event) => {
+ event.preventDefault();
+ let tag;
+ if (displayTags.length === 1) {
+ tag = displayTags[0];
+ } else if (highlightIndex > -1) {
+ tag = displayTags[highlightIndex];
+ }
+ if (tag) {
+ const newTagId = getTagId(tag);
+ onSelectTag(newTagId);
+ return;
+ }
+ }, [displayTags, highlightIndex, onSelectTag]);
+
+ const onUpArrow = useCallback((event) => {
+ event.preventDefault();
+ event.stopPropagation();
+ if (highlightIndex === 0) return;
+ setHighlightIndex(highlightIndex - 1);
+ if (highlightIndex > displayTags.length - maxItemNum) {
+ editorContainerRef.current.scrollTop -= itemHeight;
+ }
+ }, [editorContainerRef, highlightIndex, maxItemNum, displayTags, itemHeight]);
+
+ const onDownArrow = useCallback((event) => {
+ event.preventDefault();
+ event.stopPropagation();
+ if (highlightIndex === displayTags.length - 1) return;
+ setHighlightIndex(highlightIndex + 1);
+ if (highlightIndex >= maxItemNum) {
+ editorContainerRef.current.scrollTop += itemHeight;
+ }
+ }, [editorContainerRef, highlightIndex, maxItemNum, displayTags, itemHeight]);
+
+ const onHotKey = useCallback((event) => {
+ if (event.keyCode === KeyCodes.Enter) {
+ onEnter(event);
+ } else if (event.keyCode === KeyCodes.UpArrow) {
+ onUpArrow(event);
+ } else if (event.keyCode === KeyCodes.DownArrow) {
+ onDownArrow(event);
+ }
+ }, [onEnter, onUpArrow, onDownArrow]);
+
+ const onKeyDown = useCallback((event) => {
+ if (
+ event.keyCode === KeyCodes.ChineseInputMethod ||
+ event.keyCode === KeyCodes.Enter ||
+ event.keyCode === KeyCodes.LeftArrow ||
+ event.keyCode === KeyCodes.RightArrow
+ ) {
+ event.stopPropagation();
+ }
+ }, []);
+
+ useEffect(() => {
+ if (editorRef.current) {
+ const { bottom } = editorRef.current.getBoundingClientRect();
+ if (bottom > window.innerHeight) {
+ editorRef.current.style.top = 'unset';
+ editorRef.current.style.bottom = '10px';
+ }
+ }
+ if (editorContainerRef.current && selectItemRef.current) {
+ setMaxItemNum(getMaxItemNum());
+ }
+ document.addEventListener('keydown', onHotKey, true);
+ return () => {
+ document.removeEventListener('keydown', onHotKey, true);
+ };
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [onHotKey]);
+
+ useEffect(() => {
+ const highlightIndex = displayTags.length === 0 ? -1 : 0;
+ setHighlightIndex(highlightIndex);
+ }, [displayTags]);
+
+ const renderOptions = useCallback(() => {
+ if (displayTags.length === 0) {
+ const noOptionsTip = searchValue ? gettext('No tags available') : gettext('No tag');
+ return ({noOptionsTip});
+ }
+
+ return displayTags.map((tag, i) => {
+ const tagId = getTagId(tag);
+ const tagName = getTagName(tag);
+ const tagColor = getTagColor(tag);
+ return (
+
+
{
setDisplayTag={onChangeDisplayTag}
isLoadingMoreRecords={isLoadingMore}
loadMore={loadMore}
+ getTagsTableWrapperOffsets={getTagsTableWrapperOffsets}
/>
);
diff --git a/frontend/src/tag/views/all-tags/tags-table/context-menu-options.js b/frontend/src/tag/views/all-tags/tags-table/context-menu-options.js
index 981058d735a..57593063bc4 100644
--- a/frontend/src/tag/views/all-tags/tags-table/context-menu-options.js
+++ b/frontend/src/tag/views/all-tags/tags-table/context-menu-options.js
@@ -13,6 +13,7 @@ const OPERATION = {
DELETE_TAG: 'delete_tag',
DELETE_TAGS: 'delete_tags',
NEW_SUB_TAG: 'new_sub_tag',
+ MERGE_TAGS: 'merge_tags',
};
export const createContextMenuOptions = ({
@@ -25,11 +26,13 @@ export const createContextMenuOptions = ({
showRecordAsTree,
treeMetrics,
treeNodeKeyRecordIdMap,
+ menuPosition,
hideMenu,
recordGetterByIndex,
recordGetterById,
onDeleteTags,
onNewSubTag,
+ onMergeTags,
}) => {
const canDeleteTag = context.checkCanDeleteTag();
const canAddTag = context.canAddTag();
@@ -55,6 +58,10 @@ export const createContextMenuOptions = ({
onNewSubTag(option.parentTagId);
break;
}
+ case OPERATION.MERGE_TAGS: {
+ onMergeTags(option.tagsIds, menuPosition);
+ break;
+ }
default: {
break;
}
@@ -115,6 +122,13 @@ export const createContextMenuOptions = ({
tagsIds,
});
}
+ if (tagsIds.length > 1) {
+ options.push({
+ label: gettext('Merge tags'),
+ value: OPERATION.MERGE_TAGS,
+ tagsIds,
+ });
+ }
return options;
}
diff --git a/frontend/src/tag/views/all-tags/tags-table/formatter/child-tags.js b/frontend/src/tag/views/all-tags/tags-table/formatter/child-tags.js
index d27cbd527ca..7b2ec960e2b 100644
--- a/frontend/src/tag/views/all-tags/tags-table/formatter/child-tags.js
+++ b/frontend/src/tag/views/all-tags/tags-table/formatter/child-tags.js
@@ -1,12 +1,20 @@
import React, { useMemo } from 'react';
import { NumberFormatter } from '@seafile/sf-metadata-ui-component';
+import { useTags } from '../../../../hooks';
+import { getRowsByIds } from '../../../../../components/sf-table/utils/table';
const ChildTagsFormatter = ({ record, column }) => {
+ const { tagsData } = useTags();
const childTagsLinksCount = useMemo(() => {
- const subTagLinks = record[column.key];
- return Array.isArray(subTagLinks) ? subTagLinks.length : 0;
- }, [record, column]);
+ const childTagLinks = record[column.key];
+ if (!Array.isArray(childTagLinks) || childTagLinks.length === 0) {
+ return 0;
+ }
+ const childTagsIds = childTagLinks.map((link) => link.row_id);
+ const subTags = getRowsByIds(tagsData, childTagsIds);
+ return subTags.length;
+ }, [record, column, tagsData]);
return (
diff --git a/frontend/src/tag/views/all-tags/tags-table/index.js b/frontend/src/tag/views/all-tags/tags-table/index.js
index f022de20ab9..0ff5116a0c6 100644
--- a/frontend/src/tag/views/all-tags/tags-table/index.js
+++ b/frontend/src/tag/views/all-tags/tags-table/index.js
@@ -2,6 +2,7 @@ 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 MergeTagsSelector from '../../../components/merge-tags-selector';
import { createTableColumns } from './columns-factory';
import { createContextMenuOptions } from './context-menu-options';
import { gettext } from '../../../../utils/constants';
@@ -33,33 +34,15 @@ const TagsTable = ({
modifyColumnWidth: modifyColumnWidthAPI,
setDisplayTag,
loadMore,
+ getTagsTableWrapperOffsets,
}) => {
- const { tagsData, updateTag, deleteTags, addTagLinks, deleteTagLinks, addChildTag } = useTags();
+ const { tagsData, updateTag, deleteTags, addTagLinks, deleteTagLinks, addChildTag, mergeTags } = useTags();
const [isShowNewSubTagDialog, setIsShowNewSubTagDialog] = useState(false);
+ const [isShowMergeTagsSelector, setIsShowMergeTagsSelector] = 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 mergeTagsSelectorProps = useRef({});
const table = useMemo(() => {
if (!tagsData) {
@@ -121,12 +104,44 @@ const TagsTable = ({
return scroll || {};
}, []);
- const storeGridScroll = useCallback((gridScroll) => {
- window.sfTagsDataContext.localStorage.setItem(KEY_STORE_SCROLL, JSON.stringify(gridScroll));
+ 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 foldedGroups = useMemo(() => {
- return {};
+ const closeNewSubTagDialog = useCallback(() => {
+ parentTagIdRef.current = null;
+ setIsShowNewSubTagDialog(false);
+ }, []);
+
+ const onMergeTags = useCallback((tagsIds, menuPosition) => {
+ const { left, top } = getTagsTableWrapperOffsets();
+ mergeTagsSelectorProps.current.mergeTagsIds = tagsIds;
+ mergeTagsSelectorProps.current.position = {
+ left: (menuPosition.left || 0) + (left || 0),
+ top: (menuPosition.top || 0) + (top || 0),
+ };
+ setIsShowMergeTagsSelector(true);
+ }, [getTagsTableWrapperOffsets]);
+
+ const closeMergeTagsSelector = useCallback(() => {
+ mergeTagsSelectorProps.current = {};
+ setIsShowMergeTagsSelector(false);
+ }, []);
+
+ const handelAddChildTag = useCallback((tagData, callback) => {
+ addChildTag(tagData, parentTagIdRef.current, callback);
+ }, [addChildTag]);
+
+ const storeGridScroll = useCallback((gridScroll) => {
+ window.sfTagsDataContext.localStorage.setItem(KEY_STORE_SCROLL, JSON.stringify(gridScroll));
}, []);
const storeFoldedGroups = useCallback(() => {}, []);
@@ -145,8 +160,9 @@ const TagsTable = ({
context,
onDeleteTags,
onNewSubTag,
+ onMergeTags,
});
- }, [context, onDeleteTags, onNewSubTag]);
+ }, [context, onDeleteTags, onNewSubTag, onMergeTags]);
const checkCanModifyTag = useCallback((tag) => {
return context.canModifyTag(tag);
@@ -174,7 +190,6 @@ const TagsTable = ({
recordsTree={recordsTree}
keyTreeNodeFoldedMap={keyTreeNodeFoldedMap}
canModifyTags={canModifyTags}
- foldedGroups={foldedGroups}
gridScroll={gridScroll}
visibleColumns={visibleColumns}
noRecordsTipsText={gettext('No tags')}
@@ -193,6 +208,9 @@ const TagsTable = ({
{isShowNewSubTagDialog && (
)}
+ {isShowMergeTagsSelector && (
+
+ )}
>
);
};
diff --git a/seahub/repo_metadata/apis.py b/seahub/repo_metadata/apis.py
index 651769f249c..925ea916158 100644
--- a/seahub/repo_metadata/apis.py
+++ b/seahub/repo_metadata/apis.py
@@ -1996,7 +1996,7 @@ def delete(self, request, repo_id):
tags_table_id = tags_table['id']
try:
- resp = metadata_server_api.delete_rows(tags_table_id, tag_ids)
+ metadata_server_api.delete_rows(tags_table_id, tag_ids)
except Exception as e:
logger.error(e)
error_msg = 'Internal Server Error'
@@ -2066,7 +2066,7 @@ def post(self, request, repo_id):
if link_column_key == TAGS_TABLE.columns.parent_links.key or link_column_key == TAGS_TABLE.columns.sub_links.key:
try:
init_tag_self_link_columns(metadata_server_api, tags_table_id)
- link_id = TAGS_TABLE.self_link_id;
+ link_id = TAGS_TABLE.self_link_id
is_linked_back = link_column_key == TAGS_TABLE.columns.sub_links.key if True else False
except Exception as e:
logger.error(e)
@@ -2322,3 +2322,142 @@ def get(self, request, repo_id, tag_id):
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
return Response(tag_files_query)
+
+
+class MetadataMergeTags(APIView):
+ authentication_classes = (TokenAuthentication, SessionAuthentication)
+ permission_classes = (IsAuthenticated,)
+ throttle_classes = (UserRateThrottle,)
+
+ def post(self, request, repo_id):
+ target_tag_id = request.data.get('target_tag_id')
+ merged_tags_ids = request.data.get('merged_tags_ids')
+
+ if not target_tag_id:
+ return api_error(status.HTTP_400_BAD_REQUEST, 'target_tag_id invalid')
+
+ if not merged_tags_ids:
+ return api_error(status.HTTP_400_BAD_REQUEST, 'merged_tags_ids invalid')
+
+ metadata = RepoMetadata.objects.filter(repo_id=repo_id).first()
+ if not metadata or not metadata.enabled:
+ error_msg = f'The metadata module is disabled for repo {repo_id}.'
+ return api_error(status.HTTP_404_NOT_FOUND, error_msg)
+
+ repo = seafile_api.get_repo(repo_id)
+ if not repo:
+ error_msg = 'Library %s not found.' % repo_id
+ return api_error(status.HTTP_404_NOT_FOUND, error_msg)
+
+ if not can_read_metadata(request, repo_id):
+ error_msg = 'Permission denied.'
+ return api_error(status.HTTP_403_FORBIDDEN, error_msg)
+
+ metadata_server_api = MetadataServerAPI(repo_id, request.user.username)
+
+ try:
+ metadata = metadata_server_api.get_metadata()
+ except Exception as e:
+ logger.error(e)
+ error_msg = 'Internal Server Error'
+ return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
+
+ from seafevents.repo_metadata.constants import TAGS_TABLE
+ tables = metadata.get('tables', [])
+ tags_table_id = [table['id'] for table in tables if table['name'] == TAGS_TABLE.name]
+ tags_table_id = tags_table_id[0] if tags_table_id else None
+ if not tags_table_id:
+ return api_error(status.HTTP_404_NOT_FOUND, 'tags not be used')
+
+ try:
+ columns_data = metadata_server_api.list_columns(tags_table_id)
+ columns = columns_data.get('columns', [])
+
+ except Exception as e:
+ logger.error(e)
+ error_msg = 'Internal Server Error'
+ return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
+
+ op_tags_ids = [target_tag_id] + merged_tags_ids
+ op_tags_ids_str = ', '.join([f'"{id}"' for id in op_tags_ids])
+ sql = f'SELECT * FROM {TAGS_TABLE.name} WHERE `{TAGS_TABLE.columns.id.name}` in ({op_tags_ids_str})'
+ try:
+ query_new_rows = metadata_server_api.query_rows(sql)
+ op_tags = query_new_rows.get('results', [])
+ except Exception as e:
+ logger.error(e)
+ return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error')
+
+ if not op_tags:
+ return api_error(status.HTTP_400_BAD_REQUEST, 'tags not found')
+
+ target_tag = next((tag for tag in op_tags if tag.get(TAGS_TABLE.columns.id.name) == target_tag_id), None)
+ if not target_tag:
+ return api_error(status.HTTP_400_BAD_REQUEST, 'target_tag_id invalid')
+
+ merged_tags = [tag for tag in op_tags if tag[TAGS_TABLE.columns.id.name] in merged_tags_ids]
+ if not merged_tags:
+ return api_error(status.HTTP_400_BAD_REQUEST, 'merged_tags_ids invalid')
+
+ # get unique parent/child/file links from merged tags which not exist in target tag
+ exist_parent_tags_ids = [link['row_id'] for link in target_tag.get(TAGS_TABLE.columns.parent_links.key, [])]
+ exist_child_tags_ids = [link['row_id'] for link in target_tag.get(TAGS_TABLE.columns.sub_links.key, [])]
+ exist_files_ids = [link['row_id'] for link in target_tag.get(TAGS_TABLE.columns.file_links.key, [])]
+ new_parent_tags_ids = []
+ new_child_tags_ids = []
+ new_files_ids = []
+ for merged_tag in merged_tags:
+ merged_parent_tags_ids = [link['row_id'] for link in merged_tag.get(TAGS_TABLE.columns.parent_links.key, [])]
+ merged_child_tags_ids = [link['row_id'] for link in merged_tag.get(TAGS_TABLE.columns.sub_links.key, [])]
+ merged_files_ids = [link['row_id'] for link in merged_tag.get(TAGS_TABLE.columns.file_links.key, [])]
+ for merged_parent_tag_id in merged_parent_tags_ids:
+ if merged_parent_tag_id not in op_tags_ids and merged_parent_tag_id not in exist_parent_tags_ids:
+ new_parent_tags_ids.append(merged_parent_tag_id)
+ exist_parent_tags_ids.append(merged_parent_tag_id)
+
+ for merged_child_tag_id in merged_child_tags_ids:
+ if merged_child_tag_id not in op_tags_ids and merged_child_tag_id not in exist_child_tags_ids:
+ new_child_tags_ids.append(merged_child_tag_id)
+ exist_child_tags_ids.append(merged_child_tag_id)
+
+ for merged_file_id in merged_files_ids:
+ if merged_file_id not in exist_files_ids:
+ new_files_ids.append(merged_file_id)
+ exist_files_ids.append(merged_file_id)
+
+ parent_link_column = [column for column in columns if column['key'] == TAGS_TABLE.columns.parent_links.key and column['type'] == 'link']
+ parent_link_column = parent_link_column[0] if parent_link_column else None
+
+ # add new parent tags
+ if new_parent_tags_ids:
+ try:
+ metadata_server_api.insert_link(TAGS_TABLE.self_link_id, tags_table_id, { target_tag_id: new_parent_tags_ids })
+ except Exception as e:
+ logger.error(e)
+ return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error')
+
+ # add new child tags
+ if new_child_tags_ids:
+ try:
+ metadata_server_api.insert_link(TAGS_TABLE.self_link_id, tags_table_id, { target_tag_id: new_child_tags_ids }, True)
+ except Exception as e:
+ logger.error(e)
+ return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error')
+
+ # add new tag files
+ if new_files_ids:
+ try:
+ metadata_server_api.insert_link(TAGS_TABLE.file_link_id, tags_table_id, { target_tag_id: new_files_ids })
+ except Exception as e:
+ logger.error(e)
+ return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error')
+
+
+ # remove merge tags
+ try:
+ metadata_server_api.delete_rows(tags_table_id, merged_tags_ids)
+ except Exception as e:
+ logger.error(e)
+ return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error')
+
+ return Response({'success': True})
diff --git a/seahub/repo_metadata/urls.py b/seahub/repo_metadata/urls.py
index 25ce0fb2a2e..47cff477fe0 100644
--- a/seahub/repo_metadata/urls.py
+++ b/seahub/repo_metadata/urls.py
@@ -2,7 +2,7 @@
from .apis import MetadataRecords, MetadataManage, MetadataColumns, MetadataRecord, \
MetadataFolders, MetadataViews, MetadataViewsMoveView, MetadataViewsDetailView, MetadataViewsDuplicateView, FacesRecords, \
FaceRecognitionManage, FacesRecord, MetadataExtractFileDetails, PeoplePhotos, MetadataTagsStatusManage, MetadataTags, \
- MetadataTagsLinks, MetadataFileTags, MetadataTagFiles, MetadataDetailsSettingsView, MetadataOCRManageView
+ MetadataTagsLinks, MetadataFileTags, MetadataTagFiles, MetadataMergeTags, MetadataDetailsSettingsView, MetadataOCRManageView
urlpatterns = [
re_path(r'^$', MetadataManage.as_view(), name='api-v2.1-metadata'),
@@ -37,4 +37,5 @@
re_path(r'^tags-links/$', MetadataTagsLinks.as_view(), name='api-v2.1-metadata-tags-links'),
re_path(r'^file-tags/$', MetadataFileTags.as_view(), name='api-v2.1-metadata-file-tags'),
re_path(r'^tag-files/(?P.+)/$', MetadataTagFiles.as_view(), name='api-v2.1-metadata-tag-files'),
+ re_path(r'^merge-tags/$', MetadataMergeTags.as_view(), name='api-v2.1-metadata-merge-tags'),
]