mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-08 18:30:53 +00:00
feat(tag): support merge tags (#7402)
This commit is contained in:
@@ -83,8 +83,8 @@ const ContextMenu = ({
|
|||||||
|
|
||||||
const options = useMemo(() => {
|
const options = useMemo(() => {
|
||||||
if (!visible || !createContextMenuOptions) return [];
|
if (!visible || !createContextMenuOptions) return [];
|
||||||
return createContextMenuOptions({ ...customProps, hideMenu: setVisible });
|
return createContextMenuOptions({ ...customProps, hideMenu: setVisible, menuPosition: position });
|
||||||
}, [customProps, visible, createContextMenuOptions]);
|
}, [customProps, visible, createContextMenuOptions, position]);
|
||||||
|
|
||||||
|
|
||||||
if (!Array.isArray(options) || options.length === 0) return null;
|
if (!Array.isArray(options) || options.length === 0) return null;
|
||||||
|
@@ -123,6 +123,15 @@ class TagsManagerAPI {
|
|||||||
return this.req.delete(url, { data: params });
|
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();
|
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);
|
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;
|
export default Context;
|
||||||
|
@@ -198,6 +198,10 @@ export const TagsProvider = ({ repoID, currentPath, selectTagsView, children, ..
|
|||||||
storeRef.current.deleteTagLinks(columnKey, tagId, otherTagsIds, success_callback, fail_callback);
|
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) => {
|
const modifyColumnWidth = useCallback((columnKey, newWidth) => {
|
||||||
storeRef.current.modifyColumnWidth(columnKey, newWidth);
|
storeRef.current.modifyColumnWidth(columnKey, newWidth);
|
||||||
}, [storeRef]);
|
}, [storeRef]);
|
||||||
@@ -273,6 +277,7 @@ export const TagsProvider = ({ repoID, currentPath, selectTagsView, children, ..
|
|||||||
updateTag,
|
updateTag,
|
||||||
addTagLinks,
|
addTagLinks,
|
||||||
deleteTagLinks,
|
deleteTagLinks,
|
||||||
|
mergeTags,
|
||||||
updateLocalTag,
|
updateLocalTag,
|
||||||
selectTag: handelSelectTag,
|
selectTag: handelSelectTag,
|
||||||
modifyColumnWidth,
|
modifyColumnWidth,
|
||||||
|
@@ -222,7 +222,8 @@ class DataProcessor {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case OPERATION_TYPE.ADD_TAG_LINKS:
|
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);
|
this.buildTagsTree(table.rows, table);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@@ -386,14 +386,27 @@ class Store {
|
|||||||
this.applyOperation(operation);
|
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 type = OPERATION_TYPE.MODIFY_COLUMN_WIDTH;
|
||||||
const column = getColumnByKey(this.data.columns, columnKey);
|
const column = getColumnByKey(this.data.columns, columnKey);
|
||||||
const operation = this.createOperation({
|
const operation = this.createOperation({
|
||||||
type, repo_id: this.repoId, column_key: columnKey, new_width: newWidth, old_width: column.width
|
type, repo_id: this.repoId, column_key: columnKey, new_width: newWidth, old_width: column.width
|
||||||
});
|
});
|
||||||
this.applyOperation(operation);
|
this.applyOperation(operation);
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Store;
|
export default Store;
|
||||||
|
@@ -6,6 +6,8 @@ import { PRIVATE_COLUMN_KEY } from '../../constants';
|
|||||||
import { username } from '../../../utils/constants';
|
import { username } from '../../../utils/constants';
|
||||||
import { addRowLinks, removeRowLinks } from '../../utils/link';
|
import { addRowLinks, removeRowLinks } from '../../utils/link';
|
||||||
import { getRecordIdFromRecord } from '../../../metadata/utils/cell';
|
import { getRecordIdFromRecord } from '../../../metadata/utils/cell';
|
||||||
|
import { getRowById, getRowsByIds } from '../../../metadata/utils/table';
|
||||||
|
import { getChildLinks, getParentLinks, getTagFileLinks } from '../../utils/cell';
|
||||||
|
|
||||||
dayjs.extend(utc);
|
dayjs.extend(utc);
|
||||||
|
|
||||||
@@ -154,15 +156,13 @@ export default function apply(data, operation) {
|
|||||||
if (currentRowId === row_id) {
|
if (currentRowId === row_id) {
|
||||||
// add parent tags to current tag
|
// add parent tags to current tag
|
||||||
updatedRow = addRowLinks(updatedRow, PRIVATE_COLUMN_KEY.PARENT_LINKS, other_rows_ids);
|
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)) {
|
if (other_rows_ids.includes(currentRowId)) {
|
||||||
// add current tag as child tag to related tags
|
// add current tag as child tag to related tags
|
||||||
updatedRow = addRowLinks(updatedRow, PRIVATE_COLUMN_KEY.SUB_LINKS, [row_id]);
|
updatedRow = addRowLinks(updatedRow, PRIVATE_COLUMN_KEY.SUB_LINKS, [row_id]);
|
||||||
data.rows[index] = updatedRow;
|
|
||||||
data.id_row_map[currentRowId] = updatedRow;
|
|
||||||
}
|
}
|
||||||
|
data.rows[index] = updatedRow;
|
||||||
|
data.id_row_map[currentRowId] = updatedRow;
|
||||||
});
|
});
|
||||||
} else if (column_key === PRIVATE_COLUMN_KEY.SUB_LINKS) {
|
} else if (column_key === PRIVATE_COLUMN_KEY.SUB_LINKS) {
|
||||||
data.rows.forEach((row, index) => {
|
data.rows.forEach((row, index) => {
|
||||||
@@ -171,15 +171,14 @@ export default function apply(data, operation) {
|
|||||||
if (currentRowId === row_id) {
|
if (currentRowId === row_id) {
|
||||||
// add child tags to current tag
|
// add child tags to current tag
|
||||||
updatedRow = addRowLinks(updatedRow, PRIVATE_COLUMN_KEY.SUB_LINKS, other_rows_ids);
|
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)) {
|
if (other_rows_ids.includes(currentRowId)) {
|
||||||
// add current tag as parent tag to related tags
|
// add current tag as parent tag to related tags
|
||||||
updatedRow = addRowLinks(updatedRow, PRIVATE_COLUMN_KEY.PARENT_LINKS, [row_id]);
|
updatedRow = addRowLinks(updatedRow, PRIVATE_COLUMN_KEY.PARENT_LINKS, [row_id]);
|
||||||
data.rows[index] = updatedRow;
|
|
||||||
data.id_row_map[currentRowId] = updatedRow;
|
|
||||||
}
|
}
|
||||||
|
data.rows[index] = updatedRow;
|
||||||
|
data.id_row_map[currentRowId] = updatedRow;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
@@ -194,15 +193,13 @@ export default function apply(data, operation) {
|
|||||||
if (currentRowId === row_id) {
|
if (currentRowId === row_id) {
|
||||||
// remove parent tags from current tag
|
// remove parent tags from current tag
|
||||||
updatedRow = removeRowLinks(updatedRow, PRIVATE_COLUMN_KEY.PARENT_LINKS, other_rows_ids);
|
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)) {
|
if (other_rows_ids.includes(currentRowId)) {
|
||||||
// remove current tag as child tag from related tags
|
// remove current tag as child tag from related tags
|
||||||
updatedRow = removeRowLinks(updatedRow, PRIVATE_COLUMN_KEY.SUB_LINKS, [row_id]);
|
updatedRow = removeRowLinks(updatedRow, PRIVATE_COLUMN_KEY.SUB_LINKS, [row_id]);
|
||||||
data.rows[index] = updatedRow;
|
|
||||||
data.id_row_map[currentRowId] = updatedRow;
|
|
||||||
}
|
}
|
||||||
|
data.rows[index] = updatedRow;
|
||||||
|
data.id_row_map[currentRowId] = updatedRow;
|
||||||
});
|
});
|
||||||
} else if (column_key === PRIVATE_COLUMN_KEY.SUB_LINKS) {
|
} else if (column_key === PRIVATE_COLUMN_KEY.SUB_LINKS) {
|
||||||
data.rows.forEach((row, index) => {
|
data.rows.forEach((row, index) => {
|
||||||
@@ -211,19 +208,116 @@ export default function apply(data, operation) {
|
|||||||
if (currentRowId === row_id) {
|
if (currentRowId === row_id) {
|
||||||
// remove child tags from current tag
|
// remove child tags from current tag
|
||||||
updatedRow = removeRowLinks(updatedRow, PRIVATE_COLUMN_KEY.SUB_LINKS, other_rows_ids);
|
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)) {
|
if (other_rows_ids.includes(currentRowId)) {
|
||||||
// remove current tag as parent tag from related tags
|
// remove current tag as parent tag from related tags
|
||||||
updatedRow = removeRowLinks(updatedRow, PRIVATE_COLUMN_KEY.PARENT_LINKS, [row_id]);
|
updatedRow = removeRowLinks(updatedRow, PRIVATE_COLUMN_KEY.PARENT_LINKS, [row_id]);
|
||||||
data.rows[index] = updatedRow;
|
|
||||||
data.id_row_map[currentRowId] = updatedRow;
|
|
||||||
}
|
}
|
||||||
|
data.rows[index] = updatedRow;
|
||||||
|
data.id_row_map[currentRowId] = updatedRow;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return data;
|
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: {
|
case OPERATION_TYPE.MODIFY_COLUMN_WIDTH: {
|
||||||
const { column_key, new_width } = operation;
|
const { column_key, new_width } = operation;
|
||||||
const columnIndex = data.columns.findIndex(column => column.key === column_key);
|
const columnIndex = data.columns.findIndex(column => column.key === column_key);
|
||||||
|
@@ -7,6 +7,7 @@ export const OPERATION_TYPE = {
|
|||||||
RELOAD_RECORDS: 'reload_records',
|
RELOAD_RECORDS: 'reload_records',
|
||||||
ADD_TAG_LINKS: 'add_tag_links',
|
ADD_TAG_LINKS: 'add_tag_links',
|
||||||
DELETE_TAG_LINKS: 'delete_tag_links',
|
DELETE_TAG_LINKS: 'delete_tag_links',
|
||||||
|
MERGE_TAGS: 'merge_tags',
|
||||||
|
|
||||||
MODIFY_LOCAL_RECORDS: 'modify_local_records',
|
MODIFY_LOCAL_RECORDS: 'modify_local_records',
|
||||||
|
|
||||||
@@ -22,6 +23,7 @@ export const OPERATION_ATTRIBUTES = {
|
|||||||
[OPERATION_TYPE.RELOAD_RECORDS]: ['repo_id', '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.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.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_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'],
|
[OPERATION_TYPE.MODIFY_COLUMN_WIDTH]: ['column_key', 'new_width', 'old_width'],
|
||||||
};
|
};
|
||||||
|
@@ -97,6 +97,15 @@ class ServerOperator {
|
|||||||
});
|
});
|
||||||
break;
|
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: {
|
case OPERATION_TYPE.RESTORE_RECORDS: {
|
||||||
const { repo_id, rows_data } = operation;
|
const { repo_id, rows_data } = operation;
|
||||||
if (!Array.isArray(rows_data) || rows_data.length === 0) {
|
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]) || [];
|
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) => {
|
export const getTagFilesCount = (tag) => {
|
||||||
const links = tag ? tag[PRIVATE_COLUMN_KEY.TAG_FILE_LINKS] : [];
|
const links = getTagFileLinks(tag);
|
||||||
if (Array.isArray(links)) return links.length;
|
return Array.isArray(links) ? links.length : 0;
|
||||||
return 0;
|
|
||||||
};
|
};
|
||||||
export const getTagsByNameOrColor = (tags, nameOrColor) => {
|
export const getTagsByNameOrColor = (tags, nameOrColor) => {
|
||||||
if (!Array.isArray(tags) || tags.length === 0) return [];
|
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 { CenteredLoading } from '@seafile/sf-metadata-ui-component';
|
||||||
import toaster from '../../../components/toast';
|
import toaster from '../../../components/toast';
|
||||||
import TagsTable from './tags-table';
|
import TagsTable from './tags-table';
|
||||||
@@ -17,6 +17,8 @@ const AllTags = ({ updateCurrentPath, ...params }) => {
|
|||||||
const [displayTag, setDisplayTag] = useState('');
|
const [displayTag, setDisplayTag] = useState('');
|
||||||
const [isLoadingMore, setLoadingMore] = useState(false);
|
const [isLoadingMore, setLoadingMore] = useState(false);
|
||||||
|
|
||||||
|
const tagsTableWrapperRef = useRef(null);
|
||||||
|
|
||||||
const { isLoading, isReloading, tagsData, store, context, currentPath } = useTags();
|
const { isLoading, isReloading, tagsData, store, context, currentPath } = useTags();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -72,6 +74,11 @@ const AllTags = ({ updateCurrentPath, ...params }) => {
|
|||||||
}
|
}
|
||||||
}, [isLoading, isReloading, onChangeDisplayTag]);
|
}, [isLoading, isReloading, onChangeDisplayTag]);
|
||||||
|
|
||||||
|
const getTagsTableWrapperOffsets = useCallback(() => {
|
||||||
|
if (!tagsTableWrapperRef.current) return {};
|
||||||
|
return tagsTableWrapperRef.current.getBoundingClientRect();
|
||||||
|
}, []);
|
||||||
|
|
||||||
if (isReloading) return (<CenteredLoading />);
|
if (isReloading) return (<CenteredLoading />);
|
||||||
|
|
||||||
if (displayTag) {
|
if (displayTag) {
|
||||||
@@ -85,7 +92,7 @@ const AllTags = ({ updateCurrentPath, ...params }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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
|
<TagsTable
|
||||||
context={context}
|
context={context}
|
||||||
tagsData={tagsData}
|
tagsData={tagsData}
|
||||||
@@ -93,6 +100,7 @@ const AllTags = ({ updateCurrentPath, ...params }) => {
|
|||||||
setDisplayTag={onChangeDisplayTag}
|
setDisplayTag={onChangeDisplayTag}
|
||||||
isLoadingMoreRecords={isLoadingMore}
|
isLoadingMoreRecords={isLoadingMore}
|
||||||
loadMore={loadMore}
|
loadMore={loadMore}
|
||||||
|
getTagsTableWrapperOffsets={getTagsTableWrapperOffsets}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@@ -13,6 +13,7 @@ const OPERATION = {
|
|||||||
DELETE_TAG: 'delete_tag',
|
DELETE_TAG: 'delete_tag',
|
||||||
DELETE_TAGS: 'delete_tags',
|
DELETE_TAGS: 'delete_tags',
|
||||||
NEW_SUB_TAG: 'new_sub_tag',
|
NEW_SUB_TAG: 'new_sub_tag',
|
||||||
|
MERGE_TAGS: 'merge_tags',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createContextMenuOptions = ({
|
export const createContextMenuOptions = ({
|
||||||
@@ -25,11 +26,13 @@ export const createContextMenuOptions = ({
|
|||||||
showRecordAsTree,
|
showRecordAsTree,
|
||||||
treeMetrics,
|
treeMetrics,
|
||||||
treeNodeKeyRecordIdMap,
|
treeNodeKeyRecordIdMap,
|
||||||
|
menuPosition,
|
||||||
hideMenu,
|
hideMenu,
|
||||||
recordGetterByIndex,
|
recordGetterByIndex,
|
||||||
recordGetterById,
|
recordGetterById,
|
||||||
onDeleteTags,
|
onDeleteTags,
|
||||||
onNewSubTag,
|
onNewSubTag,
|
||||||
|
onMergeTags,
|
||||||
}) => {
|
}) => {
|
||||||
const canDeleteTag = context.checkCanDeleteTag();
|
const canDeleteTag = context.checkCanDeleteTag();
|
||||||
const canAddTag = context.canAddTag();
|
const canAddTag = context.canAddTag();
|
||||||
@@ -55,6 +58,10 @@ export const createContextMenuOptions = ({
|
|||||||
onNewSubTag(option.parentTagId);
|
onNewSubTag(option.parentTagId);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case OPERATION.MERGE_TAGS: {
|
||||||
|
onMergeTags(option.tagsIds, menuPosition);
|
||||||
|
break;
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -115,6 +122,13 @@ export const createContextMenuOptions = ({
|
|||||||
tagsIds,
|
tagsIds,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (tagsIds.length > 1) {
|
||||||
|
options.push({
|
||||||
|
label: gettext('Merge tags'),
|
||||||
|
value: OPERATION.MERGE_TAGS,
|
||||||
|
tagsIds,
|
||||||
|
});
|
||||||
|
}
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,12 +1,20 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { NumberFormatter } from '@seafile/sf-metadata-ui-component';
|
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 ChildTagsFormatter = ({ record, column }) => {
|
||||||
|
const { tagsData } = useTags();
|
||||||
|
|
||||||
const childTagsLinksCount = useMemo(() => {
|
const childTagsLinksCount = useMemo(() => {
|
||||||
const subTagLinks = record[column.key];
|
const childTagLinks = record[column.key];
|
||||||
return Array.isArray(subTagLinks) ? subTagLinks.length : 0;
|
if (!Array.isArray(childTagLinks) || childTagLinks.length === 0) {
|
||||||
}, [record, column]);
|
return 0;
|
||||||
|
}
|
||||||
|
const childTagsIds = childTagLinks.map((link) => link.row_id);
|
||||||
|
const subTags = getRowsByIds(tagsData, childTagsIds);
|
||||||
|
return subTags.length;
|
||||||
|
}, [record, column, tagsData]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="sf-table-child-tags-formatter sf-table-cell-formatter sf-metadata-ui cell-formatter-container">
|
<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 PropTypes from 'prop-types';
|
||||||
import SFTable from '../../../../components/sf-table';
|
import SFTable from '../../../../components/sf-table';
|
||||||
import EditTagDialog from '../../../components/dialog/edit-tag-dialog';
|
import EditTagDialog from '../../../components/dialog/edit-tag-dialog';
|
||||||
|
import MergeTagsSelector from '../../../components/merge-tags-selector';
|
||||||
import { createTableColumns } from './columns-factory';
|
import { createTableColumns } from './columns-factory';
|
||||||
import { createContextMenuOptions } from './context-menu-options';
|
import { createContextMenuOptions } from './context-menu-options';
|
||||||
import { gettext } from '../../../../utils/constants';
|
import { gettext } from '../../../../utils/constants';
|
||||||
@@ -33,33 +34,15 @@ const TagsTable = ({
|
|||||||
modifyColumnWidth: modifyColumnWidthAPI,
|
modifyColumnWidth: modifyColumnWidthAPI,
|
||||||
setDisplayTag,
|
setDisplayTag,
|
||||||
loadMore,
|
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 [isShowNewSubTagDialog, setIsShowNewSubTagDialog] = useState(false);
|
||||||
|
const [isShowMergeTagsSelector, setIsShowMergeTagsSelector] = useState(false);
|
||||||
|
|
||||||
const parentTagIdRef = useRef(null);
|
const parentTagIdRef = useRef(null);
|
||||||
|
const mergeTagsSelectorProps = useRef({});
|
||||||
const onDeleteTags = useCallback((tagsIds) => {
|
|
||||||
deleteTags(tagsIds);
|
|
||||||
|
|
||||||
const eventBus = EventBus.getInstance();
|
|
||||||
eventBus.dispatch(EVENT_BUS_TYPE.SELECT_NONE);
|
|
||||||
}, [deleteTags]);
|
|
||||||
|
|
||||||
const onNewSubTag = useCallback((parentTagId) => {
|
|
||||||
parentTagIdRef.current = parentTagId;
|
|
||||||
setIsShowNewSubTagDialog(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const closeNewSubTagDialog = useCallback(() => {
|
|
||||||
parentTagIdRef.current = null;
|
|
||||||
setIsShowNewSubTagDialog(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handelAddChildTag = useCallback((tagData, callback) => {
|
|
||||||
addChildTag(tagData, parentTagIdRef.current, callback);
|
|
||||||
}, [addChildTag]);
|
|
||||||
|
|
||||||
const table = useMemo(() => {
|
const table = useMemo(() => {
|
||||||
if (!tagsData) {
|
if (!tagsData) {
|
||||||
@@ -121,12 +104,44 @@ const TagsTable = ({
|
|||||||
return scroll || {};
|
return scroll || {};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const storeGridScroll = useCallback((gridScroll) => {
|
const onDeleteTags = useCallback((tagsIds) => {
|
||||||
window.sfTagsDataContext.localStorage.setItem(KEY_STORE_SCROLL, JSON.stringify(gridScroll));
|
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(() => {
|
const closeNewSubTagDialog = useCallback(() => {
|
||||||
return {};
|
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(() => {}, []);
|
const storeFoldedGroups = useCallback(() => {}, []);
|
||||||
@@ -145,8 +160,9 @@ const TagsTable = ({
|
|||||||
context,
|
context,
|
||||||
onDeleteTags,
|
onDeleteTags,
|
||||||
onNewSubTag,
|
onNewSubTag,
|
||||||
|
onMergeTags,
|
||||||
});
|
});
|
||||||
}, [context, onDeleteTags, onNewSubTag]);
|
}, [context, onDeleteTags, onNewSubTag, onMergeTags]);
|
||||||
|
|
||||||
const checkCanModifyTag = useCallback((tag) => {
|
const checkCanModifyTag = useCallback((tag) => {
|
||||||
return context.canModifyTag(tag);
|
return context.canModifyTag(tag);
|
||||||
@@ -174,7 +190,6 @@ const TagsTable = ({
|
|||||||
recordsTree={recordsTree}
|
recordsTree={recordsTree}
|
||||||
keyTreeNodeFoldedMap={keyTreeNodeFoldedMap}
|
keyTreeNodeFoldedMap={keyTreeNodeFoldedMap}
|
||||||
canModifyTags={canModifyTags}
|
canModifyTags={canModifyTags}
|
||||||
foldedGroups={foldedGroups}
|
|
||||||
gridScroll={gridScroll}
|
gridScroll={gridScroll}
|
||||||
visibleColumns={visibleColumns}
|
visibleColumns={visibleColumns}
|
||||||
noRecordsTipsText={gettext('No tags')}
|
noRecordsTipsText={gettext('No tags')}
|
||||||
@@ -193,6 +208,9 @@ const TagsTable = ({
|
|||||||
{isShowNewSubTagDialog && (
|
{isShowNewSubTagDialog && (
|
||||||
<EditTagDialog tags={table.rows} title={gettext('New child tag')} onToggle={closeNewSubTagDialog} onSubmit={handelAddChildTag} />
|
<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']
|
tags_table_id = tags_table['id']
|
||||||
|
|
||||||
try:
|
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:
|
except Exception as e:
|
||||||
logger.error(e)
|
logger.error(e)
|
||||||
error_msg = 'Internal Server Error'
|
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:
|
if link_column_key == TAGS_TABLE.columns.parent_links.key or link_column_key == TAGS_TABLE.columns.sub_links.key:
|
||||||
try:
|
try:
|
||||||
init_tag_self_link_columns(metadata_server_api, tags_table_id)
|
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
|
is_linked_back = link_column_key == TAGS_TABLE.columns.sub_links.key if True else False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(e)
|
logger.error(e)
|
||||||
@@ -2322,3 +2322,142 @@ class MetadataTagFiles(APIView):
|
|||||||
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||||
|
|
||||||
return Response(tag_files_query)
|
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, \
|
from .apis import MetadataRecords, MetadataManage, MetadataColumns, MetadataRecord, \
|
||||||
MetadataFolders, MetadataViews, MetadataViewsMoveView, MetadataViewsDetailView, MetadataViewsDuplicateView, FacesRecords, \
|
MetadataFolders, MetadataViews, MetadataViewsMoveView, MetadataViewsDetailView, MetadataViewsDuplicateView, FacesRecords, \
|
||||||
FaceRecognitionManage, FacesRecord, MetadataExtractFileDetails, PeoplePhotos, MetadataTagsStatusManage, MetadataTags, \
|
FaceRecognitionManage, FacesRecord, MetadataExtractFileDetails, PeoplePhotos, MetadataTagsStatusManage, MetadataTags, \
|
||||||
MetadataTagsLinks, MetadataFileTags, MetadataTagFiles, MetadataDetailsSettingsView, MetadataOCRManageView
|
MetadataTagsLinks, MetadataFileTags, MetadataTagFiles, MetadataMergeTags, MetadataDetailsSettingsView, MetadataOCRManageView
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
re_path(r'^$', MetadataManage.as_view(), name='api-v2.1-metadata'),
|
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'^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'^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'^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