mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-08 10:22:46 +00:00
feat(tag): support merge tags (#7402)
This commit is contained in:
@@ -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;
|
||||
|
@@ -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();
|
||||
|
71
frontend/src/tag/components/merge-tags-selector/index.css
Normal file
71
frontend/src/tag/components/merge-tags-selector/index.css
Normal file
@@ -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;
|
||||
}
|
202
frontend/src/tag/components/merge-tags-selector/index.js
Normal file
202
frontend/src/tag/components/merge-tags-selector/index.js
Normal file
@@ -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 (<span className="none-search-result">{noOptionsTip}</span>);
|
||||
}
|
||||
|
||||
return displayTags.map((tag, i) => {
|
||||
const tagId = getTagId(tag);
|
||||
const tagName = getTagName(tag);
|
||||
const tagColor = getTagColor(tag);
|
||||
return (
|
||||
<div key={tagId} className="sf-metadata-tags-editor-tag-item" ref={selectItemRef}>
|
||||
<div
|
||||
className={classnames('sf-metadata-tags-editor-tag-container pl-2', { 'sf-metadata-tags-editor-tag-container-highlight': i === highlightIndex })}
|
||||
onMouseDown={() => onSelectTag(tagId)}
|
||||
onMouseEnter={() => onMenuMouseEnter(i)}
|
||||
onMouseLeave={() => onMenuMouseLeave(i)}
|
||||
>
|
||||
<div className="sf-metadata-tag-color-and-name">
|
||||
<div className="sf-metadata-tag-color" style={{ backgroundColor: tagColor }}></div>
|
||||
<div className="sf-metadata-tag-name">{tagName}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
}, [displayTags, searchValue, highlightIndex, onMenuMouseEnter, onMenuMouseLeave, onSelectTag]);
|
||||
|
||||
return (
|
||||
<ModalPortal>
|
||||
<ClickOutside onClickOutside={closeSelector}>
|
||||
<div className="sf-metadata-merge-tags-selector" style={{ ...position, position: 'fixed', width: 300, zIndex: Z_INDEX_EDITOR_CONTAINER }} ref={editorRef}>
|
||||
<div className="sf-metadata-search-tags-container">
|
||||
<SearchInput
|
||||
autoFocus
|
||||
placeholder={gettext('Merge tags to:')}
|
||||
onKeyDown={onKeyDown}
|
||||
onChange={onChangeSearch}
|
||||
className="sf-metadata-search-tags"
|
||||
/>
|
||||
</div>
|
||||
<div className="sf-metadata-merge-tags-selector-container" ref={editorContainerRef}>
|
||||
{renderOptions()}
|
||||
</div>
|
||||
</div>
|
||||
</ClickOutside>
|
||||
</ModalPortal>
|
||||
);
|
||||
};
|
||||
|
||||
MergeTagsSelector.propTypes = {
|
||||
tagsTable: PropTypes.object,
|
||||
tags: PropTypes.array,
|
||||
position: PropTypes.object,
|
||||
};
|
||||
|
||||
export default MergeTagsSelector;
|
@@ -117,6 +117,9 @@ class Context {
|
||||
return this.api.deleteTagLinks(this.repoId, link_column_key, row_id_map);
|
||||
};
|
||||
|
||||
mergeTags = (target_tag_id, merged_tags_ids) => {
|
||||
return this.api.mergeTags(this.repoId, target_tag_id, merged_tags_ids);
|
||||
};
|
||||
}
|
||||
|
||||
export default Context;
|
||||
|
@@ -198,6 +198,10 @@ export const TagsProvider = ({ repoID, currentPath, selectTagsView, children, ..
|
||||
storeRef.current.deleteTagLinks(columnKey, tagId, otherTagsIds, success_callback, fail_callback);
|
||||
}, []);
|
||||
|
||||
const mergeTags = useCallback((target_tag_id, merged_tags_ids, { success_callback, fail_callback } = {}) => {
|
||||
storeRef.current.mergeTags(target_tag_id, merged_tags_ids, success_callback, fail_callback);
|
||||
}, []);
|
||||
|
||||
const modifyColumnWidth = useCallback((columnKey, newWidth) => {
|
||||
storeRef.current.modifyColumnWidth(columnKey, newWidth);
|
||||
}, [storeRef]);
|
||||
@@ -273,6 +277,7 @@ export const TagsProvider = ({ repoID, currentPath, selectTagsView, children, ..
|
||||
updateTag,
|
||||
addTagLinks,
|
||||
deleteTagLinks,
|
||||
mergeTags,
|
||||
updateLocalTag,
|
||||
selectTag: handelSelectTag,
|
||||
modifyColumnWidth,
|
||||
|
@@ -222,7 +222,8 @@ class DataProcessor {
|
||||
break;
|
||||
}
|
||||
case OPERATION_TYPE.ADD_TAG_LINKS:
|
||||
case OPERATION_TYPE.DELETE_TAG_LINKS: {
|
||||
case OPERATION_TYPE.DELETE_TAG_LINKS:
|
||||
case OPERATION_TYPE.MERGE_TAGS: {
|
||||
this.buildTagsTree(table.rows, table);
|
||||
break;
|
||||
}
|
||||
|
@@ -386,14 +386,27 @@ class Store {
|
||||
this.applyOperation(operation);
|
||||
}
|
||||
|
||||
modifyColumnWidth = (columnKey, newWidth) => {
|
||||
mergeTags(target_tag_id, merged_tags_ids, success_callback, fail_callback) {
|
||||
const type = OPERATION_TYPE.MERGE_TAGS;
|
||||
const operation = this.createOperation({
|
||||
type,
|
||||
repo_id: this.repoId,
|
||||
target_tag_id,
|
||||
merged_tags_ids,
|
||||
success_callback,
|
||||
fail_callback,
|
||||
});
|
||||
this.applyOperation(operation);
|
||||
}
|
||||
|
||||
modifyColumnWidth(columnKey, newWidth) {
|
||||
const type = OPERATION_TYPE.MODIFY_COLUMN_WIDTH;
|
||||
const column = getColumnByKey(this.data.columns, columnKey);
|
||||
const operation = this.createOperation({
|
||||
type, repo_id: this.repoId, column_key: columnKey, new_width: newWidth, old_width: column.width
|
||||
});
|
||||
this.applyOperation(operation);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default Store;
|
||||
|
@@ -6,6 +6,8 @@ import { PRIVATE_COLUMN_KEY } from '../../constants';
|
||||
import { username } from '../../../utils/constants';
|
||||
import { addRowLinks, removeRowLinks } from '../../utils/link';
|
||||
import { getRecordIdFromRecord } from '../../../metadata/utils/cell';
|
||||
import { getRowById, getRowsByIds } from '../../../metadata/utils/table';
|
||||
import { getChildLinks, getParentLinks, getTagFileLinks } from '../../utils/cell';
|
||||
|
||||
dayjs.extend(utc);
|
||||
|
||||
@@ -154,15 +156,13 @@ export default function apply(data, operation) {
|
||||
if (currentRowId === row_id) {
|
||||
// add parent tags to current tag
|
||||
updatedRow = addRowLinks(updatedRow, PRIVATE_COLUMN_KEY.PARENT_LINKS, other_rows_ids);
|
||||
data.rows[index] = updatedRow;
|
||||
data.id_row_map[currentRowId] = updatedRow;
|
||||
}
|
||||
if (other_rows_ids.includes(currentRowId)) {
|
||||
// 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;
|
||||
}
|
||||
});
|
||||
} else if (column_key === PRIVATE_COLUMN_KEY.SUB_LINKS) {
|
||||
data.rows.forEach((row, index) => {
|
||||
@@ -171,15 +171,14 @@ export default function apply(data, operation) {
|
||||
if (currentRowId === row_id) {
|
||||
// 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;
|
||||
|
||||
}
|
||||
if (other_rows_ids.includes(currentRowId)) {
|
||||
// add current tag as parent tag to related tags
|
||||
updatedRow = addRowLinks(updatedRow, PRIVATE_COLUMN_KEY.PARENT_LINKS, [row_id]);
|
||||
}
|
||||
data.rows[index] = updatedRow;
|
||||
data.id_row_map[currentRowId] = updatedRow;
|
||||
}
|
||||
});
|
||||
}
|
||||
return data;
|
||||
@@ -194,15 +193,13 @@ export default function apply(data, operation) {
|
||||
if (currentRowId === row_id) {
|
||||
// remove parent tags from current tag
|
||||
updatedRow = removeRowLinks(updatedRow, PRIVATE_COLUMN_KEY.PARENT_LINKS, other_rows_ids);
|
||||
data.rows[index] = updatedRow;
|
||||
data.id_row_map[currentRowId] = updatedRow;
|
||||
}
|
||||
if (other_rows_ids.includes(currentRowId)) {
|
||||
// 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;
|
||||
}
|
||||
});
|
||||
} else if (column_key === PRIVATE_COLUMN_KEY.SUB_LINKS) {
|
||||
data.rows.forEach((row, index) => {
|
||||
@@ -211,19 +208,116 @@ export default function apply(data, operation) {
|
||||
if (currentRowId === row_id) {
|
||||
// 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;
|
||||
}
|
||||
if (other_rows_ids.includes(currentRowId)) {
|
||||
// remove current tag as parent tag from related tags
|
||||
updatedRow = removeRowLinks(updatedRow, PRIVATE_COLUMN_KEY.PARENT_LINKS, [row_id]);
|
||||
}
|
||||
data.rows[index] = updatedRow;
|
||||
data.id_row_map[currentRowId] = updatedRow;
|
||||
}
|
||||
});
|
||||
}
|
||||
return data;
|
||||
}
|
||||
case OPERATION_TYPE.MERGE_TAGS: {
|
||||
const { target_tag_id, merged_tags_ids } = operation;
|
||||
const targetTag = getRowById(data, target_tag_id);
|
||||
const mergedTags = getRowsByIds(data, merged_tags_ids);
|
||||
if (!targetTag || mergedTags.length === 0) {
|
||||
return data;
|
||||
}
|
||||
const opTagsIds = [target_tag_id, ...merged_tags_ids];
|
||||
const parentLinks = getParentLinks(targetTag);
|
||||
const childLinks = getChildLinks(targetTag);
|
||||
const fileLinks = getTagFileLinks(targetTag);
|
||||
const idParentLinkExistMap = parentLinks.reduce((currIdParentLinkExist, link) => ({ ...currIdParentLinkExist, [link.row_id]: true }), {});
|
||||
const idChildLinkExistMap = childLinks.reduce((currIdChildLinkExist, link) => ({ ...currIdChildLinkExist, [link.row_id]: true }), {});
|
||||
const idFileLinkExistMap = fileLinks.reduce((currIdFileLinkExistMap, link) => ({ ...currIdFileLinkExistMap, [link.row_id]: true }), {});
|
||||
|
||||
// 1. get unique parent/child/file links from merged tags which not exist in target tag
|
||||
let newParentTagsIds = [];
|
||||
let newChildTagsIds = [];
|
||||
let newFilesIds = [];
|
||||
mergedTags.forEach((mergedTag) => {
|
||||
const currParentLinks = getParentLinks(mergedTag);
|
||||
const currChildLinks = getChildLinks(mergedTag);
|
||||
const currFileLinks = getTagFileLinks(mergedTag);
|
||||
currParentLinks.forEach((parentLink) => {
|
||||
const parentLinkedTagId = parentLink.row_id;
|
||||
if (!opTagsIds.includes(parentLinkedTagId) && !idParentLinkExistMap[parentLinkedTagId]) {
|
||||
newParentTagsIds.push(parentLinkedTagId);
|
||||
idParentLinkExistMap[parentLinkedTagId] = true;
|
||||
}
|
||||
});
|
||||
currChildLinks.forEach((childLink) => {
|
||||
const childLinkedTagId = childLink.row_id;
|
||||
if (!opTagsIds.includes(childLinkedTagId) && !idChildLinkExistMap[childLinkedTagId]) {
|
||||
newChildTagsIds.push(childLinkedTagId);
|
||||
idChildLinkExistMap[childLinkedTagId] = true;
|
||||
}
|
||||
});
|
||||
currFileLinks.forEach((fileLink) => {
|
||||
const linkedFileId = fileLink.row_id;
|
||||
if (!idFileLinkExistMap[linkedFileId]) {
|
||||
newFilesIds.push(linkedFileId);
|
||||
idFileLinkExistMap[linkedFileId] = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 2. delete merged tags
|
||||
const idTagMergedMap = mergedTags.reduce((currIdTagMergedMap, tag) => ({ ...currIdTagMergedMap, [tag._id]: true }), {});
|
||||
let updatedRows = [];
|
||||
data.rows.forEach((tag) => {
|
||||
const currentTagId = tag._id;
|
||||
if (idTagMergedMap[currentTagId]) {
|
||||
delete data.id_row_map[currentTagId];
|
||||
} else {
|
||||
updatedRows.push(tag);
|
||||
}
|
||||
});
|
||||
|
||||
// 3. merge parent links into target tag
|
||||
// 4. merge child links into target tag
|
||||
// 5. merge file links into target tag
|
||||
const hasNewParentLinks = newParentTagsIds.length > 0;
|
||||
const hasNewChildLinks = newChildTagsIds.length > 0;
|
||||
const hasNewFileLinks = newFilesIds.length > 0;
|
||||
if (hasNewParentLinks || hasNewChildLinks || hasNewFileLinks) {
|
||||
updatedRows.forEach((row, index) => {
|
||||
const currentRowId = row._id;
|
||||
let updatedRow = { ...row };
|
||||
if (currentRowId === target_tag_id) {
|
||||
if (hasNewParentLinks) {
|
||||
// add parent links
|
||||
updatedRow = addRowLinks(updatedRow, PRIVATE_COLUMN_KEY.PARENT_LINKS, newParentTagsIds);
|
||||
}
|
||||
if (hasNewChildLinks) {
|
||||
// add child links
|
||||
updatedRow = addRowLinks(updatedRow, PRIVATE_COLUMN_KEY.SUB_LINKS, newChildTagsIds);
|
||||
}
|
||||
if (hasNewFileLinks) {
|
||||
// add file links
|
||||
updatedRow = addRowLinks(updatedRow, PRIVATE_COLUMN_KEY.TAG_FILE_LINKS, newFilesIds);
|
||||
}
|
||||
}
|
||||
|
||||
if (newParentTagsIds.includes(currentRowId)) {
|
||||
// add target tag as child tag to related tags
|
||||
updatedRow = addRowLinks(updatedRow, PRIVATE_COLUMN_KEY.SUB_LINKS, [target_tag_id]);
|
||||
}
|
||||
if (newChildTagsIds.includes(currentRowId)) {
|
||||
// add target tag as parent tag to related tags
|
||||
updatedRow = addRowLinks(updatedRow, PRIVATE_COLUMN_KEY.PARENT_LINKS, [target_tag_id]);
|
||||
}
|
||||
updatedRows[index] = updatedRow;
|
||||
data.id_row_map[currentRowId] = updatedRow;
|
||||
});
|
||||
}
|
||||
|
||||
data.rows = updatedRows;
|
||||
return data;
|
||||
}
|
||||
case OPERATION_TYPE.MODIFY_COLUMN_WIDTH: {
|
||||
const { column_key, new_width } = operation;
|
||||
const columnIndex = data.columns.findIndex(column => column.key === column_key);
|
||||
|
@@ -7,6 +7,7 @@ export const OPERATION_TYPE = {
|
||||
RELOAD_RECORDS: 'reload_records',
|
||||
ADD_TAG_LINKS: 'add_tag_links',
|
||||
DELETE_TAG_LINKS: 'delete_tag_links',
|
||||
MERGE_TAGS: 'merge_tags',
|
||||
|
||||
MODIFY_LOCAL_RECORDS: 'modify_local_records',
|
||||
|
||||
@@ -22,6 +23,7 @@ export const OPERATION_ATTRIBUTES = {
|
||||
[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.MERGE_TAGS]: ['repo_id', 'target_tag_id', 'merged_tags_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'],
|
||||
[OPERATION_TYPE.MODIFY_COLUMN_WIDTH]: ['column_key', 'new_width', 'old_width'],
|
||||
};
|
||||
|
@@ -97,6 +97,15 @@ class ServerOperator {
|
||||
});
|
||||
break;
|
||||
}
|
||||
case OPERATION_TYPE.MERGE_TAGS: {
|
||||
const { target_tag_id, merged_tags_ids } = operation;
|
||||
this.context.mergeTags(target_tag_id, merged_tags_ids).then((res) => {
|
||||
callback({ operation });
|
||||
}).catch((error) => {
|
||||
callback({ error: gettext('Failed to merge tags') });
|
||||
});
|
||||
break;
|
||||
}
|
||||
case OPERATION_TYPE.RESTORE_RECORDS: {
|
||||
const { repo_id, rows_data } = operation;
|
||||
if (!Array.isArray(rows_data) || rows_data.length === 0) {
|
||||
|
@@ -32,10 +32,13 @@ export const getChildLinks = (tag) => {
|
||||
return (tag && tag[PRIVATE_COLUMN_KEY.SUB_LINKS]) || [];
|
||||
};
|
||||
|
||||
export const getTagFileLinks = (tag) => {
|
||||
return (tag && tag[PRIVATE_COLUMN_KEY.TAG_FILE_LINKS]) || [];
|
||||
};
|
||||
|
||||
export const getTagFilesCount = (tag) => {
|
||||
const links = tag ? tag[PRIVATE_COLUMN_KEY.TAG_FILE_LINKS] : [];
|
||||
if (Array.isArray(links)) return links.length;
|
||||
return 0;
|
||||
const links = getTagFileLinks(tag);
|
||||
return Array.isArray(links) ? links.length : 0;
|
||||
};
|
||||
export const getTagsByNameOrColor = (tags, nameOrColor) => {
|
||||
if (!Array.isArray(tags) || tags.length === 0) return [];
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import React, { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import { CenteredLoading } from '@seafile/sf-metadata-ui-component';
|
||||
import toaster from '../../../components/toast';
|
||||
import TagsTable from './tags-table';
|
||||
@@ -17,6 +17,8 @@ const AllTags = ({ updateCurrentPath, ...params }) => {
|
||||
const [displayTag, setDisplayTag] = useState('');
|
||||
const [isLoadingMore, setLoadingMore] = useState(false);
|
||||
|
||||
const tagsTableWrapperRef = useRef(null);
|
||||
|
||||
const { isLoading, isReloading, tagsData, store, context, currentPath } = useTags();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -72,6 +74,11 @@ const AllTags = ({ updateCurrentPath, ...params }) => {
|
||||
}
|
||||
}, [isLoading, isReloading, onChangeDisplayTag]);
|
||||
|
||||
const getTagsTableWrapperOffsets = useCallback(() => {
|
||||
if (!tagsTableWrapperRef.current) return {};
|
||||
return tagsTableWrapperRef.current.getBoundingClientRect();
|
||||
}, []);
|
||||
|
||||
if (isReloading) return (<CenteredLoading />);
|
||||
|
||||
if (displayTag) {
|
||||
@@ -85,7 +92,7 @@ const AllTags = ({ updateCurrentPath, ...params }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="sf-metadata-tags-wrapper sf-metadata-all-tags-wrapper">
|
||||
<div className="sf-metadata-tags-wrapper sf-metadata-all-tags-wrapper" ref={tagsTableWrapperRef}>
|
||||
<TagsTable
|
||||
context={context}
|
||||
tagsData={tagsData}
|
||||
@@ -93,6 +100,7 @@ const AllTags = ({ updateCurrentPath, ...params }) => {
|
||||
setDisplayTag={onChangeDisplayTag}
|
||||
isLoadingMoreRecords={isLoadingMore}
|
||||
loadMore={loadMore}
|
||||
getTagsTableWrapperOffsets={getTagsTableWrapperOffsets}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
|
@@ -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 (
|
||||
<div className="sf-table-child-tags-formatter sf-table-cell-formatter sf-metadata-ui cell-formatter-container">
|
||||
|
@@ -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 && (
|
||||
<EditTagDialog tags={table.rows} title={gettext('New child tag')} onToggle={closeNewSubTagDialog} onSubmit={handelAddChildTag} />
|
||||
)}
|
||||
{isShowMergeTagsSelector && (
|
||||
<MergeTagsSelector {...mergeTagsSelectorProps.current} closeSelector={closeMergeTagsSelector} mergeTags={mergeTags} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@@ -1996,7 +1996,7 @@ class MetadataTags(APIView):
|
||||
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 @@ class MetadataTagsLinks(APIView):
|
||||
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 @@ class MetadataTagFiles(APIView):
|
||||
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})
|
||||
|
@@ -2,7 +2,7 @@ from django.urls import re_path
|
||||
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 @@ urlpatterns = [
|
||||
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<tag_id>.+)/$', 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'),
|
||||
]
|
||||
|
Reference in New Issue
Block a user