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