diff --git a/frontend/src/metadata/api.js b/frontend/src/metadata/api.js index 7010346ff4..93205d6767 100644 --- a/frontend/src/metadata/api.js +++ b/frontend/src/metadata/api.js @@ -118,6 +118,12 @@ class MetadataManagerAPI { return this._sendPostRequest(url, params, { headers: { 'Content-type': 'application/json' } }); }; + duplicateView = (repoID, viewId) => { + const url = this.server + '/api/v2.1/repos/' + repoID + '/metadata/duplicate-view/'; + const params = { view_id: viewId }; + return this._sendPostRequest(url, params, { headers: { 'Content-type': 'application/json' } }); + }; + modifyView = (repoID, viewId, viewData) => { const url = this.server + '/api/v2.1/repos/' + repoID + '/metadata/views/'; const params = { diff --git a/frontend/src/metadata/hooks/metadata.js b/frontend/src/metadata/hooks/metadata.js index b8d0fccb19..af8361e42a 100644 --- a/frontend/src/metadata/hooks/metadata.js +++ b/frontend/src/metadata/hooks/metadata.js @@ -120,6 +120,20 @@ export const MetadataProvider = ({ repoID, hideMetadataView, selectMetadataView, }); }, [navigation, repoID, viewsMap, selectView]); + const duplicateView = useCallback((viewId) => { + metadataAPI.duplicateView(repoID, viewId).then(res => { + const view = res.data.view; + let newNavigation = navigation.slice(0); + newNavigation.push({ _id: view._id, type: 'view' }); + viewsMap.current[view._id] = view; + setNavigation(newNavigation); + selectView(view); + }).catch(error => { + const errorMsg = Utils.getErrorMsg(error); + toaster.danger(errorMsg); + }); + }, [navigation, repoID, viewsMap, selectView]); + const deleteView = useCallback((viewId, isSelected) => { metadataAPI.deleteView(repoID, viewId).then(res => { const newNavigation = navigation.filter(item => item._id !== viewId); @@ -170,6 +184,7 @@ export const MetadataProvider = ({ repoID, hideMetadataView, selectMetadataView, viewsMap: viewsMap.current, selectView, addView, + duplicateView, deleteView, updateView, moveView, diff --git a/frontend/src/metadata/metadata-tree-view/index.js b/frontend/src/metadata/metadata-tree-view/index.js index 4caf52dc77..17b09d67b7 100644 --- a/frontend/src/metadata/metadata-tree-view/index.js +++ b/frontend/src/metadata/metadata-tree-view/index.js @@ -23,6 +23,7 @@ const MetadataTreeView = ({ userPerm, currentPath }) => { viewsMap, selectView, addView, + duplicateView, deleteView, updateView, moveView @@ -122,6 +123,7 @@ const MetadataTreeView = ({ userPerm, currentPath }) => { view={view} onClick={(view) => selectView(view, isSelected)} onDelete={() => deleteView(view._id, isSelected)} + onCopy={() => duplicateView(view._id)} onUpdate={(update, successCallback, failCallback) => onUpdateView(view._id, update, successCallback, failCallback)} onMove={moveView} />); diff --git a/frontend/src/metadata/metadata-tree-view/view-item/index.js b/frontend/src/metadata/metadata-tree-view/view-item/index.js index 14d6bfd29a..7f4bb18ea6 100644 --- a/frontend/src/metadata/metadata-tree-view/view-item/index.js +++ b/frontend/src/metadata/metadata-tree-view/view-item/index.js @@ -16,6 +16,7 @@ const ViewItem = ({ view, onClick, onDelete, + onCopy, onUpdate, onMove, }) => { @@ -38,6 +39,7 @@ const ViewItem = ({ if (!canUpdate) return []; let value = [ { key: 'rename', value: gettext('Rename') }, + { key: 'duplicate', value: gettext('Duplicate') } ]; if (canDelete) { value.push({ key: 'delete', value: gettext('Delete') }); @@ -75,11 +77,16 @@ const ViewItem = ({ return; } + if (operationKey === 'duplicate') { + onCopy(); + return; + } + if (operationKey === 'delete') { onDelete(); return; } - }, [onDelete]); + }, [onDelete, onCopy]); const closeRenamePopover = useCallback((event) => { event.stopPropagation(); diff --git a/seahub/api2/endpoints/metadata_manage.py b/seahub/api2/endpoints/metadata_manage.py index 65d09996f1..cdd2a3d8ee 100644 --- a/seahub/api2/endpoints/metadata_manage.py +++ b/seahub/api2/endpoints/metadata_manage.py @@ -697,6 +697,52 @@ class MetadataViews(APIView): return Response({'success': True}) +class MetadataViewsDuplicateView(APIView): + authentication_classes = (TokenAuthentication, SessionAuthentication) + permission_classes = (IsAuthenticated,) + throttle_classes = (UserRateThrottle,) + + def post(self, request, repo_id): + view_id = request.data.get('view_id') + if not view_id: + error_msg = 'view_id invalid' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + record = RepoMetadata.objects.filter(repo_id=repo_id).first() + if not record or not record.enabled: + error_msg = f'The metadata module is disabled for repo {repo_id}.' + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + views = RepoMetadataViews.objects.filter( + repo_id=repo_id + ).first() + if not views: + error_msg = 'The metadata views does not exists.' + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + if view_id not in views.view_ids: + error_msg = 'view_id %s does not exists.' % view_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) + + permission = check_folder_permission(request, repo_id, '/') + if permission != 'rw': + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + try: + result = RepoMetadataViews.objects.duplicate_view(repo_id, view_id) + except Exception as e: + logger.exception(e) + error_msg = 'Internal Server Error' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + + return Response({'view': result}) + + class MetadataViewsDetailView(APIView): authentication_classes = (TokenAuthentication, SessionAuthentication) permission_classes = (IsAuthenticated,) diff --git a/seahub/repo_metadata/models.py b/seahub/repo_metadata/models.py index d65affe0f0..c098fcaca1 100644 --- a/seahub/repo_metadata/models.py +++ b/seahub/repo_metadata/models.py @@ -2,6 +2,7 @@ import logging import json import random import string +import copy from django.db import models from seahub.utils import get_no_duplicate_obj_name @@ -140,7 +141,23 @@ class RepoMetadataViewsManager(models.Manager): metadata_views.details = json.dumps(view_details) metadata_views.save() return json.loads(metadata_views.details) - + + def duplicate_view(self, repo_id, view_id): + metadata_views = self.filter(repo_id=repo_id).first() + view_details = json.loads(metadata_views.details) + exist_view_ids = metadata_views.view_ids + new_view_id = generate_view_id(4, exist_view_ids) + duplicate_view = next((copy.deepcopy(view) for view in view_details['views'] if view.get('_id') == view_id), None) + duplicate_view['_id'] = new_view_id + view_name = get_no_duplicate_obj_name(duplicate_view['name'], metadata_views.view_names) + duplicate_view['name'] = view_name + view_details['views'].append(duplicate_view) + view_details['navigation'].append({'_id': new_view_id, 'type': 'view'}) + metadata_views.details = json.dumps(view_details) + metadata_views.save() + + return duplicate_view + def delete_view(self, repo_id, view_id): metadata_views = self.filter(repo_id=repo_id).first() view_details = json.loads(metadata_views.details) diff --git a/seahub/urls.py b/seahub/urls.py index 662e0fabab..a34dcc0683 100644 --- a/seahub/urls.py +++ b/seahub/urls.py @@ -208,7 +208,7 @@ from seahub.api2.endpoints.wiki2 import Wikis2View, Wiki2View, Wiki2ConfigView, Wiki2DuplicatePageView, WikiPageTrashView from seahub.api2.endpoints.subscription import SubscriptionView, SubscriptionPlansView, SubscriptionLogsView from seahub.api2.endpoints.metadata_manage import MetadataRecords, MetadataManage, MetadataColumns, MetadataRecordInfo, \ - MetadataViews, MetadataViewsMoveView, MetadataViewsDetailView, MetadataSummarizeDocs + MetadataViews, MetadataViewsMoveView, MetadataViewsDetailView, MetadataSummarizeDocs, MetadataViewsDuplicateView from seahub.api2.endpoints.user_list import UserListView @@ -1038,5 +1038,5 @@ if settings.ENABLE_METADATA_MANAGEMENT: re_path(r'^api/v2.1/repos/(?P[-0-9a-f]{36})/metadata/views/(?P[-0-9a-zA-Z]{4})/$', MetadataViewsDetailView.as_view(), name='api-v2.1-metadata-views-detail'), re_path(r'^api/v2.1/repos/(?P[-0-9a-f]{36})/metadata/move-views/$', MetadataViewsMoveView.as_view(), name='api-v2.1-metadata-views-move'), re_path(r'^api/v2.1/repos/(?P[-0-9a-f]{36})/metadata/ai/summarize-documents/$', MetadataSummarizeDocs.as_view(), name='api-v2.1-metadata-summarize-documents'), - + re_path(r'^api/v2.1/repos/(?P[-0-9a-f]{36})/metadata/duplicate-view/$', MetadataViewsDuplicateView.as_view(), name='api-v2.1-metadata-view-duplicate'), ]