mirror of
https://github.com/haiwen/seahub.git
synced 2025-08-25 18:20:48 +00:00
feat(tag): support self link (#7225)
This commit is contained in:
parent
bde5ec063e
commit
cb73865b21
@ -105,6 +105,24 @@ class TagsManagerAPI {
|
|||||||
return this.req.put(url, params);
|
return this.req.put(url, params);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
addTagLinks = (repoID, link_column_key, row_id_map) => {
|
||||||
|
const url = this.server + '/api/v2.1/repos/' + repoID + '/metadata/tags-links/';
|
||||||
|
const params = {
|
||||||
|
link_column_key,
|
||||||
|
row_id_map,
|
||||||
|
};
|
||||||
|
return this.req.post(url, params);
|
||||||
|
};
|
||||||
|
|
||||||
|
deleteTagLinks = (repoID, link_column_key, row_id_map) => {
|
||||||
|
const url = this.server + '/api/v2.1/repos/' + repoID + '/metadata/tags-links/';
|
||||||
|
const params = {
|
||||||
|
link_column_key,
|
||||||
|
row_id_map,
|
||||||
|
};
|
||||||
|
return this.req.delete(url, { data: params });
|
||||||
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const tagsAPI = new TagsManagerAPI();
|
const tagsAPI = new TagsManagerAPI();
|
||||||
|
@ -0,0 +1,113 @@
|
|||||||
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { SearchInput } from '@seafile/sf-metadata-ui-component';
|
||||||
|
import EmptyTip from '../../../../components/empty-tip';
|
||||||
|
import Tags from './tags';
|
||||||
|
import { KeyCodes } from '../../../../constants';
|
||||||
|
import { gettext } from '../../../../utils/constants';
|
||||||
|
import { getTagsByNameOrColor } from '../../../utils';
|
||||||
|
|
||||||
|
const getSelectableTags = (allTags, idTagSelectedMap) => {
|
||||||
|
if (!idTagSelectedMap) {
|
||||||
|
return allTags;
|
||||||
|
}
|
||||||
|
return allTags.filter((tag) => !idTagSelectedMap[tag._id]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const initIdTagSelectedMap = (linkedTags) => {
|
||||||
|
let idTagSelectedMap = {};
|
||||||
|
linkedTags.forEach((linkedTag) => {
|
||||||
|
idTagSelectedMap[linkedTag._id] = true;
|
||||||
|
});
|
||||||
|
return idTagSelectedMap;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AddLinkedTags = ({ allTags, linkedTags, switchToLinkedTagsPage, addLinkedTag, deleteLinedTag }) => {
|
||||||
|
const initialIdTagSelectedMap = initIdTagSelectedMap(linkedTags);
|
||||||
|
const [idTagSelectedMap, setIdSelectedMap] = useState(initialIdTagSelectedMap);
|
||||||
|
const [selectableTags, setSelectableTags] = useState([]);
|
||||||
|
const [searchValue, setSearchValue] = useState('');
|
||||||
|
|
||||||
|
const initialSelectableTagsRef = useRef(getSelectableTags(allTags, initialIdTagSelectedMap));
|
||||||
|
|
||||||
|
const selectTag = useCallback((tag) => {
|
||||||
|
let updatedIdTagSelectedMap = { ...idTagSelectedMap };
|
||||||
|
if (updatedIdTagSelectedMap[tag._id]) {
|
||||||
|
delete updatedIdTagSelectedMap[tag._id];
|
||||||
|
setIdSelectedMap(updatedIdTagSelectedMap);
|
||||||
|
deleteLinedTag(tag._id);
|
||||||
|
} else {
|
||||||
|
updatedIdTagSelectedMap[tag._id] = true;
|
||||||
|
setIdSelectedMap(updatedIdTagSelectedMap);
|
||||||
|
addLinkedTag(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
}, [idTagSelectedMap, addLinkedTag, deleteLinedTag]);
|
||||||
|
|
||||||
|
const onKeyDown = useCallback((event) => {
|
||||||
|
if (
|
||||||
|
event.keyCode === KeyCodes.ChineseInputMethod ||
|
||||||
|
event.keyCode === KeyCodes.Enter ||
|
||||||
|
event.keyCode === KeyCodes.LeftArrow ||
|
||||||
|
event.keyCode === KeyCodes.RightArrow
|
||||||
|
) {
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onChangeSearch = useCallback((newSearchValue) => {
|
||||||
|
if (searchValue === newSearchValue) return;
|
||||||
|
setSearchValue(newSearchValue);
|
||||||
|
}, [searchValue]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let searchedTags = [];
|
||||||
|
if (!searchValue) {
|
||||||
|
searchedTags = [...initialSelectableTagsRef.current];
|
||||||
|
} else {
|
||||||
|
searchedTags = getTagsByNameOrColor(initialSelectableTagsRef.current, searchValue);
|
||||||
|
}
|
||||||
|
setSelectableTags(searchedTags);
|
||||||
|
}, [searchValue, allTags]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="sf-metadata-set-linked-tags-popover-selector">
|
||||||
|
<div className="sf-metadata-set-linked-tags-popover-header">
|
||||||
|
<div className="sf-metadata-set-linked-tags-popover-title">
|
||||||
|
<span className="sf-metadata-set-linked-tags-popover-header-operation sf-metadata-set-linked-tags-popover-back" onClick={switchToLinkedTagsPage}><i className="sf3-font sf3-font-arrow sf-metadata-set-linked-tags-popover-back-icon"></i></span>
|
||||||
|
<span>{gettext('Link existing tags')}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="sf-metadata-set-linked-tags-popover-body">
|
||||||
|
<div className="sf-metadata-set-linked-tags-popover-search-container">
|
||||||
|
<SearchInput
|
||||||
|
autoFocus
|
||||||
|
className="sf-metadata-set-linked-tags-popover-search-tags"
|
||||||
|
placeholder={gettext('Search tag')}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
onChange={onChangeSearch}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{selectableTags.length === 0 && (
|
||||||
|
<EmptyTip text={gettext('No tags available')} />
|
||||||
|
)}
|
||||||
|
{selectableTags.length > 0 && (
|
||||||
|
<div className="sf-metadata-set-linked-tags-popover-selectable-tags-wrapper">
|
||||||
|
<Tags selectable tags={selectableTags} idTagSelectedMap={idTagSelectedMap} selectTag={selectTag} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
AddLinkedTags.propTypes = {
|
||||||
|
isParentTags: PropTypes.bool,
|
||||||
|
allTags: PropTypes.array,
|
||||||
|
linkedTags: PropTypes.array,
|
||||||
|
switchToLinkedTagsPage: PropTypes.func,
|
||||||
|
addLinkedTag: PropTypes.func,
|
||||||
|
deleteLinedTag: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddLinkedTags;
|
@ -0,0 +1,130 @@
|
|||||||
|
.sf-metadata-set-linked-tags-popover .popover {
|
||||||
|
max-width: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-set-linked-tags-popover-selected,
|
||||||
|
.sf-metadata-set-linked-tags-popover-selector {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-set-linked-tags-popover-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 46px;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-set-linked-tags-popover-title {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 40px;
|
||||||
|
line-height: 40px;
|
||||||
|
padding: 0 20px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-set-linked-tags-popover-selector .sf-metadata-set-linked-tags-popover-title {
|
||||||
|
margin-left: -3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-set-linked-tags-popover-body {
|
||||||
|
flex: 1;
|
||||||
|
height: calc(100% - 46px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-set-linked-tags-popover-selectable-tags-wrapper {
|
||||||
|
height: calc(100% - 48px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-editing-tags-list {
|
||||||
|
height: 100%;
|
||||||
|
padding: 10px 20px;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-editing-tags-list.selectable:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-editing-tag:hover {
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-editing-tag-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 30px;
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 2px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #212529;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-set-linked-tags-popover-search-container {
|
||||||
|
height: 48px;
|
||||||
|
padding: 10px 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-editing-tag-color-name {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-editing-tag-color-name .sf-metadata-tag-color {
|
||||||
|
height: 14px;
|
||||||
|
width: 14px;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-editing-tag-color-name .sf-metadata-tag-name {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-editing-tag-operation {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-editing-tag-delete-icon {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-editing-tag-delete:hover .sf-metadata-editing-tag-delete-icon {
|
||||||
|
fill: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-set-linked-tags-popover-header-operation {
|
||||||
|
display: inline-flex;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
margin-right: 3px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-set-linked-tags-popover-header-operation:hover {
|
||||||
|
border-radius: 3px;
|
||||||
|
background-color: #efefef;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-set-linked-tags-popover-back-icon {
|
||||||
|
display: inline-block;
|
||||||
|
transform: rotate(180deg);
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
}
|
@ -0,0 +1,135 @@
|
|||||||
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { UncontrolledPopover } from 'reactstrap';
|
||||||
|
import isHotkey from 'is-hotkey';
|
||||||
|
import LinkedTags from './linked-tags';
|
||||||
|
import AddLinkedTags from './add-linked-tags';
|
||||||
|
import { getRowsByIds } from '../../../../metadata/utils/table';
|
||||||
|
import { useTags } from '../../../hooks';
|
||||||
|
import { getEventClassName } from '../../../../metadata/utils/common';
|
||||||
|
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
const POPOVER_WIDTH = 560;
|
||||||
|
const POPOVER_WINDOW_SAFE_SPACE = 30;
|
||||||
|
const POPOVER_MAX_HEIGHT = 520;
|
||||||
|
const POPOVER_MIN_HEIGHT = 300;
|
||||||
|
|
||||||
|
const KEY_MODE_TYPE = {
|
||||||
|
LINKED_TAGS: 'linked_tags',
|
||||||
|
ADD_LINKED_TAGS: 'add_linked_tags',
|
||||||
|
};
|
||||||
|
|
||||||
|
const SetLinkedTagsPopover = ({ isParentTags, target, placement, tagLinks, allTags, hidePopover, addTagLinks, deleteTagLinks }) => {
|
||||||
|
const { tagsData } = useTags();
|
||||||
|
const linkedRowsIds = Array.isArray(tagLinks) ? tagLinks.map((link) => link.row_id) : [];
|
||||||
|
const initialLinkedTags = getRowsByIds(tagsData, linkedRowsIds);
|
||||||
|
const [mode, setMode] = useState(KEY_MODE_TYPE.LINKED_TAGS);
|
||||||
|
const [linkedTags, setLinkedTags] = useState(initialLinkedTags);
|
||||||
|
|
||||||
|
const popoverRef = useRef(null);
|
||||||
|
|
||||||
|
const getPopoverInnerStyle = () => {
|
||||||
|
let style = { width: POPOVER_WIDTH };
|
||||||
|
const windowHeight = window.innerHeight - POPOVER_WINDOW_SAFE_SPACE;
|
||||||
|
let maxHeight = POPOVER_MAX_HEIGHT;
|
||||||
|
if (windowHeight < maxHeight) {
|
||||||
|
maxHeight = windowHeight;
|
||||||
|
}
|
||||||
|
if (maxHeight < POPOVER_MIN_HEIGHT) {
|
||||||
|
maxHeight = POPOVER_MIN_HEIGHT;
|
||||||
|
}
|
||||||
|
style.height = maxHeight;
|
||||||
|
return style;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onHidePopover = useCallback((event) => {
|
||||||
|
if (popoverRef.current && !getEventClassName(event).includes('popover') && !popoverRef.current.contains(event.target)) {
|
||||||
|
hidePopover(event);
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}, [hidePopover]);
|
||||||
|
|
||||||
|
const onHotKey = useCallback((event) => {
|
||||||
|
if (isHotkey('esc', event)) {
|
||||||
|
event.preventDefault();
|
||||||
|
hidePopover();
|
||||||
|
}
|
||||||
|
}, [hidePopover]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.addEventListener('click', onHidePopover, true);
|
||||||
|
document.addEventListener('keydown', onHotKey);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('click', onHidePopover, true);
|
||||||
|
document.removeEventListener('keydown', onHotKey);
|
||||||
|
};
|
||||||
|
}, [onHidePopover, onHotKey]);
|
||||||
|
|
||||||
|
const deleteLinedTag = useCallback((tagId) => {
|
||||||
|
let updatedLinkedTags = [...linkedTags];
|
||||||
|
const deleteIndex = updatedLinkedTags.findIndex((tag) => tag._id === tagId);
|
||||||
|
if (deleteIndex < 0) return;
|
||||||
|
updatedLinkedTags.splice(deleteIndex, 1);
|
||||||
|
setLinkedTags(updatedLinkedTags);
|
||||||
|
deleteTagLinks(tagId);
|
||||||
|
}, [linkedTags, deleteTagLinks]);
|
||||||
|
|
||||||
|
const addLinkedTag = useCallback((tag) => {
|
||||||
|
let updatedLinkedTags = [...linkedTags];
|
||||||
|
updatedLinkedTags.push(tag);
|
||||||
|
setLinkedTags(updatedLinkedTags);
|
||||||
|
addTagLinks(tag);
|
||||||
|
}, [linkedTags, addTagLinks]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UncontrolledPopover
|
||||||
|
isOpen
|
||||||
|
hideArrow
|
||||||
|
positionFixed
|
||||||
|
fade={false}
|
||||||
|
flip={false}
|
||||||
|
placement={placement}
|
||||||
|
target={target}
|
||||||
|
className="sf-metadata-set-linked-tags-popover"
|
||||||
|
boundariesElement={document.body}
|
||||||
|
>
|
||||||
|
<div ref={popoverRef} style={getPopoverInnerStyle()}>
|
||||||
|
{mode === KEY_MODE_TYPE.LINKED_TAGS ?
|
||||||
|
<LinkedTags
|
||||||
|
isParentTags={isParentTags}
|
||||||
|
linkedTags={linkedTags}
|
||||||
|
switchToAddTagsPage={() => setMode(KEY_MODE_TYPE.ADD_LINKED_TAGS)}
|
||||||
|
deleteLinedTag={deleteLinedTag}
|
||||||
|
/> :
|
||||||
|
<AddLinkedTags
|
||||||
|
allTags={allTags}
|
||||||
|
linkedTags={linkedTags}
|
||||||
|
switchToLinkedTagsPage={() => setMode(KEY_MODE_TYPE.LINKED_TAGS)}
|
||||||
|
addLinkedTag={addLinkedTag}
|
||||||
|
deleteLinedTag={deleteLinedTag}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</UncontrolledPopover>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
SetLinkedTagsPopover.propTypes = {
|
||||||
|
isParentTags: PropTypes.bool,
|
||||||
|
placement: PropTypes.string,
|
||||||
|
target: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
|
||||||
|
tagLinks: PropTypes.array,
|
||||||
|
allTags: PropTypes.array,
|
||||||
|
hidePopover: PropTypes.func,
|
||||||
|
addTagLinks: PropTypes.func,
|
||||||
|
deleteTagLinks: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
SetLinkedTagsPopover.defaultProps = {
|
||||||
|
placement: 'bottom-end',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SetLinkedTagsPopover;
|
@ -0,0 +1,37 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Button } from 'reactstrap';
|
||||||
|
import EmptyTip from '../../../../components/empty-tip';
|
||||||
|
import Tags from './tags';
|
||||||
|
import { gettext } from '../../../../utils/constants';
|
||||||
|
|
||||||
|
const LinkedTags = ({ isParentTags, linkedTags, switchToAddTagsPage, deleteLinedTag }) => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="sf-metadata-set-linked-tags-popover-selected">
|
||||||
|
<div className="sf-metadata-set-linked-tags-popover-header">
|
||||||
|
<div className="sf-metadata-set-linked-tags-popover-title">
|
||||||
|
{isParentTags ? gettext('Parent tags') : gettext('Sub tags')}
|
||||||
|
</div>
|
||||||
|
<Button size="sm" color="primary" className="mr-2" onClick={switchToAddTagsPage}>{gettext('Link existing tags')}</Button>
|
||||||
|
</div>
|
||||||
|
<div className="sf-metadata-set-linked-tags-popover-body">
|
||||||
|
{linkedTags.length === 0 && (
|
||||||
|
<EmptyTip text={gettext(isParentTags ? 'No parent tag' : 'No sub tag')} />
|
||||||
|
)}
|
||||||
|
{linkedTags.length > 0 && (
|
||||||
|
<Tags deletable tags={linkedTags} deleteTag={deleteLinedTag} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
LinkedTags.propTypes = {
|
||||||
|
isParentTags: PropTypes.bool,
|
||||||
|
linkedTags: PropTypes.array,
|
||||||
|
switchToAddTagsPage: PropTypes.func,
|
||||||
|
deleteTag: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LinkedTags;
|
@ -0,0 +1,53 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import { Icon } from '@seafile/sf-metadata-ui-component';
|
||||||
|
import { getTagColor, getTagId, getTagName } from '../../../utils';
|
||||||
|
import { debounce } from '../../../../metadata/utils/common';
|
||||||
|
|
||||||
|
const Tags = ({ tags, deletable, selectable, idTagSelectedMap, selectTag, deleteTag }) => {
|
||||||
|
|
||||||
|
const clickTag = debounce((tag) => {
|
||||||
|
if (!selectable) return;
|
||||||
|
selectTag && selectTag(tag);
|
||||||
|
}, 200);
|
||||||
|
|
||||||
|
const remove = useCallback((tagId) => {
|
||||||
|
if (!deletable) return;
|
||||||
|
deleteTag && deleteTag(tagId);
|
||||||
|
}, [deletable, deleteTag]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classnames('sf-metadata-editing-tags-list', { 'selectable': selectable })}>
|
||||||
|
{tags.map((tag) => {
|
||||||
|
const tagId = getTagId(tag);
|
||||||
|
const tagName = getTagName(tag);
|
||||||
|
const tagColor = getTagColor(tag);
|
||||||
|
return (
|
||||||
|
<div className="sf-metadata-editing-tag" key={`sf-metadata-editing-tag-${tagId}`} onClick={() => clickTag(tag)}>
|
||||||
|
<div className="sf-metadata-editing-tag-container pl-2">
|
||||||
|
<div className="sf-metadata-editing-tag-color-name">
|
||||||
|
<div className="sf-metadata-tag-color" style={{ backgroundColor: tagColor }}></div>
|
||||||
|
<div className="sf-metadata-tag-name">{tagName}</div>
|
||||||
|
</div>
|
||||||
|
<div className="sf-metadata-editing-tag-operations">
|
||||||
|
{deletable && <div className="sf-metadata-editing-tag-operation sf-metadata-editing-tag-delete" onClick={() => remove(tagId)}><Icon iconName="close" className="sf-metadata-editing-tag-delete-icon" /></div>}
|
||||||
|
{(selectable && idTagSelectedMap && idTagSelectedMap[tagId]) && <div className="sf-metadata-editing-tag-operation sf-metadata-editing-tag-selected"><Icon iconName="check-mark" className="sf-metadata-editing-tag-selected-icon" /></div>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Tags.propTypes = {
|
||||||
|
tags: PropTypes.array,
|
||||||
|
deletable: PropTypes.bool,
|
||||||
|
selectable: PropTypes.bool,
|
||||||
|
selectTag: PropTypes.func,
|
||||||
|
deleteTag: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Tags;
|
@ -9,6 +9,8 @@ export const PRIVATE_COLUMN_KEY = {
|
|||||||
TAG_NAME: '_tag_name',
|
TAG_NAME: '_tag_name',
|
||||||
TAG_COLOR: '_tag_color',
|
TAG_COLOR: '_tag_color',
|
||||||
TAG_FILE_LINKS: '_tag_file_links',
|
TAG_FILE_LINKS: '_tag_file_links',
|
||||||
|
PARENT_LINKS: '_tag_parent_links',
|
||||||
|
SUB_LINKS: '_tag_sub_links',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PRIVATE_COLUMN_KEYS = [
|
export const PRIVATE_COLUMN_KEYS = [
|
||||||
|
@ -109,6 +109,14 @@ class Context {
|
|||||||
return this.api.deleteTags(this.repoId, tags);
|
return this.api.deleteTags(this.repoId, tags);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
addTagLinks = (link_column_key, row_id_map) => {
|
||||||
|
return this.api.addTagLinks(this.repoId, link_column_key, row_id_map);
|
||||||
|
};
|
||||||
|
|
||||||
|
deleteTagLinks = (link_column_key, row_id_map) => {
|
||||||
|
return this.api.deleteTagLinks(this.repoId, link_column_key, row_id_map);
|
||||||
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Context;
|
export default Context;
|
||||||
|
@ -186,6 +186,14 @@ export const TagsProvider = ({ repoID, currentPath, selectTagsView, children, ..
|
|||||||
modifyLocalTags(tagIds, idTagUpdates, { [tagId]: originalRowUpdates }, { [tagId]: oldRowData }, { [tagId]: originalOldRowData }, { success_callback, fail_callback });
|
modifyLocalTags(tagIds, idTagUpdates, { [tagId]: originalRowUpdates }, { [tagId]: oldRowData }, { [tagId]: originalOldRowData }, { success_callback, fail_callback });
|
||||||
}, [tagsData, modifyLocalTags]);
|
}, [tagsData, modifyLocalTags]);
|
||||||
|
|
||||||
|
const addTagLinks = useCallback((columnKey, tagId, otherTagsIds, { success_callback, fail_callback } = {}) => {
|
||||||
|
storeRef.current.addTagLinks(columnKey, tagId, otherTagsIds, success_callback, fail_callback);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const deleteTagLinks = useCallback((columnKey, tagId, otherTagsIds, { success_callback, fail_callback } = {}) => {
|
||||||
|
storeRef.current.deleteTagLinks(columnKey, tagId, otherTagsIds, success_callback, fail_callback);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!handelSelectTag) return;
|
if (!handelSelectTag) return;
|
||||||
if (isLoading) return;
|
if (isLoading) return;
|
||||||
@ -247,6 +255,8 @@ export const TagsProvider = ({ repoID, currentPath, selectTagsView, children, ..
|
|||||||
deleteTags,
|
deleteTags,
|
||||||
duplicateTag,
|
duplicateTag,
|
||||||
updateTag,
|
updateTag,
|
||||||
|
addTagLinks,
|
||||||
|
deleteTagLinks,
|
||||||
updateLocalTag,
|
updateLocalTag,
|
||||||
selectTag: handelSelectTag,
|
selectTag: handelSelectTag,
|
||||||
}}>
|
}}>
|
||||||
|
@ -339,6 +339,33 @@ class Store {
|
|||||||
this.applyOperation(operation);
|
this.applyOperation(operation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addTagLinks(column_key, row_id, other_rows_ids, success_callback, fail_callback) {
|
||||||
|
const type = OPERATION_TYPE.ADD_TAG_LINKS;
|
||||||
|
const operation = this.createOperation({
|
||||||
|
type,
|
||||||
|
repo_id: this.repoId,
|
||||||
|
column_key,
|
||||||
|
row_id,
|
||||||
|
other_rows_ids,
|
||||||
|
success_callback,
|
||||||
|
fail_callback,
|
||||||
|
});
|
||||||
|
this.applyOperation(operation);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteTagLinks(column_key, row_id, other_rows_ids, success_callback, fail_callback) {
|
||||||
|
const type = OPERATION_TYPE.DELETE_TAG_LINKS;
|
||||||
|
const operation = this.createOperation({
|
||||||
|
type,
|
||||||
|
repo_id: this.repoId,
|
||||||
|
column_key,
|
||||||
|
row_id,
|
||||||
|
other_rows_ids,
|
||||||
|
success_callback,
|
||||||
|
fail_callback,
|
||||||
|
});
|
||||||
|
this.applyOperation(operation);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Store;
|
export default Store;
|
||||||
|
@ -4,6 +4,7 @@ import { UTC_FORMAT_DEFAULT } from '../../../metadata/constants';
|
|||||||
import { OPERATION_TYPE } from './constants';
|
import { OPERATION_TYPE } from './constants';
|
||||||
import { PRIVATE_COLUMN_KEY } from '../../constants';
|
import { PRIVATE_COLUMN_KEY } from '../../constants';
|
||||||
import { username } from '../../../utils/constants';
|
import { username } from '../../../utils/constants';
|
||||||
|
import { addRowLinks, removeRowLinks } from '../../utils/link';
|
||||||
|
|
||||||
dayjs.extend(utc);
|
dayjs.extend(utc);
|
||||||
|
|
||||||
@ -84,6 +85,68 @@ export default function apply(data, operation) {
|
|||||||
data.rows.push(insertRows);
|
data.rows.push(insertRows);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
case OPERATION_TYPE.ADD_TAG_LINKS: {
|
||||||
|
const { column_key, row_id, other_rows_ids } = operation;
|
||||||
|
if (column_key === PRIVATE_COLUMN_KEY.PARENT_LINKS) {
|
||||||
|
data.rows = data.rows.map((row) => {
|
||||||
|
const currentRowId = row._id;
|
||||||
|
if (currentRowId === row_id) {
|
||||||
|
// add parent tags to current tag
|
||||||
|
row = addRowLinks(row, PRIVATE_COLUMN_KEY.PARENT_LINKS, other_rows_ids);
|
||||||
|
}
|
||||||
|
if (other_rows_ids.includes(currentRowId)) {
|
||||||
|
// add current tag as sub tag to related tags
|
||||||
|
row = addRowLinks(row, PRIVATE_COLUMN_KEY.SUB_LINKS, [row_id]);
|
||||||
|
}
|
||||||
|
return row;
|
||||||
|
});
|
||||||
|
} else if (column_key === PRIVATE_COLUMN_KEY.SUB_LINKS) {
|
||||||
|
data.rows = data.rows.map((row) => {
|
||||||
|
const currentRowId = row._id;
|
||||||
|
if (currentRowId === row_id) {
|
||||||
|
// add sub tags to current tag
|
||||||
|
row = addRowLinks(row, PRIVATE_COLUMN_KEY.SUB_LINKS, other_rows_ids);
|
||||||
|
}
|
||||||
|
if (other_rows_ids.includes(currentRowId)) {
|
||||||
|
// add current tag as parent tag to related tags
|
||||||
|
row = addRowLinks(row, PRIVATE_COLUMN_KEY.PARENT_LINKS, [row_id]);
|
||||||
|
}
|
||||||
|
return row;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
case OPERATION_TYPE.DELETE_TAG_LINKS: {
|
||||||
|
const { column_key, row_id, other_rows_ids } = operation;
|
||||||
|
if (column_key === PRIVATE_COLUMN_KEY.PARENT_LINKS) {
|
||||||
|
data.rows = data.rows.map((row) => {
|
||||||
|
const currentRowId = row._id;
|
||||||
|
if (currentRowId === row_id) {
|
||||||
|
// remove parent tags from current tag
|
||||||
|
row = removeRowLinks(row, PRIVATE_COLUMN_KEY.PARENT_LINKS, other_rows_ids);
|
||||||
|
}
|
||||||
|
if (other_rows_ids.includes(currentRowId)) {
|
||||||
|
// remove current tag as sub tag from related tags
|
||||||
|
row = removeRowLinks(row, PRIVATE_COLUMN_KEY.SUB_LINKS, [row_id]);
|
||||||
|
}
|
||||||
|
return row;
|
||||||
|
});
|
||||||
|
} else if (column_key === PRIVATE_COLUMN_KEY.SUB_LINKS) {
|
||||||
|
data.rows = data.rows.map((row) => {
|
||||||
|
const currentRowId = row._id;
|
||||||
|
if (currentRowId === row_id) {
|
||||||
|
// remove sub tags from current tag
|
||||||
|
row = removeRowLinks(row, PRIVATE_COLUMN_KEY.SUB_LINKS, other_rows_ids);
|
||||||
|
}
|
||||||
|
if (other_rows_ids.includes(currentRowId)) {
|
||||||
|
// remove current tag as parent tag from related tags
|
||||||
|
row = removeRowLinks(row, PRIVATE_COLUMN_KEY.PARENT_LINKS, [row_id]);
|
||||||
|
}
|
||||||
|
return row;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,8 @@ export const OPERATION_TYPE = {
|
|||||||
DELETE_RECORDS: 'delete_records',
|
DELETE_RECORDS: 'delete_records',
|
||||||
RESTORE_RECORDS: 'restore_records',
|
RESTORE_RECORDS: 'restore_records',
|
||||||
RELOAD_RECORDS: 'reload_records',
|
RELOAD_RECORDS: 'reload_records',
|
||||||
|
ADD_TAG_LINKS: 'add_tag_links',
|
||||||
|
DELETE_TAG_LINKS: 'delete_tag_links',
|
||||||
|
|
||||||
MODIFY_LOCAL_RECORDS: 'modify_local_records',
|
MODIFY_LOCAL_RECORDS: 'modify_local_records',
|
||||||
};
|
};
|
||||||
@ -14,6 +16,8 @@ export const OPERATION_ATTRIBUTES = {
|
|||||||
[OPERATION_TYPE.DELETE_RECORDS]: ['repo_id', 'tag_ids', 'deleted_tags'],
|
[OPERATION_TYPE.DELETE_RECORDS]: ['repo_id', 'tag_ids', 'deleted_tags'],
|
||||||
[OPERATION_TYPE.RESTORE_RECORDS]: ['repo_id', 'rows_data', 'original_rows', 'link_infos', 'upper_row_ids'],
|
[OPERATION_TYPE.RESTORE_RECORDS]: ['repo_id', 'rows_data', 'original_rows', 'link_infos', 'upper_row_ids'],
|
||||||
[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.DELETE_TAG_LINKS]: ['repo_id', 'column_key', 'row_id', 'other_rows_ids'],
|
||||||
[OPERATION_TYPE.MODIFY_LOCAL_RECORDS]: ['repo_id', 'row_ids', 'id_row_updates', 'id_original_row_updates', 'id_old_row_data', 'id_original_old_row_data', 'is_copy_paste', 'is_rename', 'id_obj_id'],
|
[OPERATION_TYPE.MODIFY_LOCAL_RECORDS]: ['repo_id', 'row_ids', 'id_row_updates', 'id_original_row_updates', 'id_old_row_data', 'id_original_old_row_data', 'is_copy_paste', 'is_rename', 'id_obj_id'],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -51,6 +51,30 @@ class ServerOperator {
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case OPERATION_TYPE.ADD_TAG_LINKS: {
|
||||||
|
const { column_key, row_id, other_rows_ids } = operation;
|
||||||
|
const id_linked_rows_ids_map = {
|
||||||
|
[row_id]: other_rows_ids,
|
||||||
|
};
|
||||||
|
this.context.addTagLinks(column_key, id_linked_rows_ids_map).then(res => {
|
||||||
|
callback({ operation });
|
||||||
|
}).catch(error => {
|
||||||
|
callback({ error: gettext('Failed to add linked tags') });
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case OPERATION_TYPE.DELETE_TAG_LINKS: {
|
||||||
|
const { column_key, row_id, other_rows_ids } = operation;
|
||||||
|
const id_linked_rows_ids_map = {
|
||||||
|
[row_id]: other_rows_ids,
|
||||||
|
};
|
||||||
|
this.context.deleteTagLinks(column_key, id_linked_rows_ids_map).then(res => {
|
||||||
|
callback({ operation });
|
||||||
|
}).catch(error => {
|
||||||
|
callback({ error: gettext('Failed to delete linked tags') });
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
case OPERATION_TYPE.RESTORE_RECORDS: {
|
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) {
|
||||||
|
@ -24,6 +24,19 @@ export const getTagId = (tag) => {
|
|||||||
return tag ? tag[PRIVATE_COLUMN_KEY.ID] : '';
|
return tag ? tag[PRIVATE_COLUMN_KEY.ID] : '';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getParentLinks = (tag) => {
|
||||||
|
return (tag && tag[PRIVATE_COLUMN_KEY.PARENT_LINKS]) || [];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getSubLinks = (tag) => {
|
||||||
|
return (tag && tag[PRIVATE_COLUMN_KEY.SUB_LINKS]) || [];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getSubTagsCount = (tag) => {
|
||||||
|
const subLinks = getSubLinks(tag);
|
||||||
|
return subLinks.length;
|
||||||
|
};
|
||||||
|
|
||||||
export const getTagFilesCount = (tag) => {
|
export const getTagFilesCount = (tag) => {
|
||||||
const links = tag ? tag[PRIVATE_COLUMN_KEY.TAG_FILE_LINKS] : [];
|
const links = tag ? tag[PRIVATE_COLUMN_KEY.TAG_FILE_LINKS] : [];
|
||||||
if (Array.isArray(links)) return links.length;
|
if (Array.isArray(links)) return links.length;
|
||||||
|
27
frontend/src/tag/utils/link.js
Normal file
27
frontend/src/tag/utils/link.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
export const addRowLinks = (row, key, other_rows_ids) => {
|
||||||
|
let updatedRow = row;
|
||||||
|
let updatedLinks = Array.isArray(updatedRow[key]) ? [...updatedRow[key]] : [];
|
||||||
|
other_rows_ids.forEach((otherRowId) => {
|
||||||
|
if (updatedLinks.findIndex((linked) => linked.row_id === otherRowId) < 0) {
|
||||||
|
updatedLinks.push({ row_id: otherRowId, display_value: otherRowId });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
updatedRow[key] = updatedLinks;
|
||||||
|
return updatedRow;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const removeRowLinks = (row, key, other_rows_ids) => {
|
||||||
|
if (!Array.isArray(row[key]) || row[key].length === 0) {
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
let updatedRow = row;
|
||||||
|
let updatedLinks = [...updatedRow[key]];
|
||||||
|
other_rows_ids.forEach((otherRowId) => {
|
||||||
|
const deleteIndex = updatedLinks.findIndex((linked) => linked.row_id === otherRowId);
|
||||||
|
if (deleteIndex > -1) {
|
||||||
|
updatedLinks.splice(deleteIndex, 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
updatedRow[key] = updatedLinks;
|
||||||
|
return updatedRow;
|
||||||
|
};
|
@ -30,30 +30,36 @@
|
|||||||
background-color: #f8f8f8;
|
background-color: #f8f8f8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sf-metadata-tags-table .sf-metadata-tags-table-row .sf-metadata-tags-table-cell:first-child {
|
|
||||||
width: calc((100% - 64px) * 0.7);
|
|
||||||
height: 100%;
|
|
||||||
padding-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sf-metadata-tags-table .sf-metadata-tags-table-row .sf-metadata-tags-table-cell:nth-child(2) {
|
|
||||||
width: calc((100% - 64px) * 0.3);
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sf-metadata-tags-table .sf-metadata-tags-table-row .sf-metadata-tags-table-cell:last-child {
|
|
||||||
width: 64px;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sf-metadata-tags-table .sf-metadata-tags-table-row .sf-metadata-tags-table-cell {
|
.sf-metadata-tags-table .sf-metadata-tags-table-row .sf-metadata-tags-table-cell {
|
||||||
font-size: 14px;
|
position: relative;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
padding: 0 8px;
|
||||||
|
font-size: 14px;
|
||||||
line-height: 40px;
|
line-height: 40px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
padding: 0 8px;
|
}
|
||||||
|
|
||||||
|
.sf-metadata-tags-table .sf-metadata-tags-table-row .sf-metadata-tags-table-cell-tag {
|
||||||
|
width: calc((100% - 96px) * 0.3);
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-tags-table .sf-metadata-tags-table-row .sf-metadata-tags-table-cell-parent-tags,
|
||||||
|
.sf-metadata-tags-table .sf-metadata-tags-table-row .sf-metadata-tags-table-cell-sub-tags-count,
|
||||||
|
.sf-metadata-tags-table .sf-metadata-tags-table-row .sf-metadata-tags-table-cell-tag-files-count {
|
||||||
|
width: calc((100% - 96px) * 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-tags-table .sf-metadata-tags-table-row .sf-metadata-tags-table-cell-operations-wrapper {
|
||||||
|
width: 96px;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-tags-table .sf-metadata-tags-table-row .sf-metadata-tags-table-cell-parent-tags {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sf-metadata-tags-table .sf-metadata-tags-table-header .sf-metadata-tags-table-cell {
|
.sf-metadata-tags-table .sf-metadata-tags-table-header .sf-metadata-tags-table-cell {
|
||||||
@ -62,3 +68,11 @@
|
|||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
padding: 10px 8px;
|
padding: 10px 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sf-metadata-tags-table .sf-metadata-tags-table-cell .sf-metadata-tags-operation-pop-handler {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
@ -45,9 +45,11 @@ const Main = React.memo(({ context, tags, onChangeDisplayTag, onLoadMore }) => {
|
|||||||
return (
|
return (
|
||||||
<div className="sf-metadata-tags-table" ref={tableRef} onScroll={handelScroll}>
|
<div className="sf-metadata-tags-table" ref={tableRef} onScroll={handelScroll}>
|
||||||
<div className="sf-metadata-tags-table-header sf-metadata-tags-table-row">
|
<div className="sf-metadata-tags-table-header sf-metadata-tags-table-row">
|
||||||
<div className="sf-metadata-tags-table-cell">{gettext('Tag')}</div>
|
<div className="sf-metadata-tags-table-cell sf-metadata-tags-table-cell-tag">{gettext('Tag')}</div>
|
||||||
<div className="sf-metadata-tags-table-cell">{gettext('File count')}</div>
|
<div className="sf-metadata-tags-table-cell sf-metadata-tags-table-cell-parent-tags">{gettext('Parent tags')}</div>
|
||||||
<div className="sf-metadata-tags-table-cell"></div>
|
<div className="sf-metadata-tags-table-cell sf-metadata-tags-table-cell-sub-tags-count">{gettext('Sub tags count')}</div>
|
||||||
|
<div className="sf-metadata-tags-table-cell sf-metadata-tags-table-cell-tag-files-count">{gettext('File count')}</div>
|
||||||
|
<div className="sf-metadata-tags-table-cell sf-metadata-tags-table-cell-operations-wrapper"></div>
|
||||||
</div>
|
</div>
|
||||||
{tags.map(tag => {
|
{tags.map(tag => {
|
||||||
const id = getTagId(tag);
|
const id = getTagId(tag);
|
||||||
|
@ -4,23 +4,24 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sf-metadata-tags-table-row.freezed .sf-metadata-tags-table-cell .sf-metadata-tags-table-cell-actions,
|
||||||
.sf-metadata-tags-table-row:hover .sf-metadata-tags-table-cell .sf-metadata-tags-table-cell-actions {
|
.sf-metadata-tags-table-row:hover .sf-metadata-tags-table-cell .sf-metadata-tags-table-cell-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sf-metadata-tags-table-cell-tag {
|
.sf-metadata-tags-table-row .sf-metadata-tags-table-cell-tag {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sf-metadata-tags-table-cell-tag span:hover {
|
.sf-metadata-tags-table-row .sf-metadata-tags-table-cell-tag span:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sf-metadata-tags-table-cell-tag .sf-metadata-tag-color {
|
.sf-metadata-tags-table-row .sf-metadata-tags-table-cell-tag .sf-metadata-tag-color {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
height: 10px;
|
height: 10px;
|
||||||
width: 10px;
|
width: 10px;
|
||||||
@ -28,7 +29,7 @@
|
|||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sf-metadata-tags-table-row:hover .sf-metadata-tags-table-cell .sf-metadata-tags-table-cell-action {
|
.sf-metadata-tags-table-row .sf-metadata-tags-table-cell .sf-metadata-tags-table-cell-action {
|
||||||
height: 20px;
|
height: 20px;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -36,6 +37,11 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sf-metadata-tags-table-row:hover .sf-metadata-tags-table-cell .sf-metadata-tags-table-cell-action .op-icon {
|
.sf-metadata-tags-table-row .sf-metadata-tags-table-cell .sf-metadata-tags-table-cell-action .sf-dropdown-toggle,
|
||||||
|
.sf-metadata-tags-table-row .sf-metadata-tags-table-cell .sf-metadata-tags-table-cell-action .op-icon {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sf-metadata-tags-table-cell-parent-tags .sf-metadata-ui.tags-formatter .sf-metadata-ui-tags-container .sf-metadata-ui-tag {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
@ -1,22 +1,33 @@
|
|||||||
import React, { useCallback, useState } from 'react';
|
import React, { useCallback, useRef, useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { getTagName, getTagColor, getTagFilesCount, getTagId } from '../../../../utils/cell/core';
|
import classnames from 'classnames';
|
||||||
import { gettext } from '../../../../../utils/constants';
|
import { FileTagsFormatter } from '@seafile/sf-metadata-ui-component';
|
||||||
import EditTagDialog from '../../../../components/dialog/edit-tag-dialog';
|
import EditTagDialog from '../../../../components/dialog/edit-tag-dialog';
|
||||||
import DeleteConfirmDialog from '../../../../../metadata/components/dialog/delete-confirm-dialog';
|
import DeleteConfirmDialog from '../../../../../metadata/components/dialog/delete-confirm-dialog';
|
||||||
|
import TagMoreOperation from './tag-more-operation';
|
||||||
|
import SetLinkedTagsPopover from '../../../../components/popover/set-linked-tags-popover';
|
||||||
|
import { getTagName, getTagColor, getTagFilesCount, getTagId, getParentLinks, getSubTagsCount, getSubLinks } from '../../../../utils/cell/core';
|
||||||
|
import { gettext } from '../../../../../utils/constants';
|
||||||
import { useTags } from '../../../../hooks';
|
import { useTags } from '../../../../hooks';
|
||||||
|
import { PRIVATE_COLUMN_KEY } from '../../../../constants';
|
||||||
|
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
const Tag = ({ tags, tag, context, onChangeDisplayTag }) => {
|
const Tag = ({ tags, tag, context, onChangeDisplayTag }) => {
|
||||||
|
const { tagsData, updateTag, deleteTags, addTagLinks, deleteTagLinks } = useTags();
|
||||||
const tagId = getTagId(tag);
|
const tagId = getTagId(tag);
|
||||||
const tagName = getTagName(tag);
|
const tagName = getTagName(tag);
|
||||||
const tagColor = getTagColor(tag);
|
const tagColor = getTagColor(tag);
|
||||||
|
const parentLinks = getParentLinks(tag);
|
||||||
|
const subLinks = getSubLinks(tag);
|
||||||
|
const subTagsCount = getSubTagsCount(tag);
|
||||||
const fileCount = getTagFilesCount(tag);
|
const fileCount = getTagFilesCount(tag);
|
||||||
const [isShowEditTagDialog, setShowEditTagDialog] = useState(false);
|
const [isShowEditTagDialog, setShowEditTagDialog] = useState(false);
|
||||||
const [isShowDeleteDialog, setShowDeleteDialog] = useState(false);
|
const [isShowDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||||
|
const [freeze, setFreeze] = useState(false);
|
||||||
|
const [editingColumnKey, setEditingColumnKey] = useState(null);
|
||||||
|
|
||||||
const { updateTag, deleteTags } = useTags();
|
const operationsPopHandlerRef = useRef(null);
|
||||||
|
|
||||||
const openEditTagDialog = useCallback(() => {
|
const openEditTagDialog = useCallback(() => {
|
||||||
setShowEditTagDialog(true);
|
setShowEditTagDialog(true);
|
||||||
@ -48,23 +59,69 @@ const Tag = ({ tags, tag, context, onChangeDisplayTag }) => {
|
|||||||
}
|
}
|
||||||
}, [tagId, onChangeDisplayTag]);
|
}, [tagId, onChangeDisplayTag]);
|
||||||
|
|
||||||
|
const freezeItem = useCallback(() => {
|
||||||
|
setFreeze(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const unfreezeItem = useCallback(() => {
|
||||||
|
setFreeze(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const hideSetLinkedTagsPopover = useCallback(() => {
|
||||||
|
setEditingColumnKey(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const showParentTagsSetter = useCallback(() => {
|
||||||
|
setEditingColumnKey(PRIVATE_COLUMN_KEY.PARENT_LINKS);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const showSubTagsSetter = useCallback(() => {
|
||||||
|
setEditingColumnKey(PRIVATE_COLUMN_KEY.SUB_LINKS);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getEditingTagLinks = useCallback(() => {
|
||||||
|
return editingColumnKey === PRIVATE_COLUMN_KEY.PARENT_LINKS ? parentLinks : subLinks;
|
||||||
|
}, [editingColumnKey, parentLinks, subLinks]);
|
||||||
|
|
||||||
|
const handleAddTagLinks = useCallback((linkedTag) => {
|
||||||
|
const { _id: otherTagId } = linkedTag;
|
||||||
|
addTagLinks(editingColumnKey, tagId, [otherTagId]);
|
||||||
|
}, [editingColumnKey, tagId, addTagLinks]);
|
||||||
|
|
||||||
|
const handleDeleteTagLinks = useCallback((otherTagId) => {
|
||||||
|
deleteTagLinks(editingColumnKey, tagId, [otherTagId]);
|
||||||
|
}, [editingColumnKey, tagId, deleteTagLinks]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="sf-metadata-tags-table-row">
|
<div className={classnames('sf-metadata-tags-table-row', { 'freezed': freeze })}>
|
||||||
<div className="sf-metadata-tags-table-cell sf-metadata-tags-table-cell-tag" onClick={handleDisplayTag}>
|
<div className="sf-metadata-tags-table-cell sf-metadata-tags-table-cell-tag" onClick={handleDisplayTag}>
|
||||||
<span className="sf-metadata-tag-color" style={{ backgroundColor: tagColor }}></span>
|
<span className="sf-metadata-tag-color" style={{ backgroundColor: tagColor }}></span>
|
||||||
<span className="sf-metadata-tag-name">{tagName}</span>
|
<span className="sf-metadata-tag-name" title={tagName}>{tagName}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="sf-metadata-tags-table-cell">{fileCount}</div>
|
<div className="sf-metadata-tags-table-cell sf-metadata-tags-table-cell-parent-tags">
|
||||||
<div className="sf-metadata-tags-table-cell">
|
<FileTagsFormatter tagsData={tagsData} value={parentLinks} />
|
||||||
|
</div>
|
||||||
|
<div className="sf-metadata-tags-table-cell sf-metadata-tags-table-cell-sub-tags-count" title={subTagsCount}>{subTagsCount}</div>
|
||||||
|
<div className="sf-metadata-tags-table-cell sf-metadata-tags-table-cell-tag-files-count" title={fileCount}>{fileCount}</div>
|
||||||
|
<div className="sf-metadata-tags-table-cell sf-metadata-tags-table-cell-operations-wrapper">
|
||||||
|
<div className="sf-metadata-tags-operation-pop-handler" ref={operationsPopHandlerRef}></div>
|
||||||
<div className="sf-metadata-tags-table-cell-actions">
|
<div className="sf-metadata-tags-table-cell-actions">
|
||||||
{context.canModifyTag() && (
|
{context.canModifyTag() && (
|
||||||
<div className="sf-metadata-tags-table-cell-action" title={gettext('Edit')} onClick={openEditTagDialog}>
|
<>
|
||||||
<i className="op-icon sf3-font-rename sf3-font"></i>
|
<TagMoreOperation
|
||||||
</div>
|
freezeItem={freezeItem}
|
||||||
|
unfreezeItem={unfreezeItem}
|
||||||
|
showParentTagsSetter={showParentTagsSetter}
|
||||||
|
showSubTagsSetter={showSubTagsSetter}
|
||||||
|
/>
|
||||||
|
<div className="sf-metadata-tags-table-cell-action mr-2" title={gettext('Edit')} onClick={openEditTagDialog}>
|
||||||
|
<i className="op-icon sf3-font-rename sf3-font"></i>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
{context.checkCanDeleteTag() && (
|
{context.checkCanDeleteTag() && (
|
||||||
<div className="sf-metadata-tags-table-cell-action ml-2" title={gettext('Delete')} onClick={openDeleteConfirmDialog}>
|
<div className="sf-metadata-tags-table-cell-action" title={gettext('Delete')} onClick={openDeleteConfirmDialog}>
|
||||||
<i className="op-icon sf3-font-delete1 sf3-font"></i>
|
<i className="op-icon sf3-font-delete1 sf3-font"></i>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -77,6 +134,17 @@ const Tag = ({ tags, tag, context, onChangeDisplayTag }) => {
|
|||||||
{isShowDeleteDialog && (
|
{isShowDeleteDialog && (
|
||||||
<DeleteConfirmDialog title={gettext('Delete tag')} content={tagName} onToggle={closeDeleteConfirmDialog} onSubmit={handelDelete} />
|
<DeleteConfirmDialog title={gettext('Delete tag')} content={tagName} onToggle={closeDeleteConfirmDialog} onSubmit={handelDelete} />
|
||||||
)}
|
)}
|
||||||
|
{editingColumnKey && (
|
||||||
|
<SetLinkedTagsPopover
|
||||||
|
target={operationsPopHandlerRef.current}
|
||||||
|
isParentTags={editingColumnKey === PRIVATE_COLUMN_KEY.PARENT_LINKS}
|
||||||
|
tagLinks={getEditingTagLinks()}
|
||||||
|
allTags={tags}
|
||||||
|
hidePopover={hideSetLinkedTagsPopover}
|
||||||
|
addTagLinks={handleAddTagLinks}
|
||||||
|
deleteTagLinks={handleDeleteTagLinks}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,62 @@
|
|||||||
|
import React, { useCallback, useMemo } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ItemDropdownMenu from '../../../../../components/dropdown-menu/item-dropdown-menu';
|
||||||
|
import { gettext } from '../../../../../utils/constants';
|
||||||
|
import { isMobile } from '../../../../../utils/utils';
|
||||||
|
|
||||||
|
const KEY_MORE_OPERATION = {
|
||||||
|
SET_PARENT_TAGS: 'set_parent_tags',
|
||||||
|
SET_SUB_TAGS: 'set_sub_tags',
|
||||||
|
};
|
||||||
|
|
||||||
|
const TagMoreOperation = ({ freezeItem, unfreezeItem, showParentTagsSetter, showSubTagsSetter }) => {
|
||||||
|
|
||||||
|
const operationMenus = useMemo(() => {
|
||||||
|
let menus = [];
|
||||||
|
menus.push(
|
||||||
|
{ key: KEY_MORE_OPERATION.SET_PARENT_TAGS, value: gettext('Set parent tags') },
|
||||||
|
{ key: KEY_MORE_OPERATION.SET_SUB_TAGS, value: gettext('Set sub tags') },
|
||||||
|
);
|
||||||
|
return menus;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const clickMenu = useCallback((key) => {
|
||||||
|
switch (key) {
|
||||||
|
case KEY_MORE_OPERATION.SET_PARENT_TAGS: {
|
||||||
|
showParentTagsSetter();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case KEY_MORE_OPERATION.SET_SUB_TAGS: {
|
||||||
|
showSubTagsSetter();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [showParentTagsSetter, showSubTagsSetter]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="sf-metadata-tags-table-cell-action mr-2" title={gettext('More')}>
|
||||||
|
<ItemDropdownMenu
|
||||||
|
item={{ name: 'metadata-tag' }}
|
||||||
|
menuClassname="metadata-tag-dropdown-menu"
|
||||||
|
toggleClass="sf3-font sf3-font-more"
|
||||||
|
freezeItem={freezeItem}
|
||||||
|
unfreezeItem={unfreezeItem}
|
||||||
|
getMenuList={() => operationMenus}
|
||||||
|
onMenuItemClick={clickMenu}
|
||||||
|
menuStyle={isMobile ? { zIndex: 1050 } : {}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
TagMoreOperation.propTypes = {
|
||||||
|
freezeItem: PropTypes.func,
|
||||||
|
unfreezeItem: PropTypes.func,
|
||||||
|
showParentTagsSetter: PropTypes.func,
|
||||||
|
showSubTagsSetter: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TagMoreOperation;
|
@ -16,7 +16,7 @@ from seahub.views import check_folder_permission
|
|||||||
from seahub.repo_metadata.utils import add_init_metadata_task, gen_unique_id, init_metadata, \
|
from seahub.repo_metadata.utils import add_init_metadata_task, gen_unique_id, init_metadata, \
|
||||||
get_unmodifiable_columns, can_read_metadata, init_faces, \
|
get_unmodifiable_columns, can_read_metadata, init_faces, \
|
||||||
extract_file_details, get_someone_similar_faces, remove_faces_table, FACES_SAVE_PATH, \
|
extract_file_details, get_someone_similar_faces, remove_faces_table, FACES_SAVE_PATH, \
|
||||||
init_tags, remove_tags_table, add_init_face_recognition_task, init_ocr, remove_ocr_column
|
init_tags, init_tag_self_link_columns, remove_tags_table, add_init_face_recognition_task, init_ocr, remove_ocr_column
|
||||||
from seahub.repo_metadata.metadata_server_api import MetadataServerAPI, list_metadata_view_records
|
from seahub.repo_metadata.metadata_server_api import MetadataServerAPI, list_metadata_view_records
|
||||||
from seahub.utils.timeutils import datetime_to_isoformat_timestr
|
from seahub.utils.timeutils import datetime_to_isoformat_timestr
|
||||||
from seahub.utils.repo import is_repo_admin
|
from seahub.utils.repo import is_repo_admin
|
||||||
@ -1936,6 +1936,225 @@ class MetadataTags(APIView):
|
|||||||
return Response({'success': True})
|
return Response({'success': True})
|
||||||
|
|
||||||
|
|
||||||
|
class MetadataTagsLinks(APIView):
|
||||||
|
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
||||||
|
permission_classes = (IsAuthenticated,)
|
||||||
|
throttle_classes = (UserRateThrottle,)
|
||||||
|
|
||||||
|
def post(self, request, repo_id):
|
||||||
|
link_column_key = request.data.get('link_column_key')
|
||||||
|
row_id_map = request.data.get('row_id_map')
|
||||||
|
|
||||||
|
if not link_column_key:
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, 'link_column_key invalid')
|
||||||
|
|
||||||
|
if not row_id_map:
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, 'row_id_map 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)
|
||||||
|
|
||||||
|
link_column = [column for column in columns if column['key'] == link_column_key and column['type'] == 'link']
|
||||||
|
link_column = link_column[0] if link_column else None
|
||||||
|
if not link_column:
|
||||||
|
# init self link columns
|
||||||
|
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;
|
||||||
|
is_linked_back = link_column_key == TAGS_TABLE.columns.sub_links.key if True else False
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
error_msg = 'Internal Server Error'
|
||||||
|
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||||
|
else:
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, 'link column %s not found' % link_column_key)
|
||||||
|
else:
|
||||||
|
link_column_data = link_column.get('data', {})
|
||||||
|
link_id = link_column_data.get('link_id', '')
|
||||||
|
is_linked_back = link_column_data.get('is_linked_back', False)
|
||||||
|
|
||||||
|
if not link_id:
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, 'invalid link column')
|
||||||
|
|
||||||
|
try:
|
||||||
|
metadata_server_api.insert_link(repo_id, link_id, tags_table_id, row_id_map, is_linked_back)
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(e)
|
||||||
|
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error')
|
||||||
|
|
||||||
|
return Response({'success': True})
|
||||||
|
|
||||||
|
def put(self, request, repo_id):
|
||||||
|
link_column_key = request.data.get('link_column_key')
|
||||||
|
row_id_map = request.data.get('row_id_map')
|
||||||
|
|
||||||
|
if not row_id_map:
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, 'row_id_map 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)
|
||||||
|
|
||||||
|
link_column = [column for column in columns if column['key'] == link_column_key and column['type'] == 'link']
|
||||||
|
link_column = link_column[0] if link_column else None
|
||||||
|
if not link_column:
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, 'link column %s not found' % link_column_key)
|
||||||
|
|
||||||
|
link_column_data = link_column.get('data', {})
|
||||||
|
link_id = link_column_data.get('link_id', '')
|
||||||
|
is_linked_back = link_column_data.get('is_linked_back', False)
|
||||||
|
|
||||||
|
if not link_id:
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, 'invalid link column')
|
||||||
|
|
||||||
|
try:
|
||||||
|
metadata_server_api.update_link(repo_id, link_id, tags_table_id, row_id_map, is_linked_back)
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(e)
|
||||||
|
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error')
|
||||||
|
|
||||||
|
return Response({'success': True})
|
||||||
|
|
||||||
|
def delete(self, request, repo_id):
|
||||||
|
link_column_key = request.data.get('link_column_key')
|
||||||
|
row_id_map = request.data.get('row_id_map')
|
||||||
|
|
||||||
|
if not link_column_key:
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, 'link_id invalid')
|
||||||
|
|
||||||
|
if not row_id_map:
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, 'row_id_map 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)
|
||||||
|
|
||||||
|
link_column = [column for column in columns if column['key'] == link_column_key and column['type'] == 'link']
|
||||||
|
link_column = link_column[0] if link_column else None
|
||||||
|
if not link_column:
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, 'link column %s not found' % link_column_key)
|
||||||
|
|
||||||
|
link_column_data = link_column.get('data', {})
|
||||||
|
link_id = link_column_data.get('link_id', '')
|
||||||
|
is_linked_back = link_column_data.get('is_linked_back', False)
|
||||||
|
|
||||||
|
if not link_id:
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, 'invalid link column')
|
||||||
|
|
||||||
|
try:
|
||||||
|
metadata_server_api.delete_link(repo_id, link_id, tags_table_id, row_id_map, is_linked_back)
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(e)
|
||||||
|
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error')
|
||||||
|
|
||||||
|
return Response({'success': True})
|
||||||
|
|
||||||
|
|
||||||
class MetadataFileTags(APIView):
|
class MetadataFileTags(APIView):
|
||||||
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
||||||
permission_classes = (IsAuthenticated,)
|
permission_classes = (IsAuthenticated,)
|
||||||
@ -2013,9 +2232,9 @@ class MetadataFileTags(APIView):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
if not current_tags:
|
if not current_tags:
|
||||||
metadata_server_api.insert_link(repo_id, TAGS_TABLE.link_id, METADATA_TABLE.id, { record_id: tags })
|
metadata_server_api.insert_link(repo_id, TAGS_TABLE.file_link_id, METADATA_TABLE.id, { record_id: tags })
|
||||||
else:
|
else:
|
||||||
metadata_server_api.update_link(repo_id, TAGS_TABLE.link_id, METADATA_TABLE.id, { record_id: tags })
|
metadata_server_api.update_link(repo_id, TAGS_TABLE.file_link_id, METADATA_TABLE.id, { record_id: tags })
|
||||||
success_records.append(record_id)
|
success_records.append(record_id)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
failed_records.append(record_id)
|
failed_records.append(record_id)
|
||||||
|
@ -169,6 +169,18 @@ class MetadataServerAPI:
|
|||||||
response = requests.post(url, json=data, headers=self.headers, timeout=self.timeout)
|
response = requests.post(url, json=data, headers=self.headers, timeout=self.timeout)
|
||||||
return parse_response(response)
|
return parse_response(response)
|
||||||
|
|
||||||
|
def add_link_columns(self, link_id, table_id, other_table_id, table_column, other_table_column):
|
||||||
|
url = f'{METADATA_SERVER_URL}/api/v1/base/{self.base_id}/link-columns'
|
||||||
|
data = {
|
||||||
|
'link_id': link_id,
|
||||||
|
'table_id': table_id,
|
||||||
|
'other_table_id': other_table_id,
|
||||||
|
'table_column': table_column,
|
||||||
|
'other_table_column': other_table_column,
|
||||||
|
}
|
||||||
|
response = requests.post(url, json=data, headers=self.headers, timeout=self.timeout)
|
||||||
|
return parse_response(response)
|
||||||
|
|
||||||
def delete_column(self, table_id, column_key, permanently=False):
|
def delete_column(self, table_id, column_key, permanently=False):
|
||||||
url = f'{METADATA_SERVER_URL}/api/v1/base/{self.base_id}/columns'
|
url = f'{METADATA_SERVER_URL}/api/v1/base/{self.base_id}/columns'
|
||||||
data = {
|
data = {
|
||||||
@ -211,22 +223,35 @@ class MetadataServerAPI:
|
|||||||
|
|
||||||
|
|
||||||
# link
|
# link
|
||||||
def insert_link(self, base_id, link_id, table_id, row_id_map):
|
def insert_link(self, base_id, link_id, table_id, row_id_map, is_linked_back=False):
|
||||||
url = f'{METADATA_SERVER_URL}/api/v1/base/{base_id}/links'
|
url = f'{METADATA_SERVER_URL}/api/v1/base/{base_id}/links'
|
||||||
data = {
|
data = {
|
||||||
'link_id': link_id,
|
'link_id': link_id,
|
||||||
'table_id': table_id,
|
'table_id': table_id,
|
||||||
'row_id_map': row_id_map
|
'is_linked_back': is_linked_back,
|
||||||
|
'row_id_map': row_id_map,
|
||||||
}
|
}
|
||||||
response = requests.post(url, json=data, headers=self.headers, timeout=self.timeout)
|
response = requests.post(url, json=data, headers=self.headers, timeout=self.timeout)
|
||||||
return parse_response(response)
|
return parse_response(response)
|
||||||
|
|
||||||
def update_link(self, base_id, link_id, table_id, row_id_map):
|
def update_link(self, base_id, link_id, table_id, row_id_map, is_linked_back=False):
|
||||||
url = f'{METADATA_SERVER_URL}/api/v1/base/{base_id}/links'
|
url = f'{METADATA_SERVER_URL}/api/v1/base/{base_id}/links'
|
||||||
data = {
|
data = {
|
||||||
'link_id': link_id,
|
'link_id': link_id,
|
||||||
'table_id': table_id,
|
'table_id': table_id,
|
||||||
|
'is_linked_back': is_linked_back,
|
||||||
'row_id_map': row_id_map
|
'row_id_map': row_id_map
|
||||||
}
|
}
|
||||||
response = requests.put(url, json=data, headers=self.headers, timeout=self.timeout)
|
response = requests.put(url, json=data, headers=self.headers, timeout=self.timeout)
|
||||||
return parse_response(response)
|
return parse_response(response)
|
||||||
|
|
||||||
|
def delete_link(self, base_id, link_id, table_id, row_id_map, is_linked_back=False):
|
||||||
|
url = f'{METADATA_SERVER_URL}/api/v1/base/{base_id}/links'
|
||||||
|
data = {
|
||||||
|
'link_id': link_id,
|
||||||
|
'table_id': table_id,
|
||||||
|
'is_linked_back': is_linked_back,
|
||||||
|
'row_id_map': row_id_map
|
||||||
|
}
|
||||||
|
response = requests.delete(url, json=data, headers=self.headers, timeout=self.timeout)
|
||||||
|
return parse_response(response)
|
||||||
|
@ -2,7 +2,7 @@ from django.urls import re_path
|
|||||||
from .apis import MetadataRecords, MetadataManage, MetadataColumns, MetadataRecordInfo, \
|
from .apis import MetadataRecords, MetadataManage, MetadataColumns, MetadataRecordInfo, \
|
||||||
MetadataFolders, MetadataViews, MetadataViewsMoveView, MetadataViewsDetailView, MetadataViewsDuplicateView, FacesRecords, \
|
MetadataFolders, MetadataViews, MetadataViewsMoveView, MetadataViewsDetailView, MetadataViewsDuplicateView, FacesRecords, \
|
||||||
FaceRecognitionManage, FacesRecord, MetadataExtractFileDetails, PeoplePhotos, MetadataTagsStatusManage, MetadataTags, \
|
FaceRecognitionManage, FacesRecord, MetadataExtractFileDetails, PeoplePhotos, MetadataTagsStatusManage, MetadataTags, \
|
||||||
MetadataFileTags, MetadataTagFiles, MetadataDetailsSettingsView, MetadataOCRManageView
|
MetadataTagsLinks, MetadataFileTags, MetadataTagFiles, 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'),
|
||||||
@ -34,6 +34,7 @@ urlpatterns = [
|
|||||||
# tags api
|
# tags api
|
||||||
re_path(r'^tags-status/$', MetadataTagsStatusManage.as_view(), name='api-v2.1-metadata-tags-status'),
|
re_path(r'^tags-status/$', MetadataTagsStatusManage.as_view(), name='api-v2.1-metadata-tags-status'),
|
||||||
re_path(r'^tags/$', MetadataTags.as_view(), name='api-v2.1-metadata-tags'),
|
re_path(r'^tags/$', MetadataTags.as_view(), name='api-v2.1-metadata-tags'),
|
||||||
|
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'),
|
||||||
]
|
]
|
||||||
|
@ -177,52 +177,71 @@ def remove_faces_table(metadata_server_api):
|
|||||||
|
|
||||||
|
|
||||||
# tag
|
# tag
|
||||||
def get_tag_link_column(table_id):
|
|
||||||
from seafevents.repo_metadata.constants import METADATA_TABLE, TAGS_TABLE
|
|
||||||
columns = [
|
|
||||||
METADATA_TABLE.columns.tags.to_dict({
|
|
||||||
'link_id': TAGS_TABLE.link_id,
|
|
||||||
'table_id': METADATA_TABLE.id,
|
|
||||||
'other_table_id': table_id,
|
|
||||||
'display_column_key': TAGS_TABLE.columns.name.key,
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
|
|
||||||
return columns
|
|
||||||
|
|
||||||
|
|
||||||
def get_tag_columns(table_id):
|
def get_tag_columns(table_id):
|
||||||
from seafevents.repo_metadata.constants import METADATA_TABLE, TAGS_TABLE
|
from seafevents.repo_metadata.constants import TAGS_TABLE
|
||||||
columns = [
|
columns = [
|
||||||
TAGS_TABLE.columns.name.to_dict(),
|
TAGS_TABLE.columns.name.to_dict(),
|
||||||
TAGS_TABLE.columns.color.to_dict(),
|
TAGS_TABLE.columns.color.to_dict(),
|
||||||
TAGS_TABLE.columns.file_links.to_dict({
|
|
||||||
'link_id': TAGS_TABLE.link_id,
|
|
||||||
'table_id': METADATA_TABLE.id,
|
|
||||||
'other_table_id': table_id,
|
|
||||||
'display_column_key': METADATA_TABLE.columns.id.key,
|
|
||||||
}),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
return columns
|
return columns
|
||||||
|
|
||||||
|
|
||||||
def init_tags(metadata_server_api):
|
def init_tag_file_links_column(metadata_server_api, tag_table_id):
|
||||||
from seafevents.repo_metadata.constants import METADATA_TABLE, TAGS_TABLE
|
from seafevents.repo_metadata.constants import METADATA_TABLE, TAGS_TABLE
|
||||||
|
|
||||||
|
file_link_id = TAGS_TABLE.file_link_id
|
||||||
|
table_id = METADATA_TABLE.id
|
||||||
|
other_table_id = tag_table_id
|
||||||
|
table_column = {
|
||||||
|
'key': METADATA_TABLE.columns.tags.key,
|
||||||
|
'name': METADATA_TABLE.columns.tags.name,
|
||||||
|
'display_column_key': TAGS_TABLE.columns.name.name,
|
||||||
|
}
|
||||||
|
other_table_column = {
|
||||||
|
'key': TAGS_TABLE.columns.file_links.key,
|
||||||
|
'name': TAGS_TABLE.columns.file_links.name,
|
||||||
|
'display_column_key': TAGS_TABLE.columns.id.key,
|
||||||
|
}
|
||||||
|
metadata_server_api.add_link_columns(file_link_id, table_id, other_table_id, table_column, other_table_column)
|
||||||
|
|
||||||
|
def init_tag_self_link_columns(metadata_server_api, tag_table_id):
|
||||||
|
from seafevents.repo_metadata.constants import TAGS_TABLE
|
||||||
|
link_id = TAGS_TABLE.self_link_id
|
||||||
|
table_id = tag_table_id
|
||||||
|
other_table_id = tag_table_id
|
||||||
|
|
||||||
|
# as parent tags which is_linked_back is false
|
||||||
|
table_column = {
|
||||||
|
'key': TAGS_TABLE.columns.parent_links.key,
|
||||||
|
'name': TAGS_TABLE.columns.parent_links.name,
|
||||||
|
'display_column_key': TAGS_TABLE.columns.id.key,
|
||||||
|
}
|
||||||
|
|
||||||
|
# as sub tags which is_linked_back is true
|
||||||
|
other_table_column = {
|
||||||
|
'key': TAGS_TABLE.columns.sub_links.key,
|
||||||
|
'name': TAGS_TABLE.columns.sub_links.name,
|
||||||
|
'display_column_key': TAGS_TABLE.columns.id.key,
|
||||||
|
}
|
||||||
|
metadata_server_api.add_link_columns(link_id, table_id, other_table_id, table_column, other_table_column)
|
||||||
|
|
||||||
|
|
||||||
|
def init_tags(metadata_server_api):
|
||||||
|
from seafevents.repo_metadata.constants import TAGS_TABLE
|
||||||
|
|
||||||
remove_tags_table(metadata_server_api)
|
remove_tags_table(metadata_server_api)
|
||||||
resp = metadata_server_api.create_table(TAGS_TABLE.name)
|
resp = metadata_server_api.create_table(TAGS_TABLE.name)
|
||||||
|
|
||||||
table_id = resp['id']
|
table_id = resp['id']
|
||||||
|
|
||||||
# init link column
|
|
||||||
link_column = get_tag_link_column(table_id)
|
|
||||||
metadata_server_api.add_columns(METADATA_TABLE.id, link_column)
|
|
||||||
|
|
||||||
# init columns
|
# init columns
|
||||||
tag_columns = get_tag_columns(table_id)
|
tag_columns = get_tag_columns(table_id)
|
||||||
metadata_server_api.add_columns(table_id, tag_columns)
|
metadata_server_api.add_columns(table_id, tag_columns)
|
||||||
|
|
||||||
|
# init link columns
|
||||||
|
init_tag_file_links_column(metadata_server_api, table_id)
|
||||||
|
init_tag_self_link_columns(metadata_server_api, table_id)
|
||||||
|
|
||||||
def remove_tags_table(metadata_server_api):
|
def remove_tags_table(metadata_server_api):
|
||||||
from seafevents.repo_metadata.constants import METADATA_TABLE, TAGS_TABLE
|
from seafevents.repo_metadata.constants import METADATA_TABLE, TAGS_TABLE
|
||||||
|
Loading…
Reference in New Issue
Block a user