From 470f09165f68d204f79f9dc93586d52af1eaea33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AD=99=E6=B0=B8=E5=BC=BA?= <11704063+s-yongqiang@user.noreply.gitee.com> Date: Thu, 11 Apr 2024 11:49:28 +0800 Subject: [PATCH] UI design --- .../dialog/group-invite-members-dialog.js | 112 ++++++++++++++++++ .../src/css/group-invite-members-dialog.css | 40 +++++++ frontend/src/pages/groups/group-view.js | 17 +++ seahub/api2/endpoints/group_members.py | 70 +++++++---- seahub/urls.py | 3 +- 5 files changed, 216 insertions(+), 26 deletions(-) create mode 100644 frontend/src/components/dialog/group-invite-members-dialog.js create mode 100644 frontend/src/css/group-invite-members-dialog.css diff --git a/frontend/src/components/dialog/group-invite-members-dialog.js b/frontend/src/components/dialog/group-invite-members-dialog.js new file mode 100644 index 00000000000..a913a8ce788 --- /dev/null +++ b/frontend/src/components/dialog/group-invite-members-dialog.js @@ -0,0 +1,112 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Button, Modal, ModalHeader, ModalBody } from 'reactstrap'; +import copy from 'copy-to-clipboard'; +import toaster from '../toast'; +import { gettext } from '../../utils/constants'; +import { seafileAPI } from '../../utils/seafile-api'; +import { Utils } from '../../utils/utils'; + +import '../../css/group-invite-members-dialog.css'; + +const propTypes = { + groupID: PropTypes.string.isRequired, + toggleGroupInviteDialog: PropTypes.func.isRequired, +}; + +class GroupInviteMembersDialog extends React.Component { + + constructor(props) { + super(props); + this.state = { + inviteList: [], + }; + } + + componentDidMount() { + this.listInviteLinks(); + } + + listInviteLinks = () => { + seafileAPI.getGroupInviteLinks(this.props.groupID).then((res) => { + this.setState({ inviteList: res.data.group_invite_link_list }); + }).catch(error => { + this.onError(error); + }); + }; + + addInviteLink = () => { + seafileAPI.addGroupInviteLinks(this.props.groupID).then(() => { + this.listInviteLinks(); + }).catch(error => { + this.onError(error); + }); + }; + + deleteLink = (token) => { + seafileAPI.deleteGroupInviteLinks(this.props.groupID, token).then(() => { + this.listInviteLinks(); + }).catch(error => { + this.onError(error); + }); + }; + + onError = (error) => { + let errMsg = Utils.getErrorMsg(error, true); + if (!error.response || error.response.status !== 403) { + toaster.danger(errMsg); + } + }; + + copyLink = () => { + const inviteLinkItem = this.state.inviteList[0]; + copy(inviteLinkItem.link); + const message = gettext('Invitation link has been copied to clipboard'); + toaster.success((message), { + duration: 2 + }); + }; + + toggle = () => { + this.props.toggleGroupInviteDialog(); + }; + + render() { + const { inviteList } = this.state; + const link = inviteList[0]; + return ( + + {gettext('Invite members')} + + {link ? + <> +
+ {gettext('Group invitation link')} +
+
+
{link.link}
+
+ +
+ +
+ + : + <> +
+ {gettext('No group invitation link yet. Group invitation link let registered users to join the group by clicking a link.')} +
+ + + } +
+
+ ); + } +} + +GroupInviteMembersDialog.propTypes = propTypes; + +export default GroupInviteMembersDialog; diff --git a/frontend/src/css/group-invite-members-dialog.css b/frontend/src/css/group-invite-members-dialog.css new file mode 100644 index 00000000000..bc6dfc395b0 --- /dev/null +++ b/frontend/src/css/group-invite-members-dialog.css @@ -0,0 +1,40 @@ +.group-invite-members th, +.group-invite-members td { + vertical-align: middle; + text-align: left; +} + +.group-invite-members .no-link-tip { + line-height: 24px; + color: #999; +} + +.invite-link-item { + display: flex; + margin: 1rem 0 2.5rem; +} + +.invite-link-item .form-item { + width: calc(100% - 120px); + padding-left: 10px; + height: 40px; + line-height: 40px; + border: 1px solid #ccc; + border-right: none; +} + +.invite-link-item .invite-link-copy { + width: 72px; +} + +.invite-link-item .invite-link-copy-btn { + width: 72px; + height: 40px; + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.invite-link-item .delete-link-btn { + color: #999; + width: 40px; +} diff --git a/frontend/src/pages/groups/group-view.js b/frontend/src/pages/groups/group-view.js index a2ffdb1000d..52e12613965 100644 --- a/frontend/src/pages/groups/group-view.js +++ b/frontend/src/pages/groups/group-view.js @@ -23,6 +23,7 @@ import LeaveGroupDialog from '../../components/dialog/leave-group-dialog'; import SharedRepoListView from '../../components/shared-repo-list-view/shared-repo-list-view'; import LibDetail from '../../components/dirent-detail/lib-details'; import SortOptionsDialog from '../../components/dialog/sort-options'; +import GroupInviteMembersDialog from '../../components/dialog/group-invite-members-dialog'; import '../../css/group-view.css'; @@ -64,6 +65,7 @@ class GroupView extends React.Component { showTransferGroupDialog: false, showImportMembersDialog: false, showManageMembersDialog: false, + showInviteMembersDialog: false, groupMembers: [], isShowDetails: false, isLeaveGroupDialogOpen: false, @@ -75,6 +77,7 @@ class GroupView extends React.Component { this.loadGroup(groupID); } + UNSAFE_componentWillReceiveProps(nextProps) { if (nextProps.groupID !== this.props.groupID) { this.loadGroup(nextProps.groupID); @@ -318,6 +321,13 @@ class GroupView extends React.Component { }); }; + toggleInviteMembersDialog = () => { + this.setState({ + showInviteMembersDialog: !this.state.showInviteMembersDialog, + showGroupDropdown: false, + }); + }; + toggleLeaveGroupDialog = () => { this.setState({ isLeaveGroupDialogOpen: !this.state.isLeaveGroupDialogOpen, @@ -493,6 +503,7 @@ class GroupView extends React.Component { } { @@ -645,6 +656,12 @@ class GroupView extends React.Component { onGroupChanged={this.props.onGroupChanged} /> } + {this.state.showInviteMembersDialog && + + } ); } diff --git a/seahub/api2/endpoints/group_members.py b/seahub/api2/endpoints/group_members.py index 35df0b644eb..b6810e6c38f 100644 --- a/seahub/api2/endpoints/group_members.py +++ b/seahub/api2/endpoints/group_members.py @@ -21,7 +21,7 @@ from seahub.api2.authentication import TokenAuthentication from seahub.avatar.settings import AVATAR_DEFAULT_SIZE from seahub.base.templatetags.seahub_tags import email2nickname -from seahub.utils import string2list, is_org_context, get_file_type_and_ext +from seahub.utils import string2list, is_org_context, get_file_type_and_ext, render_error from seahub.utils.ms_excel import write_xls from seahub.utils.error_msg import file_type_error_msg from seahub.base.accounts import User @@ -31,7 +31,7 @@ from seahub.profile.models import Profile, GroupInviteLinkModel from .utils import api_check_group -from seahub.settings import SERVICE_URL +from seahub.settings import SERVICE_URL, MULTI_TENANCY from seahub.auth.decorators import login_required logger = logging.getLogger(__name__) @@ -575,6 +575,10 @@ def get(self, request, group_id): email = request.user.username group = ccnet_api.get_group(group_id) + if MULTI_TENANCY: + error_msg = ' Multiple tenancy is not supported.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + if not group: error_msg = 'group not found.' return api_error(status.HTTP_404_NOT_FOUND, error_msg) @@ -598,6 +602,10 @@ def post(self, request, group_id): email = request.user.username group = ccnet_api.get_group(group_id) + if MULTI_TENANCY: + error_msg = ' Multiple tenancy is not supported.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + if not group: error_msg = 'group not found.' return api_error(status.HTTP_404_NOT_FOUND, error_msg) @@ -615,16 +623,44 @@ def post(self, request, group_id): return Response(invite_link.to_dict()) +class GroupInviteLink(APIView): + authentication_classes = (TokenAuthentication, SessionAuthentication) + permission_classes = (IsAuthenticated,) + throttle_classes = (UserRateThrottle,) + + @api_check_group + def delete(self, request, group_id, token): + group_id = int(group_id) + email = request.user.username + + group = ccnet_api.get_group(group_id) + if MULTI_TENANCY: + error_msg = ' Multiple tenancy is not supported.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + if not group: + error_msg = 'group not found.' + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + if not is_group_owner_or_admin(group, email): + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + try: + GroupInviteLinkModel.objects.filter(token=token, group_id=group_id).delete() + except Exception as e: + logger.error(f'delete group invite links failed. {e}') + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error') + + return Response({'success': True}) + + @login_required def group_invite(request, token): """ - reigsterd user add to group + registered user add to group """ - print(request.METHOD) - print(request.user) email = request.user.username - print(request.user) - print(111) next_url = request.GET.get('next', '/') redirect_to = SERVICE_URL.rstrip('/') + '/' + next_url.lstrip('/') group_invite_link = GroupInviteLinkModel.objects.filter(token=token).first() @@ -632,32 +668,16 @@ def group_invite(request, token): return render_error(request, _('Group invite link does not exist')) if is_group_member(group_invite_link.group_id, email): - print("is group member") - print(group_invite_link.group_id, group_invite_link.created_by, email) - return HttpResponseRedirect(redirect_to) - try: - group_org_id = ccnet_api.get_org_id_by_group(group_invite_link.group_id) - except Exception as e: - logger.error(f'get org id by group failed. {e}') - return render_error(request, 'Internal Server Error') - - # org user but not same org - if request.user.org and request.user.org.org_id != group_org_id: - return render_error(request, _('You cannot join this group')) - - # non-org user but group is in org - if not request.user.org and group_org_id > 0: - return render_error(request, _('You cannot join this group')) + return HttpResponseRedirect(redirect_to) if not group_invite_link.created_by: return render_error(request, _('Group invite link broken')) try: - print(group_invite_link.group_id, group_invite_link.created_by, email) ccnet_api.group_add_member(group_invite_link.group_id, group_invite_link.created_by, email) except Exception as e: logger.error(f'group invite add user failed. {e}') return render_error(request, 'Internal Server Error') - print(redirect_to) + return HttpResponseRedirect(redirect_to) diff --git a/seahub/urls.py b/seahub/urls.py index e7171832521..f2a94ec213f 100644 --- a/seahub/urls.py +++ b/seahub/urls.py @@ -38,7 +38,7 @@ from seahub.api2.endpoints.address_book.members import AddressBookGroupsSearchMember from seahub.api2.endpoints.group_members import GroupMembers, GroupSearchMember, GroupMember, \ - GroupMembersBulk, GroupMembersImport, GroupMembersImportExample, GroupInviteLinks, group_invite + GroupMembersBulk, GroupMembersImport, GroupMembersImportExample, GroupInviteLinks, GroupInviteLink, group_invite from seahub.api2.endpoints.search_group import SearchGroup from seahub.api2.endpoints.share_links import ShareLinks, ShareLink, \ ShareLinkOnlineOfficeLock, ShareLinkDirents, ShareLinkSaveFileToRepo, \ @@ -343,6 +343,7 @@ re_path(r'^api/v2.1/search-group/$', SearchGroup.as_view(), name='api-v2.1-search-group'), re_path(r'^api/v2.1/groups/(?P\d+)/invite-links/$', GroupInviteLinks.as_view(),name='api-v2.1-group-invite-links'), + re_path(r'^api/v2.1/groups/(?P\d+)/invite-links/(?P[-0-9a-f]{8})/$', GroupInviteLink.as_view(), name='api-v2.1-group-invite-link'), re_path(r'^group-invite/(?P[-0-9a-f]{8})/$', group_invite, name='group_invite'), ## address book re_path(r'^api/v2.1/address-book/groups/(?P\d+)/sub-groups/$', AddressBookGroupsSubGroups.as_view(), name='api-v2.1-address-book-groups-sub-groups'),