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