perf: invite member code (#4118)

* perf: invite member code

* fix: ts
This commit is contained in:
Archer 2025-03-12 14:35:38 +08:00 committed by archer
parent a9e5017492
commit 7eda599181
No known key found for this signature in database
GPG Key ID: 4446499B846D4A9E
20 changed files with 284 additions and 342 deletions

View File

@ -12,6 +12,7 @@ weight: 799
1. 商业版支持单团队模式,更好的管理内部成员。
2. 知识库分块阅读器。
3. API 知识库支持 PDF 增强解析。
4. 邀请团队成员,改为邀请链接模式。
## ⚙️ 优化

View File

@ -1,3 +0,0 @@
export function isForbidden({ expires, forbidden }: { expires: Date; forbidden?: boolean }) {
return forbidden || new Date(expires) < new Date();
}

View File

@ -16,17 +16,13 @@ const InvitationSchema = new Schema({
required: true
},
usedTimesLimit: {
type: Number
},
forbidden: {
type: Boolean
},
expires: {
type: Date
},
description: {
type: String
type: Number,
default: 1,
enum: [1, -1]
},
forbidden: Boolean,
expires: Date,
description: String,
members: {
type: [String],
default: []
@ -40,10 +36,9 @@ InvitationSchema.virtual('team', {
justOne: true
});
InvitationSchema.index({ expires: 1 }, { expireAfterSeconds: 30 * 24 * 60 * 60 });
try {
InvitationSchema.index({ teamId: 1 }, { background: true });
InvitationSchema.index({ teamId: 1 });
InvitationSchema.index({ expires: 1 }, { expireAfterSeconds: 30 * 24 * 60 * 60 });
} catch (error) {
console.log(error);
}

View File

@ -23,7 +23,7 @@ export type InvitationLinkExpiresType = '30m' | '7d' | '1y';
export type InvitationLinkCreateType = {
description: string;
expires: InvitationLinkExpiresType;
usedTimesLimit: number;
usedTimesLimit: 1 | -1;
};
export type InvitationLinkUpdateType = Partial<
Omit<InvitationSchemaType, 'members' | 'teamId' | '_id'>

View File

@ -1,18 +1,39 @@
{
"1person": "1 person",
"1year": "1 Year",
"30mins": "30 Minutes",
"7days": "7 Days",
"accept": "accept",
"action": "operate",
"confirm_delete_group": "Confirm to delete group?",
"confirm_delete_member": "Confirm to delete member?",
"confirm_delete_org": "Confirm to delete organization?",
"confirm_forbidden": "Confirm forbidden",
"confirm_leave_team": "Confirmed to leave the team? \nAfter exiting, all your resources in the team are transferred to the team owner.",
"copy_link": "Copy link",
"create_group": "Create group",
"create_invitation_link": "Create Invitation Link",
"create_org": "Create organization",
"create_sub_org": "Create sub-organization",
"delete": "delete",
"delete_org": "Delete organization",
"edit_info": "Edit information",
"edit_org_info": "Edit organization information",
"expires": "Expiration",
"forbid_hint": "After forbidden, this invitation link will become invalid. This action is irreversible. Are you sure you want to deactivate?",
"forbid_success": "Forbid success",
"forbidden": "Forbidden",
"group": "group",
"group_name": "Group name",
"handle_invitation": "Handle Invitation",
"has_forbidden": "Forbidden",
"has_invited": "Invited",
"ignore": "Ignore",
"invitation_link_auto_clean_hint": "Expired links will be automatically cleaned up after 30 days",
"invitation_link_description": "Link description",
"invitation_link_list": "Invitation link list",
"invite_member": "Invite members",
"invited": "Invited",
"label_sync": "Tag sync",
"leave_team_failed": "Leaving the team exception",
"manage_member": "Managing members",
@ -31,31 +52,11 @@
"search_member_group_name": "Search member/group name",
"total_team_members": "{{amount}} members in total",
"transfer_ownership": "transfer owner",
"unlimited": "Unlimited",
"used_times_limit": "Limit",
"user_name": "username",
"user_team_invite_member": "Invite members",
"user_team_leave_team": "Leave the team",
"user_team_leave_team_failed": "Failure to leave the team",
"waiting": "To be accepted",
"invitation_link_list": "Invitation link list",
"create_invitation_link": "Create Invitation Link",
"invitation_link_description": "Link description",
"30mins": "30 Minutes",
"7days": "7 Days",
"1year": "1 Year",
"unlimited": "Unlimited",
"1person": "1 person",
"expires": "Expiration",
"used_times_limit": "Limit",
"invited": "Invited",
"has_forbidden": "Forbidden",
"forbidden": "Forbidden",
"copy_link": "Copy link",
"handle_invitation": "Handle Invitation",
"ignore": "Ignore",
"forbid_success": "Forbid success",
"forbid_hint": "After forbidden, this invitation link will become invalid. This action is irreversible. Are you sure you want to deactivate?",
"confirm_forbidden": "Confirm forbidden",
"invitation_link_auto_clean_hint": "Expired links will be automatically cleaned up after 30 days",
"has_invited": "Invited",
"invitation_link_has_been_invalid": "The invitation link has expired. Please contact the team administrator"
"waiting": "To be accepted"
}

View File

@ -89,6 +89,7 @@
"code_error.team_error.group_name_duplicate": "Duplicate group name",
"code_error.team_error.group_name_empty": "Group name cannot be empty",
"code_error.team_error.group_not_exist": "Group does not exist",
"code_error.team_error.invitation_link_invalid": "The invitation link has expired",
"code_error.team_error.not_user": "The member cannot be found",
"code_error.team_error.org_member_duplicated": "Duplicate organization member",
"code_error.team_error.org_member_not_exist": "Organization member does not exist",
@ -97,12 +98,11 @@
"code_error.team_error.over_size": "error.team.overSize",
"code_error.team_error.plugin_amount_not_enough": "Plugin Limit Reached",
"code_error.team_error.re_rank_not_enough": "Search rearrangement cannot be used in the free version~",
"code_error.team_error.too_many_invitations": "You have reached the maximum number of active invitation links, please clean up some links first",
"code_error.team_error.un_auth": "Unauthorized to Operate This Team",
"code_error.team_error.user_not_active": "The user did not accept or has left the team",
"code_error.team_error.website_sync_not_enough": "The free version cannot be synchronized with the web site ~",
"code_error.team_error.invitation_link_invalid": "Invitation link is invalid",
"code_error.team_error.you_have_been_in_the_team": "You are already in this team",
"code_error.team_error.too_many_invitations": "You have reached the maximum number of active invitation links, please clean up some links first",
"code_error.token_error_code.403": "Invalid Login Status, Please Re-login",
"code_error.user_error.balance_not_enough": "Insufficient Account Balance",
"code_error.user_error.bin_visitor_guest": "You Are Currently a Guest, Unauthorized to Operate",
@ -272,6 +272,7 @@
"compliance.compliance.dataset": "Please ensure that your content strictly complies with relevant laws and regulations and avoid containing any illegal or infringing content. \nPlease be careful when uploading materials that may contain sensitive information.",
"compliance.dataset": "Please ensure that your content strictly complies with relevant laws and regulations and avoid containing any illegal or infringing content. \nPlease be careful when uploading materials that may contain sensitive information.",
"confirm_choice": "Confirm Choice",
"contact_way": "Contact information",
"contribute_app_template": "Contribute Template",
"core.Chat": "Chat",
"core.Max Token": "Max Token",
@ -1255,11 +1256,6 @@
"user.team.Check Team": "Switch",
"user.team.Confirm Invite": "Confirm Invite",
"user.team.Create Team": "Create New Team",
"user.team.Invite Member": "Invite Member",
"user.team.Invite Member Failed Tip": "Failed to Invite Member",
"user.team.Invite Member Result Tip": "Invite Result Tip",
"user.team.Invite Member Success Tip": "Member Invitation Completed\nSuccess: {{success}} people\nInvalid Username: {{inValid}}\nAlready in Team: {{inTeam}}",
"user.team.Invite Member Tips": "The other party can view or use other resources within the team",
"user.team.Leave Team": "Leave Team",
"user.team.Leave Team Failed": "Failed to Leave Team",
"user.team.Member": "Member",
@ -1277,13 +1273,9 @@
"user.team.Team Tags Async": "Tag Sync",
"user.team.Team Tags Async Success": "Link Error Successful, Tag Information Updated",
"user.team.Update Team": "Update Team Information",
"user.team.invite.Accept Confirm": "Confirm to join this team?",
"user.team.invite.Accepted": "Joined Team",
"user.team.invite.Deal Width Footer Tip": "It will automatically close after processing",
"user.team.invite.Reject": "Invitation Rejected",
"user.team.invite.Reject Confirm": "Confirm to reject this invitation?",
"user.team.invite.accept": "Accept",
"user.team.invite.reject": "Reject",
"user.team.member.Confirm Leave": "Confirm to leave this team?",
"user.team.member.active": "Joined",
"user.team.member.reject": "Rejected",

View File

@ -72,6 +72,5 @@
"verification_code": "验证码",
"you_can_convert": "您可以兑换",
"yuan": "元",
"contact": "联系方式",
"please_bind_contact": "请绑定联系方式"
}

View File

@ -1,24 +1,53 @@
{
"1person": "1人",
"1year": "1年",
"30mins": "30分钟",
"7days": "7天",
"accept": "接受",
"action": "操作",
"confirm_delete_from_org": "确认将 {{username}} 移出部门?",
"confirm_delete_from_team": "确认将 {{username}} 移出团队?",
"confirm_delete_group": "确认删除群组?",
"confirm_delete_org": "确认删除该部门?",
"confirm_forbidden": "确认停用",
"confirm_leave_team": "确认离开该团队? \n退出后您在该团队所有的资源均转让给团队所有者。",
"copy_link": "复制链接",
"create_group": "创建群组",
"create_invitation_link": "创建邀请链接",
"create_org": "创建部门",
"create_sub_org": "创建子部门",
"delete": "删除",
"delete_from_org": "移出部门",
"delete_from_team": "移出团队",
"delete_org": "删除部门",
"edit_info": "编辑信息",
"edit_org_info": "编辑部门信息",
"expires": "有效期",
"export_members": "导出成员",
"forbid_hint": "停用后,该邀请链接将失效。 该操作不可撤销,是否确认停用?",
"forbid_success": "停用成功",
"forbidden": "停用",
"group": "群组",
"group_name": "群组名称",
"handle_invitation": "处理团队邀请",
"has_forbidden": "已失效",
"has_invited": "已邀请",
"ignore": "忽略",
"invitation_link_auto_clean_hint": "已失效链接将在30天后自动清理",
"invitation_link_description": "链接描述",
"invitation_link_list": "链接列表",
"invite_member": "邀请成员",
"invited": "已邀请",
"join_update_time": "加入/更新时间",
"label_sync": "标签同步",
"leave": "已离职",
"leave_team_failed": "离开团队异常",
"manage_member": "管理成员",
"member": "成员",
"member_group": "所属群组",
"move_member": "移动成员",
"move_org": "移动部门",
"notification_recieve": "团队通知接收",
"org": "部门",
"org_description": "介绍",
"org_name": "部门名称",
@ -29,49 +58,20 @@
"restore_tip": "确认将 {{username}} 加入团队吗?仅恢复该成员账号可用性及相关权限,无法恢复账号下资源。",
"restore_tip_title": "恢复确认",
"retain_admin_permissions": "保留管理员权限",
"search_member": "搜索成员",
"search_member_group_name": "搜索成员/群组名称",
"search_org": "搜索部门",
"set_name_avatar": "团队头像 & 团队名",
"sync_immediately": "立即同步",
"sync_member_failed": "同步成员失败",
"sync_member_success": "同步成员成功",
"total_team_members": "共 {{amount}} 名成员",
"transfer_ownership": "转让所有者",
"unlimited": "无限制",
"used_times_limit": "有效人数",
"user_name": "用户名",
"user_team_invite_member": "邀请成员",
"user_team_leave_team": "离开团队",
"user_team_leave_team_failed": "离开团队失败",
"waiting": "待接受",
"sync_immediately": "立即同步",
"sync_member_failed": "同步成员失败",
"sync_member_success": "同步成员成功",
"contact": "联系方式",
"join_update_time": "加入/更新时间",
"leave": "已离职",
"search_member": "搜索成员",
"export_members": "导出成员",
"delete_from_team": "移出团队",
"delete_from_org": "移出部门",
"confirm_delete_from_team": "确认将 {{username}} 移出团队?",
"confirm_delete_from_org": "确认将 {{username}} 移出部门?",
"search_org": "搜索部门",
"notification_recieve": "团队通知接收",
"set_name_avatar": "团队头像 & 团队名",
"invitation_link_list": "链接列表",
"create_invitation_link": "创建邀请链接",
"invitation_link_description": "链接描述",
"30mins": "30分钟",
"7days": "7天",
"1year": "1年",
"unlimited": "无限制",
"1person": "1人",
"expires": "有效期",
"used_times_limit": "有效人数",
"invited": "已邀请",
"has_forbidden": "已失效",
"forbidden": "停用",
"copy_link": "复制链接",
"handle_invitation": "处理团队邀请",
"ignore": "忽略",
"forbid_success": "停用成功",
"forbid_hint": "停用后,该邀请链接将失效。 该操作不可撤销,是否确认停用?",
"confirm_forbidden": "确认停用",
"invitation_link_auto_clean_hint": "已失效链接将在30天后自动清理",
"has_invited": "已邀请",
"invitation_link_has_been_invalid": "邀请链接已失效,请联系团队管理员"
"waiting": "待接受"
}

View File

@ -93,6 +93,7 @@
"code_error.team_error.group_name_duplicate": "群组名称重复",
"code_error.team_error.group_name_empty": "群组名称不能为空",
"code_error.team_error.group_not_exist": "群组不存在",
"code_error.team_error.invitation_link_invalid": "邀请链接已失效",
"code_error.team_error.not_user": "找不到该成员",
"code_error.team_error.org_member_duplicated": "重复的部门成员",
"code_error.team_error.org_member_not_exist": "部门成员不存在",
@ -101,12 +102,11 @@
"code_error.team_error.over_size": "error.team.overSize",
"code_error.team_error.plugin_amount_not_enough": "插件数量已达上限~",
"code_error.team_error.re_rank_not_enough": "免费版无法使用检索重排~",
"code_error.team_error.too_many_invitations": "您的有效邀请链接数已达上限,请先清理链接",
"code_error.team_error.un_auth": "无权操作该团队",
"code_error.team_error.user_not_active": "用户未接受或已离开团队",
"code_error.team_error.website_sync_not_enough": "免费版无法使用Web站点同步~",
"code_error.team_error.invitation_link_invalid": "邀请链接无效",
"code_error.team_error.you_have_been_in_the_team": "你已经在该团队中",
"code_error.team_error.too_many_invitations": "您的有效邀请链接数已达上限,请先清理链接",
"code_error.token_error_code.403": "登录状态无效,请重新登录",
"code_error.user_error.balance_not_enough": "账号余额不足~",
"code_error.user_error.bin_visitor_guest": "您当前身份为游客,无权操作",
@ -275,6 +275,7 @@
"compliance.chat": "内容由第三方 AI 生成,无法确保真实准确,仅供参考",
"compliance.dataset": "请确保您的内容严格遵守相关法律法规,避免包含任何违法或侵权的内容。请谨慎上传可能涉及敏感信息的资料。",
"confirm_choice": "确认选择",
"contact_way": "联系方式",
"contribute_app_template": "贡献模板",
"core.Chat": "对话",
"core.Max Token": "单条数据上限",
@ -1257,8 +1258,6 @@
"user.password_message": "密码最少 4 位最多 60 位",
"user.team.Balance": "团队余额",
"user.team.Check Team": "切换",
"user.team.Invite Member": "邀请成员",
"user.team.Invite Member Tips": "对方可查阅或使用团队内的其他资源",
"user.team.Leave Team": "离开团队",
"user.team.Leave Team Failed": "离开团队异常",
"user.team.Member": "成员",
@ -1273,13 +1272,9 @@
"user.team.Tags Async": "保存",
"user.team.Team Tags Async": "标签同步",
"user.team.Team Tags Async Success": "链接报错成功,标签信息更新",
"user.team.invite.Accept Confirm": "确认加入该团队?",
"user.team.invite.Accepted": "已加入团队",
"user.team.invite.Deal Width Footer Tip": "处理完会自动关闭噢~",
"user.team.invite.Reject": "已拒绝邀请",
"user.team.invite.Reject Confirm": "确认拒绝该邀请?",
"user.team.invite.accept": "接受",
"user.team.invite.reject": "拒绝",
"user.team.member.Confirm Leave": "确认离开该团队?\n退出后您在该团队所有的资源 应用、知识库、文件夹、管理的群组等)均转让给团队所有者。",
"user.team.member.active": "已加入",
"user.team.member.reject": "拒绝",

View File

@ -1,18 +1,39 @@
{
"1person": "1人",
"1year": "1年",
"30mins": "30分鐘",
"7days": "7天",
"accept": "接受",
"action": "操作",
"confirm_delete_group": "確認刪除群組?",
"confirm_delete_member": "確認刪除成員?",
"confirm_delete_org": "確認刪除該部門?",
"confirm_forbidden": "確認停用",
"confirm_leave_team": "確認離開該團隊? \n退出後您在該團隊所有的資源轉讓給團隊所有者。",
"copy_link": "複製連結",
"create_group": "建立群組",
"create_invitation_link": "建立邀請連結",
"create_org": "創建部門",
"create_sub_org": "創建子部門",
"delete": "刪除",
"delete_org": "刪除部門",
"edit_info": "編輯訊息",
"edit_org_info": "編輯部門資訊",
"expires": "有效期",
"forbid_hint": "停用後,該邀請連結將失效。 該操作不可撤銷,是否確認停用?",
"forbid_success": "停用成功",
"forbidden": "停用",
"group": "群組",
"group_name": "群組名稱",
"handle_invitation": "處理團隊邀請",
"has_forbidden": "已失效",
"has_invited": "已邀請",
"ignore": "忽略",
"invitation_link_auto_clean_hint": "已失效連結將在30天後自動清理",
"invitation_link_description": "連結描述",
"invitation_link_list": "連結列表",
"invite_member": "邀請成員",
"invited": "已邀請",
"label_sync": "標籤同步",
"leave_team_failed": "離開團隊異常",
"manage_member": "管理成員",
@ -31,31 +52,11 @@
"search_member_group_name": "搜尋成員/群組名稱",
"total_team_members": "共 {{amount}} 名成員",
"transfer_ownership": "轉讓所有者",
"unlimited": "無限制",
"used_times_limit": "有效人數",
"user_name": "使用者名稱",
"user_team_invite_member": "邀請成員",
"user_team_leave_team": "離開團隊",
"user_team_leave_team_failed": "離開團隊失敗",
"waiting": "待接受",
"invitation_link_list": "連結列表",
"create_invitation_link": "建立邀請連結",
"invitation_link_description": "連結描述",
"30mins": "30分鐘",
"7days": "7天",
"1year": "1年",
"unlimited": "無限制",
"1person": "1人",
"expires": "有效期",
"used_times_limit": "有效人數",
"invited": "已邀請",
"has_forbidden": "已失效",
"forbidden": "停用",
"copy_link": "複製連結",
"handle_invitation": "處理團隊邀請",
"ignore": "忽略",
"forbid_success": "停用成功",
"forbid_hint": "停用後,該邀請連結將失效。 該操作不可撤銷,是否確認停用?",
"confirm_forbidden": "確認停用",
"invitation_link_auto_clean_hint": "已失效連結將在30天後自動清理",
"has_invited": "已邀請",
"invitation_link_has_been_invalid": "邀請連結已失效,請聯繫團隊管理員"
"waiting": "待接受"
}

View File

@ -88,6 +88,7 @@
"code_error.team_error.group_name_duplicate": "群組名稱重複",
"code_error.team_error.group_name_empty": "群組名稱不能為空",
"code_error.team_error.group_not_exist": "群組不存在",
"code_error.team_error.invitation_link_invalid": "邀請鏈接已失效",
"code_error.team_error.not_user": "找不到該成員",
"code_error.team_error.org_member_duplicated": "重複的組織成員",
"code_error.team_error.org_member_not_exist": "組織成員不存在",
@ -96,12 +97,11 @@
"code_error.team_error.over_size": "error.team.overSize",
"code_error.team_error.plugin_amount_not_enough": "已達外掛程式數量上限",
"code_error.team_error.re_rank_not_enough": "免費版無法使用檢索重排~",
"code_error.team_error.too_many_invitations": "您的有效邀請連結數已達上限,請先清理連結",
"code_error.team_error.un_auth": "無權操作此團隊",
"code_error.team_error.user_not_active": "使用者未接受或已離開團隊",
"code_error.team_error.website_sync_not_enough": "免費版無法使用Web站點同步~",
"code_error.team_error.invitation_link_invalid": "邀請連結無效",
"code_error.team_error.you_have_been_in_the_team": "你已經在該團隊中",
"code_error.team_error.too_many_invitations": "您的有效邀請連結數已達上限,請先清理連結",
"code_error.token_error_code.403": "登入狀態無效,請重新登入",
"code_error.user_error.balance_not_enough": "帳戶餘額不足",
"code_error.user_error.bin_visitor_guest": "您目前身份為訪客,無權操作",
@ -271,6 +271,7 @@
"compliance.compliance.dataset": "請確保您的內容嚴格遵守相關法律法規,避免包含任何違法或侵權的內容。\n在上傳可能涉及敏感資訊的資料時請務必謹慎。",
"compliance.dataset": "請確保您的內容嚴格遵守相關法律法規,避免包含任何違法或侵權的內容。\n在上傳可能涉及敏感資訊的資料時請務必謹慎。",
"confirm_choice": "確認選擇",
"contact_way": "聯繫方式",
"contribute_app_template": "貢獻範本",
"core.Chat": "對話",
"core.Max Token": "單筆資料上限",
@ -1254,11 +1255,6 @@
"user.team.Check Team": "切換",
"user.team.Confirm Invite": "確認邀請",
"user.team.Create Team": "建立新團隊",
"user.team.Invite Member": "邀請成員",
"user.team.Invite Member Failed Tip": "邀請成員失敗",
"user.team.Invite Member Result Tip": "邀請結果提示",
"user.team.Invite Member Success Tip": "邀請成員完成\n成功{{success}} 人\n使用者名稱無效{{inValid}}\n已在團隊中{{inTeam}}",
"user.team.Invite Member Tips": "對方可檢視或使用團隊內的其他資源",
"user.team.Leave Team": "離開團隊",
"user.team.Leave Team Failed": "離開團隊失敗",
"user.team.Member": "成員",
@ -1276,13 +1272,9 @@
"user.team.Team Tags Async": "標籤同步",
"user.team.Team Tags Async Success": "連結錯誤修正成功,標籤資訊已更新",
"user.team.Update Team": "更新團隊資訊",
"user.team.invite.Accept Confirm": "確認加入此團隊?",
"user.team.invite.Accepted": "已加入團隊",
"user.team.invite.Deal Width Footer Tip": "處理完會自動關閉",
"user.team.invite.Reject": "已拒絕邀請",
"user.team.invite.Reject Confirm": "確認拒絕此邀請?",
"user.team.invite.accept": "接受",
"user.team.invite.reject": "拒絕",
"user.team.member.Confirm Leave": "確認離開此團隊?\n離開後您在該團隊的所有資源應用程式、知識庫、資料夾、管理的群組等均會轉讓給團隊擁有者。",
"user.team.member.active": "已加入",
"user.team.member.reject": "已拒絕",

View File

@ -79,7 +79,7 @@ const UpdateContactModal = ({
title={
mode === 'notification_account'
? t('common:support.user.info.notification_receiving_hint')
: t('account_info:contact')
: t('common:contact_way')
}
>
<ModalBody px={10}>

View File

@ -1,106 +0,0 @@
import { getInvitationInfo, postAcceptInvitationLink } from '@/web/support/user/team/api';
import {
Box,
Button,
CloseButton,
Flex,
ModalBody,
ModalCloseButton,
ModalHeader
} from '@chakra-ui/react';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next';
import { useRouter } from 'next/router';
import { useCallback } from 'react';
import { useContextSelector } from 'use-context-selector';
import { TeamContext } from './context';
import { isForbidden } from '@fastgpt/service/support/user/team/invitationLink/controllers';
import { useToast } from '@fastgpt/web/hooks/useToast';
function Invite({ invitelinkid }: { invitelinkid: string }) {
const router = useRouter();
const { t } = useTranslation();
const { onSwitchTeam, refetchMembers } = useContextSelector(TeamContext, (v) => v);
const onClose = () => {
router.push('/account/team');
};
const { toast } = useToast();
const { data: invitationInfo } = useRequest2(() => getInvitationInfo(invitelinkid), {
manual: false,
onSuccess: (data) => {
if (isForbidden(data)) {
toast({
description: t('account_team:invitation_link_has_been_invalid'),
status: 'warning'
});
onClose();
}
},
onError: onClose
});
const { runAsync: acceptInvitation } = useRequest2(() => postAcceptInvitationLink(invitelinkid), {
manual: true,
successToast: t('common:common.Success'),
onSuccess: () => {
toast({
description: t('common:common.Success'),
status: 'success'
});
onSwitchTeam(invitationInfo!.teamId);
refetchMembers();
onClose();
},
onError: (e) => {
toast({
description: t('common:common.Error'),
status: 'error'
});
onClose();
}
});
return (
<>
{invitationInfo && (
<MyModal
isOpen={true}
iconSrc="support/user/usersLight"
title={t('account_team:handle_invitation')}
iconColor={'primary.600'}
>
<ModalCloseButton onClick={onClose} />
<ModalBody>
<Flex
key={invitationInfo._id}
alignItems={'center'}
border={'1px solid'}
borderColor={'myGray.200'}
borderRadius={'md'}
px={3}
py={2}
>
<Avatar src={invitationInfo.teamAvatar} w={['16px', '23px']} />
<Box mx={2}>{invitationInfo.teamName}</Box>
<Box flex={1} />
<Button size="sm" variant={'solid'} colorScheme="green" onClick={acceptInvitation}>
{t('common:user.team.invite.accept')}
</Button>
<Button size="sm" ml={2} variant="outline" onClick={onClose}>
{t('account_team:ignore')}
</Button>
</Flex>
</ModalBody>
</MyModal>
)}
</>
);
}
export default Invite;

View File

@ -3,11 +3,13 @@ import {
Box,
Button,
Grid,
HStack,
Radio,
RadioGroup,
Input,
ModalBody,
ModalCloseButton,
ModalFooter
ModalFooter,
HStack
} from '@chakra-ui/react';
import {
InvitationLinkCreateType,
@ -28,22 +30,18 @@ function CreateInvitationModal({ onClose }: { onClose: () => void }) {
{ label: t('account_team:1year'), value: '1y' } // 1 year
];
const usedTimesLimitOptions = [
{ label: t('account_team:unlimited'), value: -1 },
{ label: t('account_team:1person'), value: 1 }
];
const { register, handleSubmit, watch, setValue } = useForm<InvitationLinkCreateType>({
defaultValues: {
description: '',
expires: expiresOptions[1].value,
usedTimesLimit: usedTimesLimitOptions[1].value
usedTimesLimit: 1
}
});
const expires = watch('expires');
const usedTimesLimit = watch('usedTimesLimit');
const { runAsync: createInvitationLink } = useRequest2(postCreateInvitationLink, {
const { runAsync: createInvitationLink, loading } = useRequest2(postCreateInvitationLink, {
manual: true,
successToast: t('common:common.Create Success'),
errorToast: t('common:common.Create Failed'),
@ -56,39 +54,47 @@ function CreateInvitationModal({ onClose }: { onClose: () => void }) {
iconSrc="common/addLight"
iconColor="primary.500"
title={<Box>{t('account_team:create_invitation_link')}</Box>}
minW={'500px'}
>
<ModalCloseButton onClick={onClose} />
<ModalBody>
<Grid gap={4} w="full" templateColumns="max-content 1fr" alignItems="center">
<FormLabel required={true}>{t('account_team:invitation_link_description')}</FormLabel>
<Input
placeholder={t('account_team:invitation_link_description')}
{...register('description', { required: true })}
/>
<Grid gap={6} templateColumns="max-content 1fr" alignItems="center">
<>
<FormLabel required={true}>{t('account_team:invitation_link_description')}</FormLabel>
<Input
placeholder={t('account_team:invitation_link_description')}
{...register('description', { required: true })}
/>
</>
<FormLabel required={true}>{t('account_team:expires')}</FormLabel>
<MySelect
list={expiresOptions}
value={expires}
onchange={(val) => setValue('expires', val)}
minW="120px"
/>
<>
<FormLabel required={true}>{t('account_team:expires')}</FormLabel>
<MySelect
list={expiresOptions}
value={expires}
onchange={(val) => setValue('expires', val)}
minW="120px"
/>
</>
<FormLabel required={true}>{t('account_team:used_times_limit')}</FormLabel>
<MySelect
list={usedTimesLimitOptions}
value={usedTimesLimit}
onchange={(val) => setValue('usedTimesLimit', val)}
minW="120px"
/>
<>
<FormLabel required={true}>{t('account_team:used_times_limit')}</FormLabel>
<RadioGroup
onChange={(val: '1' | '-1') => setValue('usedTimesLimit', Number(val) as 1 | -1)}
value={String(usedTimesLimit)}
>
<HStack gap={6}>
<Radio value="1">{t('account_team:1person')}</Radio>
<Radio value="-1">{t('account_team:unlimited')}</Radio>
</HStack>
</RadioGroup>
</>
</Grid>
</ModalBody>
<ModalFooter>
<Button isLoading={false} onClick={onClose} variant="outline">
<Button isLoading={loading} onClick={onClose} variant="outline">
{t('common:common.Cancel')}
</Button>
<Button isLoading={false} onClick={handleSubmit(createInvitationLink)} ml="4">
<Button isLoading={loading} onClick={handleSubmit(createInvitationLink)} ml="4">
{t('common:common.Confirm')}
</Button>
</ModalFooter>

View File

@ -0,0 +1,77 @@
import { getInvitationInfo, postAcceptInvitationLink } from '@/web/support/user/team/api';
import { Box, Button, Flex, ModalBody, ModalCloseButton } from '@chakra-ui/react';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next';
import { useRouter } from 'next/router';
import { useContextSelector } from 'use-context-selector';
import { TeamContext } from '../context';
function Invite({ invitelinkid }: { invitelinkid: string }) {
const router = useRouter();
const { t } = useTranslation();
const { onSwitchTeam } = useContextSelector(TeamContext, (v) => v);
const onClose = () => {
router.push('/account/team');
};
const { data: invitationInfo } = useRequest2(() => getInvitationInfo(invitelinkid), {
manual: false,
onError: onClose
});
const { runAsync: acceptInvitation, loading: accepting } = useRequest2(
() => postAcceptInvitationLink(invitelinkid),
{
manual: true,
successToast: t('common:common.Success'),
onSuccess: async () => {
onSwitchTeam(invitationInfo!.teamId);
onClose();
}
}
);
return invitationInfo ? (
<MyModal
isOpen={true}
iconSrc="support/user/usersLight"
title={t('account_team:handle_invitation')}
iconColor={'primary.600'}
>
<ModalCloseButton onClick={onClose} />
<ModalBody>
<Flex
key={invitationInfo._id}
alignItems={'center'}
border={'1px solid'}
borderColor={'myGray.200'}
borderRadius={'md'}
px={3}
py={2}
>
<Avatar src={invitationInfo.teamAvatar} w={['16px', '23px']} />
<Box mx={2}>{invitationInfo.teamName}</Box>
<Box flex={1} />
<Button
size="sm"
variant={'solid'}
colorScheme="green"
onClick={acceptInvitation}
isLoading={accepting}
>
{t('account_team:accept')}
</Button>
<Button size="sm" ml={2} variant="outline" onClick={onClose} isLoading={accepting}>
{t('account_team:ignore')}
</Button>
</Flex>
</ModalBody>
</MyModal>
) : null;
}
export default Invite;

View File

@ -1,5 +1,4 @@
import MemberTag from '@/components/support/user/team/Info/MemberTag';
import Empty from '@/pageComponents/chat/Empty';
import { getInvitationLinkList, putUpdateInvitationInfo } from '@/web/support/user/team/api';
import {
Box,
@ -9,9 +8,7 @@ import {
Grid,
HStack,
ModalBody,
ModalCloseButton,
ModalFooter,
ModalHeader,
Table,
TableContainer,
Tbody,
@ -24,7 +21,6 @@ import {
import AvatarGroup from '@fastgpt/web/components/common/Avatar/AvatarGroup';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import Icon from '@fastgpt/web/components/common/Icon';
import MyIconButton from '@fastgpt/web/components/common/Icon/button';
import MyModal from '@fastgpt/web/components/common/MyModal';
import MyPopover from '@fastgpt/web/components/common/MyPopover';
import Tag from '@fastgpt/web/components/common/Tag';
@ -68,7 +64,7 @@ const InviteModal = ({
[copyData]
);
const { runAsync: onForbid } = useRequest2(
const { runAsync: onForbid, loading: forbiding } = useRequest2(
(linkId: string) =>
putUpdateInvitationInfo({
linkId,
@ -87,21 +83,14 @@ const InviteModal = ({
isOpen
iconSrc="common/inviteLight"
iconColor="primary.600"
minW={'600px'}
title={
<Box>
<Box>{t('common:user.team.Invite Member')}</Box>
<Box color={'myGray.500'} fontSize={'xs'} fontWeight={'normal'}>
{t('common:user.team.Invite Member Tips')}
</Box>
</Box>
}
maxW={['90vw']}
title={t('account_team:invite_member')}
overflow={'unset'}
onClose={onClose}
w={'100%'}
maxW={['90vw', '820px']}
>
<ModalCloseButton onClick={onClose} />
<ModalHeader pb="0">
<Flex alignItems={'center'} justifyContent={'space-between'} mx="2">
<ModalBody maxH="500px">
<Flex alignItems={'center'} justifyContent={'space-between'} mb={4}>
<HStack>
<Icon name="common/list" w="16px" />
<Box ml="6px" fontSize="md">
@ -110,8 +99,6 @@ const InviteModal = ({
</HStack>
<Button onClick={onOpenCreate}>{t('account_team:create_invitation_link')}</Button>
</Flex>
</ModalHeader>
<ModalBody maxH="500px">
<TableContainer overflowY={'auto'}>
<Table fontSize={'sm'} overflow={'unset'}>
<Thead>
@ -149,56 +136,57 @@ const InviteModal = ({
: item.usedTimesLimit}
</Td>
<Td>
<MyPopover
w="fit-content"
Trigger={
<Box
minW="100px"
borderRadius="md"
cursor="pointer"
_hover={{ bg: 'myGray.100' }}
p="1.5"
w="fit-content"
>
<AvatarGroup max={3} avatars={item.members.map((i) => i.avatar)} />
</Box>
}
trigger="click"
closeOnBlur={true}
>
{() => (
<Box py="4" maxH="200px" w="fit-content">
<Flex mx="4" justifyContent="center" alignItems={'center'}>
<Box>{t('account_team:has_invited')}</Box>
<Box
ml="auto"
bg="myGray.200"
px="2"
borderRadius="md"
fontSize="sm"
>
{item.members.length}
</Box>
</Flex>
<Divider my="2" mx="4" />
<Grid
{item.members.length > 0 && (
<MyPopover
w="fit-content"
Trigger={
<Box
borderRadius="md"
cursor="pointer"
_hover={{ bg: 'myGray.100' }}
p="1.5"
w="fit-content"
mt="2"
gridRowGap="4"
gridTemplateColumns="1fr 1fr"
overflow="auto"
alignItems="center"
mx="4"
>
{item.members.map((member) => (
<Box key={member.tmbId} justifySelf="start">
<MemberTag name={member.name} avatar={member.avatar} />
<AvatarGroup max={3} avatars={item.members.map((i) => i.avatar)} />
</Box>
}
trigger="click"
closeOnBlur={true}
>
{() => (
<Box py="4" maxH="200px" w="fit-content">
<Flex mx="4" justifyContent="center" alignItems={'center'}>
<Box>{t('account_team:has_invited')}</Box>
<Box
ml="auto"
bg="myGray.200"
px="2"
borderRadius="md"
fontSize="sm"
>
{item.members.length}
</Box>
))}
</Grid>
</Box>
)}
</MyPopover>
</Flex>
<Divider my="2" mx="4" />
<Grid
w="fit-content"
mt="2"
gridRowGap="4"
gridTemplateColumns="1fr 1fr"
overflow="auto"
alignItems="center"
mx="4"
>
{item.members.map((member) => (
<Box key={member.tmbId} justifySelf="start">
<MemberTag name={member.name} avatar={member.avatar} />
</Box>
))}
</Grid>
</Box>
)}
</MyPopover>
)}
</Td>
<Td>
{!isForbidden && (
@ -232,6 +220,7 @@ const InviteModal = ({
{t('common:common.Cancel')}
</Button>
<Button
isLoading={forbiding}
variant="outline"
colorScheme="red"
onClick={() => {

View File

@ -41,7 +41,7 @@ import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
import { useState } from 'react';
import { downloadFetch } from '@/web/common/system/utils';
const InviteModal = dynamic(() => import('./InviteModal'));
const InviteModal = dynamic(() => import('./Invite/InviteModal'));
const TeamTagModal = dynamic(() => import('@/components/support/user/team/TeamTagModal'));
function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
@ -236,7 +236,7 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
<Th borderLeftRadius="6px" bgColor="myGray.100">
{t('account_team:user_name')}
</Th>
<Th bgColor="myGray.100">{t('account_team:contact')}</Th>
<Th bgColor="myGray.100">{t('common:contact_way')}</Th>
<Th bgColor="myGray.100">{t('account_team:org')}</Th>
<Th bgColor="myGray.100">{t('account_team:join_update_time')}</Th>
<Th borderRightRadius="6px" bgColor="myGray.100">

View File

@ -101,6 +101,7 @@ export const TeamModalContextProvider = ({ children }: { children: ReactNode })
const { runAsync: onSwitchTeam, loading: isSwitchingTeam } = useRequest2(
async (teamId: string) => {
await putSwitchTeam(teamId);
refetchMembers();
return initUserInfo();
},
{

View File

@ -260,7 +260,7 @@ const MyInfo = ({ onOpenContact }: { onOpenContact: () => void }) => {
)}
{feConfigs?.isPlus && (
<Flex mt={6} alignItems={'center'}>
<Box {...labelStyles}>{t('account_info:contact')}:&nbsp;</Box>
<Box {...labelStyles}>{t('common:contact_way')}:&nbsp;</Box>
<Box flex={1} {...(!userInfo?.contact ? { color: 'red.600' } : {})}>
{userInfo?.contact ? userInfo?.contact : t('account_info:please_bind_contact')}
</Box>

View File

@ -20,7 +20,9 @@ const PermissionManage = dynamic(
);
const GroupManage = dynamic(() => import('@/pageComponents/account/team/GroupManage/index'));
const OrgManage = dynamic(() => import('@/pageComponents/account/team/OrgManage/index'));
const HandleInviteModal = dynamic(() => import('@/pageComponents/account/team/HandleInviteModal'));
const HandleInviteModal = dynamic(
() => import('@/pageComponents/account/team/Invite/HandleInviteModal')
);
export enum TeamTabEnum {
member = 'member',
@ -99,7 +101,7 @@ const Team = () => {
</Box>
</Flex>
<Flex align={'center'} ml={6}>
<TeamSelector height={'28px'} onChange={refetchMembers} />
<TeamSelector height={'28px'} />
</Flex>
{userInfo?.team?.role === TeamMemberRoleEnum.owner && (
<Flex align={'center'} justify={'center'} ml={2} p={'0.44rem'}>