diff --git a/frontend/src/components/dirent-detail/detail/header/title/index.js b/frontend/src/components/dirent-detail/detail/header/title/index.js
index a00f754583..6f9787c95f 100644
--- a/frontend/src/components/dirent-detail/detail/header/title/index.js
+++ b/frontend/src/components/dirent-detail/detail/header/title/index.js
@@ -7,9 +7,11 @@ const Title = ({ icon, iconSize, title }) => {
return (
-
-

-
+ {icon && (
+
+

+
+ )}
{title}
);
diff --git a/frontend/src/components/dirent-detail/index.js b/frontend/src/components/dirent-detail/index.js
index f4fbc96e7e..d778aa410b 100644
--- a/frontend/src/components/dirent-detail/index.js
+++ b/frontend/src/components/dirent-detail/index.js
@@ -2,6 +2,7 @@ import React, { useEffect, useMemo } from 'react';
import PropTypes from 'prop-types';
import LibDetail from './lib-details';
import DirentDetail from './dirent-details';
+import MultiSelectionDetails from './multi-selection-details';
import ViewDetails from '../../metadata/components/view-details';
import ObjectUtils from '../../utils/object';
import { MetadataContext } from '../../metadata';
@@ -11,7 +12,18 @@ import { FACE_RECOGNITION_VIEW_ID } from '../../metadata/constants';
import { useTags } from '../../tag/hooks';
import { useMetadataStatus } from '../../hooks';
-const Detail = React.memo(({ repoID, path, currentMode, dirent, currentRepoInfo, repoTags, fileTags, onClose, onFileTagChanged }) => {
+const Detail = React.memo(({
+ repoID,
+ path,
+ currentMode,
+ dirent,
+ selectedDirents,
+ currentRepoInfo,
+ repoTags,
+ fileTags,
+ onClose,
+ onFileTagChanged
+}) => {
const { enableMetadata, enableFaceRecognition, detailsSettings, modifyDetailsSettings } = useMetadataStatus();
const { tagsData, addTag, modifyLocalFileTags } = useTags();
@@ -36,6 +48,20 @@ const Detail = React.memo(({ repoID, path, currentMode, dirent, currentRepoInfo,
if (isTag) return null;
+ // Handle multi-selection case
+ if (selectedDirents && selectedDirents.length > 1) {
+ return (
+
+ );
+ }
+
if (isView && !dirent) {
const pathParts = path.split('/');
const [, , viewId, children] = pathParts;
@@ -54,7 +80,7 @@ const Detail = React.memo(({ repoID, path, currentMode, dirent, currentRepoInfo,
{
+ const ref = useRef(null);
+ const [showEditor, setShowEditor] = useState(false);
+
+ const { tagsData, context } = useTags();
+ const canEditData = useMemo(() => window.sfMetadataContext && window.sfMetadataContext.canModifyRow() || false, []);
+
+ const displayTags = useMemo(() => {
+ if (!records || records.length === 0) return [];
+
+ return getCellValueByColumn(records[0], field) || [];
+ }, [records, field]);
+
+ const displayTagIds = useMemo(() => {
+ if (!Array.isArray(displayTags) || displayTags.length === 0) return [];
+
+ return displayTags.filter(tag => getRowById(tagsData, tag.row_id)).map(tag => tag.row_id);
+ }, [displayTags, tagsData]);
+
+ const onClick = useCallback((event) => {
+ if (!event.target) return;
+ const className = getEventClassName(event);
+ if (className.indexOf('sf-metadata-search-tags') > -1) return;
+ const dom = document.querySelector('.sf-metadata-tags-editor');
+ if (!dom) return;
+ if (dom.contains(event.target)) return;
+ if (ref.current && !ref.current.contains(event.target) && showEditor) {
+ setShowEditor(false);
+ }
+ }, [showEditor]);
+
+ const onHotKey = useCallback((event) => {
+ if (event.keyCode === KeyCodes.Esc) {
+ if (showEditor) {
+ setShowEditor(false);
+ }
+ }
+ }, [showEditor]);
+
+ useEffect(() => {
+ document.addEventListener('mousedown', onClick);
+ document.addEventListener('keydown', onHotKey, true);
+ return () => {
+ document.removeEventListener('mousedown', onClick);
+ document.removeEventListener('keydown', onHotKey, true);
+ };
+ }, [onClick, onHotKey]);
+
+ const openEditor = useCallback(() => {
+ setShowEditor(true);
+ }, []);
+
+ const updateBatchTags = useCallback((newTagIds, oldTagIds = []) => {
+ if (!records || records.length === 0) return;
+
+ const newTags = newTagIds.map(id => {
+ const tag = getRowById(tagsData, id);
+ return {
+ display_value: getTagName(tag),
+ row_id: getTagId(tag),
+ };
+ });
+
+ onChange && onChange(PRIVATE_COLUMN_KEY.TAGS, newTags);
+
+ const batchTagUpdates = records.map(record => ({
+ record_id: record._id,
+ tags: newTagIds
+ }));
+
+ tagsAPI.updateFileTags(repoID, batchTagUpdates)
+ .then(() => {
+ batchTagUpdates.forEach(({ record_id, tags }) => {
+ modifyLocalFileTags && modifyLocalFileTags(record_id, tags);
+
+ if (window?.sfMetadataContext?.eventBus) {
+ const newValue = tags ? tags.map(id => ({ row_id: id, display_value: id })) : [];
+ const update = { [PRIVATE_COLUMN_KEY.TAGS]: newValue };
+ window.sfMetadataContext.eventBus.dispatch(EVENT_BUS_TYPE.LOCAL_RECORD_CHANGED, { recordId: record_id }, update);
+ }
+ });
+ })
+ .catch((error) => {
+ const errorMsg = Utils.getErrorMsg(error);
+ toaster.danger(errorMsg);
+ });
+ }, [records, repoID, tagsData, onChange, modifyLocalFileTags]);
+
+ const onDeleteTag = useCallback((tagId, event) => {
+ event && event.stopPropagation();
+ event && event.nativeEvent && event.nativeEvent.stopImmediatePropagation();
+ const newValue = displayTagIds.slice(0);
+ let optionIdx = displayTagIds.indexOf(tagId);
+ if (optionIdx > -1) {
+ newValue.splice(optionIdx, 1);
+ }
+ updateBatchTags(newValue, displayTagIds);
+ setShowEditor(false);
+ }, [displayTagIds, updateBatchTags]);
+
+ const onSelectTag = useCallback((tagId) => {
+ const newValue = displayTagIds.slice(0);
+ if (!newValue.includes(tagId)) {
+ newValue.push(tagId);
+ }
+ updateBatchTags(newValue, displayTagIds);
+ }, [displayTagIds, updateBatchTags]);
+
+ const onDeselectTag = useCallback((tagId) => {
+ const newValue = displayTagIds.slice(0);
+ let optionIdx = displayTagIds.indexOf(tagId);
+ if (optionIdx > -1) {
+ newValue.splice(optionIdx, 1);
+ }
+ updateBatchTags(newValue, displayTagIds);
+ }, [displayTagIds, updateBatchTags]);
+
+ const renderEditor = useCallback(() => {
+ if (!showEditor) return null;
+ const { width, top, bottom } = ref.current.getBoundingClientRect();
+ const editorHeight = 400;
+ const viewportHeight = window.innerHeight;
+ let placement = 'bottom-end';
+ if (viewportHeight - bottom < editorHeight && top > editorHeight) {
+ placement = 'top-end';
+ } else if (viewportHeight - bottom < editorHeight && top < editorHeight) {
+ placement = 'left-start';
+ }
+
+ return (
+
+
+
+ );
+ }, [showEditor, field, displayTags, context, canEditData, onSelectTag, onDeselectTag]);
+
+ return (
+
+ {displayTagIds.length > 0 && ()}
+ {renderEditor()}
+
+ );
+};
+
+DirentsTagsEditor.propTypes = {
+ records: PropTypes.array.isRequired,
+ field: PropTypes.object.isRequired,
+ onBatchMetadataRefresh: PropTypes.func.isRequired,
+ repoID: PropTypes.string.isRequired,
+ modifyLocalFileTags: PropTypes.func,
+};
+
+export default DirentsTagsEditor;
diff --git a/frontend/src/components/dirent-detail/multi-selection-details/index.css b/frontend/src/components/dirent-detail/multi-selection-details/index.css
new file mode 100644
index 0000000000..5f55c62d84
--- /dev/null
+++ b/frontend/src/components/dirent-detail/multi-selection-details/index.css
@@ -0,0 +1,62 @@
+.multi-selection-thumbnails {
+ position: relative;
+ width: 100%;
+ height: 144px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-top: 16px;
+ margin-bottom: 28px;
+ border-radius: 12px;
+}
+
+.background-thumbnail {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ border: 1px solid var(--bs-border-secondary-color);
+ border-radius: 8px;
+ overflow: hidden;
+ z-index: 1;
+}
+
+.background-thumbnail img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.background-overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 2;
+}
+
+.overlay-thumbnail {
+ background-color: var(--bs-body-bg);
+}
+
+.multi-selection-thumbnails.with-background {
+ background: transparent;
+}
+
+.overlay-thumbnail {
+ position: absolute;
+ width: auto;
+ height: 144px;
+ border-radius: 8px;
+ overflow: hidden;
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3);
+ border: 1px solid var(--bs-border-secondary-color);
+}
+
+.overlay-thumbnail img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
diff --git a/frontend/src/components/dirent-detail/multi-selection-details/index.js b/frontend/src/components/dirent-detail/multi-selection-details/index.js
new file mode 100644
index 0000000000..d73037dc2a
--- /dev/null
+++ b/frontend/src/components/dirent-detail/multi-selection-details/index.js
@@ -0,0 +1,247 @@
+import React, { useCallback, useMemo, useState, useEffect } from 'react';
+import PropTypes from 'prop-types';
+import { Detail, Header, Body } from '../detail';
+import { Utils } from '../../../utils/utils';
+import { gettext, siteRoot, thumbnailSizeForGrid } from '../../../utils/constants';
+import DetailItem from '../detail-item';
+import { CellType, PRIVATE_COLUMN_KEY } from '../../../metadata/constants';
+import { useMetadataStatus } from '../../../hooks';
+import RateEditor from '../../../metadata/components/detail-editor/rate-editor';
+import metadataAPI from '../../../metadata/api';
+import Loading from '../../loading';
+import { getColumnDisplayName } from '../../../metadata/utils/column';
+import DirentsTagsEditor from './dirents-tags-editor';
+import { getCellValueByColumn, getFileObjIdFromRecord, getRecordIdFromRecord } from '../../../metadata/utils/cell';
+
+import './index.css';
+
+const MultiSelectionDetails = ({
+ repoID,
+ path,
+ selectedDirents,
+ currentRepoInfo,
+ modifyLocalFileTags,
+ onClose
+}) => {
+ const { enableMetadata, enableTags } = useMetadataStatus();
+ const [records, setRecords] = useState([]);
+ const [isLoading, setIsLoading] = useState(false);
+
+ const fileCount = useMemo(() => selectedDirents.filter(dirent => dirent.type === 'file').length, [selectedDirents]);
+ const folderCount = useMemo(() => selectedDirents.filter(dirent => dirent.type === 'dir').length, [selectedDirents]);
+
+ const onlyFiles = folderCount === 0;
+ const hasFiles = fileCount > 0;
+
+ const { imageDirents, fileDirents } = useMemo(() => ({
+ imageDirents: selectedDirents.filter(dirent =>
+ dirent.type === 'file' && Utils.imageCheck(dirent.name)
+ ),
+ fileDirents: selectedDirents.filter(dirent => dirent.type === 'file')
+ }), [selectedDirents]);
+
+ const { rateField, tagField } = useMemo(() => {
+ const rateColumn = { key: PRIVATE_COLUMN_KEY.FILE_RATE, type: CellType.RATE, name: getColumnDisplayName(PRIVATE_COLUMN_KEY.FILE_RATE) };
+ const tagColumn = { key: PRIVATE_COLUMN_KEY.TAGS, type: CellType.TAGS, name: getColumnDisplayName(PRIVATE_COLUMN_KEY.TAGS) };
+
+ return {
+ rateField: {
+ ...rateColumn,
+ name: getColumnDisplayName(rateColumn.key)
+ },
+ tagField: {
+ ...tagColumn,
+ name: getColumnDisplayName(tagColumn.key),
+ type: CellType.TAGS
+ }
+ };
+ }, []);
+
+ const getRecords = useCallback(() => {
+ if (!enableMetadata || !onlyFiles || !hasFiles) return;
+
+ setIsLoading(true);
+ const files = fileDirents.map(dirent => {
+ return {
+ parent_dir: path,
+ file_name: dirent.name
+ };
+ });
+ metadataAPI.getRecords(repoID, files)
+ .then(res => {
+ const { results } = res.data;
+ const orderedRecords = fileDirents.map(dirent => {
+ return results.find(record => {
+ const recordPath = record[PRIVATE_COLUMN_KEY.PARENT_DIR] || '';
+ const recordName = record[PRIVATE_COLUMN_KEY.FILE_NAME] || '';
+ return recordPath === path && recordName === dirent.name;
+ });
+ }).filter(Boolean);
+
+ setRecords(orderedRecords);
+ setIsLoading(false);
+ })
+ .catch (err => {
+ setRecords([]);
+ setIsLoading(false);
+ });
+ }, [enableMetadata, onlyFiles, hasFiles, fileDirents, repoID, path]);
+
+ const onChange = useCallback((key, value) => {
+ const newRecords = records.map(record => ({
+ ...record,
+ [key]: value
+ }));
+ setRecords(newRecords);
+
+ if (key === PRIVATE_COLUMN_KEY.TAGS) return;
+
+ const recordsData = records.map(record => ({
+ record_id: getRecordIdFromRecord(record),
+ record: { [key]: value },
+ obj_id: getFileObjIdFromRecord(record),
+ }));
+ metadataAPI.modifyRecords(repoID, recordsData);
+ }, [repoID, records]);
+
+ useEffect(() => {
+ getRecords();
+ }, [getRecords]);
+
+ const rate = useMemo(() => {
+ if (!records || records.length === 0) return null;
+
+ return getCellValueByColumn(records[0], rateField);
+ }, [records, rateField]);
+
+ const getThumbnailSrc = (dirent) => {
+ if (dirent.type === 'dir' || (dirent.type === 'file' && !Utils.imageCheck(dirent.name))) {
+ return Utils.getDirentIcon(dirent, true);
+ }
+
+ if (dirent.type !== 'file' || !Utils.imageCheck(dirent.name)) {
+ return null;
+ }
+
+ return currentRepoInfo.encrypted
+ ? `${siteRoot}repo/${repoID}/raw${Utils.encodePath(`${path === '/' ? '' : path}/${dirent.name}`)}`
+ : `${siteRoot}thumbnail/${repoID}/${thumbnailSizeForGrid}${Utils.encodePath(`${path === '/' ? '' : path}/${dirent.name}`)}`;
+ };
+
+ const renderOverlappingThumbnails = () => {
+ const maxOverlayThumbnails = 6;
+ const angles = [-15, 8, -8, 12, -10, 6];
+
+ const renderOverlayItem = (dirent, index, isBackgroundMode = false) => {
+ const src = getThumbnailSrc(dirent);
+ const className = `overlay-thumbnail large${!isBackgroundMode ? ' no-background-item' : ''}`;
+
+ return (
+
+

{
+ e.target.style.display = 'none';
+ const iconContainer = e.target.parentNode.querySelector('.fallback-icon');
+ if (iconContainer) iconContainer.style.display = 'flex';
+ }}
+ />
+
+ );
+ };
+
+ if (imageDirents.length === 0) {
+ const overlayItems = selectedDirents.slice(0, maxOverlayThumbnails);
+
+ return (
+
+ {overlayItems.map((dirent, index) => renderOverlayItem(dirent, index, false))}
+
+ );
+ }
+
+ // With background image mode
+ const backgroundImage = imageDirents[0];
+ const overlayItems = selectedDirents
+ .filter(dirent => dirent.name !== backgroundImage.name)
+ .slice(-maxOverlayThumbnails);
+ const backgroundSrc = getThumbnailSrc(backgroundImage);
+
+ return (
+
+
+

+
+
+ {overlayItems.map((dirent, index) => renderOverlayItem(dirent, index, true))}
+
+ );
+ };
+
+ return (
+
+
+
+ {renderOverlappingThumbnails()}
+
+
+
{gettext(`${selectedDirents.length} items have been selected`)}
+
+ {isLoading && (
+
+
+
{gettext('Loading metadata...')}
+
+ )}
+
+ {!isLoading && onlyFiles && hasFiles && window.app.pageOptions.enableFileTags && enableTags && (
+
+
+
+ )}
+ {!isLoading && onlyFiles && hasFiles && enableMetadata && (
+
+ onChange(PRIVATE_COLUMN_KEY.FILE_RATE, value)}
+ />
+
+ )}
+
+
+
+ );
+};
+
+MultiSelectionDetails.propTypes = {
+ repoID: PropTypes.string.isRequired,
+ path: PropTypes.string.isRequired,
+ selectedDirents: PropTypes.array.isRequired,
+ currentRepoInfo: PropTypes.object.isRequired,
+ modifyLocalFileTags: PropTypes.func,
+ onClose: PropTypes.func.isRequired,
+};
+
+export default MultiSelectionDetails;
diff --git a/frontend/src/metadata/api.js b/frontend/src/metadata/api.js
index 0e7a5771dd..ed3a54ec82 100644
--- a/frontend/src/metadata/api.js
+++ b/frontend/src/metadata/api.js
@@ -107,6 +107,11 @@ class MetadataManagerAPI {
return this.req.get(url, { params: params });
}
+ getRecords(repoID, files) {
+ const url = this.server + '/api/v2.1/repos/' + repoID + '/metadata/batch-records/';
+ return this.req.post(url, { files: files });
+ }
+
modifyRecord(repoID, { recordId, parentDir, fileName }, updateData) {
const url = this.server + '/api/v2.1/repos/' + repoID + '/metadata/record/';
let data = {
diff --git a/frontend/src/pages/lib-content-view/lib-content-view.js b/frontend/src/pages/lib-content-view/lib-content-view.js
index c30aead4a9..fb019c521a 100644
--- a/frontend/src/pages/lib-content-view/lib-content-view.js
+++ b/frontend/src/pages/lib-content-view/lib-content-view.js
@@ -1838,7 +1838,7 @@ class LibContentView extends React.Component {
return item.name !== name;
});
this.recalculateSelectedDirents([name], direntList);
- this.setState({ direntList: direntList });
+ this.setState({ direntList: direntList, currentDirent: null });
};
// only one scene: The moved items are inside current path
@@ -1852,6 +1852,7 @@ class LibContentView extends React.Component {
selectedDirentList: [],
isDirentSelected: false,
isAllDirentSelected: false,
+ currentDirent: null,
});
};
@@ -2378,7 +2379,7 @@ class LibContentView extends React.Component {
}
let detailPath = this.state.path;
- if (!currentDirent && currentMode !== METADATA_MODE && currentMode !== TAGS_MODE) {
+ if (!currentDirent && currentMode !== METADATA_MODE && currentMode !== TAGS_MODE && this.state.selectedDirentList.length === 0) {
detailPath = Utils.getDirName(this.state.path);
}
@@ -2577,6 +2578,7 @@ class LibContentView extends React.Component {
repoID={this.props.repoID}
currentRepoInfo={{ ...this.state.currentRepoInfo }}
dirent={detailDirent}
+ selectedDirents={this.state.selectedDirentList}
repoTags={this.state.repoTags}
fileTags={this.state.isViewFile ? this.state.fileTags : []}
onFileTagChanged={this.onFileTagChanged}
diff --git a/seahub/repo_metadata/apis.py b/seahub/repo_metadata/apis.py
index 1d20569610..0e2684b6d5 100644
--- a/seahub/repo_metadata/apis.py
+++ b/seahub/repo_metadata/apis.py
@@ -33,6 +33,7 @@ from seahub.settings import MD_FILE_COUNT_LIMIT
logger = logging.getLogger(__name__)
+
class MetadataManage(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication)
permission_classes = (IsAuthenticated, )
@@ -749,6 +750,98 @@ class MetadataColumns(APIView):
return Response({'success': True})
+class MetadataBatchRecords(APIView):
+ authentication_classes = (TokenAuthentication, SessionAuthentication)
+ permission_classes = (IsAuthenticated,)
+ throttle_classes = (UserRateThrottle,)
+
+ def post(self, request, repo_id):
+ """
+ Get metadata records for multiple files in batch
+ Request body:
+ files: list of {parent_dir, file_name} objects
+ """
+ files = request.data.get('files', [])
+ if not files or not isinstance(files, list):
+ error_msg = 'files parameter is required and must be a list'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ if len(files) > METADATA_RECORD_UPDATE_LIMIT:
+ error_msg = 'Number of records exceeds the limit of %s.' % METADATA_RECORD_UPDATE_LIMIT
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ 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)
+
+ if not can_read_metadata(request, repo_id):
+ error_msg = 'Permission denied.'
+ return api_error(status.HTTP_403_FORBIDDEN, 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)
+
+ metadata_server_api = MetadataServerAPI(repo_id, request.user.username)
+
+ from seafevents.repo_metadata.constants import METADATA_TABLE
+
+ try:
+ columns_data = metadata_server_api.list_columns(METADATA_TABLE.id)
+ all_columns = columns_data.get('columns', [])
+ metadata_columns = []
+ for column in all_columns:
+ if column.get('key') in ['_rate', '_tags']:
+ metadata_columns.append({
+ 'key': column.get('key'),
+ 'name': column.get('name'),
+ 'type': column.get('type'),
+ 'data': column.get('data', {})
+ })
+
+ except Exception as e:
+ logger.exception(e)
+ error_msg = 'Internal Server Error'
+ return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
+
+ where_conditions = []
+ parameters = []
+
+ for file_info in files:
+ parent_dir = file_info.get('parent_dir')
+ file_name = file_info.get('file_name')
+
+ if not parent_dir or not file_name:
+ continue
+
+ where_conditions.append(f'(`{METADATA_TABLE.columns.parent_dir.name}`=? AND `{METADATA_TABLE.columns.file_name.name}`=?)')
+ parameters.extend([parent_dir, file_name])
+
+ if not where_conditions:
+ return Response({
+ 'results': [],
+ 'metadata': {'columns': metadata_columns}
+ })
+
+ where_clause = ' OR '.join(where_conditions)
+ sql = f'SELECT * FROM `{METADATA_TABLE.name}` WHERE {where_clause};'
+ try:
+ query_result = metadata_server_api.query_rows(sql, parameters)
+ results = query_result.get('results', [])
+
+ return Response({
+ 'results': results,
+ 'metadata': {'columns': metadata_columns}
+ })
+
+ except Exception as e:
+ logger.exception(e)
+ error_msg = 'Internal Server Error'
+ return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
+
+
class MetadataFolders(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication)
permission_classes = (IsAuthenticated,)
@@ -2422,21 +2515,26 @@ class MetadataFileTags(APIView):
from seafevents.repo_metadata.constants import TAGS_TABLE, METADATA_TABLE
metadata_server_api = MetadataServerAPI(repo_id, request.user.username)
- success_records = []
- failed_records = []
+ row_id_map = {}
+
for file_tags in file_tags_data:
record_id = file_tags.get('record_id', '')
tags = file_tags.get('tags', [])
if not record_id:
continue
- try:
+
+ row_id_map[record_id] = tags
- metadata_server_api.update_link(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)
+ if not row_id_map:
+ return api_error(status.HTTP_400_BAD_REQUEST, 'No valid file_tags_data provided')
- return Response({'success': success_records, 'fail': failed_records})
+ try:
+ metadata_server_api.update_link(TAGS_TABLE.file_link_id, METADATA_TABLE.id, row_id_map)
+ except Exception as e:
+ logger.exception(e)
+ return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error')
+
+ return Response({'success': True })
class MetadataTagFiles(APIView):
diff --git a/seahub/repo_metadata/urls.py b/seahub/repo_metadata/urls.py
index 833399da89..1c5a88ddf7 100644
--- a/seahub/repo_metadata/urls.py
+++ b/seahub/repo_metadata/urls.py
@@ -3,12 +3,14 @@ from .apis import MetadataRecognizeFaces, MetadataRecords, MetadataManage, Metad
MetadataFolders, MetadataViews, MetadataViewsMoveView, MetadataViewsDetailView, MetadataViewsDuplicateView, FacesRecords, \
FaceRecognitionManage, FacesRecord, MetadataExtractFileDetails, PeoplePhotos, MetadataTagsStatusManage, MetadataTags, \
MetadataTagsLinks, MetadataFileTags, MetadataTagFiles, MetadataMergeTags, MetadataTagsFiles, MetadataDetailsSettingsView, \
- PeopleCoverPhoto, MetadataMigrateTags, MetadataExportTags, MetadataImportTags, MetadataGlobalHiddenColumnsView
+ PeopleCoverPhoto, MetadataMigrateTags, MetadataExportTags, MetadataImportTags, MetadataGlobalHiddenColumnsView, \
+ MetadataBatchRecords
urlpatterns = [
re_path(r'^$', MetadataManage.as_view(), name='api-v2.1-metadata'),
re_path(r'^records/$', MetadataRecords.as_view(), name='api-v2.1-metadata-records'),
re_path(r'^record/$', MetadataRecord.as_view(), name='api-v2.1-metadata-record-info'),
+ re_path(r'^batch-records/$', MetadataBatchRecords.as_view(), name='api-v2.1-metadata-batch-records'),
re_path(r'^columns/$', MetadataColumns.as_view(), name='api-v2.1-metadata-columns'),
# view