From cdf36ed1544d504b627948b040f143bc0d7531b9 Mon Sep 17 00:00:00 2001 From: Alex Happy <1223408988@qq.com> Date: Mon, 25 Sep 2023 12:25:20 +0800 Subject: [PATCH] Support mark folder sub items (#5638) * add folder-items-ex-props api * pass user-name for set folder-items ex-props * frontend feat folder-items ex-props * opt request seaf-io * opt some variables * opt some tips * opt loading style for apply ex-props * feat: update code * fix confirm bug --------- Co-authored-by: er-pai-r <18335219360@163.com> --- .../confirm-apply-folder-properties-dialog.js | 105 ++++++++++++++ .../dirent-detail/detail-list-view.js | 52 +++++-- frontend/src/css/apply-folder-properties.css | 14 ++ seahub/api2/endpoints/extended_properties.py | 130 +++++++++++++++++- seahub/settings.py | 5 + seahub/urls.py | 5 +- 6 files changed, 296 insertions(+), 15 deletions(-) create mode 100644 frontend/src/components/dialog/confirm-apply-folder-properties-dialog.js create mode 100644 frontend/src/css/apply-folder-properties.css diff --git a/frontend/src/components/dialog/confirm-apply-folder-properties-dialog.js b/frontend/src/components/dialog/confirm-apply-folder-properties-dialog.js new file mode 100644 index 0000000000..acc696a50f --- /dev/null +++ b/frontend/src/components/dialog/confirm-apply-folder-properties-dialog.js @@ -0,0 +1,105 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap'; +import { gettext } from '../../utils/constants'; +import { seafileAPI } from '../../utils/seafile-api'; +import toaster from '../toast'; +import { Utils } from '../../utils/utils'; +import Loading from '../loading'; + +import '../../css/apply-folder-properties.css'; + +const propTypes = { + toggle: PropTypes.func, + repoID: PropTypes.string, + path: PropTypes.string +}; + +class ConfirmApplyFolderPropertiesDialog extends React.Component { + + constructor(props) { + super(props); + this.state = { + submitting: true + }; + this.timer = null; + } + + componentDidMount() { + const { repoID, path } = this.props; + seafileAPI.queryFolderItemsExtendedPropertiesStatus(repoID, path).then(res => { + if (res.data.is_finished) { + this.timer && clearInterval(this.timer); + this.setState({ submitting: false }); + } else { + this.queryStatus(); + } + }).catch(error => { + // + }); + } + + componentWillUnmount() { + this.timer && clearInterval(this.timer); + } + + queryStatus = () =>{ + const { repoID, path } = this.props; + this.timer = setInterval(() => { + seafileAPI.queryFolderItemsExtendedPropertiesStatus(repoID, path).then(res => { + if (res.data.is_finished === true) { + clearInterval(this.timer); + this.timer = null; + toaster.success(gettext('Applied folder properties')); + this.props.toggle(); + } + }).catch(error => { + clearInterval(this.timer); + this.timer = null; + let errorMsg = Utils.getErrorMsg(error); + toaster.danger(errorMsg); + this.setState({ submitting: false }); + }); + }, 1000); + }; + + submit = () => { + const { repoID, path } = this.props; + this.setState({ submitting: true }); + seafileAPI.setFolderItemsExtendedProperties(repoID, path).then(() => { + this.queryStatus(); + }).catch(error => { + let errorMsg = Utils.getErrorMsg(error); + toaster.danger(errorMsg); + this.setState({ submitting: false }); + }); + }; + + render() { + const { submitting } = this.state; + + return ( + + + {gettext('Apply properties')} + + +

+ {gettext('Are you sure to apply properties to all files inside the folder?')} +

+
+ + + + +
+ ); + } + +} + +ConfirmApplyFolderPropertiesDialog.propTypes = propTypes; + +export default ConfirmApplyFolderPropertiesDialog; diff --git a/frontend/src/components/dirent-detail/detail-list-view.js b/frontend/src/components/dirent-detail/detail-list-view.js index f0eb6f9d80..4c344b0c18 100644 --- a/frontend/src/components/dirent-detail/detail-list-view.js +++ b/frontend/src/components/dirent-detail/detail-list-view.js @@ -8,6 +8,7 @@ import EditFileTagDialog from '../dialog/edit-filetag-dialog'; import ModalPortal from '../modal-portal'; import ExtraAttributesDialog from '../dialog/extra-attributes-dialog'; import FileTagList from '../file-tag-list'; +import ConfirmApplyFolderPropertiesDialog from '../dialog/confirm-apply-folder-properties-dialog'; const propTypes = { repoInfo: PropTypes.object.isRequired, @@ -26,7 +27,8 @@ class DetailListView extends React.Component { super(props); this.state = { isEditFileTagShow: false, - isShowExtraAttributes: false, + isShowExtraProperties: false, + isShowApplyProperties: false }; } @@ -61,8 +63,12 @@ class DetailListView extends React.Component { return Utils.joinPath(path, dirent.name); }; - toggleExtraAttributesDialog = () => { - this.setState({ isShowExtraAttributes: !this.state.isShowExtraAttributes }); + toggleExtraPropertiesDialog = () => { + this.setState({ isShowExtraProperties: !this.state.isShowExtraProperties }); + }; + + toggleApplyPropertiesDialog = () => { + this.setState({ isShowApplyProperties: !this.state.isShowApplyProperties }); }; renderTags = () => { @@ -78,13 +84,26 @@ class DetailListView extends React.Component { {gettext('Location')}{position} {gettext('Last Update')}{moment(direntDetail.mtime).format('YYYY-MM-DD')} {direntDetail.permission === 'rw' && ( - - -
- {gettext('Edit extra properties')} -
- - + + + +
+ {gettext('Edit extra properties')} +
+ + + + +
+ {gettext('Apply properties to files inside the folder')} +
+ + +
)} @@ -109,7 +128,7 @@ class DetailListView extends React.Component { {direntDetail.permission === 'rw' && ( -
+
{gettext('Edit extra properties')}
@@ -138,13 +157,20 @@ class DetailListView extends React.Component { /> } - {this.state.isShowExtraAttributes && ( + {this.state.isShowExtraProperties && ( + )} + {this.state.isShowApplyProperties && ( + )} diff --git a/frontend/src/css/apply-folder-properties.css b/frontend/src/css/apply-folder-properties.css new file mode 100644 index 0000000000..9f0f43f05b --- /dev/null +++ b/frontend/src/css/apply-folder-properties.css @@ -0,0 +1,14 @@ +.apply-properties-dialog .apply-properties { + justify-content: center; + align-items: center; + width: fit-content; + display: flex; + height: 38px; +} + +.apply-properties-dialog .apply-properties .loading-tip { + display: inline-block; + height: 16px; + width: 16px; + margin: 0; +} diff --git a/seahub/api2/endpoints/extended_properties.py b/seahub/api2/endpoints/extended_properties.py index 5de468624f..bb23c2bbd9 100644 --- a/seahub/api2/endpoints/extended_properties.py +++ b/seahub/api2/endpoints/extended_properties.py @@ -4,6 +4,7 @@ import os import stat from datetime import datetime +import requests from rest_framework import status from rest_framework.views import APIView from rest_framework.authentication import SessionAuthentication @@ -17,7 +18,7 @@ from seahub.api2.throttling import UserRateThrottle from seahub.api2.utils import api_error from seahub.base.templatetags.seahub_tags import email2nickname from seahub.settings import DTABLE_WEB_SERVER, SEATABLE_EX_PROPS_BASE_API_TOKEN, \ - EX_PROPS_TABLE, EX_EDITABLE_COLUMNS + EX_PROPS_TABLE, EX_EDITABLE_COLUMNS, SEAF_EVENTS_IO_SERVER_URL from seahub.tags.models import FileUUIDMap from seahub.utils import normalize_file_path, EMPTY_SHA1 from seahub.utils.repo import parse_repo_perm @@ -27,6 +28,29 @@ from seahub.views import check_folder_permission logger = logging.getLogger(__name__) +def request_can_set_ex_props(repo_id, path): + url = SEAF_EVENTS_IO_SERVER_URL.strip('/') + '/can-set-ex-props' + resp = requests.post(url, json={'repo_id': repo_id, 'path': path}) + return resp.json() + + +def add_set_folder_ex_props_task(repo_id, path, username): + url = SEAF_EVENTS_IO_SERVER_URL.strip('/') + '/set-folder-items-ex-props' + context = { + 'repo_id': repo_id, + 'path': path, + '文件负责人': email2nickname(username) + } + resp = requests.post(url, json=context) + return resp.json() + + +def query_set_ex_props_status(repo_id, path): + url = SEAF_EVENTS_IO_SERVER_URL.strip('/') + '/query-set-ex-props-status' + resp = requests.get(url, params={'repo_id': repo_id, 'path': path}) + return resp.json() + + def check_table(seatable_api: SeaTableAPI): """check EX_PROPS_TABLE is invalid or not @@ -76,6 +100,14 @@ class ExtendedPropertiesView(APIView): if not parse_repo_perm(check_folder_permission(request, repo_id, parent_dir)).can_edit_on_web: return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.') + resp_json = request_can_set_ex_props(repo_id, path) + if not resp_json.get('can_set', False): + if resp_json.get('error_type') == 'higher_being_set': + error_msg = 'Another task is running' + else: + error_msg = 'Please try again later' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + # check base try: seatable_api = SeaTableAPI(SEATABLE_EX_PROPS_BASE_API_TOKEN, DTABLE_WEB_SERVER) @@ -229,6 +261,14 @@ class ExtendedPropertiesView(APIView): if not parse_repo_perm(check_folder_permission(request, repo_id, parent_dir)).can_edit_on_web: return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.') + resp_json = request_can_set_ex_props(repo_id, path) + if not resp_json.get('can_set', False): + if resp_json.get('error_type') == 'higher_being_set': + error_msg = 'Another task is running' + else: + error_msg = 'Please try again later' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + # check base try: seatable_api = SeaTableAPI(SEATABLE_EX_PROPS_BASE_API_TOKEN, DTABLE_WEB_SERVER) @@ -316,3 +356,91 @@ class ExtendedPropertiesView(APIView): logger.exception('delete props record error: %s', e) return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error') return Response({'success': True}) + + +class FolderItemsExtendedPropertiesView(APIView): + authentication_classes = (TokenAuthentication, SessionAuthentication) + permission_classes = (IsAuthenticated,) + throttle_classes = (UserRateThrottle,) + + def post(self, request, repo_id): + if not all((DTABLE_WEB_SERVER, SEATABLE_EX_PROPS_BASE_API_TOKEN, EX_PROPS_TABLE)): + return api_error(status.HTTP_403_FORBIDDEN, 'Feature not enabled') + # arguments check + path = request.data.get('path') + if not path: + return api_error(status.HTTP_400_BAD_REQUEST, 'path invalid') + path = normalize_file_path(path) + parent_dir = os.path.dirname(path) + + dirent = seafile_api.get_dirent_by_path(repo_id, path) + if not dirent: + return api_error(status.HTTP_404_NOT_FOUND, 'Folder %s not found' % path) + if not stat.S_ISDIR(dirent.mode): + return api_error(status.HTTP_400_BAD_REQUEST, '%s is not a folder' % path) + + # permission check + if not parse_repo_perm(check_folder_permission(request, repo_id, parent_dir)).can_edit_on_web: + return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.') + + # request props from seatable + try: + seatable_api = SeaTableAPI(SEATABLE_EX_PROPS_BASE_API_TOKEN, DTABLE_WEB_SERVER) + except: + logger.error('server: %s token: %s seatable-api fail', DTABLE_WEB_SERVER, SEATABLE_EX_PROPS_BASE_API_TOKEN) + return api_error(status.HTTP_400_BAD_REQUEST, 'Props table invalid') + + sql = f"SELECT * FROM `{EX_PROPS_TABLE}` WHERE `Repo ID`='{repo_id}' AND `Path`='{path}'" + try: + result = seatable_api.query(sql) + except Exception as e: + logger.exception('query sql: %s error: %s', sql, e) + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error') + rows = result.get('results') + if not rows: + return api_error(status.HTTP_400_BAD_REQUEST, '%s not set extended properties' % path) + + resp_json = add_set_folder_ex_props_task(repo_id, path, request.user.username) + + error_type = resp_json.get('error_type') + if error_type == 'higher_being_set': + error_msg = 'Another task is running' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + elif error_type == 'server_busy': + error_msg = 'Server is busy' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + elif error_type == 'sub_folder_setting': + error_msg = 'Another task is running' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + return Response({'success': True}) + + +class FolderItemsPropertiesStatusQueryView(APIView): + authentication_classes = (TokenAuthentication, SessionAuthentication) + permission_classes = (IsAuthenticated,) + throttle_classes = (UserRateThrottle,) + + def get(self, request, repo_id): + if not all((DTABLE_WEB_SERVER, SEATABLE_EX_PROPS_BASE_API_TOKEN, EX_PROPS_TABLE)): + return api_error(status.HTTP_403_FORBIDDEN, 'Feature not enabled') + # arguments check + path = request.GET.get('path') + if not path: + return api_error(status.HTTP_400_BAD_REQUEST, 'path invalid') + path = normalize_file_path(path) + parent_dir = os.path.dirname(path) + + dirent = seafile_api.get_dirent_by_path(repo_id, path) + if not dirent: + return api_error(status.HTTP_404_NOT_FOUND, 'Folder %s not found' % path) + if not stat.S_ISDIR(dirent.mode): + return api_error(status.HTTP_400_BAD_REQUEST, '%s is not a folder' % path) + + # permission check + if not parse_repo_perm(check_folder_permission(request, repo_id, parent_dir)).can_edit_on_web: + return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.') + + resp_json = query_set_ex_props_status(repo_id, path) + + return Response(resp_json) diff --git a/seahub/settings.py b/seahub/settings.py index 684229d903..d1cc0833f5 100644 --- a/seahub/settings.py +++ b/seahub/settings.py @@ -867,6 +867,11 @@ if os.environ.get('SEAFILE_DOCS', None): LOGO_WIDTH = '' ENABLE_WIKI = True +#################### +# events io server # +#################### +SEAF_EVENTS_IO_SERVER_URL = 'http://127.0.0.1:6066' + ####################### # extended properties # ####################### diff --git a/seahub/urls.py b/seahub/urls.py index e6a0dffd79..2ff425155a 100644 --- a/seahub/urls.py +++ b/seahub/urls.py @@ -119,7 +119,8 @@ from seahub.ocm_via_webdav.ocm_api import OCMProviderView from seahub.api2.endpoints.repo_share_links import RepoShareLinks, RepoShareLink from seahub.api2.endpoints.repo_upload_links import RepoUploadLinks, RepoUploadLink -from seahub.api2.endpoints.extended_properties import ExtendedPropertiesView +from seahub.api2.endpoints.extended_properties import ExtendedPropertiesView, FolderItemsExtendedPropertiesView, \ + FolderItemsPropertiesStatusQueryView # Admin from seahub.api2.endpoints.admin.abuse_reports import AdminAbuseReportsView, AdminAbuseReportView @@ -428,6 +429,8 @@ urlpatterns = [ ## user:file:extended-props re_path(r'^api/v2.1/repos/(?P[-0-9a-f]{36})/extended-properties/$', ExtendedPropertiesView.as_view(), name='api-v2.1-extended-properties'), + re_path(r'^api/v2.1/repos/(?P[-0-9a-f]{36})/folder-items-extended-properties/$', FolderItemsExtendedPropertiesView.as_view(), name='api-v2.1-folder-items-extended-properties'), + re_path(r'^api/v2.1/repos/(?P[-0-9a-f]{36})/folder-items-extended-properties/status-query/$', FolderItemsPropertiesStatusQueryView.as_view(), name='api-v2.1-folder-items-extended-properties-status-query'), re_path(r'^api/v2.1/repos/(?P[-0-9a-f]{36})/auto-delete/$', RepoAutoDeleteView.as_view(), name='api-v2.1-repo-auto-delete'),