1
0
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:
Jerry Ren 2024-12-20 17:59:47 +08:00 committed by GitHub
parent bde5ec063e
commit cb73865b21
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 1151 additions and 71 deletions

View File

@ -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();

View File

@ -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;

View File

@ -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;
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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 = [

View File

@ -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;

View File

@ -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,
}}>

View File

@ -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;

View File

@ -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;
}

View File

@ -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'],
};

View File

@ -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) {

View File

@ -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;

View 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;
};

View File

@ -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;
}

View File

@ -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);

View File

@ -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;
}

View File

@ -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}
/>
)}
</>
);
};

View File

@ -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;

View File

@ -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)

View File

@ -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)

View File

@ -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'),
]

View File

@ -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