mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-27 15:54:39 +00:00
Export/Import tags (#7888)
* export tags * remove child links * update * optimize code and update import tags loading * optimize code * optimize var --------- Co-authored-by: 孙永强 <11704063+s-yongqiang@user.noreply.gitee.com>
This commit is contained in:
@@ -161,7 +161,7 @@ class DirPath extends React.Component {
|
|||||||
<span className="path-split">/</span>
|
<span className="path-split">/</span>
|
||||||
<span className="path-item path-item-read-only">{gettext('Tags')}</span>
|
<span className="path-item path-item-read-only">{gettext('Tags')}</span>
|
||||||
<span className="path-split">/</span>
|
<span className="path-split">/</span>
|
||||||
<TagViewName id={tagId} canSelectAllTags={canSelectAllTags} />
|
<TagViewName id={tagId} canSelectAllTags={canSelectAllTags} repoID={this.props.repoID} />
|
||||||
{children && (
|
{children && (
|
||||||
<>
|
<>
|
||||||
<span className="path-split">/</span>
|
<span className="path-split">/</span>
|
||||||
|
41
frontend/src/components/dialog/import-tags-dialog.js
Normal file
41
frontend/src/components/dialog/import-tags-dialog.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Modal, ModalBody } from 'reactstrap';
|
||||||
|
import { gettext } from '../../utils/constants';
|
||||||
|
import SeahubModalHeader from '@/components/common/seahub-modal-header';
|
||||||
|
import Loading from '../loading';
|
||||||
|
|
||||||
|
import '../../css/seahub-io-dialog.css';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
toggleDialog: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
class ImportTagsDialog extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle = () => {
|
||||||
|
this.props.toggleDialog();
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Modal className='seahub-io-dialog' isOpen={true} toggle={this.toggle}>
|
||||||
|
<SeahubModalHeader toggle={this.toggle}>{gettext('Import tags')}</SeahubModalHeader>
|
||||||
|
<ModalBody>
|
||||||
|
<>
|
||||||
|
<Loading/>
|
||||||
|
<div className="seahub-io-dialog-parsing-text">{gettext('Importing tags...')}</div>
|
||||||
|
</>
|
||||||
|
</ModalBody>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImportTagsDialog.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default ImportTagsDialog;
|
@@ -6,6 +6,7 @@ export const OPERATION = {
|
|||||||
NEW_SUB_TAG: 'new_sub_tag',
|
NEW_SUB_TAG: 'new_sub_tag',
|
||||||
MERGE_TAGS: 'merge_tags',
|
MERGE_TAGS: 'merge_tags',
|
||||||
ADD_CHILD_TAGS: 'add_child_tags',
|
ADD_CHILD_TAGS: 'add_child_tags',
|
||||||
|
EXPORT_TAGS: 'export_tags',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const POPUP_EDITOR_OPERATION_KEYS = [OPERATION.SET_SUB_TAGS, OPERATION.ADD_CHILD_TAGS];
|
export const POPUP_EDITOR_OPERATION_KEYS = [OPERATION.SET_SUB_TAGS, OPERATION.ADD_CHILD_TAGS];
|
||||||
|
@@ -230,7 +230,7 @@ const style = `
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-tip {
|
.metrics-container .loading-tip {
|
||||||
margin: 100px auto;
|
margin: 100px auto;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
@@ -143,6 +143,20 @@ class TagsManagerAPI {
|
|||||||
return this.req.post(url);
|
return this.req.post(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
exportTags = (repoID, tagsIds) => {
|
||||||
|
const url = this.server + '/api/v2.1/repos/' + repoID + '/metadata/export-tags/';
|
||||||
|
const params = {
|
||||||
|
tags_ids: tagsIds,
|
||||||
|
};
|
||||||
|
return this.req.post(url, params);
|
||||||
|
};
|
||||||
|
|
||||||
|
importTags = (repoID, file) => {
|
||||||
|
const url = this.server + '/api/v2.1/repos/' + repoID + '/metadata/import-tags/';
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
return this._sendPostRequest(url, formData);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const tagsAPI = new TagsManagerAPI();
|
const tagsAPI = new TagsManagerAPI();
|
||||||
|
@@ -5,14 +5,18 @@ import EditTagDialog from '../../dialog/edit-tag-dialog';
|
|||||||
import { isEnter, isSpace } from '../../../../utils/hotkey';
|
import { isEnter, isSpace } from '../../../../utils/hotkey';
|
||||||
import { gettext } from '../../../../utils/constants';
|
import { gettext } from '../../../../utils/constants';
|
||||||
import { useTags } from '../../../hooks';
|
import { useTags } from '../../../hooks';
|
||||||
|
import tagsAPI from '../../../api';
|
||||||
|
import ImportTagsDialog from '../../../../components/dialog/import-tags-dialog';
|
||||||
|
|
||||||
|
import toaster from '../../../../components/toast';
|
||||||
|
import { Utils } from '../../../../utils/utils';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
const AllTagsOperationToolbar = () => {
|
const AllTagsOperationToolbar = ({ repoID }) => {
|
||||||
const [isMenuOpen, setMenuOpen] = useState(false);
|
const [isMenuOpen, setMenuOpen] = useState(false);
|
||||||
const [isShowEditTagDialog, setShowEditTagDialog] = useState(false);
|
const [isShowEditTagDialog, setShowEditTagDialog] = useState(false);
|
||||||
|
const [isShowImportLoadingDialog, setShowImportLoadingDialog] = useState(false);
|
||||||
const { tagsData, addTag } = useTags();
|
const { tagsData, addTag, reloadTags } = useTags();
|
||||||
|
|
||||||
const tags = useMemo(() => {
|
const tags = useMemo(() => {
|
||||||
if (!tagsData) return [];
|
if (!tagsData) return [];
|
||||||
@@ -41,6 +45,28 @@ const AllTagsOperationToolbar = () => {
|
|||||||
addTag(tag, callback);
|
addTag(tag, callback);
|
||||||
}, [addTag]);
|
}, [addTag]);
|
||||||
|
|
||||||
|
const handleImportTags = useCallback(() => {
|
||||||
|
const fileInput = document.createElement('input');
|
||||||
|
fileInput.type = 'file';
|
||||||
|
fileInput.accept = '.json';
|
||||||
|
fileInput.onchange = async (e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
setShowImportLoadingDialog(true);
|
||||||
|
tagsAPI.importTags(repoID, file).then(res => {
|
||||||
|
toaster.success(gettext('Successfully imported tags.'));
|
||||||
|
setTimeout(() => {
|
||||||
|
reloadTags(true);
|
||||||
|
}, 10);
|
||||||
|
}).catch(error => {
|
||||||
|
const errorMsg = Utils.getErrorMsg(error);
|
||||||
|
toaster.danger(errorMsg || gettext('Failed to import tags'));
|
||||||
|
}).finally(() => {
|
||||||
|
setShowImportLoadingDialog(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
fileInput.click();
|
||||||
|
}, [reloadTags, repoID]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="dir-operation">
|
<div className="dir-operation">
|
||||||
@@ -62,12 +88,19 @@ const AllTagsOperationToolbar = () => {
|
|||||||
<i className="sf3-font sf3-font-new mr-2 dropdown-item-icon"></i>
|
<i className="sf3-font sf3-font-new mr-2 dropdown-item-icon"></i>
|
||||||
{gettext('New tag')}
|
{gettext('New tag')}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
<DropdownItem onClick={handleImportTags}>
|
||||||
|
<i className="sf3-font-import-sdoc sf3-font mr-2 dropdown-item-icon"></i>
|
||||||
|
{gettext('Import tags')}
|
||||||
|
</DropdownItem>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
{isShowEditTagDialog && (
|
{isShowEditTagDialog && (
|
||||||
<EditTagDialog tags={tags} title={gettext('New tag')} onToggle={closeAddTag} onSubmit={handelAddTags} />
|
<EditTagDialog tags={tags} title={gettext('New tag')} onToggle={closeAddTag} onSubmit={handelAddTags} />
|
||||||
)}
|
)}
|
||||||
|
{isShowImportLoadingDialog && (
|
||||||
|
<ImportTagsDialog toggleDialog={() => setShowImportLoadingDialog(false)} />
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -8,7 +8,7 @@ import { gettext } from '../../../utils/constants';
|
|||||||
import AllTagsOperationToolbar from './all-tags-operation-toolbar';
|
import AllTagsOperationToolbar from './all-tags-operation-toolbar';
|
||||||
import { EVENT_BUS_TYPE } from '../../../metadata/constants';
|
import { EVENT_BUS_TYPE } from '../../../metadata/constants';
|
||||||
|
|
||||||
const TagViewName = ({ id, canSelectAllTags }) => {
|
const TagViewName = ({ id, canSelectAllTags, repoID }) => {
|
||||||
const { tagsData, context } = useTags();
|
const { tagsData, context } = useTags();
|
||||||
|
|
||||||
const selectAllTags = () => {
|
const selectAllTags = () => {
|
||||||
@@ -31,7 +31,7 @@ const TagViewName = ({ id, canSelectAllTags }) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<AllTagsOperationToolbar/>
|
<AllTagsOperationToolbar repoID={repoID} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const tag = getRowById(tagsData, id);
|
const tag = getRowById(tagsData, id);
|
||||||
@@ -43,6 +43,7 @@ const TagViewName = ({ id, canSelectAllTags }) => {
|
|||||||
|
|
||||||
TagViewName.propTypes = {
|
TagViewName.propTypes = {
|
||||||
id: PropTypes.string,
|
id: PropTypes.string,
|
||||||
|
repoID: PropTypes.string,
|
||||||
canSelectAllTags: PropTypes.bool,
|
canSelectAllTags: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -98,6 +98,11 @@ class Context {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
checkCanExportTags = () => {
|
||||||
|
if (this.permission === 'r') return false;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
canModifyTags = () => {
|
canModifyTags = () => {
|
||||||
if (this.permission === 'r') return false;
|
if (this.permission === 'r') return false;
|
||||||
return true;
|
return true;
|
||||||
@@ -131,6 +136,14 @@ class Context {
|
|||||||
mergeTags = (target_tag_id, merged_tags_ids) => {
|
mergeTags = (target_tag_id, merged_tags_ids) => {
|
||||||
return this.api.mergeTags(this.repoId, target_tag_id, merged_tags_ids);
|
return this.api.mergeTags(this.repoId, target_tag_id, merged_tags_ids);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
exportTags = (tagsIds) => {
|
||||||
|
return this.api.exportTags(this.repoId, tagsIds);
|
||||||
|
};
|
||||||
|
|
||||||
|
importTags = (file) => {
|
||||||
|
return this.api.importTags(this.repoId, file);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Context;
|
export default Context;
|
||||||
|
@@ -42,9 +42,9 @@ export const TagsProvider = ({ repoID, currentPath, selectTagsView, tagsChangedC
|
|||||||
setTagsData(data);
|
setTagsData(data);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const reloadTags = useCallback(() => {
|
const reloadTags = useCallback((force = false) => {
|
||||||
setReloading(true);
|
setReloading(true);
|
||||||
storeRef.current.reload(PER_LOAD_NUMBER).then(() => {
|
storeRef.current.reload(PER_LOAD_NUMBER, force).then(() => {
|
||||||
setTagsData(storeRef.current.data);
|
setTagsData(storeRef.current.data);
|
||||||
setReloading(false);
|
setReloading(false);
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
@@ -326,6 +326,7 @@ export const TagsProvider = ({ repoID, currentPath, selectTagsView, tagsChangedC
|
|||||||
modifyColumnWidth,
|
modifyColumnWidth,
|
||||||
modifyLocalFileTags,
|
modifyLocalFileTags,
|
||||||
modifyTagsSort,
|
modifyTagsSort,
|
||||||
|
reloadTags,
|
||||||
}}>
|
}}>
|
||||||
{children}
|
{children}
|
||||||
</TagsContext.Provider>
|
</TagsContext.Provider>
|
||||||
|
@@ -66,9 +66,13 @@ class Store {
|
|||||||
await this.loadTagsData(limit);
|
await this.loadTagsData(limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
async reload(limit = PER_LOAD_NUMBER) {
|
async reload(limit = PER_LOAD_NUMBER, force = false) {
|
||||||
const currentTime = new Date();
|
const currentTime = new Date();
|
||||||
if (dayjs(currentTime).diff(this.loadTime, 'hours') > 1) {
|
if (force) {
|
||||||
|
this.loadTime = currentTime;
|
||||||
|
this.startIndex = 0;
|
||||||
|
await this.loadTagsData(limit);
|
||||||
|
} else if (dayjs(currentTime).diff(this.loadTime, 'hours') > 1) {
|
||||||
this.loadTime = currentTime;
|
this.loadTime = currentTime;
|
||||||
this.startIndex = 0;
|
this.startIndex = 0;
|
||||||
await this.loadTagsData(limit);
|
await this.loadTagsData(limit);
|
||||||
|
@@ -9,7 +9,7 @@ export const OPERATION_TYPE = {
|
|||||||
DELETE_TAG_LINKS: 'delete_tag_links',
|
DELETE_TAG_LINKS: 'delete_tag_links',
|
||||||
DELETE_TAGS_LINKS: 'delete_tags_links',
|
DELETE_TAGS_LINKS: 'delete_tags_links',
|
||||||
MERGE_TAGS: 'merge_tags',
|
MERGE_TAGS: 'merge_tags',
|
||||||
|
EXPORT_TAGS: 'export_tags',
|
||||||
MODIFY_LOCAL_RECORDS: 'modify_local_records',
|
MODIFY_LOCAL_RECORDS: 'modify_local_records',
|
||||||
MODIFY_LOCAL_FILE_TAGS: 'modify_local_file_tags',
|
MODIFY_LOCAL_FILE_TAGS: 'modify_local_file_tags',
|
||||||
|
|
||||||
@@ -32,6 +32,7 @@ export const OPERATION_ATTRIBUTES = {
|
|||||||
[OPERATION_TYPE.MODIFY_LOCAL_FILE_TAGS]: ['file_id', 'tags_ids'],
|
[OPERATION_TYPE.MODIFY_LOCAL_FILE_TAGS]: ['file_id', 'tags_ids'],
|
||||||
[OPERATION_TYPE.MODIFY_COLUMN_WIDTH]: ['column_key', 'new_width', 'old_width'],
|
[OPERATION_TYPE.MODIFY_COLUMN_WIDTH]: ['column_key', 'new_width', 'old_width'],
|
||||||
[OPERATION_TYPE.MODIFY_TAGS_SORT]: ['sort'],
|
[OPERATION_TYPE.MODIFY_TAGS_SORT]: ['sort'],
|
||||||
|
[OPERATION_TYPE.EXPORT_TAGS]: ['repo_id', 'tags_ids'],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const UNDO_OPERATION_TYPE = [
|
export const UNDO_OPERATION_TYPE = [
|
||||||
|
@@ -3,6 +3,7 @@ import { OPERATION_TYPE } from './operations';
|
|||||||
import { getColumnByKey } from '../../metadata/utils/column';
|
import { getColumnByKey } from '../../metadata/utils/column';
|
||||||
import ObjectUtils from '../../utils/object';
|
import ObjectUtils from '../../utils/object';
|
||||||
import { PRIVATE_COLUMN_KEY } from '../constants';
|
import { PRIVATE_COLUMN_KEY } from '../constants';
|
||||||
|
import tagsAPI from '../api';
|
||||||
|
|
||||||
const MAX_LOAD_RECORDS = 100;
|
const MAX_LOAD_RECORDS = 100;
|
||||||
|
|
||||||
@@ -115,6 +116,37 @@ class ServerOperator {
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case OPERATION_TYPE.EXPORT_TAGS: {
|
||||||
|
const { repo_id, tags_ids } = operation;
|
||||||
|
tagsAPI.exportTags(repo_id, tags_ids).then((res) => {
|
||||||
|
let fileName;
|
||||||
|
if (res.data && Array.isArray(res.data)) {
|
||||||
|
if (res.data.length === 1) {
|
||||||
|
fileName = `${res.data[0]._tag_name}.json`;
|
||||||
|
} else {
|
||||||
|
const now = new Date();
|
||||||
|
const dateStr = now.toISOString().split('T')[0];
|
||||||
|
fileName = `tags-export-${dateStr}.json`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fileName = 'tags.json';
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = new Blob([JSON.stringify(res.data, null, 2)], { type: 'application/json' });
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = fileName;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
document.body.removeChild(a);
|
||||||
|
callback({ operation });
|
||||||
|
}).catch((error) => {
|
||||||
|
callback({ error: gettext('Failed to export tags') });
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
case OPERATION_TYPE.RESTORE_RECORDS: {
|
case OPERATION_TYPE.RESTORE_RECORDS: {
|
||||||
const { repo_id, rows_data } = operation;
|
const { repo_id, rows_data } = operation;
|
||||||
if (!Array.isArray(rows_data) || rows_data.length === 0) {
|
if (!Array.isArray(rows_data) || rows_data.length === 0) {
|
||||||
@@ -194,7 +226,6 @@ class ServerOperator {
|
|||||||
}).catch (error => {
|
}).catch (error => {
|
||||||
// for debug
|
// for debug
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log(error);
|
|
||||||
this.asyncReloadRecords(restRowsIds, repoId, relatedColumnKeyMap, callback);
|
this.asyncReloadRecords(restRowsIds, repoId, relatedColumnKeyMap, callback);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@@ -27,6 +27,7 @@ export const createContextMenuOptions = ({
|
|||||||
onMergeTags,
|
onMergeTags,
|
||||||
}) => {
|
}) => {
|
||||||
const canDeleteTag = context.checkCanDeleteTag();
|
const canDeleteTag = context.checkCanDeleteTag();
|
||||||
|
const canExportTags = context.checkCanExportTags();
|
||||||
const canAddTag = context.canAddTag();
|
const canAddTag = context.canAddTag();
|
||||||
const eventBus = EventBus.getInstance();
|
const eventBus = EventBus.getInstance();
|
||||||
|
|
||||||
@@ -58,6 +59,10 @@ export const createContextMenuOptions = ({
|
|||||||
eventBus.dispatch(EVENT_BUS_TYPE.OPEN_EDITOR, null, option.value);
|
eventBus.dispatch(EVENT_BUS_TYPE.OPEN_EDITOR, null, option.value);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case OPERATION.EXPORT_TAGS: {
|
||||||
|
eventBus.dispatch(EVENT_BUS_TYPE.EXPORT_TAGS, option.tagsIds);
|
||||||
|
break;
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -78,6 +83,13 @@ export const createContextMenuOptions = ({
|
|||||||
tagsIds.push(tag._id);
|
tagsIds.push(tag._id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (canExportTags && tagsIds.length > 0) {
|
||||||
|
options.push({
|
||||||
|
label: gettext('Export tags'),
|
||||||
|
value: OPERATION.EXPORT_TAGS,
|
||||||
|
tagsIds,
|
||||||
|
});
|
||||||
|
}
|
||||||
if (canDeleteTag && tagsIds.length > 0) {
|
if (canDeleteTag && tagsIds.length > 0) {
|
||||||
if (tagsIds.length === 1) {
|
if (tagsIds.length === 1) {
|
||||||
options.push({
|
options.push({
|
||||||
@@ -125,6 +137,13 @@ export const createContextMenuOptions = ({
|
|||||||
tagsIds,
|
tagsIds,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (canExportTags && tagsIds.length > 0) {
|
||||||
|
options.push({
|
||||||
|
label: gettext('Export tags'),
|
||||||
|
value: OPERATION.EXPORT_TAGS,
|
||||||
|
tagsIds,
|
||||||
|
});
|
||||||
|
}
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,6 +188,13 @@ export const createContextMenuOptions = ({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (isNameColumn && canExportTags) {
|
||||||
|
options.push({
|
||||||
|
label: gettext('Export tags'),
|
||||||
|
value: OPERATION.EXPORT_TAGS,
|
||||||
|
tagsIds: [tag._id],
|
||||||
|
});
|
||||||
|
}
|
||||||
return options;
|
return options;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -17,6 +17,9 @@ import { isNumber } from '../../../../utils/number';
|
|||||||
import { getTreeNodeByKey, getTreeNodeId } from '../../../../components/sf-table/utils/tree';
|
import { getTreeNodeByKey, getTreeNodeId } from '../../../../components/sf-table/utils/tree';
|
||||||
import { getRowById } from '../../../../components/sf-table/utils/table';
|
import { getRowById } from '../../../../components/sf-table/utils/table';
|
||||||
import { getParentLinks } from '../../../utils/cell';
|
import { getParentLinks } from '../../../utils/cell';
|
||||||
|
import ServerOperator from '../../../store/server-operator';
|
||||||
|
import toaster from '../../../../components/toast';
|
||||||
|
import { OPERATION_TYPE } from '../../../store/operations/constants';
|
||||||
|
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
@@ -269,18 +272,35 @@ const TagsTable = ({
|
|||||||
}, 0);
|
}, 0);
|
||||||
}, [eventBus, toggleShowDirentToolbar]);
|
}, [eventBus, toggleShowDirentToolbar]);
|
||||||
|
|
||||||
|
const onExportTags = useCallback((tagsIds) => {
|
||||||
|
const operation = {
|
||||||
|
op_type: OPERATION_TYPE.EXPORT_TAGS,
|
||||||
|
repo_id: context.repoId,
|
||||||
|
tags_ids: tagsIds
|
||||||
|
};
|
||||||
|
const serverOperator = new ServerOperator(context);
|
||||||
|
serverOperator.applyOperation(operation, null, ({ error }) => {
|
||||||
|
if (error) {
|
||||||
|
toaster.danger(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [context]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubscribeUpdateSearchResult = eventBus.subscribe(EVENT_BUS_TYPE.UPDATE_SEARCH_RESULT, updateSearchResult);
|
const unsubscribeUpdateSearchResult = eventBus.subscribe(EVENT_BUS_TYPE.UPDATE_SEARCH_RESULT, updateSearchResult);
|
||||||
const unsubscribeDeleteTags = eventBus.subscribe(EVENT_BUS_TYPE.DELETE_TAGS, onDeleteTags);
|
const unsubscribeDeleteTags = eventBus.subscribe(EVENT_BUS_TYPE.DELETE_TAGS, onDeleteTags);
|
||||||
const unsubscribeMergeTags = eventBus.subscribe(EVENT_BUS_TYPE.MERGE_TAGS, onMergeTags);
|
const unsubscribeMergeTags = eventBus.subscribe(EVENT_BUS_TYPE.MERGE_TAGS, onMergeTags);
|
||||||
const unsubscribeNewSubTag = eventBus.subscribe(EVENT_BUS_TYPE.NEW_SUB_TAG, onNewSubTag);
|
const unsubscribeNewSubTag = eventBus.subscribe(EVENT_BUS_TYPE.NEW_SUB_TAG, onNewSubTag);
|
||||||
|
const unsubscribeExportTags = eventBus.subscribe(EVENT_BUS_TYPE.EXPORT_TAGS, onExportTags);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
unsubscribeUpdateSearchResult();
|
unsubscribeUpdateSearchResult();
|
||||||
unsubscribeDeleteTags();
|
unsubscribeDeleteTags();
|
||||||
unsubscribeMergeTags();
|
unsubscribeMergeTags();
|
||||||
unsubscribeNewSubTag();
|
unsubscribeNewSubTag();
|
||||||
|
unsubscribeExportTags();
|
||||||
};
|
};
|
||||||
}, [eventBus, updateSearchResult, onDeleteTags, onMergeTags, onNewSubTag, updateSelectedTagIds]);
|
}, [eventBus, updateSearchResult, onDeleteTags, onMergeTags, onNewSubTag, onExportTags]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@@ -9,6 +9,7 @@ from rest_framework.permissions import IsAuthenticated
|
|||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
|
from django.http import HttpResponse
|
||||||
from seahub.api2.utils import api_error
|
from seahub.api2.utils import api_error
|
||||||
from seahub.api2.throttling import UserRateThrottle
|
from seahub.api2.throttling import UserRateThrottle
|
||||||
from seahub.api2.authentication import TokenAuthentication
|
from seahub.api2.authentication import TokenAuthentication
|
||||||
@@ -2964,3 +2965,207 @@ class MetadataMigrateTags(APIView):
|
|||||||
error_msg = 'Internal Server Error'
|
error_msg = 'Internal Server Error'
|
||||||
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||||
return Response({'success': True})
|
return Response({'success': True})
|
||||||
|
|
||||||
|
|
||||||
|
class MetadataExportTags(APIView):
|
||||||
|
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
||||||
|
permission_classes = (IsAuthenticated,)
|
||||||
|
throttle_classes = (UserRateThrottle,)
|
||||||
|
|
||||||
|
def post(self, request, repo_id):
|
||||||
|
tags_ids = request.data.get('tags_ids', None)
|
||||||
|
if not tags_ids:
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, 'tags_ids invalid')
|
||||||
|
|
||||||
|
metadata = RepoMetadata.objects.filter(repo_id=repo_id).first()
|
||||||
|
if not metadata or not metadata.enabled or not metadata.tags_enabled:
|
||||||
|
error_msg = f'The tags 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)
|
||||||
|
|
||||||
|
from seafevents.repo_metadata.constants import TAGS_TABLE
|
||||||
|
|
||||||
|
|
||||||
|
export_data = []
|
||||||
|
tags_ids_str = ', '.join([f'"{id}"' for id in tags_ids])
|
||||||
|
sql = f'SELECT * FROM {TAGS_TABLE.name} WHERE `{TAGS_TABLE.columns.id.name}` in ({tags_ids_str})'
|
||||||
|
try:
|
||||||
|
query_result = metadata_server_api.query_rows(sql).get('results')
|
||||||
|
for tag in query_result:
|
||||||
|
tag_parent_links = tag.get(TAGS_TABLE.columns.parent_links.key, [])
|
||||||
|
tag_sub_links = tag.get(TAGS_TABLE.columns.sub_links.key, [])
|
||||||
|
export_data.append({
|
||||||
|
'_id': tag.get(TAGS_TABLE.columns.id.name, ''),
|
||||||
|
'_tag_name': tag.get(TAGS_TABLE.columns.name.name, ''),
|
||||||
|
'_tag_color': tag.get(TAGS_TABLE.columns.color.name, ''),
|
||||||
|
'_tag_parent_links': [link_info.get('row_id', '') for link_info in tag_parent_links],
|
||||||
|
'_tag_sub_links': [link_info.get('row_id', '') for link_info in tag_sub_links],
|
||||||
|
})
|
||||||
|
|
||||||
|
response = HttpResponse(
|
||||||
|
json.dumps(export_data, ensure_ascii=False),
|
||||||
|
content_type='application/json'
|
||||||
|
)
|
||||||
|
response['Content-Disposition'] = 'attachment; filename="tags.json"'
|
||||||
|
return response
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
error_msg = 'Internal Server Error'
|
||||||
|
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||||
|
|
||||||
|
|
||||||
|
class MetadataImportTags(APIView):
|
||||||
|
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
||||||
|
permission_classes = (IsAuthenticated,)
|
||||||
|
throttle_classes = (UserRateThrottle,)
|
||||||
|
|
||||||
|
def _handle_tag_links(self, new_tags, existing_tags, exist_tags_id_map, imported_existing_tags, resp, tags_table):
|
||||||
|
exist_tags_ids = [tag.get(tags_table.columns.id.name, '') for tag in existing_tags]
|
||||||
|
all_tags = new_tags + imported_existing_tags
|
||||||
|
|
||||||
|
tags_id_map = {}
|
||||||
|
imported_tags_ids = [tag_data.get(tags_table.columns.id.name, '') for tag_data in all_tags]
|
||||||
|
for index, tag in enumerate(new_tags):
|
||||||
|
old_tag_id = tag.get(tags_table.columns.id.name, '')
|
||||||
|
tag[tags_table.columns.id.name] = resp.get('row_ids', [])[index]
|
||||||
|
tags_id_map[old_tag_id] = tag.get(tags_table.columns.id.name, '')
|
||||||
|
tags_id_map.update(exist_tags_id_map)
|
||||||
|
|
||||||
|
processed_tags = [] # remove some non-existent tag ids
|
||||||
|
for tag in new_tags:
|
||||||
|
child_tags_ids = tag.get(tags_table.columns.sub_links.key, [])
|
||||||
|
new_child_tags_ids = list(set(child_tags_ids) & set(imported_tags_ids))
|
||||||
|
tag[tags_table.columns.sub_links.key] = new_child_tags_ids
|
||||||
|
processed_tags.append(tag)
|
||||||
|
for tag in imported_existing_tags:
|
||||||
|
child_tags_ids = tag.get(tags_table.columns.sub_links.key, [])
|
||||||
|
new_child_tags_ids = list(set(child_tags_ids) & set(imported_tags_ids))
|
||||||
|
tag[tags_table.columns.sub_links.key] = new_child_tags_ids
|
||||||
|
# Update the imported tag ID to an existing tag ID on the server
|
||||||
|
tag[tags_table.columns.id.name] = tags_id_map[tag.get(tags_table.columns.id.name, '')]
|
||||||
|
processed_tags.append(tag)
|
||||||
|
|
||||||
|
child_links_map = {}
|
||||||
|
# old child links -> new child links and remove exist tags
|
||||||
|
for tag in processed_tags:
|
||||||
|
tag_id = tag.get(tags_table.columns.id.name, '')
|
||||||
|
old_child_links = tag.get(tags_table.columns.sub_links.key, [])
|
||||||
|
new_child_links = [tags_id_map[link] for link in old_child_links if link in tags_id_map]
|
||||||
|
formatted_child_links = list(set(new_child_links) - set(exist_tags_ids))
|
||||||
|
if formatted_child_links:
|
||||||
|
child_links_map[tag_id] = formatted_child_links
|
||||||
|
|
||||||
|
return child_links_map
|
||||||
|
|
||||||
|
def _get_existing_tags(self, metadata_server_api, tag_names, tags_table):
|
||||||
|
tag_names_str = ', '.join([f'"{tag_name}"' for tag_name in tag_names])
|
||||||
|
sql = f'SELECT * FROM {tags_table.name} WHERE `{tags_table.columns.name.name}` in ({tag_names_str})'
|
||||||
|
|
||||||
|
exist_rows = metadata_server_api.query_rows(sql)
|
||||||
|
existing_tags = exist_rows.get('results', [])
|
||||||
|
|
||||||
|
for item in existing_tags:
|
||||||
|
tag_sub_links = item.get('_tag_sub_links', [])
|
||||||
|
if tag_sub_links:
|
||||||
|
sub_links = []
|
||||||
|
for link in tag_sub_links:
|
||||||
|
sub_links.append(link['row_id'])
|
||||||
|
item['_tag_sub_links'] = sub_links
|
||||||
|
|
||||||
|
return existing_tags
|
||||||
|
|
||||||
|
def _classify_tags(self, file_content, existing_tags, tags_table):
|
||||||
|
new_tags = []
|
||||||
|
imported_existing_tags = []
|
||||||
|
existing_id_map = {}
|
||||||
|
|
||||||
|
if existing_tags:
|
||||||
|
existing_tag_names = [tag.get(tags_table.columns.name.name, '') for tag in existing_tags]
|
||||||
|
processed_names = set()
|
||||||
|
|
||||||
|
for tag_data in file_content:
|
||||||
|
tag_name = tag_data.get(tags_table.columns.name.name, '')
|
||||||
|
|
||||||
|
if tag_name in existing_tag_names and tag_name not in processed_names:
|
||||||
|
idx = existing_tag_names.index(tag_name)
|
||||||
|
imported_existing_tags.append(tag_data)
|
||||||
|
existing_id_map[tag_data.get(tags_table.columns.id.name, '')] = (
|
||||||
|
existing_tags[idx].get(tags_table.columns.id.name, '')
|
||||||
|
)
|
||||||
|
elif tag_name not in processed_names:
|
||||||
|
new_tags.append(tag_data)
|
||||||
|
processed_names.add(tag_name)
|
||||||
|
else:
|
||||||
|
new_tags = file_content
|
||||||
|
|
||||||
|
return new_tags, imported_existing_tags, existing_id_map
|
||||||
|
|
||||||
|
def post(self, request, repo_id):
|
||||||
|
file = request.FILES.get('file', None)
|
||||||
|
if not file:
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, 'file invalid')
|
||||||
|
|
||||||
|
metadata = RepoMetadata.objects.filter(repo_id=repo_id).first()
|
||||||
|
if not metadata or not metadata.enabled or not metadata.tags_enabled:
|
||||||
|
error_msg = f'The tags 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 = f'Library {repo_id} not found.'
|
||||||
|
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)
|
||||||
|
from seafevents.repo_metadata.constants import TAGS_TABLE
|
||||||
|
try:
|
||||||
|
tags_table = get_table_by_name(metadata_server_api, TAGS_TABLE.name)
|
||||||
|
if not tags_table:
|
||||||
|
return api_error(status.HTTP_404_NOT_FOUND, 'tags table not found')
|
||||||
|
tags_table_id = tags_table['id']
|
||||||
|
file_content = json.loads(file.read().decode('utf-8'))
|
||||||
|
tag_names = [tag.get(TAGS_TABLE.columns.name.name, '') for tag in file_content]
|
||||||
|
if not tag_names:
|
||||||
|
return Response({'success': True})
|
||||||
|
|
||||||
|
existing_tags = self._get_existing_tags(metadata_server_api, tag_names, TAGS_TABLE)
|
||||||
|
new_tags, imported_existing_tags, existing_id_map = self._classify_tags(
|
||||||
|
file_content, existing_tags, TAGS_TABLE
|
||||||
|
)
|
||||||
|
|
||||||
|
if new_tags:
|
||||||
|
create_tags_data = [
|
||||||
|
{
|
||||||
|
TAGS_TABLE.columns.name.name: tag.get(TAGS_TABLE.columns.name.name, ''),
|
||||||
|
TAGS_TABLE.columns.color.name: tag.get(TAGS_TABLE.columns.color.name, '')
|
||||||
|
}
|
||||||
|
for tag in new_tags
|
||||||
|
]
|
||||||
|
resp = metadata_server_api.insert_rows(tags_table_id, create_tags_data)
|
||||||
|
else:
|
||||||
|
return Response({'success': True})
|
||||||
|
|
||||||
|
# child links map structure: {tag_id: [child_tag_id1, child_tag_id2], ....}
|
||||||
|
child_links_map = self._handle_tag_links(
|
||||||
|
new_tags, existing_tags, existing_id_map,
|
||||||
|
imported_existing_tags, resp, TAGS_TABLE
|
||||||
|
)
|
||||||
|
|
||||||
|
if child_links_map:
|
||||||
|
metadata_server_api.insert_link(TAGS_TABLE.self_link_id, tags_table_id, child_links_map, True)
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(e)
|
||||||
|
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error')
|
||||||
|
return Response({'success': True})
|
||||||
|
@@ -3,7 +3,7 @@ from .apis import MetadataRecognizeFaces, MetadataRecords, MetadataManage, Metad
|
|||||||
MetadataFolders, MetadataViews, MetadataViewsMoveView, MetadataViewsDetailView, MetadataViewsDuplicateView, FacesRecords, \
|
MetadataFolders, MetadataViews, MetadataViewsMoveView, MetadataViewsDetailView, MetadataViewsDuplicateView, FacesRecords, \
|
||||||
FaceRecognitionManage, FacesRecord, MetadataExtractFileDetails, PeoplePhotos, MetadataTagsStatusManage, MetadataTags, \
|
FaceRecognitionManage, FacesRecord, MetadataExtractFileDetails, PeoplePhotos, MetadataTagsStatusManage, MetadataTags, \
|
||||||
MetadataTagsLinks, MetadataFileTags, MetadataTagFiles, MetadataMergeTags, MetadataTagsFiles, MetadataDetailsSettingsView, \
|
MetadataTagsLinks, MetadataFileTags, MetadataTagFiles, MetadataMergeTags, MetadataTagsFiles, MetadataDetailsSettingsView, \
|
||||||
MetadataOCRManageView, PeopleCoverPhoto, MetadataMigrateTags
|
MetadataOCRManageView, PeopleCoverPhoto, MetadataMigrateTags, MetadataExportTags, MetadataImportTags
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
re_path(r'^$', MetadataManage.as_view(), name='api-v2.1-metadata'),
|
re_path(r'^$', MetadataManage.as_view(), name='api-v2.1-metadata'),
|
||||||
@@ -44,4 +44,6 @@ urlpatterns = [
|
|||||||
re_path(r'^merge-tags/$', MetadataMergeTags.as_view(), name='api-v2.1-metadata-merge-tags'),
|
re_path(r'^merge-tags/$', MetadataMergeTags.as_view(), name='api-v2.1-metadata-merge-tags'),
|
||||||
re_path(r'^tags-files/$', MetadataTagsFiles.as_view(), name='api-v2.1-metadata-tags-files'),
|
re_path(r'^tags-files/$', MetadataTagsFiles.as_view(), name='api-v2.1-metadata-tags-files'),
|
||||||
re_path(r'^migrate-tags/$', MetadataMigrateTags.as_view(), name='api-v2.1-metadata-migrate-tags'),
|
re_path(r'^migrate-tags/$', MetadataMigrateTags.as_view(), name='api-v2.1-metadata-migrate-tags'),
|
||||||
|
re_path(r'^export-tags/$', MetadataExportTags.as_view(), name='api-v2.1-metadata-export-tags'),
|
||||||
|
re_path(r'^import-tags/$', MetadataImportTags.as_view(), name='api-v2.1-metadata-import-tags'),
|
||||||
]
|
]
|
||||||
|
Reference in New Issue
Block a user