From b2cd9b6dd14a0f41bef0b914bc524457af81d1d3 Mon Sep 17 00:00:00 2001 From: wx-11 <168356742+wx-11@users.noreply.github.com> Date: Wed, 21 Jan 2026 15:31:40 +0800 Subject: [PATCH 001/131] i18n: complete zh translations (#38251) --- packages/i18n/src/locales/zh.i18n.json | 3063 +++++++++++++++++++++++- packages/livechat/src/i18n/zh-HK.json | 97 +- packages/livechat/src/i18n/zh-TW.json | 107 +- packages/livechat/src/i18n/zh.json | 37 +- 4 files changed, 3246 insertions(+), 58 deletions(-) diff --git a/packages/i18n/src/locales/zh.i18n.json b/packages/i18n/src/locales/zh.i18n.json index 626d7b5bb2fa9..3244a2b493e55 100644 --- a/packages/i18n/src/locales/zh.i18n.json +++ b/packages/i18n/src/locales/zh.i18n.json @@ -1,5 +1,6 @@ { "500": "内部服务器错误", + "private": "私有", "files": "文件", "#channel": "#频道", "%_of_conversations": "% 的会话", @@ -10,14 +11,90 @@ "2_Erros_Information_and_Debug": "2 - 错误、信息和调试", "@username": "@用户名", "@username_message": "@用户名 ", - "API": "API", + "ABAC": "ABAC(基于属性的访问控制)", + "ABAC_Enabled": "启用 ABAC(基于属性的访问控制)", + "ABAC_Enabled_Description": "基于用户和房间属性控制房间访问。", + "Abac_Cache_Decision_Time_Seconds": "ABAC 决策缓存时间(秒)", + "Abac_Cache_Decision_Time_Seconds_Description": "缓存访问控制决策的秒数,设为 0 表示禁用缓存。", + "ABAC_Enabled_callout": "用户属性通过 LDAP 同步。<1>了解更多", + "ABAC_Learn_More": "了解 ABAC", + "ABAC_automatically_disabled_callout": "ABAC 已自动禁用", + "ABAC_automatically_disabled_callout_description": "请续订许可证以继续不受限制地使用所有 <1>ABAC 功能。", + "ABAC_Warning_Modal_Title": "停用 ABAC", + "ABAC_Warning_Modal_Confirm_Text": "停用 ABAC", + "ABAC_Warning_Modal_Content": "将无法在现有 ABAC 管理的房间中自动或手动管理用户。要恢复默认访问控制,需在 <1>ABAC > Rooms 中将房间移出 ABAC 管理。", + "ABAC_ShowAttributesInRooms": "在房间内显示 ABAC 属性", + "ABAC_ShowAttributesInRooms_Description": "在侧边栏显示分配给房间的 ABAC 属性", + "ABAC_Room": "房间", + "ABAC_Room_Attribute": "房间属性", + "ABAC_Element": "ABAC 元素", + "ABAC_Element_Name": "元素名称", + "ABAC_Cannot_delete_attribute": "无法删除已分配到房间的属性", + "ABAC_Cannot_delete_attribute_content": "请先从所有房间移除 {{attributeName}},然后再删除。", + "ABAC_Cannot_delete_attribute_value_in_use": "无法删除已分配到房间的属性值。<1>查看房间", + "ABAC_Delete_room_attribute": "删除属性", + "ABAC_Delete_room_attribute_content": "确定要删除 {{attributeName}} 吗?
由于目前未分配到任何房间,现有房间不会受影响。", + "ABAC_Attribute_created": "已创建属性 {{attributeName}}", + "ABAC_Attribute_updated": "已更新属性 {{attributeName}}", + "ABAC_Attribute_deleted": "已删除属性 {{attributeName}}", + "ABAC_New_attribute": "新建属性", + "ABAC_Edit_attribute": "编辑属性", + "ABAC_Edit_attribute_description": "属性值不可编辑,只能新增或删除。", + "ABAC_New_attribute_description": "创建属性,稍后可分配给房间。", + "ABAC_No_logs": "暂无日志", + "ABAC_No_logs_description": "与 ABAC 管理相关的活动将显示在此处。", + "ABAC_Search_attributes": "搜索属性", + "ABAC_Remove_attribute": "删除属性值", + "ABAC_Delete_room": "将房间移出 ABAC 管理", + "ABAC_Delete_room_annotation": "操作需谨慎", + "ABAC_Delete_room_content": "将 {{roomName}} 移出 ABAC 管理可能导致意外用户获得访问权限。", + "ABAC_Room_removed": "房间 {{roomName}} 已移出 ABAC 管理", + "ABAC_Add_Attribute": "添加属性", + "ABAC_Attribute_Values": "属性值", + "ABAC_Edit_Room": "编辑房间", + "ABAC_Add_room": "添加房间", + "ABAC_No_repeated_attributes": "该属性已添加", + "ABAC_No_repeated_values": "该值已添加", + "ABAC_Room_created": "对 {{roomName}} 的访问现已仅限符合属性的用户", + "ABAC_Room_to_be_managed": "待由 ABAC 管理的房间", + "ABAC_Room_updated": "{{roomName}} 的 ABAC 设置已更新", + "ABAC_Search_Attribute": "搜索属性", + "ABAC_Search_rooms": "搜索房间、属性或属性值", + "ABAC_Select_Attribute_Values": "选择属性值", + "ABAC_Update_room_confirmation_modal_annotation": "操作需谨慎", + "ABAC_Update_room_confirmation_modal_title": "更新 ABAC 房间", + "ABAC_Update_room_content": "{{roomName}} 当前由 ABAC 管理,修改可能影响可访问该房间的用户。", + "abac-management": "管理 ABAC 配置", + "abac_removed_user_from_the_room": "已被 ABAC 移除", + "ABAC_No_attributes": "暂无属性", + "ABAC_No_attributes_description": "创建自定义特征以决定谁可访问房间。", + "ABAC_No_rooms": "暂无房间", + "ABAC_No_rooms_description": "ABAC 管理的房间会显示在这里。", + "ABAC_All_Attributes_deleted": "所有属性已删除", + "ABAC_Key_removed": "键已移除", + "ABAC_Key_renamed": "键已重命名", + "ABAC_Value_removed": "值已移除", + "ABAC_Key_added": "键已添加", + "ABAC_Key_updated": "键已更新", + "ABAC_Revoked_Object_Access": "自动移除", + "ABAC_Granted_Object_Access": "手动添加已批准", + "ABAC_room_membership": "房间成员", + "ABAC_Managed": "ABAC 管理中", + "ABAC_Managed_description": "仅符合条件的用户可访问启用属性控制的房间,属性用于判定访问权限。", + "ABAC_Room_Attributes": "房间属性列表", + "ABAC_Logs": "日志", + "ABAC_Invalid_attribute": "属性名称或属性值包含非法字符", + "AI_Actions": "AI 操作", + "API": "API(应用程序接口)", "API_Add_Personal_Access_Token": "添加新的个人访问令牌", - "API_Allow_Infinite_Count": "允许获取一切", - "API_Allow_Infinite_Count_Description": "是否允许在一次 REST API 调用中返回所有内容?", + "API_Allow_Infinite_Count": "允许返回全部结果", + "API_Allow_Infinite_Count_Description": "是否允许单次 REST API 调用返回所有记录?", "API_Analytics": "分析", + "API_Apply_permission_view-outside-room_on_users-list": "对 `users.list` 接口启用 `view-outside-room` 权限校验", + "API_Apply_permission_view-outside-room_on_users-list_Description": "临时开关,用于强制权限检查;在下个主版本中将移除,并始终强制该权限。", "API_CORS_Origin": "CORS 源", - "API_Default_Count": "默认计数", - "API_Default_Count_Description": "消费者未提供时 REST API 结果的默认计数", + "API_Default_Count": "默认返回条数", + "API_Default_Count_Description": "当调用方未指定时 REST API 默认返回的记录数", "API_Drupal_URL": "Drupal 服务器 URL", "API_Drupal_URL_Description": "例如:`https://domain.com`(不包括尾部斜线)", "API_Embed": "嵌入链接预览", @@ -30,6 +107,7 @@ "API_EmbedSafePorts_Description": "逗号分隔允许预览的端口列表。", "API_Embed_Description": "当用户发布一个网站链接时,是否启用嵌入链接预览。", "API_Embed_UserAgent": "嵌入请求用户代理", + "API_EmbedTimeout": "Embed Request default timeout (in seconds)", "API_Enable_CORS": "启用 CORS", "API_Enable_Direct_Message_History_EndPoint": "启用私聊消息历史端点", "API_Enable_Direct_Message_History_EndPoint_Description": "这会启用 `/api/v1/im.messages.others` ,它允许查看调用者未参与的其他用户私聊消息。", @@ -42,11 +120,11 @@ "API_Enable_Rate_Limiter_Limit_Calls_Default_Description": "在下面定义的时间范围内 REST API 每个端点的默认允许调用量", "API_Enable_Rate_Limiter_Limit_Time_Default": "超过速率后的默认限制时间(毫秒)", "API_Enable_Rate_Limiter_Limit_Time_Default_Description": "用于限制 REST API 每个端点的调用次数的默认超时(毫秒)", - "API_Enable_Shields": "启用盾牌", - "API_Enable_Shields_Description": "在`/api/v1/shield.svg`中启用屏蔽", + "API_Enable_Shields": "启用 Shield 徽章", + "API_Enable_Shields_Description": "在 `/api/v1/shield.svg` 启用 Shield 徽章", "API_GitHub_Enterprise_URL": "服务器URL", "API_GitHub_Enterprise_URL_Description": "例如:`https://domain.com`(末尾无斜线)", - "API_Gitlab_URL": "GitLab URL", + "API_Gitlab_URL": "GitLab 地址", "API_Personal_Access_Token_Generated": "个人访问令牌生成成功", "API_Personal_Access_Token_Generated_Text_Token_s_UserId_s": "请仔细保存您的令牌,因为之后您将无法再查看它。
令牌:{{token}}
您的用户ID:{{userId}} ", "API_Personal_Access_Token_Name": "个人访问令牌名称", @@ -54,25 +132,39 @@ "API_Personal_Access_Tokens_Regenerate_Modal": "如果你丢失或忘记令牌,你能重新生成,但是谨记所有使用此令牌的应用应该更新", "API_Personal_Access_Tokens_Remove_Modal": "你确定删除此个人访问令牌?", "API_Personal_Access_Tokens_To_REST_API": "个人访问令牌访问 REST API", - "API_Shield_Types": "盾牌类型", - "API_Shield_Types_Description": "屏蔽类型以逗号分隔列表形式启用,可以从“在线”,“频道”或“*”中为所有人选择", - "API_Shield_user_require_auth": "对用户 shields 启用验证", + "API_Rate_Limiter": "API 速率限制器", + "API_Shield_Types": "Shield 类型", + "API_Shield_Types_Description": "以逗号分隔启用的 Shield 类型,可选“online”、“channel”或“*”(全部)", + "API_Shield_user_require_auth": "用户 Shield 需认证", "API_Token": "API 令牌", "API_Upper_Count_Limit": "最大记录量", - "API_Upper_Count_Limit_Description": "REST API 应返回的最大记录数量(在没有限制的条件下)?", + "API_Upper_Count_Limit_Description": "REST API 在无额外限制时可返回的最大记录数量。", "API_User_Limit": "添加所有用户到频道的用户限制", - "API_Wordpress_URL": "WordPress URL", + "API_Wordpress_URL": "WordPress 地址", "APIs": "API", + "A_cloud-based_platform_for_those_needing_a_plug-and-play_app": "面向即插即用需求的云端平台。", "A_new_owner_will_be_assigned_automatically_to__count__rooms": "新的所有者将被自动分配至 {{count}} 个聊天室。", "A_new_owner_will_be_assigned_automatically_to_the__roomName__room": "新的所有者将被自动分配至聊天室 {{roomName}} 。", "A_new_owner_will_be_assigned_automatically_to_those__count__rooms__rooms__": "新的所有者将被自动分配至以下 {{count}} 个聊天室:
{{rooms}}。", + "A_secure_and_highly_private_self-managed_solution_for_conference_calls": "一个安全且高度私有的自托管会议通话方案。", + "A_workspace_admin_needs_to_install_and_configure_a_conference_call_app": "需要工作区管理员安装并配置会议通话应用。", "Accept": "接受", + "Accept_Call": "接听来电", "Accept_incoming_livechat_requests_even_if_there_are_no_online_agents": "没有客服在线时也接受入站 omnichannel 请求", "Accept_new_livechats_when_agent_is_idle": "客服空闲时接受新的 omnichannel 请求", + "Accept_receive_inquiry_no_online_agents": "允许部门在无可用客服时接收转派的咨询", + "Accept_receive_inquiry_no_online_agents_Hint": "该方法仅对自动分配路由方式生效,不适用于手动选择。", "Accept_with_no_online_agents": "无在线客服时仍接受", "Access_Token_URL": "访问令牌 URL", + "Access_Your_Account": "访问您的账户", "Access_not_authorized": "访问未被授权", + "Accessibility": "无障碍", + "Accessibility_activation": "您可以在此启用一系列功能以提升浏览体验。", + "Accessibility_and_Appearance": "无障碍与外观", + "Accessibility_feature_documentation": "无障碍功能文档", + "Accessibility_statement": "无障碍声明", "Accessing_permissions": "访问权限", + "Account": "账户", "Account_SID": "账号 SID", "Accounts": "账号", "Accounts_Admin_Email_Approval_Needed_Default": "

用户[name]([email])已注册。

请检查 “管理 -> 用户” 将其激活或删除。

", @@ -83,6 +175,9 @@ "Accounts_AllowDeleteOwnAccount": "允许用户删除自己的帐号", "Accounts_AllowEmailChange": "允许修改电子邮箱", "Accounts_AllowEmailNotifications": "允许电子邮件通知", + "Accounts_AllowFeaturePreview": "允许功能预览", + "Accounts_AllowFeaturePreview_Description": "使功能预览对所有工作区成员可用。", + "Accounts_AllowInvisibleStatusOption": "允许隐身状态选项", "Accounts_AllowPasswordChange": "允许修改密码", "Accounts_AllowPasswordChangeForOAuthUsers": "允许 OAuth 用户更改密码", "Accounts_AllowRealNameChange": "允许更改名称", @@ -104,13 +199,19 @@ "Accounts_BlockedUsernameList": "已屏蔽的用户名列表", "Accounts_BlockedUsernameList_Description": "以逗号分隔的已屏蔽用户名列表(不区分大小写)", "Accounts_CustomFieldsToShowInUserInfo": "要在用户信息中显示的自定义信息", + "Accounts_CustomFieldsToShowInUserInfo_Description": "值必须是对象数组,键为标签,值为字段名。示例:`[{\"Role Label\": \"role\"}, {\"Twitter Label\": \"twitter\"}]`,更多信息见 [自定义字段](https://docs.rocket.chat/docs/custom-fields)", "Accounts_CustomFields_Description": "应该为有效的 JSON,键为字段名,值为该字段的字典设置项。例如: \n`{\"role\":{ \"type\": \"select\", \"defaultValue\": \"student\", \"options\": [\"teacher\", \"student\"], \"required\": true, \"modifyRecordField\": { \"array\": true, \"field\": \"roles\" } }, \"twitter\": { \"type\": \"text\", \"required\": true, \"minLength\": 2, \"maxLength\": 10 }}`", "Accounts_DefaultUsernamePrefixSuggestion": "默认用户名前缀建议", "Accounts_Default_User_Preferences": "默认用户首选项", + "Accounts_Default_User_Preferences_alsoSendThreadToChannel_Description": "允许用户选择“同时发送到频道”的行为。", "Accounts_Default_User_Preferences_audioNotifications": "音频通知默认提醒", "Accounts_Default_User_Preferences_desktopNotifications": "桌面通知默认提醒", "Accounts_Default_User_Preferences_not_available": "因用户首选项尚未由用户设置而获取失败", + "Accounts_Default_User_Preferences_omnichannelTranscriptEmail_Description": "在会话结束时始终将记录发送给联系人。此偏好可能被管理员设置覆盖。", + "Accounts_Default_User_Preferences_omnichannelTranscriptPDF_Description": "在会话结束时始终将记录导出为 PDF。", "Accounts_Default_User_Preferences_pushNotifications": "移动端通知默认提醒", + "Accounts_Default_User_Preferences_showThreadsInMainChannel_Description": "启用后,线程下的所有回复将直接显示在主频道。禁用后,线程回复将根据发送者的选择显示。", + "Accounts_Description": "修改工作区成员账户设置。", "Accounts_Directory_DefaultView": "默认目录清单", "Accounts_EmailVerification": "只允许已验证的用户登录", "Accounts_EmailVerification_Description": "请确保 SMTP 设置正确再使用此功能", @@ -126,10 +227,11 @@ "Accounts_Enrollment_Email_Subject_Default": "欢迎访问 [Site_Name]", "Accounts_ForgetUserSessionOnWindowClose": "关闭窗口时自动注销用户", "Accounts_Iframe_api_method": "API 方法", - "Accounts_Iframe_api_url": "API URL", + "Accounts_Iframe_api_url": "API 地址", "Accounts_LoginExpiration": "保持登录的天数", "Accounts_ManuallyApproveNewUsers": "手动审核新用户", "Accounts_OAuth_Apple": "使用 Apple 登录", + "Accounts_OAuth_Apple_Description": "如果只想在移动端启用 Apple 登录,可将所有字段留空。", "Accounts_OAuth_Custom_Access_Token_Param": "访问令牌参数名称", "Accounts_OAuth_Custom_Authorize_Path": "授权路径", "Accounts_OAuth_Custom_Avatar_Field": "头像字段", @@ -143,12 +245,17 @@ "Accounts_OAuth_Custom_Groups_Claim": "用于频道映射的角色/组字段", "Accounts_OAuth_Custom_Identity_Path": "身份路径", "Accounts_OAuth_Custom_Identity_Token_Sent_Via": "身份令牌发送自", + "Accounts_OAuth_Custom_Key_Field": "键字段", "Accounts_OAuth_Custom_Login_Style": "登录样式", "Accounts_OAuth_Custom_Map_Channels": "映射角色/组至频道", "Accounts_OAuth_Custom_Merge_Roles": "从 SSO 合并角色", "Accounts_OAuth_Custom_Merge_Users": "合并用户", + "Accounts_OAuth_Custom_Merge_Users_Distinct_Services": "合并来自不同服务的用户", + "Accounts_OAuth_Custom_Merge_Users_Distinct_Services_Description": "当给定的键字段与现有用户匹配时,允许来自此 OAuth 服务的用户合并到现有用户(不论其来源服务)。", "Accounts_OAuth_Custom_Name_Field": "名称字段", "Accounts_OAuth_Custom_Roles_Claim": "角色/组字段名称", + "Accounts_OAuth_Custom_Roles_To_Sync": "要同步的角色", + "Accounts_OAuth_Custom_Roles_To_Sync_Description": "用户登录和创建时要同步的 OAuth 角色(以逗号分隔)。", "Accounts_OAuth_Custom_Scope": "范围", "Accounts_OAuth_Custom_Secret": "秘密", "Accounts_OAuth_Custom_Show_Button_On_Login_Page": "在登录页面显示按钮", @@ -210,7 +317,7 @@ "Accounts_OAuth_Wordpress_scope": "范围", "Accounts_OAuth_Wordpress_secret": "WordPress 秘密", "Accounts_OAuth_Wordpress_server_type_custom": "自定义", - "Accounts_OAuth_Wordpress_server_type_wordpress_com": "Wordpress.com", + "Accounts_OAuth_Wordpress_server_type_wordpress_com": "WordPress.com", "Accounts_OAuth_Wordpress_server_type_wp_oauth_server": "WP OAuth 服务器插件", "Accounts_OAuth_Wordpress_token_path": "Token 路径", "Accounts_PasswordReset": "重置密码", @@ -238,13 +345,16 @@ "Accounts_RegistrationForm_Public": "公共", "Accounts_RegistrationForm_SecretURL": "注册表单 Secret URL", "Accounts_RegistrationForm_SecretURL_Description": "您必须提供一个随机字符串,该字符串将被添加到您的注册地址中。例如:`https://open.rocket.chat/register/[secret_hash]`", - "Accounts_RegistrationForm_Secret_URL": "Secret URL", + "Accounts_RegistrationForm_Secret_URL": "私密 URL", "Accounts_Registration_AuthenticationServices_Default_Roles": "认证服务的默认角色", "Accounts_Registration_AuthenticationServices_Default_Roles_Description": "默认授予经验证服务注册用户的角色(用逗号分隔)", "Accounts_Registration_AuthenticationServices_Enabled": "使用认证服务注册", "Accounts_Registration_InviteUrlType": "邀请网址类型", "Accounts_Registration_InviteUrlType_Direct": "私聊", "Accounts_Registration_InviteUrlType_Proxy": "代理", + "Accounts_Registration_Users_Default_Roles": "用户默认角色", + "Accounts_Registration_Users_Default_Roles_Description": "通过手动注册(含 API)注册的用户将被赋予的默认角色(以逗号分隔)", + "Accounts_Registration_Users_Default_Roles_Enabled": "启用手动注册默认角色", "Accounts_RequireNameForSignUp": "姓名必须填写", "Accounts_RequirePasswordConfirmation": "请输入确认密码", "Accounts_RoomAvatarExternalProviderUrl": "房间头像外部提供者 URL", @@ -262,12 +372,16 @@ "Accounts_TwoFactorAuthentication_By_Email_Code_Expiration": "从邮件验证码过期秒数", "Accounts_TwoFactorAuthentication_By_Email_Enabled": "启用基于邮件的两步验证", "Accounts_TwoFactorAuthentication_By_Email_Enabled_Description": "启用了此项的已验证邮箱用户将收到用于授权对应操作(例如登录,更改个人资料)的验证码邮件。", + "Accounts_TwoFactorAuthentication_By_TOTP_Enabled": "启用基于 TOTP 的双因素认证", + "Accounts_TwoFactorAuthentication_By_TOTP_Enabled_Description": "用户可使用任意 TOTP 应用(如 Google Authenticator 或 Authy)设置双因素认证。", "Accounts_TwoFactorAuthentication_Enabled": "启用基于 TOTP 的两步验证", "Accounts_TwoFactorAuthentication_Enabled_Description": "用户可以使用 TOTP 应用进行两步验证,例如谷歌身份验证器或 Authy。", "Accounts_TwoFactorAuthentication_Enforce_Password_Fallback": "强制密码回退", "Accounts_TwoFactorAuthentication_Enforce_Password_Fallback_Description": "如果用户启用两步验证并设置了密码,用户进行重要的操作时必须输入密码。", "Accounts_TwoFactorAuthentication_MaxDelta": "最大 Delta", "Accounts_TwoFactorAuthentication_MaxDelta_Description": "最大差值确定在任何给定时间有多少令牌有效。令牌每30秒产生一次,并且对于(30 * 最大差值)秒有效。 \n示例:将最大差值设置为10时,每个令牌可以在时间戳之前或之后使用达300秒。在客户端的时钟与服务器没有正确同步时,这很有用。", + "Accounts_TwoFactorAuthentication_Max_Invalid_Email_Code_Attempts": "允许的最大无效邮件 OTP 次数", + "Accounts_TwoFactorAuthentication_Max_Invalid_Email_Code_Attempts_Description": "系统允许的无效邮件 OTP 代码最大次数,超过后将自动生成新代码。强烈建议与“按用户名阻止失败登录尝试”一起使用。", "Accounts_TwoFactorAuthentication_RememberFor": "记住两步验证(秒)", "Accounts_TwoFactorAuthentication_RememberFor_Description": "如果在时间内已经提供了两步验证码,将不再请求。", "Accounts_UseDNSDomainCheck": "使用 DNS 域名检查", @@ -275,43 +389,95 @@ "Accounts_UserAddedEmailSubject_Default": "您已被添加到 [Site_Name]", "Accounts_UserAddedEmail_Default": "

欢迎访问 [Site_Name]

转到 [Site_URL] 并尝试当今最先进的开源聊天解决方案!

您可以使用您的电子邮件地址:[email] 和密码:[password] 进行登陆。您可能需要在您首次登录后更改密码。", "Accounts_UserAddedEmail_Description": "您可以使用以下占位符: \n - 全名 [name],名字 [fname],姓氏 [lname]。 \n - `[email]` 为用户的 email。 \n - `[password]` 为用户的密码。 \n - `[Site_Name]` 和 `[Site_URL]` 分为网站名称和网站地址。 ", + "Accounts_Verify_Email_For_External_Accounts": "将外部账户的邮箱标记为已验证", "Accounts_denyUnverifiedEmail": "拒绝未经验证的电子邮件地址", "Accounts_iframe_enabled": "已启用", - "Accounts_iframe_url": "Iframe URL", + "Accounts_iframe_url": "Iframe 地址", + "Accounts_twoFactorAuthentication_email_available_for_OAuth_users": "让 OAuth 用户可使用邮件双因素认证", + "Accounts_twoFactorAuthentication_email_available_for_OAuth_users_Description": "使用 OAuth 的用户将通过邮件接收临时代码,用于授权登录、保存资料等操作。", + "Action": "操作", + "Action_Available_After_Custom_Content_Added": "添加自定义内容后此操作可用", + "Action_Available_After_Custom_Content_Added_And_Visible": "自定义内容添加并对所有人可见后此操作可用", + "Action_not_available_encrypted_content": "{{action}} 在加密内容中不可用", "Action_required": "需要操作", "Activate": "激活", + "Activation": "激活", "Active": "活跃", + "ActiveSessions": "活跃会话", + "ActiveSessionsPeak": "活跃会话峰值", + "ActiveSessionsPeak_InfoText": "过去 30 天内的最高活跃连接数", + "ActiveSessions_InfoText": "并发连接总数。单个用户可多次连接。为防性能问题,用户在线状态服务在连接数达到 200 或以上时将被禁用。", + "ActiveSessions_available": "可用会话", + "Active_connections": "活跃连接", "Active_users": "活跃用户", "Activity": "活动", + "Actor": "执行者", "Add": "添加", + "Add_Value": "添加值", + "Add_room": "添加房间", + "Add-on": "附加组件", + "Add-on_required": "需要附加组件", + "Add-on_required_modal_enable_content": "未订阅所需附加组件无法启用该应用。请联系销售获取此应用的附加组件。", "Add_Domain": "添加域名", "Add_Reaction": "添加回应", "Add_Role": "添加角色", + "Add_Sender_To_ReplyTo": "将发件人加入 Reply-To", + "Add_Server": "添加服务器", + "Add_URL": "添加 URL", "Add_User": "添加用户", + "Add_a_Message": "添加消息", "Add_agent": "添加客服", + "Add_contact": "添加联系人", "Add_custom_oauth": "添加自定义 OAuth ", + "Add_email": "添加邮箱", + "Add_emoji": "添加表情", "Add_files_from": "添加文件", + "Add_link": "添加链接", "Add_manager": "添加管理员", + "Add_members": "添加成员", "Add_monitor": "新增监控", + "Add_more_users": "添加更多用户", + "Add_people": "添加人员", + "Add_phone": "添加电话", + "Add_them": "添加他们", + "Add_topic": "添加主题", "Add_user": "添加用户", "Add_users": "添加用户", + "Added__username__to_team": "已添加 @{{user_added}} 到该团队", + "Added__username__to_this_team": "已将 @{{user_added}} 添加到该团队", "Adding_OAuth_Services": "添加 OAuth 服务", "Adding_permission": "权限添加", "Adding_user": "正在添加用户", "Additional_Feedback": "其他反馈", - "Additional_emails": "其他 Email", + "Additional_emails": "其他邮箱", + "Address": "地址", + "Adjustable_font_size": "可调字体大小", + "Adjustable_font_size_description": "为偏好更大或更小文字的用户设计,以提升可读性。该灵活性让用户可按需求调整界面,从而增强包容性。", + "Adjustable_layout": "可调布局", "Admin_Info": "管理员信息", "Admin_disabled_encryption": "您的管理员并没有启用端到端加密", "Administration": "管理", "Adult_images_are_not_allowed": "成人图像是不允许的", + "Advanced_contact_profile": "高级联系人资料", + "Advanced_contact_profile_description": "为单个联系人管理多个邮箱和电话号码,形成完整的多渠道历史,提升信息掌握与沟通效率。", + "Advanced_settings": "高级设置", + "Aerospace_and_Defense": "航空航天与国防", "After_OAuth2_authentication_users_will_be_redirected_to_this_URL": "通过 OAuth2 认证后,用户将被重定向至此地址", + "After_guest_registration": "访客注册后", "Agent": "客服", + "agent": "坐席", "Agent_Info": "客服信息", + "Agent_Name": "客服名称", "Agent_Name_Placeholder": "请输入客服名称…", "Agent_added": "客服已添加", + "Agent_deactivated": "客服已被停用", "Agent_messages": "客服消息", "Agent_removed": "已移除客服", "Agents": "客服", + "Agree": "同意", + "AirGapped_Restriction_Warning": "**您的离线(气隙)工作区将在 {{remainingDays}} 天后进入只读模式。** \n 用户仍可访问房间并阅读消息,但无法发送新消息。 \n 连接到互联网或[升级为高级许可证](https://go.rocket.chat/i/air-gapped) 以避免限制。", + "Airgapped_workspace_restriction": "此离线(气隙)工作区处于只读模式。<1>连接互联网或升级为高级方案以恢复完整功能。", + "Airgapped_workspace_warning": "此离线(气隙)工作区将在 {{remainingDays}} 天后进入只读模式。<1>连接互联网或升级为高级方案以避免限制。", "Alerts": "警报", "Alias": "别名", "Alias_Format": "别名格式", @@ -319,13 +485,27 @@ "Alias_Set": "别名集", "Aliases": "别名", "All": "全部", + "All_Apps": "全部应用", + "All_directions": "全部方向", + "All_Prices": "所有价格", + "All_Settings": "全部设置", "All_added_tokens_will_be_required_by_the_user": "所有添加的令牌都将被用户需要", + "All_categories": "所有分类", "All_channels": "所有频道", "All_closed_chats_have_been_removed": "已移除所有关闭的聊天", "All_logs": "所有日志", "All_messages": "所有消息", + "All_roles": "所有角色", + "All_rooms": "所有房间", + "All_status": "所有状态", + "All_time": "全部时间", "All_users": "全部用户", "All_users_in_the_channel_can_write_new_messages": "该频道中的所有用户都可以发送新消息", + "All_visible": "全部可见", + "allow-external-voice-calls": "允许外部语音通话", + "allow-external-voice-calls_description": "允许用户与外部集成的用户进行通话的权限。", + "allow-internal-voice-calls": "允许内部语音通话", + "allow-internal-voice-calls_description": "允许用户与工作区内其他用户进行通话的权限。", "Allow_Invalid_SelfSigned_Certs": "允许无效的自签名证书", "Allow_Invalid_SelfSigned_Certs_Description": "允许无效的自签名 SSL 证书用于验证和预览链接。", "Allow_Marketing_Emails": "允许营销电子邮件", @@ -337,26 +517,60 @@ "Allow_switching_departments": "允许访客切换部门", "Almost_done": "快完成了", "Alphabetical": "按英文字母顺序", + "Also_send_thread_message_to_channel_behavior": "线程消息同时发送到频道的行为", "Also_send_to_channel": "同时发送至频道", "Always_open_in_new_window": "总是在新窗口中打开", + "Always_send_the_transcript_to_contacts_at_the_end_of_the_conversations": "在会话结束时总是将记录发送给联系人。", + "Always_show_thread_replies_in_main_channel": "始终在主频道中显示线程回复", + "An_app_needs_to_be_installed_and_configured": "需要安装并配置一个应用。", + "An_update_is_available": "有可用更新", + "Analytic_reports": "分析报告", "Analytics": "分析", + "Analytics_Description": "了解用户如何与工作区交互。", "Analytics_Google": "Google 统计", "Analytics_Google_id": "跟踪 ID", "Analytics_features_enabled": "功能已启用", "Analytics_features_messages_Description": "跟踪与用户对消息的操作相关的自定义事件。", "Analytics_features_rooms_Description": "跟踪与频道或分组操作相关的自定义事件(创建、离开、删除)。", "Analytics_features_users_Description": "跟踪与用户操作相关的自定义事件(密码重置次数、个人资料图片变化等)。", + "Analytics_page_briefing_first_paragraph": "Rocket.Chat 会收集功能使用情况、会话时长等匿名数据,用于改进产品。", + "Analytics_page_briefing_second_paragraph": "我们不会收集个人或敏感数据以保护隐私。本节展示收集项,以体现透明和信任。", + "Analyze_practical_usage": "分析用户、消息与频道的实际使用统计", "And_more": "以及其它 {{length}} 条", "Animals_and_Nature": "动物与自然", "Announcement": "公告", - "Apiai_Key": "Api.ai Key", + "Anonymous": "匿名", + "Answer_call": "接听来电", + "Anyone_can_access": "任何人都可访问", + "Anyone_can_react_to_messages": "任何人都可以对消息表态", + "Anyone_can_send_new_messages": "任何人都可以发送新消息", + "Apiai_Key": "Api.ai 密钥", "Apiai_Language": "Api.ai 语言", + "App": "应用", "App_Details": "应用细节", + "App_Info": "应用信息", "App_Information": "应用信息", "App_Installation": "应用安装", + "App_Installation_Deprecation": "从 URL 安装应用已弃用,将在下一个主版本中移除。", + "App_Installation_Deprecation_Title": "弃用警告", + "App_Request_Admin_Message": "你好 {{admin_name}},{{user_name}} 提交了在此工作区安装 {{app_name}} 应用的请求。 \n \n 随附留言: \n>{{message}} \n \n 如需了解并安装 {{app_name}} 应用,[点击这里]({{learn_more}})。", + "App_Settings_Saved_Successfully": "{{appName}} 已成功保存", + "App_Store": "应用商店", "App_Url_to_Install_From": "从 URL 安装", "App_Url_to_Install_From_File": "从文件安装", "App_author_homepage": "作者主页", + "App_cannot_be_enabled_without_add-on": "未订阅附加组件无法启用该应用。", + "App_has_been_disabled_addon_message": { + "other": "应用 {{appNames}} 因附加组件无效而被禁用。需要有效的附加组件订阅才能重新启用。" + }, + "App_id": "应用 ID", + "App_limit_exceeded": "应用数量超出限制", + "App_limit_reached": "已达到应用数量上限", + "App_name": "应用名称", + "App_not_enabled": "应用未启用", + "App_not_found": "未找到应用", + "App_request_enduser_message": "你请求的应用 {{appName}} 已在此工作区安装。 \n [点击这里]({{learnmore}}) 了解该应用。", + "App_requests_by_workspace": "工作区成员的应用请求会显示在这里", "App_status_auto_enabled": "已启用", "App_status_constructed": "已构造", "App_status_disabled": "禁用", @@ -369,6 +583,9 @@ "App_status_unknown": "未知", "App_support_url": "支持网址", "App_user_not_allowed_to_login": "应用用户不被允许直接登录。", + "App_version_incompatible_tooltip": "应用与 Rocket.Chat 版本不兼容", + "App_will_lose_grandfathered_status": "**该应用将失去应用数量上限的豁免。** \n \n社区版工作区最多可启用 {{limit}} 个应用。卸载该应用将导致其失去豁免政策。", + "App_will_lose_grandfathered_status_private": "**该应用将失去应用数量上限的豁免。** \n \n由于社区版无法启用私有应用,该工作区未来若要再次启用此应用需升级为高级方案。", "Appearance": "外观", "Application_Name": "应用名称", "Application_added": "应用已添加", @@ -376,17 +593,36 @@ "Application_updated": "应用已更新", "Apply": "申请", "Apply_and_refresh_all_clients": "应用并刷新所有客户端", + "Apply_filters": "应用筛选", "Apps": "应用", + "Apps_Cannot_Be_Updated": "应用无法更新", + "Apps_Count_Enabled": { + "other": "已启用 {{count}} 个应用" + }, + "Apps_Count_Enabled_tooltip": "社区版工作区最多可启用 {{number}} 个{{context}}应用", + "Apps_Currently_Enabled": "当前已启用 {{enabled}}/{{limit}} 个{{context}}应用", "Apps_Engine_Version": "应用程序引擎版本", + "Apps_Error_": "未知应用错误。", + "Apps_Error_app_file_error": "无法获取用于安装应用的文件。", + "Apps_Error_app_storage_error": "无法将应用文件保存到存储。", + "Apps_Error_app_user_error": "无法创建应用用户。", + "Apps_Error_private_app_install_disabled": "该工作区已禁用私有应用安装和更新。", "Apps_Essential_Alert": "此应用对以下事件为必须:", "Apps_Essential_Disclaimer": "此应用禁用时上方列出的事件将被影响。如果想让 Rocket.Chat 在没有此应用的情况下运行,您需要卸载它。", "Apps_Framework_Development_Mode": "启用开发模式", "Apps_Framework_Development_Mode_Description": "开发模式允许您安装那些不在 Rocket.Chat 市场中的应用。", + "Apps_Framework_Source_Package_Storage_FileSystem_Alert": "请确保所选目录存在且 Rocket.Chat 具有访问权限(如读写权限)。", + "Apps_Framework_Source_Package_Storage_FileSystem_Path": "应用源包存储目录", + "Apps_Framework_Source_Package_Storage_FileSystem_Path_Description": "文件系统中存储应用源代码(zip 格式)的绝对路径", + "Apps_Framework_Source_Package_Storage_Type": "应用源包存储类型", + "Apps_Framework_Source_Package_Storage_Type_Alert": "更改应用存储位置可能导致已安装应用出现不稳定", + "Apps_Framework_Source_Package_Storage_Type_Description": "选择应用源代码的存储位置。每个应用可能有数 MB 大小。", "Apps_Framework_enabled": "启用应用框架", "Apps_Game_Center": "游戏中心", "Apps_Game_Center_Back": "返回游戏中心", "Apps_Game_Center_Invite_Friends": "邀请您的朋友加入", "Apps_Game_Center_Play_Game_Together": "@here 来一起玩 {{name}} 吧!", + "Apps_InfoText_limited": "社区版工作区最多可启用 {{marketplaceAppsMaxCount}} 个应用市场应用。私有应用仅可在 <1>高级方案 中启用。", "Apps_Interface_IPostExternalComponentClosed": "外部组件关闭后事件", "Apps_Interface_IPostExternalComponentOpened": "外部组件开启后事件", "Apps_Interface_IPostMessageDeleted": "消息删除后事件", @@ -407,6 +643,20 @@ "Apps_Interface_IPreRoomCreatePrevent": "房间创建前事件", "Apps_Interface_IPreRoomDeletePrevent": "房间删除前事件", "Apps_Interface_IPreRoomUserJoined": "用户加入房间前事件(私聊,频道)", + "Apps_License_Message_appId": "该应用尚未签发许可证", + "Apps_License_Message_bundle": "许可证签发给不包含该应用的套餐", + "Apps_License_Message_expire": "许可证已失效,需要续订", + "Apps_License_Message_maxSeats": "许可证无法覆盖当前活跃用户数量。请增加席位数", + "Apps_License_Message_publicKey": "解密许可证时出错。请在连接服务中同步工作区后重试", + "Apps_License_Message_renewal": "许可证已过期,需要续订", + "Apps_License_Message_seats": "许可证席位不足以覆盖当前活跃用户数量。请增加席位数", + "Apps_Logs_TTL": "应用日志保留天数", + "Apps_Logs_TTL_14days": "14 天", + "Apps_Logs_TTL_30days": "30 天", + "Apps_Logs_TTL_7days": "7 天", + "Apps_Logs_TTL_Alert": "根据日志集合的大小,修改此设置可能会在短时间内导致变慢。", + "Apps_Manual_Update_Modal_Body": "要更新它吗?", + "Apps_Manual_Update_Modal_Title": "此应用已安装", "Apps_Marketplace_Deactivate_App_Prompt": "是否确定要禁用此应用程序?", "Apps_Marketplace_Login_Required_Description": "从 Rocket.Chat 市场购买应用需要在注册您的工作区并登陆。", "Apps_Marketplace_Login_Required_Title": "需要登录市场", @@ -414,13 +664,28 @@ "Apps_Marketplace_Uninstall_App_Prompt": "是否确定要卸载此应用程序?", "Apps_Marketplace_Uninstall_Subscribed_App_Anyway": "无论如何都要卸载它", "Apps_Marketplace_Uninstall_Subscribed_App_Prompt": "此应用具有一个订阅信息,卸载不会取消订阅。如果要执行此操作,请在卸载之前取消订阅。", + "Apps_Marketplace_pricingPlan_+*_monthly": " {{price}}+* / 月", + "Apps_Marketplace_pricingPlan_+*_monthly_perUser": " {{price}}+* / 月/用户", + "Apps_Marketplace_pricingPlan_+*_monthly_perUser_trialDays": " {{price}}+* / 月/用户 - {{trialDays}} 天试用", + "Apps_Marketplace_pricingPlan_+*_monthly_trialDays": " {{price}}+* / 月 - {{trialDays}} 天试用", + "Apps_Marketplace_pricingPlan_+*_yearly": " {{price}}+* / 年", + "Apps_Marketplace_pricingPlan_+*_yearly_perUser": " {{price}}+* / 年/用户", + "Apps_Marketplace_pricingPlan_+*_yearly_perUser_trialDays": " {{price}}+* / 年/用户 - {{trialDays}} 天试用", + "Apps_Marketplace_pricingPlan_+*_yearly_trialDays": " {{price}}+* / 年 - {{trialDays}} 天试用", "Apps_Marketplace_pricingPlan_monthly": "{{price}} /月", "Apps_Marketplace_pricingPlan_monthly_perUser": "每个用户{{price}} /月", + "Apps_Marketplace_pricingPlan_monthly_perUser_trialDays": "{{price}} / 月/用户 - {{trialDays}} 天试用", + "Apps_Marketplace_pricingPlan_monthly_trialDays": "{{price}} / 月 - {{trialDays}} 天试用", + "Apps_Marketplace_pricingPlan_yearly_perUser_trialDays": "{{price}} / 年/用户 - {{trialDays}} 天试用", + "Apps_Marketplace_pricingPlan_yearly_trialDays": "{{price}} / 年 - {{trialDays}} 天试用", "Apps_Permissions_No_Permissions_Required": "这个应用不需要额外的权限", + "Apps_Permissions_Review_Modal_Subtitle": "此应用希望访问以下权限。是否同意?", "Apps_Permissions_Review_Modal_Title": "这个应用需要以下权限", "Apps_Permissions_api": "注册HTTP API端口", + "Apps_Permissions_cloud_workspace-token": "代表此服务器与云服务交互", "Apps_Permissions_env_read": "读取服务器的环境变量", "Apps_Permissions_livechat-custom-fields_write": "修改Livechat的自定义字段 (custom field)", + "Apps_Permissions_livechat-department_multiple": "读取多个Livechat部门信息", "Apps_Permissions_livechat-department_read": "读取Livechat部门信息", "Apps_Permissions_livechat-department_write": "修改Livechat部们信息", "Apps_Permissions_livechat-message_read": "读取Livechat消息", @@ -433,8 +698,10 @@ "Apps_Permissions_message_read": "读取消息", "Apps_Permissions_message_write": "修改消息", "Apps_Permissions_networking": "使用服务器的网络接口", + "Apps_Permissions_experimental_default": "使用实验性API", "Apps_Permissions_persistence": "持久化数据到数据库", "Apps_Permissions_room_read": "读取房间信息", + "Apps_Permissions_room_system_view-all": "查看工作区中的所有房间", "Apps_Permissions_room_write": "修改房间信息", "Apps_Permissions_scheduler": "使用定时器去执行定时任务", "Apps_Permissions_server-setting_read": "读取服务器设置", @@ -445,6 +712,7 @@ "Apps_Permissions_upload_write": "上传文件到服务器", "Apps_Permissions_user_read": "读取用户信息", "Apps_Permissions_user_write": "修改用户信息", + "Apps_Private_App_Is_Exempt": "{{appName}} 已安装并豁免于应用数量限制策略。\n被豁免的应用无法更新。", "Apps_Settings": "应用的设置", "Apps_User_Already_Exists": "与待安装App同名的用户 “{{username}}” 已存在,请在安装前重命名或删除该用户。", "Apps_WhatIsIt": "应用程序:它们是什么?", @@ -452,28 +720,64 @@ "Apps_WhatIsIt_paragraph2": "首先,在这种情况下的应用程序不涉及移动应用程序。事实上,最好从插件或高级集成角度考虑它们。", "Apps_WhatIsIt_paragraph3": "其次,它们是动态脚本或软件包,它允许您自定义 Rocket.Chat 实例而不必分叉代码库。但请记住,这是一个新的功能集,因为它可能不是 100% 稳定。此外,我们仍在开发功能集,因此当前并非所有功能都可以在此进行定制。有关开发应用程序的更多信息,请点击此处阅读:", "Apps_WhatIsIt_paragraph4": "但话虽如此,如果您有兴趣启用此功能并尝试使用,请点击此按钮启用 Apps 系统。", + "Apps_context_enterprise": "企业版", + "Apps_context_explore": "探索", "Apps_context_installed": "已安装", "Apps_context_premium": "企业", + "Apps_context_private": "私有应用", + "Apps_context_requested": "已请求", + "Apps_disabled_when_Enterprise_trial_ended": "企业版试用结束后应用已禁用", + "Apps_disabled_when_Premium_trial_ended": "高级版试用结束后应用已禁用", + "Apps_disabled_when_Premium_trial_ended_description": "社区版工作区最多可启用 5 个应用市场应用。私有应用仅可在高级方案中启用。请联系工作区管理员重新启用应用。", + "Apps_disabled_when_Premium_trial_ended_description_admin": "社区版工作区最多可启用 5 个应用市场应用。私有应用仅可在高级方案中启用。请重新启用所需应用。", "Archive": "归档", "Archived": "已归档", "Are_you_sure": "你确定吗?", + "Are_you_sure_delete_department": "你确定要删除此部门吗?此操作无法撤销。请在下方输入部门名称以确认。", + "Are_you_sure_you_want_to_clear_all_unread_messages": "你确定要清除所有未读消息吗?", + "Are_you_sure_you_want_to_close_this_chat": "你确定要关闭此聊天吗?", "Are_you_sure_you_want_to_delete_this_record": "您确定要删除这条记录吗?", "Are_you_sure_you_want_to_delete_your_account": "您确定要删除您的帐号吗?", "Are_you_sure_you_want_to_disable_Facebook_integration": "你确定要禁用 Facebook 集成吗?", + "Are_you_sure_you_want_to_pin_this_message": "你确定要固定这条消息吗?", + "Are_you_sure_you_want_to_reset_the_name_of_all_priorities": "你确定要重置所有优先级名称吗?", + "Are_you_sure_delete_contact": "你确定要删除 {{contactName}} 及其全部 {{channelsCount}} 条会话历史记录吗?要确认,请在下方输入 '{{confirmationText}}'。", + "Are_you_sure_you_want_to_discard_this_outbound_message": "你确定要丢弃这条外发消息吗?", + "Ask_enable_advanced_contact_profile": "请联系工作区管理员启用高级联系人资料", + "Asset_preview": "资产预览", "Assets": "资产", + "Assets_Description": "修改工作区的徽标、图标、favicon(站点图标) 等。", + "Assets_livechat_widget_logo": "Livechat 小部件徽标 (svg, png, jpg)", "Assign_admin": "分配管理员", + "Assign_extension": "分配分机", "Assign_new_conversations_to_bot_agent": "将新对话分配给漫游器代理", "Assign_new_conversations_to_bot_agent_description": "路由系统在尝试与人工代理进行新对话之前将尝试查找漫游器代理。", + "Associate": "关联", + "Associate_Extension": "关联分机", + "Associate_User_to_Extension": "将用户关联到分机", "At_least_one_added_token_is_required_by_the_user": "至少需要一个由用户添加的令牌", "AtlassianCrowd": "Atlassian人群", + "AtlassianCrowd_Description": "集成 Atlassian Crowd。", "Attachment_File_Uploaded": "文件已上传", "Attribute_handling": "属性处理", + "Attribute_Values": "属性值", + "Attributes": "属性", + "Attribute_based_access_control": "基于属性的访问控制(ABAC)", + "Attribute_based_access_control_title": "在整个组织内自动化复杂访问管理", + "Attribute_based_access_control_description": "ABAC 通过动态用户属性自动授予或撤销房间访问,不再依赖固定角色。", + "ABAC_Attributes": "主体属性", + "ABAC_Attributes_description": "ABAC 用于判定房间访问的用户特征", "Audio": "音频", "Audio_Notification_Value_Description": "可以是任何自定义音效或默认音效:beep, chelle, ding, droplet, highbell, seasons", "Audio_Notifications_Default_Alert": "音频通知默认提醒", "Audio_Notifications_Value": "默认消息通知音效", "Audio_message": "音频消息", + "Audio_record": "录音", + "Audio_recorder": "录音器", "Audios": "音效", + "Audit": "审计", + "Auditing": "审计", + "Auth": "认证", "Auth_Token": "验证令牌", "Authentication": "认证", "Author": "作者", @@ -481,6 +785,7 @@ "Author_Site": "作者站点", "Authorization_URL": "授权 URL", "Authorize": "授权", + "Authorize_access_to_your_account": "授权访问您的账号", "AutoLinker": "自动添加链接", "AutoLinker_Email": "自动为电子邮件添加链接", "AutoLinker_Phone": "自动为电话号码添加链接", @@ -493,25 +798,34 @@ "AutoLinker_Urls_www": "自动为 “www” 网址添加链接", "AutoTranslate": "自动翻译", "AutoTranslate_APIKey": "API密钥", + "AutoTranslate_AutoEnableOnJoinRoom": "为非默认语言成员自动翻译", + "AutoTranslate_AutoEnableOnJoinRoom_Description": "启用后,当语言偏好不同于工作区默认语言的用户加入房间时,将为其自动翻译。", "AutoTranslate_Change_Language_Description": "更改自动翻译不会翻译之前的消息。", - "AutoTranslate_DeepL": "DeepL", + "AutoTranslate_DeepL": "DeepL(翻译服务)", + "AutoTranslate_Disabled_for_room": "已为 #{{roomName}} 禁用自动翻译", "AutoTranslate_Enabled": "启用自动翻译", "AutoTranslate_Enabled_Description": "启用自动翻译功能后,拥有`auto-translate` 权限的用户可以将所有消息自动翻译成用户选择的语言。这可能需要付费。", + "AutoTranslate_Enabled_for_room": "已为 #{{roomName}} 启用自动翻译", "AutoTranslate_Google": "谷歌", "AutoTranslate_Microsoft": "微软", - "AutoTranslate_Microsoft_API_Key": "Ocp-Apim-Subscription-Key", + "AutoTranslate_Microsoft_API_Key": "Ocp-Apim-Subscription-Key(订阅密钥)", "AutoTranslate_ServiceProvider": "服务提供者", + "AutoTranslate_language_set_to": "自动翻译语言设置为 {{language}}", "Auto_Load_Images": "自动载入图片", "Auto_Selection": "自动选择", "Auto_Translate": "自动翻译", "Automatic_Translation": "自动翻译", + "Automatic_translation_not_available": "自动翻译不可用", + "Automatic_translation_not_available_info": "此房间已启用端到端加密(E2E),自动翻译无法处理加密消息。", "Available": "可用", "Available_agents": "空闲客服", "Available_departments": "可用部门", "Avatar": "头像", "Avatar_URL": "头像地址", "Avatar_changed_successfully": "头像更新成功", + "Avatar_format_invalid": "格式无效,仅允许图片类型。", "Avatar_url_invalid_or_error": "提供的链接无效或无法访问。请使用不同的链接再试一次。", + "Avatars": "头像", "Avg_chat_duration": "平均聊天时间", "Avg_first_response_time": "平均首次响应时间", "Avg_of_abandoned_chats": "放弃聊天的平均值", @@ -521,22 +835,40 @@ "Avg_of_waiting_time": "平均等待时间", "Avg_reaction_time": "平均回应时间", "Avg_response_time": "平均响应时间", + "Awaiting_confirmation": "等待确认", "Away": "离开", + "BBB_Enable_Teams": "启用于团队", + "BBB_End_Meeting": "结束会议", + "BBB_Join_Meeting": "加入会议", + "BBB_Start_Meeting": "开始会议", + "BBB_Video_Call": "BBB 视频通话", + "BBB_You_have_no_permission_to_start_a_call": "您无权限发起通话", "Back": "返回", + "Back_in_history": "返回历史记录", + "Back_to_canned_responses": "返回自动回复", "Back_to_Manage_Apps": "返回到应用程序管理", + "Back_to__roomName__channel": "返回 {{roomName}} 频道", + "Back_to__roomName__team": "返回 {{roomName}} 团队", "Back_to_applications": "返回应用列表", + "Back_to_calendar": "返回日历", "Back_to_chat": "回到聊天室", + "Back_to_home": "返回主页", "Back_to_imports": "返回导入", "Back_to_integration_detail": "返回查看集成的详细信息", "Back_to_integrations": "返回集成页", "Back_to_login": "返回登录界面", "Back_to_permissions": "返回权限页", + "Back_to_threads": "返回讨论串", "Backup_codes": "备份代码", + "Be_the_first_to_join": "成为第一个加入的人", + "Belongs_To": "所属", "Best_first_response_time": "最快首次响应时间", "Beta_feature_Depends_on_Video_Conference_to_be_enabled": "测试功能。需要视频会议功能支持才能启用。", "Better": "更好", "Bio": "个人经历", "Bio_Placeholder": "个人经历占位符", + "Block": "屏蔽", + "Block_IP_Address": "屏蔽 IP 地址", "Block_Multiple_Failed_Logins_Attempts_Until_Block_By_Ip": "在按 IP 屏蔽前允许失败的尝试次数", "Block_Multiple_Failed_Logins_Attempts_Until_Block_by_User": "在按用户屏蔽前允许失败的尝试次数", "Block_Multiple_Failed_Logins_By_Ip": "按 IP 屏蔽失败的登录尝试", @@ -545,41 +877,69 @@ "Block_Multiple_Failed_Logins_Enabled": "启用在数据中收集日志", "Block_Multiple_Failed_Logins_Ip_Whitelist": "IP 白名单", "Block_Multiple_Failed_Logins_Ip_Whitelist_Description": "白名单 IP 的逗号分隔列表", + "Block_Multiple_Failed_Logins_Notify_Failed": "通知失败的登录尝试", + "Block_Multiple_Failed_Logins_Notify_Failed_Channel": "发送通知的频道", + "Block_Multiple_Failed_Logins_Notify_Failed_Channel_Desc": "通知将在此接收。请确保频道存在。频道名称不应包含 # 符号。", "Block_Multiple_Failed_Logins_Time_To_Unblock_By_Ip_In_Minutes": "解除 IP 屏蔽的分钟数", + "Block_Multiple_Failed_Logins_Time_To_Unblock_By_Ip_In_Minutes_Description": "这是 IP 地址被屏蔽的时长,以及在计数器重置前允许发生失败尝试的时间。", "Block_Multiple_Failed_Logins_Time_To_Unblock_By_User_In_Minutes": "解除用户屏蔽的分钟数", + "Block_Multiple_Failed_Logins_Time_To_Unblock_By_User_In_Minutes_Description": "这是用户被屏蔽的时长,以及在计数器重置前允许发生失败尝试的时间。", "Block_User": "屏蔽用户", + "Block_channel": "屏蔽频道", + "Block_channel_description": "你确定要屏蔽此频道吗?来自此对话的消息将不再到达此工作区。", "Blockchain": "区块链", + "Blocked": "已屏蔽", + "Blocked_IP_Addresses": "已屏蔽的 IP 地址", + "Blockstack": "Blockstack(去中心化登录)", "Blockstack_Auth_Description": "认证描述", "Blockstack_ButtonLabelText": "按钮标签文本", + "Blockstack_Description": "让工作区成员在不依赖任何第三方或远程服务器的情况下登录。", "Blockstack_Generate_Username": "生成用户名", "Body": "正文", + "Bold": "粗体", + "Bot": "机器人", "BotHelpers_userFields": "用户字段", "BotHelpers_userFields_Description": "用户字段中的 CSV 是可以被机器人 helper 方法访问的。", "Bots": "机器人", + "Bots_Description": "设置在开发机器人时可引用并使用的字段。", "Branch": "分支", + "Broadcast": "广播", "Broadcast_Connected_Instances": "广播连接实例", "Broadcast_channel": "广播频道", "Broadcast_channel_Description": "只有授权用户才能发送新消息,但其他用户可以回复消息", + "Broadcast_hint_enabled": "只有 {{roomType}} 拥有者可以发送新消息,但任何人都可以在讨论串中回复。", "Broadcasting_api_key": "广播 API Key", "Broadcasting_client_id": "广播客户端 ID", "Broadcasting_client_secret": "广播客户端 Secret", "Broadcasting_enabled": "启用广播", "Broadcasting_media_server_url": "广播媒体服务器 URL", "Browse_Files": "浏览文件", + "Browser": "浏览器", "Browser_does_not_support_audio_element": "您的浏览器不支持音频元素。", + "Browser_does_not_support_recording_video": "您的浏览器不支持录制视频", "Browser_does_not_support_video_element": "您的浏览器不支持视频元素。", - "Bugsnag_api_key": "Bugsnag API Key", + "Bugsnag_api_key": "Bugsnag API 密钥", "Build_Environment": "构建环境", + "Bundles": "捆绑包", "Busiest_day": "最忙的一天", "Busiest_time": "最忙的时间", "Business_Hour": "营业时间", "Business_Hour_Removed": "已移除营业时间", "Business_Hours": "营业时间", "Business_hours_enabled": "已启用营业时间", + "Business_hours_is_disabled": "营业时间已禁用", + "Business_hours_is_disabled_description": "在工作区管理面板中启用营业时间,让客户了解你的可用时间以及预计响应时间。", "Business_hours_updated": "已更新营业时间", + "Business_hours_will_update_automatically": "营业时间将自动更新", "Busy": "忙碌", + "Buy": "购买", + "Buy_more": "购买更多", + "Buy_more_seats": "购买更多席位", + "By": "由", + "CAS": "CAS(中央认证服务)", "CAS_Creation_User_Enabled": "允许创建用户", "CAS_Creation_User_Enabled_Description": "允许从 CAS 单据数据创建 CAS 用户", + "CAS_Description": "中央认证服务允许成员使用一组凭据,通过多种协议登录多个站点。", "CAS_Login_Layout": "CAS 登录布局", "CAS_Sync_User_Data_Enabled": "始终同步用户数据", "CAS_Sync_User_Data_Enabled_Description": "在登录时始终将外部 CAS 用户数据同步到可用属性中。注意:无论如何,帐户创建时都会同步属性。", @@ -607,78 +967,169 @@ "CRM_Integration": "CRM 集成", "CROWD_Allow_Custom_Username": "允许在 Rocket.Chat 中自定义用户名", "CROWD_Reject_Unauthorized": "拒绝未经授权的", + "CSV": "CSV(逗号分隔值)", + "Calendar_BusyStatus_Enabled": "Outlook 日历状态同步", + "Calendar_BusyStatus_Enabled_Description": "在计划的 Outlook 会议期间自动将用户状态设为忙碌,并在结束后恢复为之前的状态。", + "Calendar_MeetingUrl_Regex": "会议 URL 正则表达式", + "Calendar_MeetingUrl_Regex_Description": "用于在事件描述中检测会议 URL 的表达式。将使用首个匹配且为有效 URL 的分组。HTML 编码的 URL 将自动解码。", + "Calendar_settings": "日历设置", "Call": "调用", + "Call_Already_Ended": "通话已结束", + "Call_ID": "通话 ID", + "Call_in_progress": "通话进行中", + "Call_info": "通话信息", + "Call_info_could_not_be_loaded": "无法加载通话信息", + "Call_Information": "通话信息", + "Call_again": "再次呼叫", + "Call_back": "回拨", + "Call_declined": "通话已拒绝!", + "Call_ended": "通话已结束", + "Call_ended_bold": "*语音通话已结束*", + "Call_not_answered_bold": "*语音通话未接听*", + "Call_failed_bold": "*语音通话失败*", + "Call_transferred_bold": "*语音通话已转接*", + "Call_history": "通话记录", + "Call_history_provides_a_record_of_when_calls_took_place_and_who_joined": "通话记录用于记录通话发生的时间及参与者。", + "Call_not_found": "未找到通话", + "Call_not_found_error": "当通话 URL 无效或连接出现问题时可能会发生。请检查通话 URL 的来源并重试,若问题仍然存在,请联系您的工作区管理员。", + "Call_ongoing": "通话中", + "Call_provider": "通话服务提供商", + "Call_ringer_volume": "来电铃声音量", + "Call_ringer_volume_hint": "用于所有来电语音和视频通话通知", + "Call_started": "通话已开始", + "Call_terminated": "通话已终止", + "Call_transfered_to__name__": "通话已转接至 {{name}}", + "Call_unavailable_for_federation": "通话在联邦房间中不可用", + "Call_was_not_answered": "通话未接听", "Caller": "调用者", + "Caller_Id": "来电显示 ID", + "Calling": "呼叫中", + "Calling__roomName__": "正在呼叫 {{roomName}}", + "Calls": "通话", + "Calls_in_queue": { + "other": "{{count}} calls in queue" + }, + "Cam_off": "关闭摄像头", + "Cam_on": "开启摄像头", + "Camera_access_not_allowed": "视频消息 - 摄像头访问被拒绝,请检查浏览器设置。", "Cancel": "取消", + "Cancel__planName__subscription": "取消 {{planName}} 订阅", "Cancel_message_input": "取消", + "Cancel_recording": "取消录制", + "Cancel_subscription": "取消订阅", + "Cancel_subscription_message": "此工作区将降级为社区版并失去高级功能的免费访问权限。

虽然你仍可继续使用 Rocket.Chat,但你的团队将无法使用无限移动推送通知、已读回执、应用市场应用 <4>以及其他功能。", "Canceled": "已取消", + "Canned_Response_Created": "已创建自动回复", + "Canned_Response_Delete_Warning": "删除自动回复无法撤销。", "Canned_Response_Removed": "已移除自动回复", + "Canned_Response_Sharing_Department_Description": "所选部门中的任何人都可以访问此自动回复", + "Canned_Response_Sharing_Private_Description": "只有你和 Omnichannel 管理员可以访问此自动回复", + "Canned_Response_Sharing_Public_Description": "任何人都可以访问此自动回复", + "Canned_Response_Updated": "自动回复已更新", "Canned_Responses": "自动回复", "Canned_Responses_Enable": "启用自动回复", "Cannot_invite_users_to_direct_rooms": "不能邀请用户加入私聊", "Cannot_open_conversation_with_yourself": "不能和你自己私聊", "Cannot_share_your_location": "不能分享您的位置…", + "Cannot_upload_file_character_limit": "无法上传文件,描述超过 {{count}} 个字符限制", + "Cant_join": "无法加入", "Categories": "类别", + "Categories*": "类别*", "Certificates_and_Keys": "证书和密钥", "Change_Room_Type": "聊天室类型更改", + "Changed_from": "变更自", + "Changed_to": "变更为", "Changing_email": "正在更改电子邮件", "Channel": "频道", "Channel_Archived": "频道 `#%s` 已成功存档", "Channel_Export": "导出频道", "Channel_Name_Placeholder": "请输入频道名称...", "Channel_Unarchived": "已成功取消频道 `#%s` 的归档", + "Channel__roomName__": "频道 {{roomName}}。", "Channel_already_Unarchived": "频道 `#%s` 已经处于未封存状态", "Channel_already_exist": "频道 `#%s` 已存在。", "Channel_already_exist_static": "该频道已经存在。", "Channel_created": "频道 `#%s` 已创建。", "Channel_doesnt_exist": "不存在 `#%s` 频道。", + "Channel_info": "频道信息", "Channel_name": "频道名称", + "Channel_not_joined": "未加入频道", "Channel_to_listen_on": "要监听的频道", + "Channel_what_is_this_channel_about": "该频道是做什么的?", "Channels": "频道", + "Channels_added": "频道已成功添加", "Channels_are_where_your_team_communicate": "频道是您团队沟通的地方", "Channels_list": "公共频道列表", "Chart": "购物车", "Chat_Duration": "聊天时长", "Chat_History": "聊天历史", "Chat_Now": "现在聊天", + "Chat_On_Hold": "聊天挂起", + "Chat_On_Hold_Successfully": "聊天已成功挂起", "Chat_button": "聊天按钮", "Chat_close": "聊天关闭", "Chat_closed": "聊天已关闭", "Chat_closed_by_agent": "客服关闭了聊天", "Chat_closed_successfully": "聊天已成功关闭", + "Chat_opened_by_visitor": "聊天由访客发起", "Chat_queued": "聊天已排队", "Chat_removed": "已移除聊天", + "Chat_resumed": "聊天已恢复", "Chat_start": "聊天开始", + "Chat_started": "聊天已开始", "Chat_taken": "聊天已进行", + "Chat_transcript": "聊天记录", "Chat_window": "聊天窗口", + "Chat_with_leader": "与主管聊天", "Chatops_Enabled": "启用 Chatops", "Chatops_Title": "Chatops 面版", "Chatops_Username": "Chatops 用户名", + "Chats": "聊天", "Chats_removed": "已移除聊天", "Check_All": "全选", "Check_Progress": "检查进度", + "Check_back_later": "稍后再查看", + "Check_device_activity": "检查设备活动", + "Check_if_the_spelling_is_correct": "检查拼写是否正确", + "Check_support_availability": "查看 <1>支持 是否可用", "Choose_a_room": "选择一个聊天室", "Choose_messages": "选择信息", "Choose_the_alias_that_will_appear_before_the_username_in_messages": "选择将在消息中的用户名前出现的别名。", "Choose_the_username_that_this_integration_will_post_as": "选择该集成以什么用户名身份发布信息", + "Choose_theme_description": "选择最符合你需求的界面外观。", "Choose_users": "选择用户", + "Clean_History_unavailable_for_federation": "联邦房间无法清理历史记录", "Clean_Usernames": "清除用户名", "Clear_all_unreads_question": "清除所有未读标记?", "Clear_filters": "清除筛选", + "Clear_livechat_session_when_chat_ended": "聊天结束后清除访客会话", + "Clear_selection": "清除选择", "Click_here": "点击此处", "Click_here_for_more_details_or_contact_sales_for_a_new_license": "点击此处 获取更多细节或联系 {{email}} 获取新的许可。", "Click_here_for_more_info": "点此查看更多信息", + "Click_here_to_clear_the_selection": "点击此处清除选择", + "Click_here_to_enter_your_password": "点击此处输入密码", + "Click_here_to_view_and_save_your_new_E2EE_password": "点击此处查看并保存新的端到端加密(E2EE)密码。", "Click_the_messages_you_would_like_to_send_by_email": "点击您想通过电子邮件发送的消息", "Click_to_join": "点击加入!", "Click_to_load": "点击以加载", + "Client": "客户端", "Client_ID": "客户端 ID", "Client_Secret": "客户端 Secret", "Clients_will_refresh_in_a_few_seconds": "客户端将在几秒钟内刷新", "Close": "关闭", + "Close_Dialpad": "关闭拨号盘", + "Close_dialpad": "关闭拨号盘", + "Close_Window": "关闭窗口", + "Close_chat": "关闭聊天", + "Close_gallery": "关闭图库", "Close_room_description": "将关闭此聊天。您确定要继续吗?", + "Close_sidebar": "关闭侧边栏", "Closed": "已关闭", "Closed_At": "关闭于", "Closed_automatically": "已被系统自动关闭", + "Closed_automatically_because_chat_was_onhold_for_seconds": "由于聊天挂起超过 {{onHoldTime}} 秒,已自动关闭", + "Closed_automatically_chat_queued_too_long": "系统自动关闭(队列等待时间超过上限)", "Closed_by_visitor": "由访客关闭", "Closing_chat": "正在关闭聊天", "Closing_chat_message": "关闭聊天信息", @@ -693,12 +1144,14 @@ "Cloud_Service_Agree_PrivacyTerms": "云服务隐私条款", "Cloud_Service_Agree_PrivacyTerms_Description": "我同意 [条款](https://rocket.chat/terms) 和 [隐私协议](https://rocket.chat/privacy)", "Cloud_Service_Agree_PrivacyTerms_Login_Disabled_Warning": "您应当接受云隐私条款(安装向导>云信息>云服务隐私条款同意书)以连接你的云工作区", + "Cloud_Workspace_Id": "云工作区 ID", "Cloud_address_to_send_registration_to": "您用来注册 Cloud 的电子邮件的地址。", "Cloud_click_here": "在复制文本后,前往 [cloud console (click here)]({{cloudConsoleUrl}})。", "Cloud_connectivity": "云连接性", "Cloud_console": "云控制台", "Cloud_error_code": "代码: {{errorCode}}", "Cloud_error_in_authenticating": "验证出错", + "Cloud_hosting": "Rocket.Chat 云托管", "Cloud_login_to_cloud": "登录至 Rocket.Chat Cloud", "Cloud_logout": "登出 Rocket.Chat 云", "Cloud_manually_input_token": "手动输入在云控制台中获取的令牌。", @@ -725,56 +1178,126 @@ "Cloud_workspace_support": "如果您遇到任何云服务的问题, 请先尝试同步。如果问题依旧, 请在云控制台中开启一个支持工单。", "Collaborative": "协作", "Collapse": "折叠", + "Collapse_all": "全部折叠", "Collapse_Embedded_Media_By_Default": "关闭默认的嵌入式媒体", + "Collapse_group": "折叠 {{group}}", "Color": "颜色", "Colors": "颜色", "Commands": "指令", + "Comment": "评论", "Comment_to_leave_on_closing_session": "为关闭的会话留下评论", + "Commit": "提交", "Commit_details": "提交细节", "Common_Access": "通用访问", "Community": "社区", + "Community_Private_apps_limit_exceeded": "社区版应用数量已超出上限。", + "Community_cap_description": "社区版工作区并发连接上限为 200。超过该限制后,用户将无法看到彼此的状态。此限制不影响消息的发送和接收。", + "Compare_plans": "比较方案", "Completed": "已完成", + "Composer_readonly_airgapped": "<0>工作区处于只读模式。 管理员可通过连接互联网或升级到高级方案来恢复完整功能。", "Computer": "电脑", "Condensed": "简明", + "Condition": "条件", + "Conference_call_apps": "会议通话应用", + "Conference_call_has_ended": "_通话已结束。_", + "Conference_name": "会议名称", + "Configuration_update": "配置更新", + "Configuration_update_confirmed": "配置更新已确认", "Configure_Incoming_Mail_IMAP": "编辑入站邮件(IMAP)", "Configure_Outgoing_Mail_SMTP": "编辑出站邮件(SMTP)", + "Configure_video_conference": "配置视频会议", + "Configure_video_conference_to_make_it_available_on_this_workspace": "配置视频会议以使其在此工作区可用", + "Config_needed": "需要配置", + "Confirm": "确认", + "Confirm_configuration_update": "确认配置更新", + "Confirm_configuration_update_description": "识别数据和云连接数据将被保留。

警告:如果这实际上是一个新工作区,请返回并选择新工作区选项,以避免通信冲突。", + "Confirm_new_E2EE_password": "确认新的端到端加密(E2EE)密码", "Confirm_new_password": "确认新密码", + "Confirm_new_workspace": "确认新工作区", + "Confirm_new_workspace_description": "识别数据和云连接数据将被重置。

警告:更改工作区 URL 可能会影响许可证。", "Confirm_password": "确认密码", "Confirm_your_password": "确认密码", + "Confirm_contact_removal": "确认移除联系人", + "Confirmation": "确认", + "Conflicts_found": "发现冲突", "Connect": "连接", + "ConnectWorkspace_Button": "连接工作区", "Connect_SSL_TLS": "使用 SSL/TLS 连接", "Connected": "已连接", "Connection_Closed": "连接关闭", "Connection_Reset": "连接重置", + "Connection_error": "连接错误", + "Connection_failed": "LDAP 连接失败", "Connectivity_Services": "连接性服务", "Consulting": "咨询", + "Consumer_Packaged_Goods": "消费品", "Contact": "联系人", + "contact": "联系人", "Contact_Center": "联系人中心", "Contact_Chat_History": "联系聊天历史", + "Contact_detail": "联系人详情", + "Contact_Info": "联系人信息", "Contact_Manager": "联系人管理员", "Contact_Name": "联系人名称", "Contact_Profile": "联系人资料", + "Contact_blocked": "联系人已屏蔽", + "Contact_email": "联系人邮箱", + "Contact_has_been_created": "联系人已创建", + "Contact_has_been_updated": "联系人已更新", + "Contact_has_been_deleted": "联系人已删除", + "Contact_history_is_preserved": "联系人历史记录已保留", + "Contact_identification": "联系人识别", "Contact_not_found": "未找到联系人", + "Contact_sales": "联系销售", + "Contact_sales_renew_date": "<0>联系销售以查看方案续费日期", + "Contact_sales_start_using_VoIP": "联系销售以开始使用 VoIP。", + "Contact_sales_trial": "联系销售完成购买以避免 <1>降级后果。", + "Contact_unblocked": "联系人已解除屏蔽", + "Contact_your_workspace_admin_to_start_using_VoIP": "请联系工作区管理员以开始使用 VoIP。", "Contacts": "联系人", "Contains_Security_Fixes": "包含安全修复程序", "Content": "内容", "Continue": "继续", + "Continue_Adding": "继续添加?", "Continuous_sound_notifications_for_new_livechat_room": "新 omnichannel 聊天室的连续声音通知", "Conversation": "会话", "Conversation_closed": "会话已关闭:{{comment}}。", + "Conversation_closed_without_comment": "对话已关闭", "Conversation_closing_tags": "会话关闭标签", "Conversation_closing_tags_description": "将会在对话关闭时自动分配关闭标签", "Conversation_finished": "会话结束", "Conversation_finished_message": "会话完成时的消息", "Conversation_finished_text": "会话完成时的文本", + "Conversation_in_progress": "对话进行中", + "Conversation_with__roomName__": "与 {{roomName}} 的对话。", + "Conversational_transcript": "对话记录", "Conversations": "会话", + "Conversations_by_agents": "按客服统计的对话", + "Conversations_by_channel": "按频道统计的对话", + "Conversations_by_department": "按部门统计的对话", + "Conversations_by_status": "按状态统计的对话", + "Conversations_by_tag": "按标签统计的对话", "Conversations_per_day": "每日会话", + "Convert": "转换", "Convert_Ascii_Emojis": "自动识别文字中的表情", + "Convert_to_channel": "转换为频道", + "Converted__roomName__to_a_channel": "已将 #{{roomName}} 转换为频道", + "Converted__roomName__to_a_team": "已将 #{{roomName}} 转换为团队", + "Converted__roomName__to_channel": "已转换 #{{roomName}} 为频道", + "Converted__roomName__to_team": "已转换 #{{roomName}} 为团队", + "Converting_channel_to_a_team": "你正在将此频道转换为团队。所有成员将被保留。", + "Converting_team_to_channel": "正在将团队转换为频道", "Copied": "已复制", "Copy": "复制", + "Copy_Link": "复制链接", + "Copy_link": "复制链接", + "Copy_password": "复制密码", + "Copy_phone_number": "复制电话号码", "Copy_text": "复制文本", "Copy_to_clipboard": "复制到剪贴板", "Count": "计数", + "CountMAC_InfoText": "(MAC,月活跃联系人) 指日历月内参与互动的唯一 Omnichannel 联系人数量。", + "CountSeats_InfoText": "每个唯一用户占用一个席位。已停用用户不占用席位。", "Counters": "计数器", "Country": "国家", "Country_Afghanistan": "阿富汗", @@ -1018,33 +1541,60 @@ "Country_Zimbabwe": "津巴布韦", "Create": "创建", "Create_A_New_Channel": "创建新频道", + "Create_SLA_policy": "创建服务级别协议(SLA)策略", + "Create_a_password": "创建密码", + "Create_a_public_channel_that_new_workspace_members_can_join": "创建一个新工作区成员可以加入的公开频道。", + "Create_an_account": "创建账号", + "Create_canned_response": "创建自动回复", + "Create_channel": "创建频道", + "Create_channels": "创建多个频道", + "Create_custom_field": "创建自定义字段", + "Create_department": "创建部门", + "Create_direct_message": "新建私信", "Create_new": "新建", + "Create_new_members": "创建新成员", + "Create_tag": "创建标签", + "Create_trigger": "创建触发器", "Create_unique_rules_for_this_channel": "为此频道创建独特的规则", + "Create_unit": "创建单位", "Created": "已创建", "Created_as": "创建为", "Created_at": "创建于", "Created_at_s_by_s": "创建于 %s, 由 %s ", "Created_at_s_by_s_triggered_by_s": "创建于 %s,由%s 通过 %s 触发", "Created_by": "创建由", + "Crowd_Connection_successful": "Crowd 连接成功", "Crowd_Remove_Orphaned_Users": "移除孤立用户", "Crowd_sync_interval_Description": "同步之间的时间间隔。例如“每24小时”或“每周的第一天”,在[Cron Text Parser](http://bunkat.github.io/later/parsers.html#text)上的更多示例", "Current_File": "当前文件", "Current_Import_Operation": "当前导入操作", "Current_Status": "当前状态", + "Currently_we_dont_support_joining_servers_with_this_many_people": "目前不支持加入人数如此多的服务器", "Custom": "自定义", "Custom CSS": "自定义 CSS", "CustomSoundsFilesystem": "自定义声音文件系统", + "CustomSoundsFilesystem_Description": "指定自定义声音的存储方式。", + "Custom_API": "自定义(API)", + "Custom_APP": "自定义(APP)", "Custom_Emoji": "自定义表情", "Custom_Emoji_Add": "添加新表情", "Custom_Emoji_Added_Successfully": "自定义表情添加成功", "Custom_Emoji_Delete_Warning": "表情删除操作无法撤销。", "Custom_Emoji_Error_Invalid_Emoji": "无效表情", "Custom_Emoji_Error_Name_Or_Alias_Already_In_Use": "自定义表情或别名已经被占用。", + "Custom_Emoji_Error_Same_Name_And_Alias": "自定义表情名称与别名应不同。", "Custom_Emoji_Has_Been_Deleted": "自定义表情已删除。", "Custom_Emoji_Info": "自定义表情信息", "Custom_Emoji_Updated_Successfully": "自定义表情更新成功", + "Custom_Field_Not_Found": "未找到自定义字段", "Custom_Field_Removed": "已移除自定义字段", "Custom_Fields": "自定义字段", + "Custom_fields": "自定义字段", + "Custom_Integration": "自定义集成", + "Custom_OAuth_has_been_added": "已添加自定义 OAuth", + "Custom_OAuth_has_been_removed": "已移除自定义 OAuth", + "Custom_OAuth_name": "自定义 OAuth 名称", + "Custom_OAuth_name_hint": "输入一个唯一名称,便于识别和管理不同的认证方式。", "Custom_Script_Logged_In": "为已登录用户准备的自定义脚本", "Custom_Script_Logged_In_Description": "任何登录的用户都将运行的自定义脚本(例如当登录的用户进入聊天时)。", "Custom_Script_Logged_Out": "为已登出用户准备的自定义脚本", @@ -1061,6 +1611,7 @@ "Custom_Sound_Info": "自定义声音信息", "Custom_Sound_Saved_Successfully": "自定义声音保存成功", "Custom_Status": "自定义状态", + "Custom_time_range": "自定义时间范围", "Custom_Translations": "自定义翻译", "Custom_Translations_Description": "应使用 JSON 文件格式,键为语言,值为翻译。例如: \n `{\"en\": {\"Channels\": \"Rooms\"},\"pt\": {\"Channels\": \"Salas\"}}`", "Custom_User_Status": "自定义用户状态", @@ -1074,10 +1625,17 @@ "Custom_User_Status_Info": "自定义用户状态信息", "Custom_User_Status_Updated_Successfully": "自定义用户状态更新成功", "Custom_agent": "自定义代理", + "Custom_content": "自定义内容", "Custom_dates": "自定义日期", "Custom_oauth_helper": "设置完 OAuth 提供者之后,你还需要设置回调 URL。请使用

%s
。", + "Custom_roles": "自定义角色", + "Custom_roles_upsell_add_custom_roles_workspace": "为工作区添加自定义角色", + "Custom_roles_upsell_add_custom_roles_workspace_description": "自定义角色允许你为工作区内人员设置权限。设置所需角色,确保大家在安全环境中工作。", + "Customer": "客户", + "Customer_without_registered_email": "该客户没有注册邮箱地址", "Customize": "定制", - "DAU_value": "DAU {{value}}", + "Customize_Content": "自定义内容", + "DAU_value": "DAU(日活跃用户) {{value}}", "DB_Migration": "数据库迁移", "DB_Migration_Date": "数据库迁移日期", "DDP_Rate_Limit_Connection_By_Method_Enabled": "连接每方法限制: 启用", @@ -1095,32 +1653,64 @@ "DDP_Rate_Limit_User_Enabled": "用户限制: 启用", "DDP_Rate_Limit_User_Interval_Time": "用户限制: 间隔时间", "DDP_Rate_Limit_User_Requests_Allowed": "用户限制: 允许请求", + "DDP_Rate_Limiter": "DDP 速率限制", "Daily_Active_Users": "每日活跃用户", "Dashboard": "控制面板", + "Data_modified": "数据已修改", "Data_processing_consent_text": "数据处理同意书", "Data_processing_consent_text_description": "使用此设置来表示您可以在对话中收集,存储和处理客户的个人信息。", "Date": "日期", "Date_From": "从", "Date_to": "到", + "Daylight_savings_time": "该时区采用夏令时", "Days": "日", "Deactivate": "禁用", + "Deactivated": "已停用", "Decline": "下降", "Default": "默认", + "Default_Custom_Timezone": "自定义时区", + "Default_Referrer_Policy": "默认 Referrer 策略", + "Default_Referrer_Policy_Description": "此项控制从其他服务器请求嵌入媒体时发送的 'referrer' 头。更多信息请参见 [MDN 链接](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy)。注意,需要完整刷新页面才能生效", + "Default_Server_Timezone": "服务器时区", + "Default_Timezone_For_Reporting": "报告默认时区", + "Default_Timezone_For_Reporting_Description": "设置展示仪表盘或发送邮件时使用的默认时区", + "Default_User_Timezone": "用户当前时区", + "Default_provider": "默认提供商", "Default_value": "默认值", "Delete": "删除", + "Delete_Department?": "删除部门?", + "Delete_Contact": "删除联系人", "Delete_File_Warning": "删除文件是永久的。这不能被撤销。", + "Delete_Role_Warning": "此操作无法撤销", + "Delete_Role_Warning_Not_Enterprise": "此操作无法撤销。由于当前方案已不支持该功能,你将无法创建新的自定义角色。", + "Delete_Room_Warning": "删除此 {{roomType}} 将同时删除其中所有消息。此操作无法撤销。", "Delete_User_Warning": "删除用户将删除该用户的所有消息。这不能被撤销。", "Delete_User_Warning_Delete": "删除用户将删除该用户的所有消息。这不能被撤销。", "Delete_User_Warning_Keep": "用户将被删除,但他们的消息将保持可见。这不能被撤销。", "Delete_User_Warning_Unlink": "删除用户将从用户的所有消息中删除用户名。这不能被撤销。", + "Delete_account": "删除账号", + "Delete_account?": "删除账号?", "Delete_all_closed_chats": "删除所有已关闭的聊天", "Delete_message": "删除消息", "Delete_my_account": "删除我的帐号", + "Delete_roomType": "删除 {{roomType}}", + "Delete_roomType_description": "删除此 {{roomType}} 将同时删除其中所有消息。此操作无法撤销。", "Deleted": "已删除!", + "Deleted__roomName__": "已删除 #{{roomName}}", + "Deleted__roomName__room": "已删除 #{{roomName}}", + "Deleted_roomType": "{{roomName}} {{roomType}} 已删除", + "Deleted_user": "已删除用户", + "Deleting": "正在删除", "Department": "部门", + "department": "部门", + "Department_Removal_Disabled": "管理员已禁用删除选项", + "Department_archived": "部门已归档", + "Department_name": "部门名称", "Department_not_found": "找不到该部门", "Department_removed": "已删除部门", + "Department_unarchived": "部门已取消归档", "Departments": "部门", + "Deployment": "部署", "Deployment_ID": "部署 ID", "Description": "描述", "Desktop": "桌面", @@ -1132,10 +1722,36 @@ "Desktop_Notifications_Duration_Description": "桌面通知显示秒数。这可能会影响 OS X 通知中心。输入 0 使用默认浏览器设置,避免影响 OS X 通知中心。", "Desktop_Notifications_Enabled": "桌面通知已启用", "Desktop_Notifications_Not_Enabled": "桌面通知未启用", + "Desktop_apps": "桌面应用", "Details": "详细信息", + "Device_Changes_Not_Available": "此浏览器不支持设备更改。为确保可用性,请使用 Rocket.Chat 官方桌面应用。", + "Device_Changes_Not_Available_Insecure_Context": "设备更改仅在安全环境中可用(例如 https://)。", + "Device_ID": "设备 ID", + "Device_Info": "设备信息", + "Device_Logged_Out": "设备已登出", + "Device_Logout_Text": "设备将从工作区登出,当前会话将结束。用户可使用同一设备再次登录。", + "Device_Management": "设备管理", + "Device_Management_Allow_Login_Email_preference": "允许工作区成员关闭登录检测邮件", + "Device_Management_Allow_Login_Email_preference_Description": "成员可自行设置偏好。当登录过期频繁导致反复登录时很有用。", + "Device_Management_Client": "客户端", + "Device_Management_Description": "配置安全和访问控制策略。", + "Device_Management_Device": "设备", "Device_Management_Device_Unknown": "未知", + "Device_Management_Email_Body": "你可以使用以下占位符:`

{Login_Detected}

[name] ([username]) {Logged_In_Via}

{Device_Management_Client}: [browserInfo]
{Device_Management_OS}: [osInfo]
{Device_Management_Device}: [deviceInfo]
{Device_Management_IP}:[ipInfo]

[userAgent]

{Access_Your_Account}

{Or_Copy_And_Paste_This_URL_Into_A_Tab_Of_Your_Browser}
[SITE_URL]

{Thank_You_For_Choosing_RocketChat}

`", + "Device_Management_Email_Subject": "[Site_Name] - 检测到登录", + "Device_Management_Enable_Login_Emails": "启用登录检测邮件", + "Device_Management_Enable_Login_Emails_Description": "每当检测到成员账户的新登录时,都会发送邮件。", + "Device_Management_IP": "IP 地址", + "Device_Management_OS": "操作系统(OS)", + "Device_settings_lowercase": "设备设置", + "Device_settings_not_supported_by_browser": "设备设置(浏览器不支持)", + "Devices": "设备", + "Devices_Set": "设备集", + "Dialed_number_doesnt_exist": "拨号号码不存在", + "Dialed_number_is_incomplete": "拨号号码不完整", "Different_Style_For_User_Mentions": "用户提及的不同风格", "Direct": "私聊", + "DirectMesssage_maxUsers": "私信最大用户数", "Direct_Message": "私聊消息", "Direct_Messages": "私聊消息", "Direct_Reply": "直接回复", @@ -1159,20 +1775,36 @@ "Direct_Reply_Username_Description": "请使用绝对电子邮件地址,标签将被覆盖", "Direct_message": "私聊消息", "Direct_message_creation_description": "您将创建与多个用户的聊天。这将允许您同时和多个用户进行私聊。", + "Direct_message_creation_description_hint": "创建后无法再添加更多人员", + "Direct_message_creation_error": "请选择至少一个人", "Direct_message_someone": "和某人私聊", "Direct_message_you_have_joined": "您加入了新的私聊与", + "Direction": "方向", "Directory": "目录", + "Disable": "禁用", + "Disable_E2E_encryption": "禁用端到端加密(E2E)", "Disable_Facebook_integration": "禁用 Facebook 集成", "Disable_Notifications": "禁用通知", + "Disable_another_app": "禁用另一个应用或升级到高级方案以启用此应用。", + "Disable_at_least_more_apps": "你需要禁用至少 {{numberOfExceededApps}} 个其他应用,或升级到高级方案以启用此应用。", + "Disable_voice_calling": "禁用语音通话", "Disabled": "已禁用", + "Disabled_E2E_Encryption_for_this_room": "已为此房间禁用端到端加密(E2E)", + "Disabled_apps_admin_message": "存在一个或多个具有有效许可证的已禁用应用。前往 {{marketplace}} > {{installed}} 查看。", "Disallow_reacting": "不允许回应", "Disallow_reacting_Description": "不允许回应", "Discard": "放弃", + "Discard_message": "丢弃消息", "Disconnect": "断开连接", + "Disconnect_workspace": "断开工作区连接", + "Disconnected": "已断开连接", + "Discover_public_channels_and_teams_in_the_workspace_directory": "在工作区目录中发现公共频道和团队。", "Discussion": "讨论", + "Discussion_Description": "讨论是组织对话的另一种方式,允许从外部频道邀请用户参与特定对话。", "Discussion_description": "帮助保持事态更新! 通过创建讨论,可创建一个和所选频道双向关联的子频道。", "Discussion_first_message_disabled_due_to_e2e": "在此讨论的创建完成后您可以发送端到端加密的消息", "Discussion_first_message_title": "你的消息", + "Discussion_info": "讨论信息", "Discussion_name": "讨论名称", "Discussion_start": "开始一个讨论", "Discussion_target_channel": "父频道或者组", @@ -1180,29 +1812,46 @@ "Discussion_target_channel_prefix": "您正创建讨论于", "Discussion_title": "创建一个新讨论", "Discussions": "讨论", + "Discussions_unavailable_for_federation": "联邦房间不支持讨论", + "Dismiss": "忽略", + "Dismiss_quoted_message": "忽略引用消息", + "Display": "显示", + "Display_Avatars_Sidebar": "在侧边栏显示头像", + "Display_avatars": "显示头像", "Display_chat_permissions": "显示聊天权限", + "Display_mentions_counter": "仅显示直接提及的徽标", "Display_offline_form": "显示离线表单", "Display_setting_permissions": "显示更改设置的权限", "Display_unread_counter": "显示未读消息的数量", + "Displayed_next_to_name": "显示在姓名旁", "Displays_action_text": "显示动作文字", + "Do_It_Later": "稍后再做", "Do_Nothing": "什么也不做", "Do_not_display_unread_counter": "不要显示此频道的任何计数器", "Do_not_provide_this_code_to_anyone": "不要把这个代码给任何人。", + "Do_nothing": "不做任何操作", "Do_you_want_to_accept": "你想接受吗?", "Do_you_want_to_change_to_s_question": "您是否想修改为 %s?", "Document_Domain": "文档域", + "Documentation": "文档", "Domain": "域名", "Domain_added": "域名已添加", "Domain_removed": "已移除域名", "Domains": "域名", "Domains_allowed_to_embed_the_livechat_widget": "允许嵌入即时聊天小部件的逗号分隔的域列表。留空以允许所有域。", + "Done": "完成", "Dont_ask_me_again": "不要再问我了!", "Dont_ask_me_again_list": "不要再问我列表", + "Dont_cancel": "不取消", "Download": "下载", + "Download_Destkop_App": "下载桌面应用", + "Download_Disabled": "已禁用下载", "Download_Info": "下载信息", "Download_My_Data": "下载我的数据(HTML)", + "Download_Pending_Avatars": "下载挂起的头像", "Download_Pending_Files": "下载挂起的文件", "Download_Snippet": "下载", + "Download_file": "下载文件", "Downloading_file_from_external_URL": "从外部 URL 下载文件", "Drop_to_upload_file": "拖放上传", "Dry_run": "试运行", @@ -1213,41 +1862,85 @@ "Duplicate_file_name_found": "发现重复文件名。", "Duplicate_private_group_name": "名为 `%s` 的私人组已存在", "Duplicated_Email_address_will_be_ignored": "重复的电子邮件地址将被忽略。", + "Duration": "时长", + "E2EE_Composer_Unencrypted_Message": "你正在发送未加密消息", + "E2EE_password_reset": "端到端加密(E2EE)密码已重置", + "E2EE_alert": "启用 E2EE 会影响其他功能 ", + "E2E_Allow_Unencrypted_Messages": "在加密房间中允许未加密消息", + "E2E_Allow_Unencrypted_Messages_Description": "允许在包含加密内容的房间中发送明文消息。这些消息不会被加密。", + "E2E_Enable_Encrypt_Files": "加密文件", + "E2E_Enable_Encrypt_Files_Description": "加密在加密房间内发送的文件。请在[文件上传设置](admin/settings/FileUpload)中检查可能的冲突。", "E2E_Enable_description": "启用选项以创建加密组,并能够更改要加密的组和私聊消息。", "E2E_Enabled": "端到端已启用", "E2E_Enabled_Default_DirectRooms": "默认为私聊启用加密", + "E2E_Enabled_Default_DirectRooms_Description": "每次创建新的私信房间时默认启用加密。", "E2E_Enabled_Default_PrivateRooms": "默认为私人聊天室启用加密", + "E2E_Enabled_Default_PrivateRooms_Description": "每次创建新的私有频道、私有团队或其关联讨论时默认启用加密。", + "E2E_Enabled_Mentions": "提及", + "E2E_Enabled_Mentions_Description": "通知人员,并在加密内容中高亮用户、频道和团队提及。", "E2E_Encryption_Password_Explanation": "您现在可以创建加密的私有组和私聊。您也可以将现有的私人组或直接消息更改为加密。

这是端到端加密,因此编码/解码邮件的密钥不会保存在服务器上。因此,您需要将密码存储在安全的地方。您需要在希望使用端到端加密的其他设备上输入。", + "E2E_Encryption_disabled_for_room": "已为 #{{roomName}} 禁用端到端加密", + "E2E_Encryption_enabled_for_room": "已为 #{{roomName}} 启用端到端加密", + "E2E_Invalid_Key": "未找到该房间的端到端加密(E2E)密钥", + "E2E_Key_Error": "该消息为端到端加密,因加密密钥错误而无法解密", "E2E_Reset_Email_Content": "您将被自动登出。当您再次登录时,Rocket.Chat 将生成新的密钥并恢复您对任何有至少一个成员在线加密聊天室的访问权。由于端到端加密的原理,Rocket.Chat 将不能恢复您对无成员在线的聊天室访问权。", "E2E_Reset_Other_Key_Warning": "重置当前的端到端加密密钥将使用户登出。当用户再次登录时,Rocket.Chat 将生成新的密钥并恢复用户对任何有至少一个成员在线加密聊天室的访问权。由于端到端加密的原理,Rocket.Chat 将不能恢复用户对无成员在线的聊天室访问权。", + "E2E_disable_encryption": "禁用加密", + "E2E_disable_encryption_description": "禁用端到端加密(E2EE)将损害此 {{roomType}} 的隐私。所有 {{roomType}} 成员将无法访问任何加密内容。

加密可稍后重新启用。", + "E2E_disable_encryption_reset_keys_description": "如果没有人能够访问加密内容,你可以改为重置加密密钥。", "E2E_enable": "启用端到端加密", + "E2E_enable_encryption": "启用加密", + "E2E_enable_encryption_description": "使用端到端加密(E2EE)保持对话私密,确保只有预期接收者可访问此 {{roomType}} 中的消息和文件。", + "E2E_indecipherable": "该消息为端到端加密,因多次房间密钥重置而无法解密", "E2E_key_reset_email": "端到端加密重置通知", + "E2E_message_encrypted_placeholder": "该消息已端到端加密。要查看,请在账户设置中输入加密密钥。", "E2E_password_request_text": "要访问加密的私人组和私聊,请输入密码。
您需要在使用的每个客户端上输入此密码来对消息进行编码/解码,因为密钥未存储在服务器上。", "E2E_password_reveal_text": "创建具有端到端加密的安全私人聊天室和私聊消息。此密码不会存储在服务器上。您可以在所有设备上使用它。", + "E2E_password_save_text": "此内容仅显示一次,请立即保存。", + "E2E_reset_encryption_keys": "重置加密密钥", + "E2E_reset_encryption_keys_button": "重置 {{roomType}} 加密密钥", + "E2E_reset_encryption_keys_description": "或者,重置加密密钥将保持加密启用,但可能无法访问之前加密的内容。", + "E2E_reset_encryption_keys_error": "加密密钥重置失败", + "E2E_reset_encryption_keys_modal_description": "仅在没有 {{roomType}} 成员拥有有效密钥以恢复访问之前加密内容时,才建议重置 E2EE 密钥。所有成员可能会失去访问此前加密内容的权限。

<3>了解更多 关于重置加密密钥的信息。

请谨慎操作。", + "E2E_reset_encryption_keys_success": "加密密钥已重置", + "E2E_unavailable_for_federation": "联邦房间不支持 E2E", + "ECDH_Enabled": "为数据传输启用第二层加密(ECDH)", "Edit": "编辑", "Edit_Business_Hour": "编辑营业时间", + "Edit_Canned_Response": "编辑自动回复", "Edit_Canned_Responses": "编辑自动回复", "Edit_Contact_Profile": "编辑联系人资料", "Edit_Custom_Field": "编辑自定义字段", "Edit_Department": "编辑部门", + "Edit_Federated_User_Not_Allowed": "无法编辑联邦用户", "Edit_Invite": "编辑邀请", "Edit_Priority": "编辑优先级", + "Edit_SLA_Policy": "编辑服务级别协议(SLA)策略", "Edit_Status": "编辑状态", "Edit_Tag": "编辑标签", "Edit_Trigger": "编辑触发器", "Edit_Unit": "编辑单位", "Edit_User": "编辑用户", + "Edit_channel": "编辑频道", + "Edit_discussion": "编辑讨论", "Edit_previous_message": "`%s` - 编辑上一条消息", + "Edit_team": "编辑团队", + "Editing_message": "正在编辑消息", + "Editing_message_hint": "按 esc 取消 · 按 enter 保存", "Editing_room": "正在编辑聊天室", "Editing_user": "正在编辑用户", + "Editor": "编辑器", "Education": "教育", "Email": "电子邮箱", "Email_Change_Disabled": "您的 Rocket.Chat 管理员已禁止修改电子邮箱地址", "Email_Changed_Description": "您可以使用以下占位符: \n - `[email]`为电子邮件地址, \n - `[Site_Name]` 和 `[Site_URL]` 分别为应用程序名和网址。", "Email_Changed_Email_Subject": "[Site_Name] - 电子邮箱已更改", + "Email_Description": "在 Rocket.Chat 内发送广播邮件的配置。", "Email_Footer_Description": "您可以使用以下占位符: \n - [Site_Name] 和 `[Site_URL]` 分别作为应用程序的名称和网址。 ", "Email_Header_Description": "您可以使用以下占位符: \n - [Site_Name] 和 `[Site_URL]` 分别作为应用程序的名称和网址。 ", "Email_Inbox": "电子邮件收件箱", + "Email_Inbox_has_been_added": "邮件收件箱已添加", + "Email_Inbox_has_been_removed": "邮件收件箱已移除", "Email_Inboxes": "电子邮件收件箱", "Email_Notification_Mode": "离线电子邮件通知", "Email_Notification_Mode_All": "所有提及/直接提及", @@ -1260,34 +1953,81 @@ "Email_body": "邮件正文", "Email_changed_section": "电子邮箱已更改", "Email_from": "从", + "Email_is_required": "需要邮箱", "Email_notification_show_message": "在电子邮件通知中显示消息", "Email_or_username": "电子邮件或用户名", "Email_sent": "已发送的电子邮件", "Email_subject": "邮件主题", + "Email_two-factor_authentication": "邮箱双因素认证", + "Email_verification_isnt_required": "登录不需要邮箱验证。要启用,请在 账户 > 注册 中开启设置", "Email_verified": "邮箱已验证", "Emoji": "表情符号", "EmojiCustomFilesystem": "自定义表情文件系统", + "EmojiCustomFilesystem_Description": "指定表情的存储方式。", + "Emoji_picker": "表情选择器", + "Empty_no_agent_selected": "为空,未选择客服", + "Empty_no_unit_selected": "为空,未选择单位", "Empty_title": "空标题", "Enable": "启用", "Enable_Auto_Away": "启用自动离开", + "Enable_CSP": "启用内容安全策略(CSP)", + "Enable_CSP_Description": "除非你有自定义构建且因内联脚本遇到问题,否则不要禁用此选项", "Enable_Desktop_Notifications": "启用桌面通知", + "Enable_E2E_encryption": "启用端到端加密(E2E)", + "Enable_Password_History": "启用密码历史", + "Enable_Password_History_Description": "启用后,用户无法将密码更新为最近使用过的一些密码。", "Enable_Svg_Favicon": "启用 SVG 图标", + "Enable_business_hours": "启用营业时间", + "Enable_encryption": "启用加密", "Enable_inquiry_fetch_by_stream": "启用使用流从服务器获取询价数据", + "Enable_of_limit_apps_currently_enabled": "**当前已启用 {{enabled}} / {{limit}} 个 {{context}} 应用。** \n \n社区版工作区最多可启用 {{limit}} 个 {{context}} 应用。 \n \n**{{appName}} 将默认禁用。** 请禁用另一个 {{context}} 应用或升级到高级方案以启用此应用。", + "Enable_of_limit_apps_currently_enabled_exceeded": "**当前已启用 {{enabled}} / {{limit}} 个 {{context}} 应用。** \n \n社区版应用上限已超出。 \n \n社区版工作区最多可启用 {{limit}} 个 {{context}} 应用。 \n \n**{{appName}} 将默认禁用。** 你需要禁用至少 {{exceed}} 个其他 {{context}} 应用或升级到高级方案以启用此应用。", "Enable_omnichannel_auto_close_abandoned_rooms": "启用自动关闭被访客废弃的聊天室", + "Enable_to_bypass_email_verification": "启用以跳过邮箱验证", "Enable_two-factor_authentication": "启用两步验证", + "Enable_two-factor_authentication_callout_description": "此工作区现要求你的账户启用双因素认证(2FA)。在继续其他操作前必须完成 2FA 的设置并启用。", + "Enable_unlimited_apps": "启用无限应用", + "Enable_voice_calling": "启用语音通话", "Enabled": "已启用", + "Enabled_E2E_Encryption_for_this_room": "已为此房间启用端到端加密(E2E)", "Encrypted": "加密的", + "Encrypted_RoomType": "已加密的 {{roomType}}", "Encrypted_channel_Description": "端到端加密频道。搜索不适用于加密频道,并且频道消息通知可能不会显示消息内容。", + "Encrypted_content_cannot_be_searched": "加密内容无法搜索。", + "Encrypted_content_cannot_be_searched_and_audited": "加密内容无法搜索或审计", + "Encrypted_content_cannot_be_searched_and_audited_subtitle": "已选择一个或多个加密房间进行审计。", + "Encrypted_content_will_not_appear_search": "房间已加密,加密内容不会出现在搜索中", "Encrypted_field_hint": "端到端加密频道。搜索不适用于加密频道,并且频道消息通知可能不会显示消息内容。", + "Encrypted_file_not_allowed": "不允许加密文件", "Encrypted_message": "加密消息", + "Encrypted_message_preview_unavailable": "消息已加密,无法预览", + "Encrypted_messages": "端到端加密的 {{roomType}}。搜索无法用于加密的 {{roomType}},通知可能不会显示消息内容。", + "Encrypted_messages_false": "消息未加密", + "Encrypted_not_available": "公共 {{roomType}} 不可用", "Encrypted_setting_changed_successfully": "加密设置修改成功", + "Enter_current_E2EE_password_to_set_new": "要设置新密码,请先 <1>输入当前端到端加密(E2EE)密码。", "Encryption_key_saved_successfully": "您的加密密钥保存成功", "End": "结束", + "Ended": "已结束", + "End_Time": "结束时间", + "End_Date": "结束日期", + "End-to-end_encryption": "端到端加密", + "End-to-end_encryption_Description": "确保对话保持私密", + "E2E_encryption_enabled": "端到端加密(E2E)已启用", + "End_To_End_Encryption_Not_Enabled": "未启用端到端加密", + "End_conversation": "结束对话", + "End_suspicious_sessions": "结束所有可疑会话", + "Engagement": "参与度", "Engagement_Dashboard": "合约仪表盘", + "Enrich_your_workspace": "使用参与度仪表板丰富你的工作区视角。分析关于用户、消息和频道的实际使用统计。高级方案包含此功能。", + "Ensure_secure_workspace_access": "确保安全的工作区访问", + "Enter": "输入", "Enter_Alternative": "备选模式(Enter + Ctrl/Alt/Shift/CMD 发送消息)", "Enter_Behaviour": "回车键", "Enter_Behaviour_Description": "此项用来设置回车键发送消息还是换行", + "Enter_E2E_password": "输入端到端加密(E2EE)密码", "Enter_Normal": "常规模式(Enter 发送消息)", + "Enter_TOTP_password": "输入 TOTP 密码", "Enter_a_custom_message": "输入自定义消息", "Enter_a_department_name": "输入部门名称", "Enter_a_name": "输入名称", @@ -1296,12 +2036,27 @@ "Enter_a_tag": "输入标签", "Enter_a_username": "输入用户名", "Enter_authentication_code": "输入验证码", + "Enter_code_here": "在此输入验证码", + "Enter_code_provided_by_authentication_app": "输入认证应用提供的验证码", "Enter_name_here": "在此输入名称", + "Enter_the_code_provided_by_your_authentication_app_to_continue": "输入认证应用提供的验证码以继续。你也可以使用一个备用代码。", + "Enter_the_code_we_just_emailed_you": "输入我们刚发给你的验证码。", "Enter_to": "进入", + "Enter_username_or_number": "输入用户名或号码", "Enter_your_E2E_password": "输入您的端到端密码", + "Enter_your_E2E_password_to_access": "输入端到端加密密码以访问", + "Enter_your_password_to_delete_your_account": "输入密码以删除你的账号。此操作无法撤销。", + "Enter_your_username_to_delete_your_account": "输入用户名以删除你的账号。此操作无法撤销。", "Enterprise": "企业", + "Enterprise_Departments_description_free_trial": "社区版工作区只能创建一个部门。立即开始企业版免费试用以创建多个部门!", + "Enterprise_Departments_description_upgrade": "社区版工作区只能创建一个部门。升级到企业版以移除限制并增强工作区能力。", + "Enterprise_Description": "手动更新高级许可证。", "Enterprise_License": "企业许可证", - "Enterprise_License_Description": "如果您的工作区已经注册,且您的许可证由Rock.Chat Cloud提供,那么您无需在这里手动更新许可证。", + "Enterprise_License_Description": "如果您的工作区已经注册,且许可证由 Rocket.Chat Cloud 提供,则无需在此手动更新许可证。", + "Enterprise_Only": "仅限企业版", + "Enterprise_cap_description": "企业版工作区的在线状态服务没有上限。", + "Enterprise_capabilities": "企业功能", + "Enterprise_capability": "企业功能", "Entertainment": "娱乐", "Error": "错误", "Error_404": "错误:404", @@ -1315,12 +2070,18 @@ "Error_login_blocked_for_user": "此用户的登录被临时禁用了", "Error_sending_livechat_offline_message": "发送 omnichannel 离线消息时出错", "Error_sending_livechat_transcript": "发送 omnichannel 聊天记录时出错", + "Error_something_went_wrong": "糟糕!出现问题。请刷新页面或联系管理员。", + "Error_loading__name__information": "加载 {{name}} 信息时出错", "Errors_and_Warnings": "错误和警告", "Esc_to": "退出", "Estimated_due_time": "预计到期时间", "Estimated_due_time_in_minutes": "预计到期时间(分钟)", + "Estimated_wait_time": "预计等待时间", + "Estimated_wait_time_in_minutes": "预计等待时间(分钟)", "Event_Trigger": "事件触发", "Event_Trigger_Description": "选择将触发此出站 WebHook 集成的事件类型", + "Event_notifications": "事件通知", + "Event_notifications_description": "禁用此设置将阻止应用通知你即将到来的事件。", "Everyone_can_access_this_channel": "每个人都可以访问此频道", "Exact": "精确", "Example_payload": "示例载荷", @@ -1330,28 +2091,57 @@ "Exclude_pinned": "排除固定消息", "Execute_Synchronization_Now": "立即执行同步", "Exit_Full_Screen": "退出全屏", + "Expand": "展开", + "Expand_group": "展开 {{group}}", + "Expand_all": "全部展开", + "Expand_view": "展开视图", "Experimental_Feature_Alert": "这是一个实验性特性!请注意这个特性随时都会改变、损坏甚至在未来版本中不加任何通知地被移除。", "Expiration": "到期", "Expiration_(Days)": "到期(天)", + "Expired": "已过期", + "Explore": "探索", + "Explore_marketplace": "探索应用市场", + "Explore_the_marketplace_to_find_awesome_apps": "探索应用市场,为 Rocket.Chat 找到精彩应用", + "Export": "导出", "Export_Messages": "导出消息", "Export_My_Data": "导出我的数据(JSON)", + "export-messages-as-pdf": "将消息导出为 PDF", + "Export_most_recent_logs": "导出最新日志", + "Export_as_PDF": "导出为 PDF", "Export_as_file": "作为文件导出", + "Export_conversation_transcript_as_PDF": "将对话记录导出为 PDF", + "Export_enabled_at_the_end_of_the_conversation": "对话结束时可导出", "Extended": "已扩展", + "Extension": "分机", + "Extension_removed": "分机已移除", + "Extensions": "分机", + "External": "外部", "External_Domains": "外部域名", "External_Queue_Service_URL": "外部队列服务 URL", "External_Service": "外部服务", "External_Users": "外部用户", + "External_service_action_hint": "使用外部服务发送自定义消息。更多信息请查看文档。", + "External_service_returned_valid_response": "外部服务返回了有效响应", + "External_service_test_hint": "在保存触发器前点击“发送测试”。", + "External_service_url": "外部服务 URL", + "Extra_CSP_Domains": "额外 CSP 域名", + "Extra_CSP_Domains_Description": "要添加到内容安全策略(CSP)的额外域名", + "Extremely_likely": "极有可能", "FEDERATION_Discovery_Method": "发现方法", "FEDERATION_Discovery_Method_Description": "你可以用 hub 或者 SRV 和 TXT 条目的 DNS 记录。", "FEDERATION_Domain": "域名", "FEDERATION_Domain_Alert": "开启此功能后不要更改这里, 当前还不能处理域变更。", "FEDERATION_Domain_Description": "添加此服务器应该关联的域, 如: @rocket.chat。", + "Old_Federation_Alert": "此联邦版本已不再受支持。请配置上方名为“原生联邦”的新方案。
此处可查看关于 Matrix 联邦支持的更多信息", + "Rocket.Chat Federation": "Rocket.Chat 联邦(不支持)", + "Matrix Bridge": "Matrix 桥接(不支持)", "FEDERATION_Public_Key": "公钥", "FEDERATION_Public_Key_Description": "需要分享此凭据给你的对等端", "FEDERATION_Status": "状态", "FEDERATION_Test_Setup": "测试安装", "FEDERATION_Test_Setup_Error": "无法通过您的配置找到服务器,请检查您的设定。", "FEDERATION_Test_Setup_Success": "您的联邦配置生效并且可被其他服务器找到!", + "Facebook": "Facebook(脸书)", "Facebook_Page": "Facebook页面", "Failed": "失败", "Failed_To_Download_Files": "下载文件失败", @@ -1359,27 +2149,115 @@ "Failed_To_Load_Import_History": "加载导入历史失败", "Failed_To_Load_Import_Operation": "加载导入操作操作失败", "Failed_To_Start_Import": "启动导入操作失败", + "Failed_To_upload_Import_File": "上传导入文件失败", "Failed_to_activate_invite_token": "无法激活邀请令牌", "Failed_to_add_monitor": "添加监控失败", + "Failed_to_copy_phone_number": "复制电话号码失败", "Failed_to_generate_invite_link": "生成邀请链接失败", + "Failed_to_transfer_call": "转接通话失败", "Failed_to_validate_invite_token": "验证邀请令牌失败", + "Failure": "失败", + "Fallback_forward_department": "转接的备用部门", + "Fallback_forward_department_description": "允许定义一个备用部门,当当前没有在线客服时,接收转接至此的聊天", + "Fallback_message": "备用消息", "False": "否", "Favorite": "喜爱", "Favorite_Rooms": "启用房間收藏", "Favorites": "收藏", "Feature_Depends_on_Livechat_Visitor_navigation_as_a_message_to_be_enabled": "此功能依赖于 “发送访客导航历史记录为消息” 启用。", + "Feature_Limiting": "功能限制", + "Feature_depends_on_selected_call_provider_to_be_enabled_from_administration_settings": "此功能依赖于在管理设置中启用上面选择的通话服务提供商(管理 -> 视频会议)。", + "Feature_preview": "功能预览", + "Feature_preview_admin_page_callout": "在此启用的功能将对每个用户的功能预览偏好生效。", + "Feature_preview_admin_page_description": "选择向工作区成员开放的功能预览。", + "Feature_preview_page_callout": "功能预览正在测试中,可能不稳定或不完整。功能正式发布后可能会成为高级功能。", + "Feature_preview_page_description": "启用当前正在开发的最新功能。", + "Featured": "精选", "Features": "特性", + "Federated": "联邦", + "Federation": "联邦", + "Federation_Description": "联邦允许远程工作区通过 Matrix 协议相互通信。", + "Federation_Enable": "启用联邦", + "Federation_Example_matrix_server": "示例:matrix.org", + "Federation_Federated_room_search": "联邦房间搜索", + "Federation_Matrix": "联邦 V2", + "Federation_Matrix_Federated": "联邦", + "Federation_Matrix_Federated_Description": "创建联邦房间后将无法启用加密或广播", + "Federation_Matrix_Federated_Description_disabled": "此工作区当前未启用联邦", + "Federation_Matrix_as_token": "AppService 令牌", + "Federation_Matrix_bridge_localpart": "AppService 用户 Localpart", + "Federation_Matrix_bridge_url": "桥接 URL", + "Federation_Matrix_check_configuration": "验证配置", + "Federation_Matrix_configuration_status": "配置状态", + "Federation_Matrix_enable_ephemeral_events": "启用 Matrix 临时事件", + "Federation_Matrix_enable_ephemeral_events_Alert": "此操作需要重启。
启用用户输入指示等临时事件可能会影响 Matrix Homeserver 和 Rocket.Chat 服务器在联邦通信中的性能", "Federation_Matrix_enabled": "已启用", + "Federation_Matrix_error_applying_room_roles": "在联邦网络中应用房间角色时发生错误", + "Federation_Matrix_giving_same_permission_warning": "你正在授予此用户与你相同的权限,此更改无法撤销。是否继续?", + "Federation_Matrix_homeserver_domain": "Homeserver 域名", + "Federation_Matrix_homeserver_domain_alert": "不应使用第三方客户端连接到 Homeserver,只能使用 Rocket.Chat", + "Federation_Matrix_homeserver_url": "Homeserver URL(服务器地址)", + "Federation_Matrix_homeserver_url_alert": "建议使用新的、空的 Homeserver 来配合联邦使用", + "Federation_Matrix_hs_token": "Homeserver 令牌", + "Federation_Matrix_id": "AppService ID(标识)", + "Federation_Matrix_Federated_Description_invalid_version": "该房间由旧版联邦创建,已被无限期阻止。<1>点击此处 了解更多 Matrix 联邦支持信息", + "Federation_Matrix_join_public_rooms_is_enterprise": "加入联邦房间是企业版功能", + "Federation_Matrix_join_public_rooms_is_premium": "加入联邦房间是高级功能", + "Federation_Matrix_losing_privileges": "失去权限", + "Federation_Matrix_losing_privileges_warning": "你正在降级自己,此操作无法撤销。如果你是最后一个有权限的用户,将无法恢复权限。仍要继续吗?", + "Federation_Matrix_max_size_of_public_rooms_users": "加入远程服务器公共房间时的最大成员数", + "Federation_Matrix_max_size_of_public_rooms_users_Alert": "请注意,允许用户加入的房间越大,加入所需时间越长,且消耗的资源越多。了解更多", + "Federation_Matrix_max_size_of_public_rooms_users_desc": "远程服务器公共房间可加入的用户上限。超过该设置的房间仍会被列出,但用户无法加入", + "Federation_Matrix_not_allowed_to_change_moderator": "无权更改版主", + "Federation_Matrix_not_allowed_to_change_owner": "无权更改所有者", + "Federation_Matrix_registration_file": "注册文件", + "Federation_Matrix_registration_file_Alert": "重要:启用临时事件将使服务器接收你所连接的所有服务器中所有用户的输入状态。
要启用此功能,请更新注册文件(你用于将 Rocket.Chat 注册到 homeserver 的 .yaml 文件),添加以下内容:
de.sorunome.msc2409.push_ephemeral: true", + "Federation_Matrix_serve_well_known": "提供 Well Known", + "Federation_Matrix_serve_well_known_Alert": "如果使用 DNS srv 记录进行联邦,请保持关闭;或在联邦流量较大时使用反向代理返回静态 JSON。了解更多。", + "Federation_Matrix_serve_well_known_Description": "由 Rocket.Chat 直接提供 /.well-known/matrix/server 和 /.well-known/matrix/client,而非通过反向代理提供联邦服务", "Federation_Public_key": "公钥", + "Federation_Search_federated_rooms": "搜索联邦房间", + "Federation_is_currently_disabled_on_this_workspace": "此工作区当前未启用联邦", + "Federation_slash_commands": "联邦命令", + "Federation_Service_Enabled": "启用原生联邦", + "Federation_Service_Enabled_Description": "使用 Matrix 协议启用服务器间通信的原生联邦。", + "Federation_Service_EDU_Process_Typing": "处理输入事件", + "Federation_Service_EDU_Process_Typing_Description": "在联邦服务器之间发送和接收用户输入消息事件。", + "Federation_Service_EDU_Process_Typing_Alert": "启用输入事件可能会显著增加服务器负载和网络流量,尤其是在用户较多时。仅在你了解其影响并具备处理额外负载的资源时启用此选项。", + "Federation_Service_EDU_Process_Presence": "处理在线状态事件", + "Federation_Service_EDU_Process_Presence_Description": "在联邦服务器之间发送和接收用户在线状态(在线、离线等)事件。", + "Federation_Service_EDU_Process_Presence_Alert": "启用在线状态事件可能会显著增加服务器负载和网络流量,尤其是在用户较多时。仅在你了解其影响并具备处理额外负载的资源时启用此选项。", + "Federation_Service_Alert": "测试功能:适用于非关键部署
此功能正在进行最终的性能与韧性审计,尚不推荐用于关键生产数据。 功能仍可能出现间歇性问题。用户必须由工作区管理员明确授予 'access-federation' 权限才能与联邦房间交互。", + "Federation_Service_Domain": "联邦域名", + "Federation_Service_Domain_Description": "此服务器应响应的域名,例如:`acme.com`。该域名将作为用户 ID 的后缀(如 `@user:acme.com`)。
如果你的聊天服务器可通过与联邦域名不同的域名访问,请按文档在 Web 服务器上配置 `.well-known` 文件。", + "Federation_Service_Domain_Alert": "仅填写域名,不要包含 http(s)://、斜杠或其后的路径。
请使用 `acme.com`,而不是 `https://acme.com/chat`。", + "Federation_Service_Matrix_Signing_Algorithm": "签名密钥算法", + "Federation_Service_Matrix_Signing_Version": "签名密钥版本", + "Federation_Service_Matrix_Signing_Key": "签名密钥", + "Federation_Service_Matrix_Signing_Key_Description": "用于认证联邦请求的私有 base64 签名密钥。通常为 Ed25519 算法密钥(版本 4),并以 base64 编码。它对于联邦服务器之间通过 Matrix 协议的安全通信至关重要,应妥善保密。", + "Federation_Service_max_allowed_size_of_public_rooms_to_join": "加入远程服务器公共房间时的最大成员数", + "Federation_Service_max_allowed_size_of_public_rooms_to_join_Alert": "请注意,允许用户加入的房间越大,加入所需时间越长,且消耗的资源越多。了解更多", + "Federation_Service_max_allowed_size_of_public_rooms_to_join_Description": "远程服务器公共房间可加入的用户上限。超过该设置的房间仍会被列出,但用户无法加入", + "Federation_Service_Join_Encrypted_Rooms": "允许加入加密的联邦房间", + "Federation_Service_Join_Non_Private_Rooms": "允许加入非私有房间", + "Federation_Service_Allow_List": "域名允许列表", + "Federation_Service_Allow_List_Description": "将联邦限制为给定的域名允许列表。", "Field": "字段", "Field_removed": "已移除字段", "Field_required": "字段必须填写", "File": "文件", - "FileSize_Bytes": "{{fileSize}} Bytes", - "FileSize_KB": "{{fileSize}} KB", - "FileSize_MB": "{{fileSize}} MB", + "Font_Default": "默认", + "Font_Extra_large": "超大", + "Font_Large": "大", + "Font_Medium": "中", + "Font_Small": "小", + "FileSize_Bytes": "{{fileSize}} 字节", + "FileSize_KB": "{{fileSize}} KB(千字节)", + "FileSize_MB": "{{fileSize}} MB(兆字节)", "FileType": "文件类型", "FileUpload": "文件上传", + "FileUpload_Cannot_preview_file": "无法预览文件", + "FileUpload_Description": "配置文件上传与存储。", "FileUpload_Disabled": "已禁用文件上传。", "FileUpload_Enable_json_web_token_for_files": "为文件上传启用 Json 网络令牌(JWT)保护", "FileUpload_Enable_json_web_token_for_files_description": "将 JWT 附加到已上传文件网址", @@ -1392,34 +2270,49 @@ "FileUpload_GoogleStorage_AccessId_Description": "访问 ID 通常采用电子邮件格式,例如:“`example-test@example.iam.gserviceaccount.com`”", "FileUpload_GoogleStorage_Bucket": "Google Storage Bucket 名称", "FileUpload_GoogleStorage_Bucket_Description": "选择存储上传文件的 Bucket 名称", + "FileUpload_GoogleStorage_ProjectId": "项目 ID", + "FileUpload_GoogleStorage_ProjectId_Description": "来自 Google 开发者控制台的项目 ID", "FileUpload_GoogleStorage_Proxy_Avatars": "代理头像", "FileUpload_GoogleStorage_Proxy_Avatars_Description": "通过您的服务器代理头像文件传输,而不是直接访问资产的URL", "FileUpload_GoogleStorage_Proxy_Uploads": "代理上传", "FileUpload_GoogleStorage_Proxy_Uploads_Description": "通过您的服务器代理上传文件传输,而不是直接访问资产的URL", + "FileUpload_GoogleStorage_Proxy_UserDataFiles": "代理用户数据文件", + "FileUpload_GoogleStorage_Proxy_UserDataFiles_Description": "通过你的服务器代理用户数据文件传输,而不是直接访问资产 URL", "FileUpload_GoogleStorage_Secret": "Google Storage 密钥", "FileUpload_GoogleStorage_Secret_Description": "请使用 [这些指令](https://github.com/CulturalMe/meteor-slingshot#google-cloud) 并将结果粘贴在此处。", "FileUpload_MaxFileSize": "文件上传大小限制(以字节为单位)", "FileUpload_MaxFileSizeDescription": "将其设置为-1以删除文件大小限制。", "FileUpload_MediaTypeBlackList": "阻止的媒体类型", "FileUpload_MediaTypeBlackListDescription": "媒体类型的逗号分隔列表。此设定优先于接受的媒体类型。", + "FileUpload_MediaTypeBlackList_Alert": "未知文件扩展名的默认媒体类型为 \"application/octet-stream\"。若仅允许已知扩展名,可将其加入“已屏蔽媒体类型”列表。", "FileUpload_MediaTypeWhiteList": "支持的媒体类型", "FileUpload_MediaTypeWhiteListDescription": "以逗号分隔的媒体类型列表。留空表示允许所有媒体类型。", "FileUpload_MediaType_NotAccepted": "不支持该类型媒体文件", + "FileUpload_MediaType_NotAccepted__type__": "媒体类型不被接受:{{type}}", "FileUpload_ProtectFiles": "保护已上传的文件", "FileUpload_ProtectFilesDescription": "只有已登录的用户可以访问", + "FileUpload_ProtectFilesEnabled_JWTNotSet": "上传的文件已受保护,但未配置 JWT 访问。Twilio 发送媒体消息需要此配置。请在 设置 -> FileUpload 中设置", + "FileUpload_Restrict_to_room_members": "限制仅房间成员可访问文件", + "FileUpload_Restrict_to_room_members_Description": "限制房间内上传的文件仅房间成员可访问", + "FileUpload_Restrict_to_users_who_can_access_room": "限制仅可访问房间的用户查看文件", + "FileUpload_Restrict_to_users_who_can_access_room_Description": "限制房间内上传的文件仅对可访问该房间的用户可见。该选项与“限制仅房间成员可访问文件”互斥,因为此选项允许非房间成员但具备特殊权限的用户访问上传文件,例如 Omnichannel 管理员与监控。", "FileUpload_RotateImages": "在上传时旋转图像", "FileUpload_RotateImages_Description": "启用此设置可能会导致图像质量损失", "FileUpload_S3_AWSAccessKeyId": "Amazon S3 AWSAccessKeyId", + "FileUpload_S3_AWSAccessKeyId_desc": "如果运行在附带实例配置文件且对所配置存储桶拥有正确 S3 权限的 EC2 实例上,请留空。", "FileUpload_S3_AWSSecretAccessKey": "Amazon S3 AWSSecretAccessKey", + "FileUpload_S3_AWSSecretAccessKey_desc": "如果运行在附带实例配置文件且对所配置存储桶拥有正确 S3 权限的 EC2 实例上,请留空。", "FileUpload_S3_Acl": "Amazon S3 acl", "FileUpload_S3_Bucket": "Amazon S3 bucket name", - "FileUpload_S3_BucketURL": "Bucket URL", + "FileUpload_S3_BucketURL": "存储桶 URL", "FileUpload_S3_CDN": "下载用 CDN 域名", "FileUpload_S3_ForcePathStyle": "强制路径样式", "FileUpload_S3_Proxy_Avatars": "代理头像", "FileUpload_S3_Proxy_Avatars_Description": "通过您的服务器代理头像文件传输,而不是直接访问资产的URL", "FileUpload_S3_Proxy_Uploads": "代理上传", "FileUpload_S3_Proxy_Uploads_Description": "通过您的服务器代理上传文件传输,而不是直接访问资产的URL", + "FileUpload_S3_Proxy_UserDataFiles": "代理用户数据文件", + "FileUpload_S3_Proxy_UserDataFiles_Description": "通过你的服务器代理用户数据文件传输,而不是直接访问资产 URL", "FileUpload_S3_Region": "地区", "FileUpload_S3_SignatureVersion": "签名版本", "FileUpload_S3_URLExpiryTimeSpan": "网址过期期限", @@ -1430,6 +2323,8 @@ "FileUpload_Webdav_Proxy_Avatars_Description": "通过您的服务器代理头像文件传输,而不是直接访问资产的 URL", "FileUpload_Webdav_Proxy_Uploads": "代理上传", "FileUpload_Webdav_Proxy_Uploads_Description": "通过您的服务器代理上传文件传输,而不是直接访问资产的URL", + "FileUpload_Webdav_Proxy_UserDataFiles": "代理用户数据文件", + "FileUpload_Webdav_Proxy_UserDataFiles_Description": "通过你的服务器代理用户数据文件传输,而不是直接访问资产 URL", "FileUpload_Webdav_Server_URL": "WebDAV 服务器访问 URL", "FileUpload_Webdav_Upload_Folder_Path": "上传文件夹路径", "FileUpload_Webdav_Upload_Folder_Path_Description": "WebDAV 文件上传路径", @@ -1440,6 +2335,7 @@ "File_Path": "文件路径", "File_Type": "文件类型", "File_URL": "文件 URL", + "File_Upload_Disabled": "文件上传已禁用", "File_exceeds_allowed_size_of_bytes": "文件大小超过允许的 {{size}} 字节", "File_name_Placeholder": "搜索文件...", "File_not_allowed_direct_messages": "私聊中不允许文件共享。", @@ -1449,16 +2345,37 @@ "File_uploaded": "文件已上传", "File_uploaded_successfully": "文件上传成功", "Files": "文件", + "Files_list": "文件列表", "Files_only": "只删除附加的文件,保留消息", "Filter": "过滤器", + "Filter_By_Price": "按价格筛选", + "Filter_By_Status": "按状态筛选", + "Filter_by_Custom_Fields": "按自定义字段筛选", + "Filter_by_category": "按类别筛选", + "Filter_by_role": "按角色筛选", + "Filter_by_room": "按房间类型筛选", + "Filter_by_visibility": "按可见性筛选", + "Filters": "筛选器", + "Filters_and_secondary_sidebar": "筛选器与辅助侧边栏", + "Filters_and_secondary_sidebar_description": "通过侧边栏筛选器与辅助导航层减少干扰、提升专注度。可筛选提及、收藏房间、讨论,或按特定房间筛选以查看其关联频道与讨论。", "Filters_applied": "已应用过滤器", "Financial_Services": "金融服务", + "Finish": "完成", + "Finish_Registration": "完成注册", + "Finish_purchase": "完成购买", + "Finish_recording": "结束录制", + "Finish_your_purchase_trial": "完成购买以避免 <1>降级后果。", "First_Channel_After_Login": "登录后的第一个频道", + "First_message_hint": "讨论可以从“如何上传图片?”这类问题开始", "First_response_time": "首次响应时间", "Flags": "旗帜", "Follow_message": "关注消息", "Follow_social_profiles": "关注我们的社交资讯,fork我们在github的项目或者在trello上分享你关于rocket.chat的想法。", + "Follower": { + "other": "Followers" + }, "Following": "关注", + "Font_size": "字号", "Fonts": "字体", "Food_and_Drink": "饮食", "Footer": "页脚", @@ -1481,37 +2398,60 @@ "Forgot_Password_Email_Subject": "[Site_Name] - 密码恢复", "Forgot_password": "忘记密码", "Forgot_password_section": "忘记密码", + "Forgot_E2EE_Password": "忘记端到端加密(E2EE)密码?", + "Format": "格式", "Forward": "转发", "Forward_chat": "转发对话", + "Forward_in_history": "历史前进", + "Forward_message": "转发消息", "Forward_to_department": "转到部门", "Forward_to_user": "转到用户", "Forwarding": "转发", "Free": "免费", + "Free_Apps": "免费应用", + "Free_Edition": "免费版", "Frequently_Used": "常用", "Friday": "星期五", "From": "从", "From_Email": "从电子邮件", "From_email_warning": "警告From 字段来自于邮件服务器的设置。", "Full_Name": "全称", + "Full_log": "完整日志", "Full_Screen": "全屏", + "Fully_integrated_voip_receive_internal_external_calls_without_switching_between_apps_external_systems": "完全集成的 Rocket.Chat VoIP 让你的团队无需在应用或外部系统间切换即可拨打和接听内外部通话。", "Gaming": "游戏", "General": "通用", + "General_Description": "配置工作区常规设置。", + "General_Settings": "常规设置", "Generate_New_Link": "产生新链接", "Generate_new_key": "生成新钥", "Generating_key": "生成钥", + "Get_all_apps": "获取团队所需的全部应用", "Give_the_application_a_name_This_will_be_seen_by_your_users": "给该应用设置一个名称。该名称将被您的用户看到。", "Global": "全局", "Global Policy": "全局策略", "Global_Search": "全球搜索", "Global_purge_override_warning": "制定了全局保留政策。如果禁用“覆盖全局保留策略”,则只能应用比全局策略更严格的策略。", + "Glossary_of_simplified_terms": "简化术语表", + "Go_to_accessibility_and_appearance": "前往无障碍与外观设置", + "Go_to_href": "前往:{{href}}", + "Go_to_settings": "前往设置", + "Go_to_workspace_settings": "前往工作区设置", "Go_to_your_workspace": "转到您的工作区", "GoogleCloudStorage": "Google 云存储", "GoogleNaturalLanguage_ServiceAccount_Description": "服务帐户密钥JSON文件。更多信息可以在[这里]找到(https://cloud.google.com/natural-language/docs/common/auth#set_up_a_service_account)", "GoogleTagManager_id": "谷歌跟踪代码管理器 ID", + "Google_Meet_Enterprise_only": "Google Meet(仅企业版)", + "Google_Meet_Premium_only": "Google Meet(仅高级版)", + "Google_Play": "Google Play(谷歌应用商店)", + "Got_it": "知道了", "Government": "政府", - "Graphql_CORS": "GraphQL CORS", + "Grandfathered_app": "应用豁免于应用数量限制策略", + "Graphql_CORS": "GraphQL CORS(跨域)", "Graphql_Enabled": "启用 GraphQL", "Graphql_Subscription_Port": "GraphQL 订阅(Subscription)端口", + "Grid_view": "网格视图", + "Group": "群组", "Group_by": "分组依据", "Group_by_Type": "按类型分组", "Group_discussions": "组讨论", @@ -1519,9 +2459,13 @@ "Group_mentions_disabled_x_members": "已禁用在 {{total}} 以上成员的聊天室中使用 `@all` 和 `@here` 组提及。", "Group_mentions_only": "仅组提及", "Grouping": "分组", + "Guest": "访客", + "HTML": "HTML(超文本标记语言)", + "Hang_up_and_transfer_call": "挂断并转接通话", "Hash": "哈希值", "Header": "标头", "Header_and_Footer": "页头和页尾", + "Healthcare": "医疗健康", "Helpers": "助手", "Here_is_your_authentication_code": "这是您的验证码:", "Hex_Color_Preview": "Hex 颜色预览", @@ -1531,21 +2475,39 @@ "Hide": "隐藏", "Hide_Group_Warning": "您确定要隐藏用户组 “{{roomName}}” 吗?", "Hide_Livechat_Warning": "您确定要隐藏与 “{{roomName}}” 的聊天吗?", + "Hide_On_Workspace": "在工作区隐藏", "Hide_Private_Warning": "您确定要隐藏与 “{{roomName}}” 的讨论吗?", "Hide_Room_Warning": "您确定要隐藏频道 “{{roomName}}” 吗?", "Hide_System_Messages": "隐藏系统信息", "Hide_Unread_Room_Status": "隐藏未读聊天室状态", + "Hide_additional_fields": "隐藏附加字段", "Hide_counter": "隐藏计数器", "Hide_flextab": "通过点击隐藏右边栏", "Hide_roles": "隐藏角色", "Hide_room": "隐藏", "Hide_usernames": "隐藏用户名", + "Hide_video": "隐藏视频", + "High": "高", + "High_scalability": "高可扩展性", + "Highest": "最高", + "Highlighted_chosen_word": "高亮所选词", "Highlights": "高亮", "Highlights_How_To": "当其他人发出的消息中包含此处的关键词时,你会收到提醒。请使用逗号分隔多个关键词。不区分大小写。", "Highlights_List": "高亮词语", "History": "历史", + "History_navigation": "历史导航", + "Hold": "挂起", + "Hold_Call": "通话挂起", + "Hold_Call_EE_only": "通话挂起(仅企业版)", + "Hold_Call_Premium_only": "通话挂起(仅高级方案)", + "Hold_EE_only": "挂起(仅企业版)", + "Home": "主页", + "Homepage": "主页", + "Homepage_Custom_Content_Default_Message": "管理员可插入 HTML 内容显示在此空白区域。", + "Hospitality_Businness": "酒店业", "Host": "服务器", "Hours": "小时", + "How_and_why_we_collect_usage_data": "我们如何以及为何收集使用数据", "How_friendly_was_the_chat_agent": "在线客服的友善度如何?", "How_knowledgeable_was_the_chat_agent": "在线客服的知识储备如何", "How_long_to_wait_after_agent_goes_offline": "客服下线后等待的时间", @@ -1554,8 +2516,12 @@ "How_responsive_was_the_chat_agent": "在线客服的反应速度如何?", "How_satisfied_were_you_with_this_chat": "您对这次聊天是否满意?", "How_to_handle_open_sessions_when_agent_goes_offline": "客服离线时如何处理正在进行的会话", + "Http_timeout": "HTTP 超时(毫秒)", + "Http_timeout_value": "5000", "IMAP_intercepter_Not_running": "IMAP 拦截器未运行", "IMAP_intercepter_already_running": "IMAP 拦截器已在运行", + "IP": "IP 地址", + "IP_Address": "IP 地址", "IRC_Channel_Join": "JOIN 命令的输出", "IRC_Channel_Leave": "PART 命令的输出", "IRC_Channel_Users": "NAMES 命令的输出", @@ -1564,6 +2530,7 @@ "IRC_Enabled": "为了集成 IRC 支持。修改这个值需要重新启动 Rocket.Chat。", "IRC_Enabled_Alert": "对 IRC 的支持正在进行途中。目前不建议在生产系统上使用。", "IRC_Federation": "IRC联盟", + "IRC_Federation_Description": "连接到其他 IRC 服务器。", "IRC_Federation_Disabled": "IRC联合会被禁用。", "IRC_Hostname": "要连接的 IRC 主机服务器。", "IRC_Login_Fail": "在连接 IRC 服务器失败时输出。", @@ -1572,6 +2539,8 @@ "IRC_Port": "要绑定的 IRC 主机服务器的端口。", "IRC_Private_Message": "PRIVMSG 命令的输出", "IRC_Quit": "在退出一个 IRC 会话时输出。", + "I_Saved_My_Password": "我已保存密码", + "Icon": "图标", "Idle_Time_Limit": "空闲时间限制", "Idle_Time_Limit_Description": "自动将状态切换为离开的等待时间,计数单位为秒", "If_this_email_is_registered": "如果此电子邮件已注册,我们将发送有关如何重置密码的说明。如果您很短时间内没有收到电子邮件,请返回并重试。", @@ -1592,19 +2561,23 @@ "Iframe_X_Frame_Options": "X-Frame-Options 选项", "Iframe_X_Frame_Options_Description": "X-Frame-Options 选项。 [您可以在这里看到所有选项。](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/X-Frame-Options#Syntax)", "Ignore": "忽略", + "Ignore_Two_Factor_Authentication": "忽略双因素认证", "Ignored": "忽略", + "Image_gallery": "图片库", "Images": "图片", "Impersonate_next_agent_from_queue": "从队列中模拟下一个客服", "Impersonate_user": "模拟用户", "Impersonate_user_description": "启用后,集成帖子将作为触发集成的用户", "Import": "导入", "Import_New_File": "导入新文件", + "Import_Operation_Failed": "导入操作失败", "Import_Type": "导入类型", "Import_requested_successfully": "导入已成功请求", "Importer_Archived": "已归档", "Importer_CSV_Information": "CSV导入程序需要特定格式,请阅读文档以了解如何构建您的zip文件:", "Importer_ExternalUrl_Description": "您还可以将URL用于可公开访问的文件:", "Importer_From_Description": "将 {{from}} 数据导入 Rocket.Chat。", + "Importer_From_Description_CSV": "将 CSV 数据导入 Rocket.Chat。上传文件必须为 ZIP 文件。", "Importer_Prepare_Restart_Import": "重新启动导入", "Importer_Prepare_Start_Import": "开始导入", "Importer_Prepare_Uncheck_Archived_Channels": "取消选中已归档的频道", @@ -1631,24 +2604,45 @@ "Importing_messages": "导入消息", "Importing_users": "导入用户", "In_progress": "进行中", + "Inactivity_Time": "不活跃时间", "Inbox_Info": "收件箱信息", "Include_Offline_Agents": "包括离线克服", + "Includes": "包含", "Inclusive": "包括的", + "Incoming": "来电", + "Incoming_Calls": "来电", "Incoming_Livechats": "等候处理的聊天", "Incoming_WebHook": "入站 WebHook", + "Incoming_call": "来电", + "Incoming_call_ellipsis": "来电...", + "Incoming_call_from": "来电来自", + "Incoming_call_from__roomName__": "来自 {{roomName}} 的来电", + "Incoming_call_transfer": "来电转接", + "Incoming_voice_call": "来电语音通话", + "Incoming_voice_call_canceled_suddenly": "来电语音通话被突然取消。", + "Incoming_voice_call_canceled_user_not_registered": "来电语音通话因意外错误被取消。", "Industry": "行业", "Info": "信息", + "Information_to_keep_top_of_mind": "需要重点记住的信息", + "Inline_code": "行内代码", + "Input": "输入", "Insert_Contact_Name": "添加联系人名称", + "Insert_Placeholder": "插入占位符", "Install": "安装", "Install_Extension": "安装扩展程序", "Install_FxOs": "在火狐浏览器中安装 Rocket.Chat", "Install_FxOs_done": "干得漂亮!您现在可以通过点击主屏幕上的图标来使用 Rocket.Chat 。祝您使用愉快!", "Install_FxOs_error": "抱歉,未能同预期般生效!出现了下面的错误:", "Install_FxOs_follow_instructions": "请在设备上确认安装应用(根据提示点击“安装”)", + "Install_Zapier_from_marketplace": "从应用市场安装 Zapier 应用以避免中断", + "Install_Zapier_from_marketplace_new_workspaces": "从应用市场安装 Zapier 应用以配置新的集成", + "Install_anyway": "仍要安装", "Install_package": "安装软件包", + "Install_rocket_chat_on_your_preferred_desktop_platform": "在你偏好的桌面平台安装 Rocket.Chat。", "Installation": "安装", "Installed": "已安装", "Installed_at": "安装与", + "Installing": "正在安装", "Instance": "实例", "Instance_Record": "实例记录", "Instances": "实例", @@ -1696,6 +2690,7 @@ "Integrations_Outgoing_Type_SendMessage": "消息已发送", "Integrations_Outgoing_Type_UserCreated": "用户创建", "Integrations_for_all_channels": "输入 all_public_channels 监听所有公共频道,all_private_groups 监听所有私有组,all_direct_messages 监听所有私聊消息。", + "Integrations_table": "集成表", "InternalHubot": "内部 Hubot", "InternalHubot_EnableForChannels": "为公共频道启用", "InternalHubot_EnableForDirectMessages": "为私聊启用", @@ -1709,8 +2704,12 @@ "Invalid_Department": "无效部门", "Invalid_Export_File": "上传的文件不是有效的 %s 导出文件。", "Invalid_Import_File_Type": "无效导入文件类型。", + "Invalid_OAuth_client": "无效的 OAuth 客户端", + "Invalid_apps_admin_message": "存在一个或多个应用处于无效状态。前往 {{marketplace}} > {{installed}} 查看。", + "Invalid_apps_banner_text": "存在一个或多个应用处于无效状态。点击此处查看。", "Invalid_confirm_pass": "两次输入的密码不一致", "Invalid_email": "输入的电子邮件地址无效", + "Invalid_field": "字段不能为空", "Invalid_name": "用户名不能为空", "Invalid_notification_setting_s": "无效通知设置:%s", "Invalid_or_expired_invite_token": "无效或过期的邀请令牌", @@ -1724,31 +2723,52 @@ "Invalid_username": "输入的用户名无效", "Invisible": "隐身", "Invitation": "邀请", + "Invitation_date": "邀请日期", "Invitation_Email_Description": "您可以使用以下占位符: \n - [email] 作为收件人邮箱地址。 \n - [Site_Name] 和 `[Site_URL]` 分别作为应用程序的名称和网址。 ", "Invitation_HTML": "邀请邮件 HTML", "Invitation_HTML_Default": "

您已被邀请到 [Site_Name]

转到[Site_URL],并尝试当今最先进的开源聊天解决方案!

", "Invitation_Subject": "邀请邮件主题", "Invitation_Subject_Default": "您已被邀请到 [Site_Name]", + "Invite": "邀请", + "Invited__date__": "已于 {{date}} 邀请", "Invite_Link": "邀请链接", "Invite_Users": "邀请用户", + "Invite_and_add_members_to_this_workspace_to_start_communicating": "邀请并添加成员到此工作区以开始沟通。", + "Invite_link_generated": "邀请链接已生成", + "Invite_removed": "邀请已成功移除", "Invite_user_to_join_channel": "邀请一位用户加入此频道", "Invite_user_to_join_channel_all_from": "邀请 [#channel] 的所有用户加入此频道", "Invite_user_to_join_channel_all_to": "邀请此频道所有用户加入 [#channel]", + "Invites": "邀请", "IssueLinks_Incompatible": "警告:不要和“十六进制颜色预览”同时启用。", "IssueLinks_LinkTemplate": "问题链接模板", "IssueLinks_LinkTemplate_Description": "问题链接模板; %s 将被问题编号替换。", "Issue_Links": "问题跟踪器链接", + "It_Security": "IT 安全", + "It_Will_Hide_All_Other_Content_Blocks_In_The_Homepage": "将隐藏主页中的所有其他内容块", + "It_Will_Show_All_Other_Content_Blocks_In_The_Homepage": "将显示主页中的所有其他内容块", "It_works": "有用", "Italic": "斜体", "Items_per_page:": "每页条数:", + "JSON": "JSON(数据格式)", + "Jitsi_included_with_Community": "Jitsi(社区版包含)", "Job_Title": "职称", "Join": "加入", "Join_Chat": "加入聊天", "Join_audio_call": "加入音频对话", + "Join_call": "加入通话", + "Join_channel": "加入频道", + "Join_channel_to_view_history": "加入 {{channel}} 以查看历史记录。", + "Join_conference": "加入会议", "Join_default_channels": "加入默认频道", + "Join_discussion": "加入讨论", + "Join_my_room_to_start_the_video_call": "加入我的房间以开始视频通话", + "Join_rooms": "加入房间", "Join_the_Community": "加入社区", "Join_the_given_channel": "加入该频道", "Join_video_call": "加入视频对话", + "Join_with_password": "使用密码加入", + "Join_your_team": "加入你的团队", "Joined_at": "加入于", "Jump": "跳转", "Jump_to_first_unread": "跳转到第一条未读消息", @@ -1762,6 +2782,7 @@ "Katex_Parenthesis_Syntax": "允许括号语法", "Katex_Parenthesis_Syntax_Description": "允许使用 \\[katex 段落\\] 和\\(内嵌 katex\\)语法", "Keep_default_user_settings": "保持默认设置", + "Keep_editing": "继续编辑", "Keyboard_Shortcuts_Edit_Previous_Message": "编辑上一条消息", "Keyboard_Shortcuts_Keys_1": "Command(或 Ctrl)+ p 或 Command(或 Ctrl)+ k", "Keyboard_Shortcuts_Keys_2": "向上箭头", @@ -1769,7 +2790,7 @@ "Keyboard_Shortcuts_Keys_4": "Command(或 Alt)+ 向上箭头", "Keyboard_Shortcuts_Keys_5": "Command(或 Alt)+ 向右箭头", "Keyboard_Shortcuts_Keys_6": "Command(或 Alt)+ 向下箭头", - "Keyboard_Shortcuts_Keys_7": "Shift + Enter", + "Keyboard_Shortcuts_Keys_7": "Shift + Enter(换行)", "Keyboard_Shortcuts_Keys_8": "Shift(或Ctrl)+ ESC ", "Keyboard_Shortcuts_Mark_all_as_read": "将所有频道中的消息标记为已读", "Keyboard_Shortcuts_Move_To_Beginning_Of_Message": "移动到消息的开头", @@ -1778,7 +2799,7 @@ "Keyboard_Shortcuts_Open_Channel_Slash_User_Search": "打开频道/用户搜索", "Keyboard_Shortcuts_Title": "键盘快捷键", "Knowledge_Base": "知识库", - "LDAP": "LDAP", + "LDAP": "LDAP(轻量级目录访问协议)", "LDAP_Advanced_Sync": "高级同步", "LDAP_Authentication": "启用", "LDAP_Authentication_Password": "密码", @@ -1787,30 +2808,61 @@ "LDAP_Avatar_Field": "用户头像字段", "LDAP_Avatar_Field_Description": "用来作为用户 *头像* 的字段。留空以优先使用 `thumbnailPhoto` 并以 `jpegPhoto`作为回退。", "LDAP_Background_Sync": "后台同步", + "LDAP_Background_Sync_Avatars": "头像后台同步", + "LDAP_Background_Sync_Avatars_Description": "启用独立的后台进程以同步用户头像。", + "LDAP_Background_Sync_Avatars_Interval": "头像后台同步间隔", + "LDAP_Background_Sync_Disable_Missing_Users": "自动禁用在 LDAP 中不存在的用户", + "LDAP_Background_Sync_Disable_Missing_Users_Description": "当 LDAP 中找不到用户数据时,此选项将停用 Rocket.Chat 中的该用户。该用户拥有的房间将自动分配给新所有者,若无其他用户可访问则移除。", "LDAP_Background_Sync_Import_New_Users": "后台同步导入新用户", "LDAP_Background_Sync_Import_New_Users_Description": "将导入LDAP中存在的所有用户(基于您的筛选条件),并且不存在于Rocket.Chat中", "LDAP_Background_Sync_Interval": "后台同步间隔", "LDAP_Background_Sync_Interval_Description": "同步之间的时间间隔。例如“每24小时”或“每周的第一天”,在[Cron Text Parser](http://bunkat.github.io/later/parsers.html#text)上的更多示例,", "LDAP_Background_Sync_Keep_Existant_Users_Updated": "后台同步更新现有用户", "LDAP_Background_Sync_Keep_Existant_Users_Updated_Description": "将在每个**同步间隔**上同步已经从LDAP导入的所有用户的头像,字段,用户名等(基于您的配置)**", + "LDAP_Background_Sync_Merge_Existent_Users": "后台同步合并现有用户", + "LDAP_Background_Sync_Merge_Existent_Users_Description": "将合并 LDAP 和 Rocket.Chat 中同时存在的所有用户(基于你的过滤条件)。要启用此项,请在“数据同步”标签中启用“合并现有用户”。", + "LDAP_DataSync_ABAC": "同步 ABAC(基于属性的访问控制) 属性", + "LDAP_Background_Sync_ABAC_Attributes": "ABAC 属性后台同步", + "LDAP_Background_Sync_ABAC_Attributes_Description": "启用独立后台进程同步用户 ABAC 属性。", + "LDAP_Background_Sync_ABAC_Attributes_Interval": "ABAC 属性后台同步间隔", + "LDAP_ABAC_AttributeMap": "ABAC 属性映射", + "LDAP_ABAC_AttributeMap_Description": "将 LDAP 用户属性映射为 Rocket.Chat 的 ABAC 属性。 \n 例如 `{\\\"department\\\":\\\"dept\\\", \\\"region\\\":\\\"region\\\"}` 会把 LDAP 属性 `department` 映射为 ABAC 属性 `dept`,`region` 映射为 `region`。 \n 结构必须是 JSON 对象,键为 LDAP 属性名,值为要写入用户的 ABAC 属性名。", "LDAP_BaseDN": "基准 DN", "LDAP_BaseDN_Description": "请填写有效的 DN 作为基础域(Domain Base),以便在该DN指向的 LDAP 子树中检索用户和组。您可添加多个DN,但组与组内用户必须定义在同一个基础域中。若您限定了某些 DN 作为基础域,则只能检索到这些DN下的用户。建议使用 LDAP 中的顶级目录作为基础域,配合下面的“自定义域搜索”筛选条件来进行用户的限制。", "LDAP_CA_Cert": "CA 证书", "LDAP_Connect_Timeout": "连接超时(ms)", + "LDAP_Connection": "连接", + "LDAP_Connection_Authentication": "认证", "LDAP_Connection_Encryption": "加密", "LDAP_Connection_Timeouts": "超时", "LDAP_Connection_successful": "LDAP 连接成功", + "LDAP_CustomFieldMap": "自定义字段映射", + "LDAP_DataSync": "数据同步", "LDAP_DataSync_Advanced": "高级同步", + "LDAP_DataSync_AutoLogout": "自动注销已停用用户", + "LDAP_DataSync_Avatar": "头像", "LDAP_DataSync_BackgroundSync": "后台同步", + "LDAP_DataSync_Channels": "同步频道", + "LDAP_DataSync_CustomFields": "同步自定义字段", "LDAP_DataSync_DataMap": "映射", + "LDAP_DataSync_Roles": "同步角色", + "LDAP_DataSync_Teams": "同步团队", + "LDAP_DataSync_UseVariables": "使用变量", + "LDAP_DataSync_VariableMap": "变量配置", "LDAP_Default_Domain": "默认域名", "LDAP_Default_Domain_Description": "如果提供了默认域,将用于为未从LDAP导入电子邮件的用户创建唯一的电子邮件。该电子邮件将被安装为“username@default_domain”或“unique_id@default_domain”。 \n 示例:`rocket.chat`", "LDAP_Description": "LDAP(轻量目录访问协议)是一种层次数据库,常被企业用于提供单点登录机制——该机制允许用户使用同一套帐号密码登录多个网站或服务。想要了解LDAP认证的设置及示例,可参考我们的wiki页: https://rocket.chat/docs/administrator-guides/authentication/ldap/", + "LDAP_Documentation": "LDAP 文档", + "LDAP_Email_Field": "邮箱字段", "LDAP_Enable": "启用", "LDAP_Enable_Description": "尝试使用 LDAP 进行身份验证。", + "LDAP_Enable_LDAP_Groups_To_RC_Teams": "启用 LDAP 到 Rocket.Chat 团队的映射", "LDAP_Encryption": "加密", "LDAP_Encryption_Description": "请指定与 LDAP 服务器通信时,使用何种加密方法。可选方法包括:`不使用加密`,`SSL/LDAPS`(全程启用加密)和`StartTLS`(建立连接后启用加密)", "LDAP_Enterprise": "企业", + "LDAP_Extension_Field": "分机字段", + "LDAP_FederationHomeServer_Field": "联邦 Homeserver 字段", + "LDAP_FederationHomeServer_Field_Description": "Homeserver 只能在创建用户时指定。更改此项不会影响已同步的用户。", "LDAP_Find_User_After_Login": "登录后找到用户", "LDAP_Find_User_After_Login_Description": "绑定后将执行用户DN的搜索,以确保绑定成功,从而防止在AD配置允许的情况下使用空密码进行登录。", "LDAP_Group_Filter_Enable": "启用 LDAP 用户组过滤", @@ -1825,6 +2877,7 @@ "LDAP_Group_Filter_Group_Name_Description": "它属于用户的组名", "LDAP_Group_Filter_ObjectClass": "组 ", "LDAP_Group_Filter_ObjectClass_Description": "识别组的*objectclass*。 \n 举例: **OpenLDAP:** `groupOfUniqueNames`", + "LDAP_Groups_To_Rocket_Chat_Teams": "从 LDAP 到 Rocket.Chat 的团队映射。", "LDAP_Host": "服务器", "LDAP_Host_Description": "LDAP服务器的域名或 IP,如 `ldap.example.com` 或 `10.0.0.30`。", "LDAP_Idle_Timeout": "空闲超时(ms)", @@ -1835,8 +2888,10 @@ "LDAP_Login_Fallback_Description": "如果无法在 LDAP 上成功登录,则尝试在 默认/本地 账户系统上登录。这在 LDAP 服务器因为某些原因宕机时会有帮助。", "LDAP_Merge_Existing_Users": "合并现有用户", "LDAP_Merge_Existing_Users_Description": "*小心!* 当从 LDAP 导入和已经存在用户同名的用户时,会将 LDAP 信息和密码设置在已有用户上。", + "LDAP_Name_Field": "姓名字段", "LDAP_Port": "LDAP 端口", "LDAP_Port_Description": "LDAP 端口。比如:`389` 或 `636`", + "LDAP_Prevent_Username_Changes": "防止 LDAP 用户更改其 Rocket.Chat 用户名", "LDAP_Query_To_Get_User_Teams": "用于获取用户组的 LDAP 查询", "LDAP_Reconnect": "重新连接", "LDAP_Reconnect_Description": "尝试在执行操作时由于某种原因中断连接时自动重新连接", @@ -1847,13 +2902,18 @@ "LDAP_Search_Size_Limit": "搜索大小限制", "LDAP_Search_Size_Limit_Description": "要返回的最大条目数。 \n **注意**此数字应大于**搜索页面大小**", "LDAP_Server_Type": "服务器类型", + "LDAP_Server_Type_AD": "Active Directory(活动目录)", "LDAP_Server_Type_Other": "其他", + "LDAP_Sync_AutoLogout_Enabled": "启用自动注销", + "LDAP_Sync_AutoLogout_Interval": "自动注销间隔", + "LDAP_Sync_Custom_Fields": "同步自定义字段", "LDAP_Sync_Now": "后台立即同步", "LDAP_Sync_Now_Description": "将立即执行 **后台同步**,而不是等待 **同步间隔**,即使 **后台同步** 为 False。 \n 此操作是异步的,请参阅日志以获取有关处理", "LDAP_Sync_User_Active_State": "同步用户激活状态", "LDAP_Sync_User_Active_State_Both": "启用和禁用用户", "LDAP_Sync_User_Active_State_Description": "根据 LDAP 状态确定用户在 Rocket.Chat 应该启用还是禁用。其中`pwdAccountLockedTime`属性将会被用于确认用户是否被禁用。", "LDAP_Sync_User_Active_State_Disable": "禁用用户", + "LDAP_Sync_User_Active_State_Enable": "启用用户", "LDAP_Sync_User_Active_State_Nothing": "什么也不做", "LDAP_Sync_User_Avatar": "同步用户头像", "LDAP_Sync_User_Data_Channels": "自动同步 LDAP 组到频道", @@ -1868,6 +2928,10 @@ "LDAP_Sync_User_Data_Channels_Enforce_AutoChannels_Description": "**注意**:启用此选项将删除频道中没有对应 LDAP 组的所有用户!请谨慎使用。", "LDAP_Sync_User_Data_Channels_Filter": "用户组过滤器", "LDAP_Sync_User_Data_Channels_Filter_Description": "LDAP搜索过滤器,用于检查用户是否在组中。", + "LDAP_Sync_User_Data_Channels_GroupMembershipValidationStrategy": "组成员验证策略", + "LDAP_Sync_User_Data_Channels_GroupMembershipValidationStrategy_Description": "确定应如何验证用户在 LDAP 组中的成员关系。 \n - **对每个组应用过滤器**:对 LDAP 组频道映射中定义的每个组(键)应用 LDAP 用户组过滤器。这种方式较慢,但在需要使用 `#{groupName}` 替换标签定义成员关系时很有用; \n - **一次应用过滤器以获取所有成员关系**:对每个用户仅应用一次 LDAP 用户组过滤器。该用户将被视为 LDAP 搜索结果中所有组的成员。这是一个**更快**的选项,适用于过滤器未使用 `#{groupName}` 替换标签的情况(例如按组中的 `member` 字段过滤)。", + "LDAP_Sync_User_Data_GroupMembershipValidationStrategy_EachGroup": "对每个组应用过滤器", + "LDAP_Sync_User_Data_GroupMembershipValidationStrategy_Once": "一次应用过滤器以获取所有成员关系", "LDAP_Sync_User_Data_Roles": "同步 LDAP 群组", "LDAP_Sync_User_Data_RolesMap": "用户数据群组对应", "LDAP_Sync_User_Data_RolesMap_Description": "将LDAP组对应到Rocket.Chat用户角色 \n 例如,`{\"rocket-admin\":\"admin\", \"tech-support\":\"support\", \"manager\":[\"leader\", \"moderator\"]}`会将rocket-admin LDAP组对应到Rocket.Chat的“管理员”角色。", @@ -1877,11 +2941,24 @@ "LDAP_Sync_User_Data_Roles_BaseDN_Description": "用于查找用户的LDAP BaseDN。", "LDAP_Sync_User_Data_Roles_Filter": "用户组过滤器", "LDAP_Sync_User_Data_Roles_Filter_Description": "LDAP搜索过滤器,用于检查用户是否在组中。", + "LDAP_Sync_User_Data_Roles_GroupMembershipValidationStrategy": "组成员验证策略", + "LDAP_Sync_User_Data_Roles_GroupMembershipValidationStrategy_Description": "确定应如何验证用户在 LDAP 组中的成员关系。 \n - **对每个组应用过滤器**:对 LDAP 组频道映射中定义的每个组(键)应用 LDAP 用户组过滤器。这种方式较慢,但在需要使用 `#{groupName}` 替换标签定义成员关系时很有用; \n - **一次应用过滤器以获取所有成员关系**:对每个用户仅应用一次 LDAP 用户组过滤器。该用户将被视为 LDAP 搜索结果中所有组的成员。这是一个**更快**的选项,适用于过滤器未使用 `#{groupName}` 替换标签的情况(例如按组中的 `member` 字段过滤)。", + "LDAP_Teams_BaseDN": "LDAP 团队 BaseDN", + "LDAP_Teams_BaseDN_Description": "用于查找用户团队的 LDAP BaseDN。", + "LDAP_Teams_Name_Field": "LDAP 团队名称属性", + "LDAP_Teams_Name_Field_Description": "Rocket.Chat 用于加载团队名称的 LDAP 属性。若用逗号分隔,可指定多个属性名。", "LDAP_Timeout": "超时(ms)", "LDAP_Timeout_Description": "在返回错误之前等待搜索结果多少英里", "LDAP_Unique_Identifier_Field": "唯一识别字段", "LDAP_Unique_Identifier_Field_Description": "使用哪个字段来连接 LDAP 和 Rocket.Chat 的用户信息。可使用逗号分隔的多个字段。 \n 默认值: `objectGUID,ibm-entryUUID,GUID,dominoUNID,nsuniqueId,uidNumber`", + "LDAP_Update_Data_On_Login": "登录时更新用户数据", + "LDAP_Update_Data_On_OAuth_Login": "使用 OAuth 服务登录时更新用户数据", "LDAP_UserSearch": "用户搜索", + "LDAP_UserSearch_Filter": "搜索过滤器", + "LDAP_UserSearch_GroupFilter": "群组过滤器", + "LDAP_User_Found": "已找到 LDAP 用户", + "LDAP_User_Search_AttributesToQuery": "要查询的属性", + "LDAP_User_Search_AttributesToQuery_Description": "指定 LDAP 查询返回的属性,以逗号分隔。默认返回所有属性。`*` 表示所有常规属性,`+` 表示所有操作属性。请确保包含所有 Rocket.Chat 同步选项所需的属性。", "LDAP_User_Search_Field": "搜索字段", "LDAP_User_Search_Field_Description": "该 LDAP 属性是用于识别登录者的字段。对于 Windows AD,通常应使用 `sAMAccountName` 字段,而其他 LDAP 方案(如 OpenLDAP)可能使用 `uid` 字段。假如要使用邮箱地址作为帐号,可使用 `mail` 之类的字段。 \n 若要同时使用多种认证字段,可通过逗号分隔字段名。举例:若要同时使用用户名和邮箱作为认证帐号,可使用`sAMAccountName,mail`。", "LDAP_User_Search_Filter": "过滤器", @@ -1889,46 +2966,122 @@ "LDAP_User_Search_Scope": "范围", "LDAP_Username_Field": "用户名字段", "LDAP_Username_Field_Description": "指定新用户登录时,使用 LDAP 中哪个字段的值作为*用户名*。若要使用登录页面所用的用户名,请留空。 \n 可使用模版标签,比如 `#{givenName}.#{sn}`。 \n 默认字段为 `sAMAccountName`。", + "LDAP_Username_To_Search": "要搜索的用户名", "LDAP_Validate_Teams_For_Each_Login": "每次登录时验证映射", + "LDAP_Validate_Teams_For_Each_Login_Description": "确定是否应在用户每次登录 Rocket.Chat 时更新其团队。若关闭,仅在首次登录时加载团队。", "Label": "标签", "Language": "语言", + "Language_Bulgarian": "保加利亚语", + "Language_Chinese": "中文", + "Language_Czech": "捷克语", + "Language_Danish": "丹麦语", "Language_Dutch": "荷兰语", "Language_English": "英语", + "Language_Estonian": "爱沙尼亚语", + "Language_Finnish": "芬兰语", "Language_French": "法语", "Language_German": "德语", + "Language_Greek": "希腊语", + "Language_Hungarian": "匈牙利语", "Language_Italian": "意大利语", + "Language_Japanese": "日语", + "Language_Latvian": "拉脱维亚语", + "Language_Lithuanian": "立陶宛语", "Language_Not_set": "没有具体", "Language_Polish": "波兰语", "Language_Portuguese": "葡萄牙语", + "Language_Romanian": "罗马尼亚语", "Language_Russian": "俄罗斯语", + "Language_Slovak": "斯洛伐克语", + "Language_Slovenian": "斯洛文尼亚语", "Language_Spanish": "西班牙语", + "Language_Swedish": "瑞典语", "Language_Version": "英语版本", + "Language_setting_warning": "服务器语言设置不会影响用户客户端
每个用户都有自己的语言偏好,此设置更改后也会保留。", + "Larger_amounts_of_active_connections": "如需更高的活跃连接数,可考虑我们的 <1>多实例方案。", + "Last_role_in_permission_warning": "这是拥有该权限的最后一个角色。移除后将导致此页面在 UI 中被软锁定,需要通过数据库访问才能恢复。是否继续?", + "Last_5_minutes": "最近 5 分钟", + "Last_15_minutes": "最近 15 分钟", + "Last_30_minutes": "最近 30 分钟", + "Last_1_hour": "最近 1 小时", + "Last_15_days": "最近 15 天", "Last_30_days": "最近 30 天", + "Last_6_months": "最近 6 个月", "Last_7_days": "最近 7 天", "Last_90_days": "最近 90 天", + "Last_Call": "最近通话", "Last_Chat": "上次聊天", + "Last_Heartbeat_Time": "最后心跳时间", "Last_Message": "上次消息", "Last_Message_At": "最后消息于", "Last_Status": "最近状态", "Last_Updated": "最近更新时间", "Last_active": "最近活跃", + "Last_channel": "最近频道", + "Last_contacts": "最近联系人", "Last_login": "上次登录", + "Last_message__date__": "最后消息:{{date}}", "Last_seen": "上次遇见", "Last_token_part": "最后的令牌部分", + "Last_contact__time__": "上次联系 {{time}}", + "Last_message_received__time__": "最后收到的消息 {{time}}", + "Latest": "最新", "Launched_successfully": "成功启动", "Layout": "布局", + "Layout_Custom_Body_Only": "仅显示自定义内容", + "Layout_Custom_Body_Only_Description": "将隐藏主页中的所有其他内容块。", + "Layout_Custom_Content_Description": "此处放置你的自定义内容。在高级方案中,可将其放入白色区块或占据主页全部可用空间。", + "Layout_Description": "自定义工作区外观。", "Layout_Home_Body": "首页正文", + "Layout_Home_Custom_Block_Visible": "在主页显示自定义内容", + "Layout_Home_Page_Content": "布局 / 主页内容", + "Layout_Home_Page_Content_Title": "主页内容", "Layout_Home_Title": "首页标题", "Layout_Legal_Notice": "法律声明", + "Layout_Login_Hide_Logo": "隐藏徽标", + "Layout_Login_Hide_Logo_Description": "在登录页隐藏徽标。", + "Layout_Login_Hide_Powered_By": "隐藏“Powered by”", + "Layout_Login_Hide_Powered_By_Description": "在登录页隐藏“Powered by”。", + "Layout_Login_Hide_Title": "隐藏标题", + "Layout_Login_Hide_Title_Description": "在登录页隐藏标题。", + "Layout_Login_Template": "登录模板", + "Layout_Login_Template_Description": "自定义登录页外观。", + "Layout_Login_Template_Horizontal": "水平", + "Layout_Login_Template_Vertical": "垂直", "Layout_Login_Terms": "登录条款", + "Layout_Login_Terms_Content": "继续即表示你同意我们的 服务条款隐私政策法律声明。", "Layout_Privacy_Policy": "隐私政策", "Layout_Show_Home_Button": "显示 “Home 键”", "Layout_Sidenav_Footer": "侧面导航页脚", + "Layout_Sidenav_Footer_Dark": "侧边导航底部 - 深色主题", "Layout_Sidenav_Footer_Dark_description": "页脚大小为 260 x 70px", "Layout_Sidenav_Footer_description": "页脚大小为 260 x 70px", "Layout_Terms_of_Service": "服务条款", "Lead_capture_email_regex": "领导捕获电子邮件正则表达式", "Lead_capture_phone_regex": "领导捕获手机正则表达式", + "Leaders": "负责人", + "Learn_how_to_unlock_the_myriad_possibilities_of_rocket_chat": "了解如何解锁 Rocket.Chat 的无限可能。", + "Learn_more": "了解更多", + "Learn_more_about_E2EE": "了解更多关于 E2EE 的信息", + "Learn_more_about_SLA_policies": "了解更多关于 SLA 策略的信息", + "Learn_more_about_accessibility": "了解我们在无障碍方面的承诺:", + "Learn_more_about_agents": "了解更多关于客服的信息", + "Learn_more_about_business_hours": "了解更多关于营业时间的信息", + "Learn_more_about_canned_responses": "了解更多关于自动回复的信息", + "Learn_more_about_contacts": "了解更多关于联系人的信息", + "Learn_more_about_conversations": "了解更多关于对话的信息", + "Learn_more_about_current_chats": "了解更多关于当前聊天的信息", + "Learn_more_about_custom_fields": "了解更多关于自定义字段的信息", + "Learn_more_about_departments": "了解更多关于部门的信息", + "Learn_more_about_managers": "了解更多关于管理员的信息", + "Learn_more_about_monitors": "了解更多关于监控的信息", + "Learn_more_about_tags": "了解更多关于标签的信息", + "Learn_more_about_triggers": "了解更多关于触发器的信息", + "Learn_more_about_units": "了解更多关于单位的信息", + "Learn_more_about_voice_channel": "了解更多关于语音频道的信息", + "You_have_been_invited_to_have_a_conversation_with": "你已被邀请与以下对象开始对话", + "Learn_more_about_Federation": "了解更多关于联邦的信息", + "Least_recent_updated": "最久未更新", "Leave": "离开", "Leave_Group_Warning": "您确定要离开用户组 “{{roomName}}” 吗?", "Leave_Livechat_Warning": "你确定要离开和 “{{roomName}}” 的 omnichannel 吗?", @@ -1938,21 +3091,37 @@ "Leave_room": "离开", "Leave_the_current_channel": "离开当前频道", "Leave_the_description_field_blank_if_you_dont_want_to_show_the_role": "如果不想显示对应角色,请将描述字段留空", + "Left": "已离开", + "Let_moderators_know_what_the_issue_is": "告知版主问题所在", + "Let_them_know": "告知他们", "Lets_get_you_new_one_": "新版本即将到来", + "License": "许可证", + "Line": "行", + "Link": "链接", + "Link_Preview": "链接预览", "List_of_Channels": "频道列表", "List_of_Direct_Messages": "私聊列表", "List_of_departments_for_forward": "允许转发的部门列表(可选)", "List_of_departments_for_forward_description": "允许设置一个列表来限制可从此部门接收聊天的部门", "List_of_departments_to_apply_this_business_hour": "要适用此营业时间的部门", + "List_view": "列表视图", "LiveStream & Broadcasting": "LiveStream 和广播", + "LiveStream & Broadcasting_Description": "Rocket.Chat 与 YouTube Live 的集成允许频道所有者将摄像头画面实时直播到频道内。", "Livechat": "即时聊天", + "Livechat_AdditionalWidgetScripts": "Livechat 小部件附加脚本", + "Livechat_AdditionalWidgetScripts_Description": "使用此设置向小部件包中添加额外的 JS 脚本。可用逗号分隔脚本列表,例如:`https://yourUrl/customScript1.js,https://yourUrl/customScript2.js`", "Livechat_Agents": "客服", "Livechat_AllowedDomainsList": "即时聊天允许的域名", "Livechat_Appearance": "Livechat 外观", + "Livechat_Block_Unknown_Contacts": "阻止未知联系人", + "Livechat_Block_Unknown_Contacts_Description": "不在联系人列表中的人发起的对话将无法被接起。", + "Livechat_Block_Unverified_Contacts": "阻止未验证联系人", + "Livechat_Block_Unverified_Contacts_Description": "未验证人员发起的对话将无法被接起。", + "Livechat_Calls": "Livechat 通话", "Livechat_Dashboard": "Omnichannel 仪表盘", "Livechat_DepartmentOfflineMessageToChannel": "发送部门的 Livechat 离线信息到频道", - "Livechat_Facebook_API_Key": "OmniChannel API Key", - "Livechat_Facebook_API_Secret": "OmniChannel API Secret", + "Livechat_Facebook_API_Key": "Omnichannel API 密钥", + "Livechat_Facebook_API_Secret": "Omnichannel API 密钥(Secret)", "Livechat_Facebook_Enabled": "已启用 Facebook 集成", "Livechat_Inquiry_Already_Taken": "Omnichannel 问询已进行", "Livechat_Installation": "Livechat 安装", @@ -1960,27 +3129,51 @@ "Livechat_Monitors": "监控", "Livechat_OfflineMessageToChannel_enabled": "发送 Livechat 离线信息到频道", "Livechat_Queue": "Omnichannel 队列", + "Livechat_Require_Contact_Verification": "要求联系人验证。", + "Livechat_Require_Contact_Verification_Description": "建议对所有联系人进行验证以符合零信任安全策略。未验证人员的消息不会出现在队列中,但仍会显示在联络中心。", "Livechat_Routing_Method": "Omnichannel 路由方法", "Livechat_Take_Confirm": "你想接下这个客户吗?", "Livechat_Triggers": "Livechat 触发器", "Livechat_Users": "Omnichannel 用户", + "Livechat_WidgetLayoutClasses": "Livechat 小部件附加 CSS", + "Livechat_WidgetLayoutClasses_Description": "使用此设置向小部件包中添加额外的 CSS。可用逗号分隔 CSS 文件列表,例如:`https://yourUrl/customFile1.css,https://yourUrl/customFile2.css`", + "Livechat_abandoned_rooms_action": "如何处理访客放弃", "Livechat_abandoned_rooms_closed_custom_message": "聊天室自动因访客无活动而关闭时的自定义信息", "Livechat_agents": "Omnichannel 客服", + "Livechat_allow_manual_on_hold": "允许客服手动将聊天挂起", + "Livechat_allow_manual_on_hold_Description": "启用后,客服将获得将聊天挂起的选项", + "Livechat_allow_manual_on_hold_upon_agent_engagement_only": "仅在客服参与后才可挂起聊天", + "Livechat_allow_manual_on_hold_upon_agent_engagement_only_Description": "仅当客服是对话中最后一条消息的发送者时才允许挂起聊天。", + "Livechat_auto_close_on_hold_chats_custom_message": "挂起队列聊天自动关闭的自定义消息", + "Livechat_auto_close_on_hold_chats_custom_message_Description": "当挂起队列中的房间被系统自动关闭时发送的自定义消息", + "Livechat_auto_close_on_hold_chats_timeout": "挂起队列聊天关闭前的等待时长?", + "Livechat_auto_close_on_hold_chats_timeout_Description": "定义聊天在挂起队列中停留多久后由系统自动关闭,单位为秒", "Livechat_auto_transfer_chat_timeout": "自动将未应答聊天转给其他客服的超时(秒)", "Livechat_auto_transfer_chat_timeout_Description": "此事件只在聊天刚开始时生效。在第一次因无活动而转移后,聊天室将不再被监控。", + "Livechat_background": "Livechat 背景", + "Livechat_background_description": "使用十六进制颜色(#F5455C)、颜色名(red)或图片 URL(`url('https://example.com/image.png')`)定义背景。该字段遵循 CSS 标准。[查看文档](https://developer.mozilla.org/en-US/docs/Web/CSS/background)。", "Livechat_business_hour_type": "营业时间类型(单个或多个)", "Livechat_chat_transcript_sent": "聊天记录已发送:{{transcript}}", + "Livechat_close_chat": "关闭聊天", "Livechat_custom_fields_options_placeholder": "用于选择预定义值的逗号分隔列表。在元素间不允许使用空格。", "Livechat_custom_fields_public_description": "公共自定义字段将在外部应用中显示,例如 LiveChat 等等。", + "Livechat_email_transcript_has_been_requested": "已请求聊天记录,可能需要几秒钟。", "Livechat_enable_message_character_limit": "启用消息字数限制", "Livechat_enabled": "Omnichannel 启用", "Livechat_forward_open_chats": "前开口聊天", "Livechat_forward_open_chats_timeout": "超时(以秒为单位)转发聊天", "Livechat_guest_count": "访客计数器", "Livechat_hide_system_messages": "隐藏系统信息", + "Livechat_hide_expand_chat": "隐藏“展开聊天”", + "Livechat_hide_expand_chat_description": "从小部件移除“展开聊天”按钮", + "Livechat_hide_watermark": "隐藏“powered by Rocket.Chat”", + "Livechat_hide_watermark_description": "从小部件移除 Rocket.Chat 徽标", "Livechat_last_chatted_agent_routing": "优先最近接待客服", "Livechat_last_chatted_agent_routing_Description": "最新接待客服设置优先分配给访客上一个聊天过的在线客服。", "Livechat_managers": "Omnichannel 管理员", + "Livechat_max_queue_wait_time_action": "达到最大等待时间时如何处理排队聊天", + "Livechat_maximum_queue_wait_time": "队列最大等待时间", + "Livechat_maximum_queue_wait_time_description": "队列中聊天保留的最大时间(分钟)。-1 表示不限", "Livechat_message_character_limit": "Livechat 消息字符限制", "Livechat_monitors": "Livechat 监控", "Livechat_offline": "Omnichannel 离线", @@ -1992,17 +3185,28 @@ "Livechat_title": "即时聊天标题", "Livechat_title_color": "即时聊天标题背景颜色", "Livechat_transcript_already_requested_warning": "此聊天的聊天记录已被请求并将在会话结束后尽快发送。", + "Livechat_transcript_email_subject": "聊天记录邮件主题自定义", + "Livechat_transcript_email_subject_Description": "允许自定义通过邮件发送的聊天记录主题。关闭房间时可通过传入 `subject` 属性覆盖。留空则使用默认主题。", "Livechat_transcript_has_been_requested": "已请求聊天记录。", "Livechat_transcript_request_has_been_canceled": "聊天记录请求已取消。", + "Livechat_transcript_send_always": "始终通过邮件向访客发送对话记录", + "Livechat_transcript_send_always_Description": "结束后自动通过邮件发送对话记录,不受客服偏好影响。", "Livechat_transcript_sent": "Omnichannel 聊天记录已发送", + "Livechat_transcript_show_system_messages": "在聊天记录中包含系统消息", + "Livechat_transfer_failed_fallback": "原部门({{from}})没有在线客服。聊天已成功转移至 {{to}}", "Livechat_transfer_return_to_the_queue": "{{from}}将聊天返回队列", + "Livechat_transfer_return_to_the_queue_auto_transfer_unanswered_chat": "{{from}} 因 {{duration}} 秒未应答将聊天返回队列", + "Livechat_transfer_return_to_the_queue_with_a_comment": "{{from}} 将聊天返回队列并备注:{{comment}}", "Livechat_transfer_to_agent": "{{from}} 将聊天转移至 {{to}}", + "Livechat_transfer_to_agent_auto_transfer_unanswered_chat": "{{from}} 因 {{duration}} 秒未应答将聊天转移至 {{to}}", "Livechat_transfer_to_agent_with_a_comment": "{{from}} 将聊天转移至 {{to}},备注:{{comment}}", "Livechat_transfer_to_department": "{{from}} 将聊天转移至部门 {{to}}", "Livechat_transfer_to_department_with_a_comment": "{{from}} 将聊天转移至部门 {{to}},备注:{{comment}}", "Livechat_user_sent_chat_transcript_to_visitor": "{{agent}} 已将聊天记录发送至 {{guest}}", "Livechat_visitor_email_and_transcript_email_do_not_match": "访客邮件地址和聊天记录邮件地址不匹配", "Livechat_visitor_transcript_request": "{{guest}} 已请求聊天记录", + "Livechat_widget_position_on_the_screen": "Livechat 小部件在屏幕上的位置", + "Livestream": "直播", "Livestream_close": "关闭 Livestream", "Livestream_enable_audio_only": "只启用音频模式", "Livestream_enabled": "Livestream 启用", @@ -2011,17 +3215,22 @@ "Livestream_popout": "打开直播", "Livestream_source_changed_succesfully": "直播源已成功更改", "Livestream_switch_to_room": "切换到当前聊天室的直播", + "Livestream_unavailable_for_federation": "联邦房间不支持直播", "Livestream_url": "直播来源网址", "Livestream_url_incorrect": "直播网址不正确", "Load_Balancing": "负载均衡", + "Load_Rotation": "负载轮转", "Load_more": "加载更多", + "Loading": "加载中", "Loading...": "加载中...", "Loading_more_from_history": "加载更多", "Loading_suggestion": "载入建议中...", + "Local": "本地", "Local_Domains": "本地域名", "Local_Password": "本地密码", "Local_Time": "本地时间", "Local_Time_time": "本地时间:{{time}}", + "Local_Timezone": "本地时区", "Localization": "本地化", "Location": "位置", "Log_Exceptions_to_Channel": "将异常记录至频道", @@ -2035,9 +3244,14 @@ "Log_Trace_Subscriptions": "跟踪订阅调用", "Log_Trace_Subscriptions_Filter": "跟踪订阅过滤器", "Log_Trace_Subscriptions_Filter_Description": "这里的文本将被作为正则表达式求值(`new RegExp('text')`)。保持空白以显示每个调用的跟踪。", + "Log_in_to_sync": "登录以同步", + "Log_out_devices_remotely": "远程注销设备", + "Logged_In_Via": "登录方式", + "Logged_Out_Banner_Text": "此设备上的会话已结束,请重新登录以继续。", "Logged_out_of_other_clients_successfully": "登出其它设备成功", "Login": "登录", "Login_Attempts": "失败的登录尝试", + "Login_Detected": "检测到登录", "Login_Logs": "登录日志", "Login_Logs_ClientIp": "在登录尝试日志中显示客户端 IP", "Login_Logs_Enabled": "(在控制台上)记录失败的登录尝试", @@ -2047,13 +3261,21 @@ "Login_with": "使用 %s 登录", "Logistics": "后勤", "Logout": "退出", + "Logout_Device": "注销设备", "Logout_Others": "从其它已登录的设备上登出", "Logs": "日志", + "Logs_from": "日志来自", + "Logs_Description": "配置服务器日志的接收方式。", + "Long_press_to_do_x": "长按以执行 {{action}}", "Longest_chat_duration": "最长聊天时长", "Longest_reaction_time": "最长回应时间", "Longest_response_time": "最长的响应时间", "Looked_for": "已查询", - "MAU_value": "MAU {{value}}", + "Low": "低", + "Lowest": "最低", + "MAC_Available": "{{macLeft, number}} 个 MAC 可用", + "MAC_InfoText": "(MAC)计费月内参与互动的唯一 Omnichannel 联系人数量。", + "MAU_value": "MAU(月活跃用户) {{value}}", "Mail_Message_Invalid_emails": "你提供了一个或多个无效电子邮件地址:%s", "Mail_Message_Missing_subject": "您必须提供邮件主题", "Mail_Message_Missing_to": "您必须选则一个或多个用户,或者提供一个或多个电子邮箱地址(多个电子邮箱地址之间使用逗号分隔)。", @@ -2067,12 +3289,22 @@ "Make_Admin": "设置为管理员", "Make_sure_you_have_a_copy_of_your_codes_1": "请确保您有一份您代码的副本:", "Make_sure_you_have_a_copy_of_your_codes_2": "如果您无法访问身份验证器应用,可使用其中的一条代码来登录。", + "Manage": "管理", + "Manage_Devices": "管理设备", + "Manage_Omnichannel": "管理 Omnichannel", + "Manage_conversations_in_the_contact_center": "在 <1>联络中心 管理对话。", + "Manage_server_list": "管理服务器列表", + "Manage_servers": "管理服务器", + "Manage_subscription": "管理订阅", + "Manage_which_devices": "管理连接到此工作区的设备以确保安全。包括设备 ID、登录数据等信息,并支持远程注销设备。", + "Manage_workspace": "管理工作区", "Manager_added": "管理员已添加", "Manager_removed": "已移除管理员", "Managers": "管理员", "Managing_assets": "资产管理", "Managing_integrations": "集成管理", "Manual_Selection": "手动选择", + "Manually_created_users_briefing": "手动创建的用户将初始显示为待处理。首次登录后将显示为活跃。", "Manufacturing": "制造业", "MapView_Enabled": "启用 Mapview", "MapView_Enabled_Description": "启用 mapview 将在聊天输入字段左侧显示位置分享按钮。", @@ -2081,6 +3313,7 @@ "Mark_all_as_read": "将所有消息(所有频道)标记为已读", "Mark_as_read": "标记为已读", "Mark_as_unread": "标记为未读", + "Mark_email_as_verified": "将邮箱标记为已验证", "Mark_read": "标记为已读", "Mark_unread": "标记为未读", "Markdown_Headers": "Markdown 标题", @@ -2093,8 +3326,23 @@ "Markdown_Parser": "降价解析器", "Markdown_SupportSchemesForLink": "Markdown 支持的链接协议", "Markdown_SupportSchemesForLink_Description": "由英文逗号分割的协议列表", + "Marketplace": "应用市场", + "Marketplace_Bad_Marketplace_Connection": "无法连接到应用市场。请检查网络连接。", + "Marketplace_Failed_To_Fetch_Apps": "从应用市场获取应用失败。请稍后再试。", + "Marketplace_Failed_To_Fetch_Categories": "从应用市场获取分类失败。请稍后再试。", + "Marketplace_Internal_Error": "与应用市场通信时发生内部错误。请稍后再试。", + "Marketplace_Invalid_Apps_Engine_Version": "已安装的 Apps Engine 版本与应用市场不兼容。请更新 Apps Engine 至最新版本。", + "Marketplace_app_last_updated": "最后更新 {{lastUpdated}}", + "Marketplace_apps": "应用市场应用", + "Marketplace_error": "无法连接到互联网,或你的工作区为离线安装。", + "Marketplace_unavailable": "应用市场不可用", + "Marketplace_unavailable_description": "此工作区无法访问应用市场,因为运行了不受支持的 Rocket.Chat 版本。请联系工作区管理员更新并恢复访问。", "Marketplace_view_marketplace": "查看市场", + "Master_volume": "主音量", + "Master_volume_hint": "控制工作区所有声音的音量", + "Max_Retry": "重连服务器的最大尝试次数", "Max_length_is": "最大长度 %s", + "Max_logs_export": "最大(2000)", "Max_number_incoming_livechats_displayed": "已在队列中显示最大数量的条目", "Max_number_incoming_livechats_displayed_description": "(可选) Omnichannel 队列中显示的最大消息数量。", "Max_number_of_chats_per_agent": "最多同时进行的聊天数", @@ -2104,12 +3352,20 @@ "Maximum_number_of_guests_reached": "已达到最大访客数量", "Me": "我", "Media": "媒体", + "Media_URL": "媒体 URL", "Medium": "中", "Members": "成员", "Members_List": "成员列表", "Mentions": "提及", + "Mentions_all_room_members": "提及所有房间成员", + "Mentions_channel": "提及频道", "Mentions_default": "提及(默认)", + "Mentions_online_room_members": "提及在线房间成员", "Mentions_only": "只提及", + "Mentions_user": "提及用户", + "Mentions_with_@_symbol": "使用 @ 符号提及", + "Mentions_with_@_symbol_description": "提及会通知并高亮群组或特定用户的消息,便于定向沟通。\n\n在提及功能中使用“@”符号可优化屏幕阅读器体验,确保依赖屏幕阅读器的用户能更容易理解并参与这些提及。", + "Mentions_you": "提及你", "Merge_Channels": "合并频道", "Message": "消息", "MessageBox_view_mode": "消息框查看模式", @@ -2133,40 +3389,77 @@ "Message_Attachments": "消息附件", "Message_Attachments_GroupAttach": "组附件按钮", "Message_Attachments_GroupAttachDescription": "这将图标分组在可扩展菜单下。占用较少的屏幕空间。", + "Message_Attachments_Strip_Exif": "移除受支持文件中的 EXIF 元数据", + "Message_Attachments_Strip_ExifDescription": "从图片文件(jpeg、tiff 等)中移除 EXIF 元数据。该设置不追溯历史,禁用期间上传的文件仍会包含 EXIF 数据", + "Message_Attachments_Thumbnails_Enabled": "启用图片缩略图以节省带宽", + "Message_Attachments_Thumbnails_EnabledDesc": "将提供缩略图而非原图以减少带宽使用。可通过附件名称旁的图标下载原始分辨率图片。", + "Message_Attachments_Thumbnails_Height": "缩略图最大高度(像素)", + "Message_Attachments_Thumbnails_Width": "缩略图最大宽度(像素)", "Message_Audio": "音频消息", "Message_AudioRecorderEnabled": "音频录制已启用", "Message_AudioRecorderEnabled_Description": "需要在 “文件上传” 中设置 “audio/mp3” 为可接受的媒体类型。", + "Message_Audio_Recording_Disabled": "音频消息 - 已禁用消息音频录制", "Message_Audio_bitRate": "音频消息比特率", "Message_BadWordsFilterList": "添加脏话到黑名单", "Message_BadWordsFilterListDescription": "添加一串由逗号分隔的脏话列表到过滤器", "Message_BadWordsWhitelist": "将黑名单中移除词语", "Message_BadWordsWhitelistDescription": "添加逗号分隔的列表从过滤器中移除词语", "Message_Characther_Limit": "消息字符数限制", + "Message_Code_highlight": "代码高亮语言列表", + "Message_Code_highlight_Description": "用于高亮代码块的语言列表(逗号分隔,支持的语言见 [highlight.js](https://github.com/highlightjs/highlight.js/tree/11.6.0#supported-languages))", + "Message_CustomDomain_AutoLink": "自动链接的自定义域名白名单", + "Message_CustomDomain_AutoLink_Description": "如果希望自动链接内部地址(如 `https://internaltool.intranet` 或 `internaltool.intranet`),需将 `intranet` 域名添加到此字段,多个域名用逗号分隔。", + "Message_CustomFields": "自定义字段校验", + "Message_CustomFields_Description": "自定义字段将按照此设置定义的规则进行校验。\n有关校验选项的更多信息,请查看 [ajv.js.org](https://ajv.js.org/json-schema.html)。\n属性 `type` 和 `additionalProperties` 将分别被强制为 `object` 和 `false`。", + "Message_CustomFields_Enabled": "允许消息自定义字段", "Message_DateFormat": "日期格式", "Message_DateFormat_Description": "参见:[Moment.js](http://momentjs.com/docs/#/displaying/format/)", + "Message_Description": "配置消息设置。", "Message_ErasureType": "消息擦除类型", "Message_ErasureType_Delete": "删除所有消息", "Message_ErasureType_Description": "确定如何处理删除其帐户的用户的消息。", "Message_ErasureType_Keep": "保留消息和用户名", "Message_ErasureType_Unlink": "删除用户和消息之间的链接", + "Message_Formatting_toolbox": "格式化工具栏", "Message_GlobalSearch": "全局搜索", "Message_GroupingPeriod": "合并周期(以秒为单位)", "Message_GroupingPeriodDescription": "同一用户在规定秒数内发布的消息将被合并。", + "Message_HideType_added_user_to_team": "用户已加入团队", "Message_HideType_au": "隐藏“用户添加”消息", + "Message_HideType_ui": "用户被邀请加入房间", + "Message_HideType_uir": "用户拒绝房间邀请", + "Message_HideType_changed_announcement": "房间公告已更改", + "Message_HideType_changed_description": "房间描述已更改", + "Message_HideType_livechat_closed": "隐藏“对话结束”消息", + "Message_HideType_livechat_started": "隐藏“对话开始”消息", + "Message_HideType_livechat_transfer_history": "隐藏“对话已转接”消息", "Message_HideType_mute_unmute": "隐藏“用户静音/取消静音”消息", "Message_HideType_r": "隐藏“已变更聊天室名称”消息", + "Message_HideType_removed_user_from_team": "用户已从团队移除", "Message_HideType_rm": "隐藏“已移除消息”消息", + "Message_HideType_room_allowed_reacting": "房间允许反应", "Message_HideType_room_archived": "隐藏“聊天室已归档”消息", "Message_HideType_room_changed_avatar": "隐藏“已变更聊天室头像”消息", "Message_HideType_room_changed_privacy": "隐藏“已变更聊天室类型”消息", + "Message_HideType_room_changed_topic": "房间主题已更改", "Message_HideType_room_disabled_encryption": "隐藏“已禁用聊天室加密”消息", + "Message_HideType_room_disallowed_reacting": "房间禁止反应", "Message_HideType_room_enabled_encryption": "隐藏“已启用聊天室加密”消息", + "Message_HideType_room_removed_read_only": "房间已允许发言", + "Message_HideType_room_set_read_only": "房间已设为只读", "Message_HideType_room_unarchived": "隐藏“已取消归档聊天室”消息", "Message_HideType_ru": "隐藏“已移除用户”消息", "Message_HideType_subscription_role_added": "隐藏“已设置角色”消息", "Message_HideType_subscription_role_removed": "隐藏“角色不再被定义”消息", "Message_HideType_uj": "隐藏“用户加入”消息", + "Message_HideType_ujt": "用户加入团队", "Message_HideType_ul": "隐藏“用户离开”消息", + "Message_HideType_ult": "用户离开团队", + "Message_HideType_user_added_room_to_team": "用户将房间添加到团队", + "Message_HideType_user_converted_to_channel": "用户将团队转换为频道", + "Message_HideType_user_converted_to_team": "用户将频道转换为团队", + "Message_HideType_user_deleted_room_from_team": "用户从团队中删除房间", + "Message_HideType_user_removed_room_from_team": "用户从团队中移除房间", "Message_HideType_ut": "隐藏“用户加入会话”消息", "Message_HideType_wm": "隐藏“欢迎”消息", "Message_Id": "消息 ID", @@ -2174,6 +3467,7 @@ "Message_KeepHistory": "保存消息历史记录", "Message_MaxAll": "ALL 消息支持的最大频道人数", "Message_MaxAllowedSize": "最大消息长度", + "Message_not_sent_try_again": "消息未发送。\n请重试", "Message_QuoteChainLimit": "链接报价的最大数量", "Message_Read_Receipt_Enabled": "显示读取收据", "Message_Read_Receipt_Store_Users": "详细的读取收据", @@ -2189,26 +3483,44 @@ "Message_UserId": "用户 ID", "Message_VideoRecorderEnabled": "录像机启用", "Message_VideoRecorderEnabledDescription": "要求 'video/webm' 文件在 '文件上传' 设置中成为可接受的媒体类型。", + "Message_Video_Recording_Disabled": "已禁用消息视频录制", + "Message_actions": "消息操作", + "Message_audit": "消息审计", "Message_auditing": "消息审计", "Message_auditing_log": "消息审计日志", + "Message_composer_toolbox_primary_actions": "输入框主要操作", + "Message_composer_toolbox_secondary_actions": "输入框次要操作", "Message_deleting_blocked": "已不能删除该消息", "Message_editing": "编辑留言", + "Message_has_been_edited": "消息已编辑", + "Message_has_been_edited_at": "消息已于 {{date}} 编辑", + "Message_has_been_edited_by": "消息已由 {{username}} 编辑", + "Message_has_been_edited_by_at": "消息已由 {{username}} 于 {{date}} 编辑", + "Message_has_been_forwarded": "消息已转发", "Message_has_been_pinned": "已固定消息", "Message_has_been_starred": "已标星消息", "Message_has_been_unpinned": "已取消固定消息", "Message_has_been_unstarred": "已取消标星消息", "Message_info": "消息信息", "Message_is_removed": "已移除消息", + "Message_list": "消息列表", "Message_pinning": "固定消息", "Message_removed": "已移除消息", + "Message_request": "消息请求", + "Message_sent": "消息已发送", "Message_sent_by_email": "邮件发送的消息", "Message_starring": "给信息加星标", "Message_too_long": "消息过长", "Message_view_mode_info": "这会改变消息在屏幕上占用的空间。", + "Message_viewed": "消息已查看", + "Message_with_attachment": "带附件的消息", "Messages": "消息", + "Messages_exported_successfully": "消息已成功导出", "Messages_sent": "已发送消息", "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here": "入站 WebHook 的消息会发布到这里。", - "Meta": "Meta", + "Messages_cannot_be_unsent": "消息无法撤回", + "Meta": "元数据", + "Meta_Description": "设置自定义 Meta 属性。", "Meta_custom": "自定义元标记", "Meta_fb_app_id": "Facebook 应用 ID", "Meta_google-site-verification": "谷歌网站验证", @@ -2216,9 +3528,16 @@ "Meta_msvalidate01": "微软网站验证", "Meta_robots": "机器人", "Method": "方法", + "Mic_off": "关闭麦克风", + "Mic_on": "开启麦克风", + "Microphone": "麦克风", + "Microphone_access_not_allowed": "麦克风访问被拒绝,请检查浏览器设置。", "Min_length_is": "最小长度是 %s", "Minimum": "最小值", "Minimum_balance": "最小余额", + "Missing_configuration": "缺少配置", + "Mixed_status": "混合状态", + "Mixed_status_tooltip": "不同工作区实例中的多个应用状态", "Mobex_sms_gateway_address": "Mobex SMS网关地址", "Mobex_sms_gateway_address_desc": "具有指定端口的Mobex服务的IP或主机。例如。`http://192.168.1.1:1401`或`https://www.example.com:1401`", "Mobex_sms_gateway_from_number": "从", @@ -2230,30 +3549,82 @@ "Mobex_sms_gateway_restful_address_desc": "您 Mobex REST API 的 IP 或主机。例:`http://192.168.1.1:8080` 或 `https://www.example.com:8080`", "Mobex_sms_gateway_username": "用户名", "Mobile": "移动", + "Mobile_Description": "定义从移动设备连接工作区的行为。", "Mobile_Push_Notifications_Default_Alert": "默认的移动通知", + "Mobile_apps": "移动应用", + "Moderation": "内容审核", + "Moderation_Action_View_reports": "查看被举报的消息", + "Moderation_Are_you_sure_you_want_to_deactivate_this_user": "除非重新激活,否则用户将无法登录。其被举报的消息将从对应房间永久删除。", + "Moderation_Are_you_sure_you_want_to_delete_all_reported_messages_from_this_user": "该用户的所有被举报消息将从对应房间永久删除,举报将被关闭。", + "Moderation_Are_you_sure_you_want_to_delete_this_message": "此消息将从对应房间永久删除,举报将被关闭。", + "Moderation_Are_you_sure_you_want_to_reset_the_avatar": "重置用户头像将永久移除其当前头像。", + "Moderation_Avatar_reset_success": "头像已重置", + "Moderation_Deactivate_User": "停用用户", + "Moderation_Delete_all_messages": "删除所有消息", "Moderation_Delete_message": "删除消息", + "Moderation_Delete_this_message": "删除此消息", + "Moderation_Dismiss_all_reports": "关闭所有举报", + "Moderation_Dismiss_all_reports_confirm": "所有举报将被删除,被举报消息不会受影响。", + "Moderation_Dismiss_and_delete": "关闭并删除", + "Moderation_Dismiss_reports": "关闭举报", + "Moderation_Dismiss_reports_confirm": "举报将被删除,被举报消息不会受影响。", + "Moderation_Duplicate_messages": "重复消息", + "Moderation_Duplicate_messages_warning": "以下内容可能包含在多个房间发送的相同消息。", + "Moderation_Go_to_message": "转到消息", + "Moderation_Hide_reports": "隐藏举报", + "Moderation_Message_already_deleted": "消息已删除", + "Moderation_Message_context_header": "被举报消息", + "Moderation_Message_deleted": "消息已删除,举报已关闭", + "Moderation_Messages_deleted": "消息已删除,举报已关闭", + "Moderation_Report_date": "举报日期", + "Moderation_Reported_message": "被举报消息", + "Moderation_Reports": "举报", + "Moderation_Reports_all_dismissed": "所有举报已关闭", + "Moderation_Reports_dismissed": "举报已关闭", + "Moderation_Reset_user_avatar": "重置用户头像", + "Moderation_See_messages": "查看消息", + "Moderation_See_reports": "查看举报", + "Moderation_Show_reports": "显示举报", + "Moderation_User_deactivated": "用户已停用", + "Moderation_User_deleted_warning": "发送消息的用户不存在或已被移除。", + "Moderators": "版主", "Monday": "星期一", + "MongoDB": "MongoDB(数据库)", "MongoDB_Deprecated": "MongoDB已弃用", "MongoDB_version_s_is_deprecated_please_upgrade_your_installation": "MongoDB版本 %s 已弃用,请尽快升级安装。", "Mongo_storageEngine": "Mongo 存储引擎", "Mongo_version": "Mongo 版本", "Monitor_added": "已添加监控", "Monitor_history_for_changes_on": "监控更改历史于", + "Monitor_new_and_suspicious_logins": "监控新的和可疑的登录", "Monitor_removed": "已移除监控", "Monitors": "监控", "Monthly_Active_Users": "阅读活跃用户", + "Monthly_active_contacts": "月活跃联系人", "More": "更多", + "More_about_Enterprise_Edition": "了解更多关于企业版", + "More_about_Premium_plans": "了解更多关于高级方案", + "More_actions": "更多操作", "More_channels": "更多频道", "More_direct_messages": "更多私聊", "More_groups": "更多私人组", + "More_options": "更多选项", "More_unreads": "更多未阅读", "Most_popular_channels_top_5": "最受欢迎的频道(前5)", + "Most_recent_requested": "最近请求", + "Most_recent_updated": "最近更新", "Move_beginning_message": "`%s` - 移动到消息的开头", "Move_end_message": "`%s` - 移至消息末尾", + "Move_queue": "移至队列", "Msgs": "消息", + "Multi_line_code": "多行代码", + "Multiple_monolith_instances_alert": "在无有效高级许可证的情况下运行多个实例,部分功能可能无法按预期工作", + "Mute": "静音", "Mute_Focused_Conversations": "静音重点对话", "Mute_Group_Mentions": "静音 @all 和 @here 提及", "Mute_all_notifications": "静音所有通知", + "Mute_and_dismiss": "静音并关闭", + "Mute_microphone": "麦克风静音", "Mute_someone_in_room": "禁止某人在聊天室中发言", "Mute_user": "禁止用户发言", "Muted": "已被禁止发言", @@ -2266,14 +3637,22 @@ "N_new_messages": "%s 条新消息", "Name": "姓名", "Name_Placeholder": "请输入你的名字...", + "Name_cannot_have_spaces": "名称不能包含空格", + "Name_cannot_have_special_characters": "名称不能包含空格或特殊字符", "Name_cant_be_empty": "名字不可以为空", "Name_of_agent": "客服的名称", "Name_optional": "姓名(可选)", + "Navigation": "导航", "Navigation_History": "浏览历史记录", + "Navigation_bar": "导航栏", + "Navigation_bar_description": "导航栏为更高层级的导航,旨在帮助用户快速找到所需内容。其紧凑设计与直观组织可优化屏幕空间,同时便捷访问关键功能与区域。", "Never": "从不", "New": "新", "New_Application": "新应用", "New_Business_Hour": "新的营业时间", + "New_Call": "新通话", + "New_call": "新通话", + "New_Call_Enterprise_Edition_Only": "新通话(仅企业版)", "New_CannedResponse": "新的自动回复", "New_Custom_Field": "新的自定义字段", "New_Department": "新部门", @@ -2282,84 +3661,190 @@ "New_Message_Notification": "新消息通知", "New_Priority": "新优先级", "New_Room_Notification": "新聊天室通知", + "New_SLA_Policy": "新 SLA 策略", "New_Tag": "新标签", "New_Trigger": "新的触发器", "New_Unit": "新单位", "New_chat_in_queue": "新聊天等候处理", "New_chat_priority": "已变更优先级:{{user}} 变更优先级为 {{priority}}", "New_chat_transfer": "新聊天转移:{{transfer}}", + "New_chat_transfer_fallback": "已转接至备用部门:{{fallback}}", + "New_contact": "新联系人", + "New_custom_status": "新自定义状态", "New_discussion": "新讨论", "New_discussion_first_message": "通常, 一个讨论由一个问题开始, 例如 \"如何上传图片?\"", "New_discussion_name": "有意义的讨论房间名称", + "New_E2EE_password": "新的端到端加密(E2EE)密码", "New_integration": "新的集成", "New_line_message_compose_input": "`%s` - 消息编写中的新行", "New_logs": "新日志", "New_messages": "新消息", + "New_messages_cannot_be_sent": "无法发送新消息", + "New_navigation": "增强的导航体验", + "New_navigation_description": "探索我们改进的导航体验,采用清晰范围设计,便于访问所需内容。此变更为未来的导航管理改进奠定基础。", "New_password": "新密码", "New_role": "新角色", + "New_user": "新用户", + "New_user_manually_created": "手动创建的新用户", "New_users": "新用户", + "New_version_available": "有新版本可用", "New_version_available_(s)": "新版本可用(%s)", "New_videocall_request": "新的视频通话请求", "New_visitor_navigation": "新的导航:{{history}}", + "New_voice_call": "新语音通话", + "New_workspace": "新工作区", + "New_workspace_confirmed": "新工作区已确认", "Newer_than": "比...更新", "Newer_than_may_not_exceed_Older_than": "“晚于“不得超过”早于“", + "Next": "下一步", + "Next_image": "下一张图片", "Nickname": "昵称", "Nickname_Placeholder": "输入您的昵称…", "No": "否", "No_Canned_Responses": "无自动回复", + "No_Canned_Responses_Yet": "暂无自动回复", + "No_Canned_Responses_Yet-description": "使用自动回复为常见问题提供快速且一致的答复。", "No_Discussions_found": "未找到讨论", "No_Encryption": "未使用加密", "No_Limit": "无限制", + "No_Referrer": "无 Referrer", + "No_Referrer_When_Downgrade": "降级时不发送 Referrer", + "No_SLA_policies_yet": "暂无 SLA 策略", + "No_SLA_policies_yet_description": "使用 SLA 策略根据预计等待时间调整 Omnichannel 队列顺序。", "No_Threads": "找不到帖子", + "No_agents_yet": "暂无客服", + "No_agents_yet_description": "添加客服与受众互动并提供优化的客户服务。", + "No_app_label_provided": "未提供应用标签", + "No_app_matches": "无匹配应用", + "No_app_matches_for": "未找到匹配应用:", + "No_apps_installed": "未安装应用", "No_available_agents_to_transfer": "没有可用的客服来进行转移", + "No_calls_yet": "暂无通话", + "No_calls_yet_description": "所有通话将显示在此处。", + "No_channels_in_team": "该团队暂无频道", "No_channels_yet": "您尚未加入这个频道。", + "No_channels_yet_description": "与此联系人相关的频道将显示在此处。", + "No_chats_in_progress": "暂无进行中的聊天", + "No_chats_in_progress_description": "分配给你的聊天将显示在此处。", + "No_chats_in_queue": "暂无排队聊天", + "No_chats_in_queue_description": "排队聊天将显示在此处。", + "No_chats_on_hold": "暂无挂起聊天", + "No_chats_on_hold_description": "挂起聊天将显示在此处。", + "No_chats_yet": "暂无聊天", + "No_chats_yet_description": "你的所有聊天将显示在此处。", + "No_comment_provided": "未提供备注", + "No_contacts_yet": "暂无联系人", + "No_contacts_yet_description": "所有联系人将显示在此处。", + "No_content_was_provided": "未提供内容", + "No_custom_fields_yet": "暂无自定义字段", + "No_custom_fields_yet_description": "在联系人或工单详情中添加自定义字段,或在在线聊天注册表单中展示给新访客。", + "No_data_available_for_the_selected_period": "所选时间段无可用数据", "No_data_found": "未找到数据", + "No_data_to_export": "无数据可导出", + "No_departments_yet": "暂无部门", + "No_departments_yet_description": "将客服组织到部门,设置工单转发方式并监控绩效。", "No_direct_messages_yet": "无私聊消息。", + "No_channels_or_discussions": "暂无频道或讨论", + "No_channels_or_discussions_description": "此团队的频道和讨论将显示在此处。", + "No_discussions": "暂无讨论", + "No_discussions_description": "你加入的讨论将显示在此处。", + "No_discussions_channels_filter_description": "该频道的讨论将显示在此处。", + "No_discussions_dms_filter_description": "该用户的讨论将显示在此处。", "No_discussions_yet": "尚无讨论", "No_emojis_found": "未找到颜文字", + "No_feature_to_preview": "没有可预览的功能", + "No_favorite_rooms": "暂无收藏房间", + "No_favorite_rooms_description": "你的收藏房间将显示在此处。", + "No_files_found": "未找到文件", + "No_files_found_to_prune": "未找到可清理的文件", "No_files_left_to_download": "没有需要下载的文件", "No_groups_yet": "您还没有私人组。", + "No_history": "无历史记录", + "No_history_yet": "暂无历史记录", + "No_history_yet_description": "与此联系人的全部消息历史将显示在此处。", + "No_installed_app_matches": "无匹配已安装应用", "No_integration_found": "由提供的ID找不到集成。", "No_livechats": "您没有任何实时聊天会话", + "No_managers_yet": "暂无管理员", + "No_managers_yet_description": "管理员可访问所有 Omnichannel 控制项,并可进行监控与操作。", + "No_marketplace_matches_for": "应用市场无匹配项:", + "No_members_found": "未找到成员", "No_mentions_found": "没有发现任何提及", + "No_mentions": "无提及", + "No_mentions_description": "@{username}、@all、@here 提及以及高亮词将显示在此处。", + "No_message_reports": "无消息举报", "No_messages_found_to_prune": "未找到需修剪的消息", "No_messages_yet": "还没有消息", + "No_monitors_yet": "暂无监控", + "No_monitors_yet_description": "监控对 Omnichannel 具有部分控制权,可查看其分配业务单元的部门分析与活动。", "No_pages_yet_Try_hitting_Reload_Pages_button": "还没有页面。尝试点击“重新加载页面”按钮。", + "No_permission": "无权限", "No_pinned_messages": "未固定的信息", "No_previous_chat_found": "未找到上次的聊天", + "No_private_apps_installed": "未安装私有应用", + "No_phone_number_yet_edit_contact": "暂无电话号码 <1>编辑联系人", + "No_phone_number_available_for_selected_channel": "所选频道无可用电话号码", + "No_release_information_provided": "未提供发布信息", + "No_requested_apps": "暂无请求的应用", + "No_requests": "暂无请求", "No_results_found": "无结果", "No_results_found_for": "未找到结果:", "No_snippet_messages": "没有片段", + "No_spaces_or_special_characters": "不能包含空格或特殊字符", "No_starred_messages": "没有星标消息", "No_such_command": "没有该命令:`/{{command}}`", + "No_tags_yet": "暂无标签", + "No_tags_yet_description": "为工单添加标签,便于组织和查找相关对话。", + "No_triggers_yet": "暂无触发器", + "No_triggers_yet_description": "触发器是使在线聊天小部件打开并自动发送消息的事件。", + "No_templates_available": "暂无模板", + "No_units_yet": "暂无单位", + "No_units_yet_description": "使用单位对部门分组并更好地管理。", + "No_user_reports": "无用户举报", + "No_rooms": "暂无房间", + "No_rooms_description": "@{username}、@all、@here 提及以及高亮词将显示在此处。", "Nobody_available": "无人可用", "Node_version": "Node 版本", "None": "无", "Nonprofit": "非营利", "Normal": "正常", "Not_Available": "不可用", + "Not_answered": "未接听", + "Not_available_for_ABAC_enabled_rooms": "不适用于已启用 ABAC 的房间", "Not_Following": "取消关注", "Not_Imported_Messages_Title": "下列消息未能导入成功", + "Not_Visible_To_Workspace": "对工作区不可见", + "Not_assigned": "未分配", "Not_authorized": "未经授权", + "Not_available_for_this_workspace": "此工作区不可用", "Not_enough_data": "数据不足", "Not_following": "未关注", "Not_found_or_not_allowed": "未找到或者不允许", "Not_in_channel": "不在频道中", + "Not_likely": "不太可能", "Not_started": "尚未开始", "Not_verified": "未验证", + "Notes": "备注", "Nothing": "没有", "Nothing_found": "没有找到", + "Notice_that_public_channels_will_be_public_and_visible_to_everyone": "注意,公共频道将对所有人可见。", "Notification_Desktop_Default_For": "显示桌面通知", + "Notification_Desktop_show_voice_calls": "显示语音通话的桌面通知", "Notification_Push_Default_For": "推送移动通知", "Notification_RequireInteraction": "关闭桌面通知要求用户操作", "Notification_RequireInteraction_Description": "仅当 Chrome 浏览器版本 >50 时适用。使用 *requireInteraction* 参数来无限期显示桌面通知,直到用户与其互动。", + "Notification_volume": "通知音量", + "Notification_volume_hint": "用于消息通知,无论工作区是否打开", "Notifications": "通知", "Notifications_Max_Room_Members": "禁用所有消息通知之前的最大聊天室人数", "Notifications_Max_Room_Members_Description": "所有消息通知被禁用前的聊天室内的最大成员数量。用户仍然可以更改每个房间的设置,以单独接收所有通知。 (0禁用)", "Notifications_Muted_Description": "如果您选择将所有内容静音,除了提及之外,当有新消息时,您不会在列表中看到突出显示的聊天室。屏蔽通知将覆盖通知设置。", "Notifications_Preferences": "通知首选项", + "Notify_Calendar_Events": "通知日历事件", "Notify_active_in_this_room": "通知此房间的活跃用户", "Notify_all_in_this_room": "提醒聊天室中的所有人", + "Now_Its_Visible_For_Everyone": "现在所有人都可见", + "Now_Its_Visible_Only_For_Admins": "现在仅管理员可见", "Num_Agents": "# 客服", "Number_in_seconds": "秒数", "Number_of_events": "事件数量", @@ -2369,7 +3854,12 @@ "Number_of_most_recent_chats_estimate_wait_time": "用于计算预计等待时间的最近聊天数", "Number_of_most_recent_chats_estimate_wait_time_description": "此数值将定义用于计算队列等候时间的近期服务聊天室数量", "Number_of_users_autocomplete_suggestions": "自动建议数量", + "OAuth": "OAuth(开放授权)", "OAuth_Application": "OAuth 应用", + "OAuth_Description": "配置除用户名和密码之外的认证方式。", + "OAuth_Full_Access_Warning": "{{appName}} 将拥有对你账户的完全、无限制访问权限,包括代表你执行任何操作。仅在完全信任此应用时继续。", + "OAuth_button_colors_alert": "更改颜色可能导致不符合 WCAG(Web 内容无障碍指南)要求。请确保新颜色符合推荐的对比度与可读性标准,以保持对所有用户的可访问性。", + "OS": "操作系统", "OS_Arch": "OS 架构", "OS_Cpus": "OS CPU 数", "OS_Freemem": "OS 空闲内存", @@ -2400,86 +3890,207 @@ "Offline_messages": "离线消息", "Offline_success_message": "离线成功消息", "Offline_unavailable": "离线不可用", + "Ok": "确定", "Old Colors": "旧颜色", "Old Colors (minor)": "旧颜色(小)", "Older_than": "早于", + "Omnichannel": "Omnichannel(全渠道)", + "Omnichannel_Agent": "Omnichannel 客服", "Omnichannel_Contact_Center": "Omnichannel 联系中心", + "Omnichannel_Contact_Center_Chats": "Omnichannel 联络中心聊天", + "Omnichannel_Description": "设置 Omnichannel 以集中与客户沟通,无论他们如何与你联系。", "Omnichannel_Directory": "Omnichannel 目录", "Omnichannel_External_Frame": "外部框架", "Omnichannel_External_Frame_Enabled": "已启用外部框架", "Omnichannel_External_Frame_Encryption_JWK": "加密密钥(JWK)", "Omnichannel_External_Frame_Encryption_JWK_Description": "如提供则会使用提供的密钥加密用户的令牌,外部系统需要对数据进行解密才能使用令牌", "Omnichannel_External_Frame_URL": "外部框架 URL", + "Omnichannel_filters": "Omnichannel 筛选器", + "Omnichannel_Ignore_automatic_responses_for_performance_metrics": "性能指标中忽略机器人活动", + "Omnichannel_On_Hold_due_to_inactivity": "因 {{timeout}} 秒内未收到 {{guest}} 回复,聊天已自动挂起", + "Omnichannel_On_Hold_manually": "聊天已由 {{user}} 手动挂起", + "Omnichannel_Reports_Agents_Empty_Subtitle": "该图表显示接收对话量最高的客服。", + "Omnichannel_Reports_Channels_Empty_Subtitle": "该图表显示使用最多的渠道。", + "Omnichannel_Reports_Departments_Empty_Subtitle": "该图表显示接收对话最多的部门。", "Omnichannel_Reports_Status_Closed": "已关闭", + "Omnichannel_Reports_Status_Empty_Subtitle": "对话开始后该图表将更新。", "Omnichannel_Reports_Status_Open": "打开", + "Omnichannel_Reports_Summary": "获取运营洞察并导出指标。", + "Omnichannel_Reports_Tags_Empty_Subtitle": "该图表显示最常用的标签。", + "Omnichannel_actions": "Omnichannel 操作", + "Omnichannel_allow_force_close_conversations": "强制关闭对话 API", + "Omnichannel_allow_force_close_conversations_Description": "允许客服和管理员通过 API 强制关闭对话。", + "Omnichannel_allow_force_close_conversations_alert": "仅在工作区存在房间状态无效问题时启用。", + "Omnichannel_allow_visitors_to_close_conversation": "允许访客结束对话", + "Omnichannel_allow_visitors_to_close_conversation_Description": "禁用后,访客将无法通过 UI 或 API 结束进行中的对话。", "Omnichannel_appearance": "Omnichannel 外观", + "Omnichannel_calculate_dispatch_service_queue_statistics": "计算并下发 Omnichannel 等待队列统计", + "Omnichannel_calculate_dispatch_service_queue_statistics_Description": "处理并下发等待队列统计信息,如排队位置与预计等待时间。若未使用 *Livechat channel*,建议禁用该设置以避免服务器执行不必要的处理。", + "Omnichannel_chat_closed_due_to_inactivity": "因 {{timeout}} 秒内未收到 {{guest}} 回复,聊天已自动关闭", "Omnichannel_contact_manager_routing": "分配新的对话至联系人管理员", "Omnichannel_contact_manager_routing_Description": "在聊天开始时如果联系人管理员在线,此设置将聊天分配给已分配的联系人管理员。", + "Omnichannel_enable_department_removal": "启用部门删除", + "Omnichannel_enable_department_removal_alert": "部门删除后无法恢复,建议改为归档部门。", + "Omnichannel_hide_conversation_after_closing": "关闭后隐藏对话", + "Omnichannel_hide_conversation_after_closing_description": "关闭对话后将重定向到主页。", + "Omnichannel_max_fallback_forward_depth": "备用转发部门最大深度", + "Omnichannel_max_fallback_forward_depth_Description": "当目标部门设置了备用转发部门时,房间转接的最大跳转次数。达到上限后,聊天将不再转接并停止流程。根据配置情况,设置较高的数值可能导致性能问题。", + "Omnichannel_onHold_Chat": "将聊天挂起", + "Omnichannel_on_hold_chat_automatically": "收到 {{guest}} 的新消息后,聊天已从挂起中自动恢复", + "Omnichannel_on_hold_chat_resumed": "挂起聊天已恢复:{{comment}}", + "Omnichannel_on_hold_chat_resumed_manually": "{{user}} 手动从挂起中恢复了聊天", + "Omnichannel_placed_chat_on_hold": "聊天已挂起:{{comment}}", + "Omnichannel_quick_actions": "Omnichannel 快捷操作", + "Omnichannel_sorting_disclaimer": "Omnichannel 对话按 {{sortingMechanism}} 排序,编辑房间以应用。", + "Omnichannel_transcript_email": "通过邮件发送聊天记录。", + "Omnichannel_transcript_pdf": "将聊天记录导出为 PDF。", "On": "打开", + "On_All_Contacts": "适用于所有联系人", + "On_Hold": "挂起", + "On_Hold_Chats": "挂起聊天", + "On_Hold_conversations": "挂起的对话", + "Once": "一次", "Online": "在线", "Only_Members_Selected_Department_Can_View_Channel": "只有所选部门的成员可以查看此频道的聊天", "Only_On_Desktop": "桌面模式(仅在桌面上输入时发送)", + "Only_admins_can_perform_this_setup": "仅管理员可执行此设置", + "Only_authorized_users_can_react_to_messages": "仅授权用户可对消息作出反应", "Only_authorized_users_can_write_new_messages": "只有授权用户可以发送消息", "Only_from_users": "仅修剪这些用户的内容(留空以修剪每个人的内容)", + "Only_invited_people": "仅受邀人员可加入", + "Only_invited_users_can_acess_this_channel": "仅受邀用户可访问该频道", + "Only_people_with_permission_can_send_messages_here": "只有有权限的人才能在此发送消息", "Only_works_with_chrome_version_greater_50": "仅适用于版本在 50 以上的谷歌浏览器(Chrome)", "Only_you_can_see_this_message": "只有你能看到这条信息", "Oops!": "哎呀", "Oops_page_not_found": "糟糕,页面未找到", "Open": "打开", + "Open-source_conference_call_solution": "开源会议通话解决方案。", "Open_Days": "开放日", + "Open_Dialpad": "打开拨号盘", + "Open_dialpad": "打开拨号盘", "Open_Livechats": "正在进行的聊天", + "Open_Outlook": "打开 Outlook", + "Open_call": "打开通话", + "Open_call_in_new_tab": "在新标签页打开通话", "Open_channel_user_search": "`%s` - 打开频道/用户搜索", + "Open_chat": "打开聊天", "Open_conversations": "活跃的会话", "Open_days_of_the_week": "每周开放日", + "Open_directory": "打开目录", + "Open_settings": "打开设置", + "Open_sidebar": "打开侧边栏", "Open_thread": "开启讨论串", "Opened": "已开启", "Opened_in_a_new_window": "在新窗口中打开。", "Opens_a_channel_group_or_direct_message": "打开频道、组或私聊", + "Operating_withing_plan_limits": "在方案限制内运行", + "Optional": "可选", "Options": "选项", + "Or_Copy_And_Paste_This_URL_Into_A_Tab_Of_Your_Browser": "或将此 URL 复制并粘贴到浏览器标签页中", "Or_talk_as_anonymous": "或者匿名讲话", "Order": "订购", "Organization_Email": "组织电邮", "Organization_Info": "组织信息", "Organization_Name": "机构名称", "Organization_Type": "组织类型", + "Origin": "来源", + "Origin_When_Cross_Origin": "跨域时的 Origin", "Original": "原版的", "Other": "其他", "Others": "其他", + "Outbound_message": "外发消息", + "Outbound_message_sent_to__name__": "外发消息已发送至:{{name}}", + "Outbound_message_not_sent": "外发消息未发送。", + "Outbound_message_department_hint": "将回复分配给部门。", + "Outbound_message_agent_hint": "留空则由指定部门的任意客服处理回复。", + "Outbound_message_agent_hint_no_permission": "你无权限分配客服,回复将分配给部门。", + "Out_of_seats": "席位不足", + "Outdated": "已过时", + "Outgoing": "外呼", + "Outgoing_voice_call": "外呼语音通话", "Outgoing_WebHook": "出站 WebHook", "Outgoing_WebHook_Description": "实时获取Rocket.Chat数据。", + "Outlook_Calendar": "Outlook 日历", "Outlook_Calendar_Enabled": "已启用", + "Outlook_Calendar_Exchange_Url": "Exchange URL(服务器地址)", + "Outlook_Calendar_Exchange_Url_Description": "EWS API 的默认主机 URL。", + "Outlook_Calendar_Outlook_Url": "Outlook URL(访问地址)", + "Outlook_Calendar_Outlook_Url_Description": "用于启动 Outlook Web 应用的默认 URL。", + "Outlook_Calendar_Url_Mapping": "域名到 Outlook URL 映射", + "Outlook_Calendar_Url_Mapping_Description": "将日历域名映射到 Outlook URL。若有多个域名并希望为每个域名使用不同的 Outlook URL,此设置很有用。", + "Outlook_Sync_Failed": "加载 Outlook 事件失败。", + "Outlook_Sync_Success": "Outlook 事件已同步。", + "Outlook_authentication": "Outlook 认证", + "Outlook_authentication_description": "禁用此项以清除本机保存的 Outlook 凭据。", + "Outlook_authentication_disabled": "Outlook 认证已禁用", + "Outlook_calendar": "Outlook 日历", + "Outlook_calendar_event": "Outlook 日历事件", + "Outlook_calendar_settings": "Outlook 日历设置", "Output_format": "输出格式", + "Override_Destination_Channel": "允许在请求体参数中覆盖目标频道", "Override_URL_to_which_files_are_uploaded_This_url_also_used_for_downloads_unless_a_CDN_is_given": "重写 URL 为文件上传地址。如果没有设置 CDN,url 也会被当作下载链接。", + "Owner": "所有者", + "Owners": "所有者", + "PDF": "PDF(文档)", + "PID": "PID(进程ID)", "Page_URL": "页面地址", + "Page_not_exist_or_not_permission": "页面不存在或你可能没有访问权限", + "Page_not_found": "页面未找到", "Page_title": "页面标题", + "Pages": "页面", + "Pages_and_actions": "页面与操作", + "Paid_Apps": "付费应用", "Parent_channel_doesnt_exist": "频道不存在", "Parent_channel_or_team": "父频道或者组", + "Participants": "参与者", "Password": "密码", "Password_Change_Disabled": "您的 Rocket.Chat 管理员已经禁止了修改密码的功能。 ", "Password_Changed_Description": "您可以使用以下占位符: \n - `[password]`为暂时密码。 \n - `[name]`、`[fname]`、`[lname]` 为用户的全名、名和姓。 \n - `[email]`为用户的邮箱 \n - `[Site_Name]`和[Site_URL]为应用名和网址。 ", "Password_Changed_Email_Subject": "[Site_Name] - 密码已更改", + "Password_History": "密码历史", + "Password_History_Amount": "密码历史长度", + "Password_History_Amount_Description": "用于防止用户重复使用的最近密码数量。", "Password_Policy": "密码策略", + "Password_Policy_Aria_Description": "以下列出密码要求校验项", "Password_changed_section": "密码已更改", "Password_changed_successfully": "密码更改成功", + "Password_must_have": "密码必须包含:", + "Password_must_meet_the_complexity_requirements": "密码必须满足复杂度要求。", "Password_to_access": "访问密码", "Passwords_do_not_match": "密码不匹配", "Past_Chats": "过去的聊天", "Paste": "粘贴", "Paste_error": "从剪贴板读取时出错", "Paste_here": "在此粘贴", + "Pause": "暂停", "Payload": "有效载荷", "Peer_Password": "对等端密码", + "Pending": "待处理", + "Pending Avatars": "待处理头像", + "Pending Files": "待处理文件", + "Pending_action": "待处理操作", "People": "人", + "People_can_only_join_by_being_invited": "仅可通过邀请加入", "Permalink": "永久链接", "Permissions": "权限", + "Person_Or_Channel": "用户或频道", "Personal_Access_Tokens": "个人访问令牌", + "Pexip_Enterprise_only": "Pexip(仅企业版)", + "Pexip_Premium_only": "Pexip(仅高级版)", + "Pharmaceutical": "制药行业", "Phone": "电话", + "Phone_Number": "电话号码", "Phone_already_exists": "电话已存在", + "Phone_call": "电话通话", "Phone_number": "电话号码", + "Phone_number_copied": "电话号码已复制", "Pin": "固定", "Pin_Message": "固定消息", "Pinned_Messages": "已固定的信息", "Pinned_a_message": "已固定消息:", + "Pinned_messages_are_visible_to_everyone": "置顶消息对所有人可见", + "Pinned_messages_unavailable_for_federation": "联邦房间不支持置顶消息。", "PiwikAdditionalTrackers": "额外的Piwik网站", "PiwikAdditionalTrackers_Description": "如果您想将相同的数据跟踪到不同的网站,请输入以下格式的Piwik网站网址和SiteID:`[{ \"trackerURL\" : \"https://my.piwik.domain3/\", \"siteId\" : 15 },{ \"trackerURL\" : \"https://my.piwik.domain2/\", \"siteId\" : 42 } ]`", "PiwikAnalytics_cookieDomain": "所有子域", @@ -2493,9 +4104,16 @@ "Placeholder_for_email_or_username_login_field": "登录窗 E-mail 或用户的占位符", "Placeholder_for_password_login_confirm_field": "密码登录字段的确认占位符", "Placeholder_for_password_login_field": "登录窗中密码的占位符", + "Placeholder": "占位符", + "Plan_limits_reached": "已达到方案限制", + "Platform_Linux": "Linux(系统)", + "Platform_Mac": "Mac(系统)", + "Platform_Windows": "Windows(系统)", + "Play": "播放", "Please_add_a_comment": "请添加一条评论", "Please_add_a_comment_to_close_the_room": "请添加评论并关闭聊天室", "Please_answer_survey": "请花费几分钟来反馈这次交谈的体验", + "Please_enter_E2EE_password": "请输入端到端加密(E2EE)密码", "Please_enter_usernames": "请输入用户名...", "Please_enter_value_for_url": "请输入头像的 URL。", "Please_enter_your_new_password_below": "请在下方输入新密码:", @@ -2511,39 +4129,80 @@ "Please_select_an_user": "请选择一个用户", "Please_select_enabled_yes_or_no": "请选择要启用的项", "Please_select_visibility": "请选择可见度", + "Please_try_again": "请重试。", "Please_wait": "请稍候", "Please_wait_activation": "请稍候,这可能需要一些时间。", "Please_wait_while_your_account_is_being_deleted": "正在删除您的帐号,请稍候……", "Please_wait_while_your_profile_is_being_saved": "请稍候,正在保存您的个人资料……", + "Policies": "策略", "Pool": "池", "Port": "端口", "Post_as": "以该身份发送", "Post_to": "发布至", "Post_to_Channel": "发布至频道", "Post_to_s_as_s": "向 %s 推送,以 %s 的身份", + "Powered_by_JoyPixels": "由 JoyPixels 提供", + "Powered_by_RocketChat": "由 Rocket.Chat 提供", "Preferences": "偏好设置", "Preferences_saved": "偏好设置已保存", + "Premium": "高级版", + "Premium_Departments_description_free_trial": "社区版工作区只能创建一个部门。立即开始高级版免费试用以创建多个部门!", + "Premium_Departments_description_upgrade": "社区版工作区只能创建一个部门。升级到高级方案以移除限制并增强工作区能力。", + "Premium_Departments_title": "将客户分配到队列并提升客服效率", + "Premium_License": "高级许可证", + "Premium_License_alert": "如果移除许可证,需要重启工作区才能生效。
如果工作区已连接云端,请先在云端取消许可证,否则云端会在重启期间再次向工作区提供许可证。", + "Premium_and_unlimited_apps": "高级版与无限应用", + "Premium_cap_description": "高级方案没有在线状态服务上限。", + "Premium_capabilities": "高级功能", + "Premium_capability": "高级功能", + "Premium_omnichannel_capabilities": "高级 Omnichannel 功能", + "Premium_only": "仅高级版", + "Please_provide_a_name_for_your_token": "请为你的令牌提供名称", "Preparing_data_for_import_process": "正在为导入过程准备数据", "Preparing_list_of_channels": "正在准备频道列表", "Preparing_list_of_messages": "正在准备消息列表", "Preparing_list_of_users": "正在准备用户列表", "Presence": "存在", + "Presence_broadcast_disabled": "在线状态广播已在内部禁用", + "Presence_broadcast_disabled_Description": "显示在线状态广播是否被自动禁用。当没有高级许可证且并发连接超过 200 时,可能会发生此情况。", + "Presence_service": "在线状态服务", + "Presence_service_cap": "在线状态服务上限", + "Preview": "预览", + "Previous_image": "上一张图片", "Previous_month": "前一个月", "Previous_week": "前一个星期", + "Price": "价格", "Priorities": "优先级", + "Priorities_restored": "优先级已恢复", "Priority": "优先级", "Priority_removed": "已移除优先级", + "Priority_saved": "优先级已保存", "Privacy": "隐私条款", "Privacy_Policy": "隐私政策", + "Privacy_policy": "隐私政策", + "Privacy_summary": "隐私摘要", "Private": "私人", + "Private_Apps": "私有应用", + "Private_Apps_Count_Enabled": { + "other": "{{count}} private apps enabled" + }, "Private_Channel": "私人频道", "Private_Channels": "私人频道", "Private_Chats": "私人聊天", + "Private_Discussion": "私有讨论", "Private_Group": "私人组", "Private_Groups": "私人组", "Private_Groups_list": "私人组列表", "Private_Team": "私人团队", + "Private_app_install_modal_content": "社区版工作区无法启用私有应用。你可以上传该应用,但它将被禁用。", + "Private_app_install_modal_title": "上传被禁用的私有应用", + "Private_apps": "私有应用", + "Private_apps_are_side-loaded": "私有应用为侧载,无法在应用市场获取。", + "Private_apps_premium_message": "私有应用仅可在高级方案中启用", + "Private_apps_upgrade_empty_state_description": "通过私有应用按需定制 Rocket.Chat。", + "Private_apps_upgrade_empty_state_title": "升级以解锁私有应用", "Private_channels": "私人频道", + "Proceed_with_caution": "请谨慎操作", "Productivity": "产出", "Profile": "资料", "Profile_details": "档案详细信息", @@ -2574,8 +4233,11 @@ "Purchase_for_price": "以 $%s 购买", "Purchased": "已购买", "Push": "推送", + "Push_Description": "为使用移动设备的工作区成员启用并配置推送通知。", "Push_Notifications": "推送通知", + "Push_Setting_Legacy_Warning": "旧版通知提供商将在 2024 年 6 月 20 日后弃用。参见:https://firebase.google.com/support/faq#fcm-23-deprecation", "Push_Setting_Requires_Restart_Alert": "更改此值需要重新启动 Rocket.Chat 。", + "Push_UseLegacy": "使用旧版通知提供商", "Push_apn_cert": "APN 证书", "Push_apn_dev_cert": "APN Dev 证书 ", "Push_apn_dev_key": "APN Dev 密钥", @@ -2589,8 +4251,10 @@ "Push_gateway_description": "可使用多行来指定多个网关", "Push_gcm_api_key": "GCM API key", "Push_gcm_project_number": "GCM 项目数字", + "Push_google_api_credentials": "Google FCM API 凭据", "Push_production": "生产", "Push_request_content_from_server": "使用收据上的服务器获取完整消息内容", + "Push_request_content_from_server_Description": "不要将消息内容包含在推送数据中以避免暴露给 Apple/Google,而是仅推送消息 ID。移动端客户端会动态从服务器获取内容并在显示前更新通知。若发生 API 错误,将显示“你有一条新消息”。该设置仅在高级方案中生效。", "Push_show_message": "在通知中显示消息", "Push_show_username_room": "在通知中显示频道/群组/用户名", "Push_test_push": "测试", @@ -2598,33 +4262,97 @@ "Query_description": "决定向哪些用户发送电子邮件的额外条件。取消订阅的用户会自动从请求中删除。这必须是有效的 JSON 数据,例如:`{\"createdAt\":{\"$gt\":{\"$date\": \"2015-01-01T00:00:00.000Z\"}}}`。", "Query_is_not_valid_JSON": "查询非合法 JSON", "Queue": "队列", + "Queue_Time": "排队时间", + "Queue_delay_timeout": "队列处理延迟超时", + "Queue_management": "队列管理", + "Queued": "已排队", + "Queues": "队列", "Quote": "引用", "Random": "随机", + "Rate Limiter": "速率限制器", + "Rate Limiter_Description": "控制服务器发送或接收请求的速率,以防止网络攻击和爬取。", + "Rate_Limiter_Limit_RegisterUser": "用于注册用户的速率限制器默认调用次数", + "Rate_Limiter_Limit_RegisterUser_Description": "在 API 速率限制器部分定义的时间范围内,允许用于用户注册端点(REST 与实时 API)的默认调用次数。", "React_when_read_only": "允许回应", "React_when_read_only_changed_successfully": "当只读变更成功时允许回应", + "React_with__reaction__": "已使用 {{reaction}} 作出反应", "Reacted_with": "的反应是", "Reactions": "反应", + "Read_Receipts": "已读回执", "Read_by": "阅读", "Read_only": "只读", "Read_only_changed_successfully": "只读更改成功", "Read_only_channel": "只读频道", + "Read_only_field_hint_disabled": "任何人都可以发送新消息", + "Read_only_field_hint_enabled": "仅 {{roomType}} 的所有者可以发送新消息", "Read_only_group": "只读组", + "Readability": "可读性", "RealName_Change_Disabled": "您的Rocket.Chat管理员已禁用更改名称", "Real_Estate": "房地产", "Real_Time_Monitoring": "实时监控", + "Reason": "原因", "Reason_To_Join": "加入的理由", + "Reason_for_joining": "加入原因", + "Reason_for_report": "举报原因", "Receive_Group_Mentions": "接收 @all 和 @here 提及", + "Receive_Login_Detection_Emails": "接收登录检测邮件", + "Receive_Login_Detection_Emails_Description": "当检测到你的账户有新登录时接收邮件。", "Receive_alerts": "接收提醒", + "Receive_login_notifications": "接收登录通知", + "Recent": "最近", "Recent_Import_History": "最近的导入历史", + "Reconnecting": "正在重新连接", "Record": "记录", + "Records": "记录", + "Recipient": "收件人", "Redirect_URI": "跳转 URI", + "Redirect_URL_does_not_match": "跳转 URL 不匹配", "Refresh": "刷新", "Refresh_keys": "刷新 key", + "Refresh_logs": "刷新日志", "Refresh_oauth_services": "刷新OAuth服务", "Refresh_your_page_after_install_to_enable_screen_sharing": "安装完成后需要刷新页面才能支持屏幕共享", + "Refreshing": "正在刷新", "Regenerate_codes": "重新生成代码", "Regexp_validation": "正则表达式验证", "Register": "注册一个新帐号", + "RegisterWorkspace_Button": "注册工作区", + "RegisterWorkspace_Connection_Error": "连接时发生错误", + "RegisterWorkspace_Disconnect_Error": "断开连接时发生错误", + "RegisterWorkspace_Disconnect_Subtitle": "断开工作区连接将导致以下内容丢失", + "RegisterWorkspace_Features_Marketplace_Description": "在此工作区安装 Rocket.Chat 应用市场应用。", + "RegisterWorkspace_Features_Marketplace_Disconnect": "将无法再安装应用。", + "RegisterWorkspace_Features_Marketplace_Title": "应用市场", + "RegisterWorkspace_Features_MobileNotifications_Description": "允许工作区成员在移动设备上接收通知。", + "RegisterWorkspace_Features_MobileNotifications_Disconnect": "工作区成员将无法在移动设备上接收通知。", + "RegisterWorkspace_Features_MobileNotifications_Title": "移动推送通知", + "RegisterWorkspace_Features_Omnichannel_Description": "通过全球最受欢迎的社交渠道与受众沟通,无论他们身在何处。", + "RegisterWorkspace_Features_Omnichannel_Disconnect": "Omnichannel 功能将不再可用。", + "RegisterWorkspace_Features_Omnichannel_Title": "Omnichannel(全渠道)", + "RegisterWorkspace_Features_ThirdPartyLogin_Description": "允许工作区成员使用一组第三方应用登录。", + "RegisterWorkspace_Features_ThirdPartyLogin_Disconnect": "第三方登录选项将不再可用。", + "RegisterWorkspace_Features_ThirdPartyLogin_Title": "第三方登录", + "RegisterWorkspace_NotConnected_Subtitle": "连接此工作区即可获得", + "RegisterWorkspace_NotConnected_Title": "工作区已断开连接", + "RegisterWorkspace_NotRegistered_Description": "注册工作区的好处", + "RegisterWorkspace_NotRegistered_Subtitle": "注册此工作区即可获得", + "RegisterWorkspace_NotRegistered_Title": "工作区未注册", + "RegisterWorkspace_Registered_Benefits": "注册后可自动更新许可证、接收关键漏洞通知并访问 Rocket.Chat Cloud 服务。不会向 Rocket.Chat 共享任何敏感的工作区数据。", + "RegisterWorkspace_Registered_Description": "以下服务可用", + "RegisterWorkspace_Registered_Subtitle": "由于此工作区已注册,以下内容可用", + "RegisterWorkspace_Setup_Email_Verification": "请验证下方安全码与邮件中的一致。", + "RegisterWorkspace_Setup_Have_Account_Subtitle": "输入你的 Cloud 账户邮箱以将此工作区关联到你的账户。", + "RegisterWorkspace_Setup_Have_Account_Title": "已有账户?", + "RegisterWorkspace_Setup_Label": "Cloud 账户邮箱", + "RegisterWorkspace_Setup_No_Account_Subtitle": "输入你的邮箱以创建新的 Cloud 账户并关联此工作区。", + "RegisterWorkspace_Setup_No_Account_Title": "还没有账户?", + "RegisterWorkspace_Setup_Steps": "第 {{step}} 步,共 {{numberOfSteps}} 步", + "RegisterWorkspace_Setup_Subtitle": "要注册此工作区,需要将其关联到 Rocket.Chat Cloud 账户。", + "RegisterWorkspace_Syncing_Complete": "同步完成", + "RegisterWorkspace_Syncing_Error": "同步工作区时发生错误", + "RegisterWorkspace_Token_Step_Two": "复制令牌并粘贴到下方。", + "RegisterWorkspace_Token_Title": "使用令牌注册工作区", + "RegisterWorkspace_with_email": "使用邮箱注册工作区", "Register_Server": "注册服务器", "Register_Server_Info": "使用 Rocket.Chat 科技集团提供的预配置网关和代理。", "Register_Server_Opt_In": "产品和安全更新", @@ -2639,25 +4367,49 @@ "Register_Server_Standalone_Service_Providers": "使用服务提供商创建帐户", "Register_Server_Standalone_Update_Settings": "更新预配置的设置", "Register_Server_Terms_Alert": "请同意条款以完成注册", + "Register_new_account": "注册新帐号", "Registration": "注册", "Registration_Succeeded": "注册成功", + "Registration_Token": "注册令牌", + "Registration_status": "注册状态", "Registration_via_Admin": "通过管理员注册", "Regular_Expressions": "正则表达式", + "Reject_call": "拒绝通话", + "Reject_invitation": "拒绝邀请", + "Reject_dm_invitation_description": "你将拒绝加入与 {{username}} 的对话邀请。此操作无法撤销。", + "Reject_channel_invitation_description": "你将拒绝来自 {{username}} 的加入 {{roomName}} 邀请。此操作无法撤销。", "Release": "发布", + "Releases": "发布记录", "Religious": "宗教", "Reload": "刷新", "Reload_Pages": "重新加载页面", + "Reload_page": "重新加载页面", + "Reload_to_update": "重新加载以更新", + "Remember_my_credentials": "记住我的凭据", "Remove": "移除", "Remove_Admin": "移除管理员", + "Remove_Channel_Links": "移除频道链接", + "Remove_RocketChat_Watermark": "移除 Rocket.Chat 水印", + "Remove_RocketChat_Watermark_InfoText": "当付费许可证生效时水印将自动移除。", "Remove_as_leader": "作为领导者移除", "Remove_as_moderator": "取消主持", "Remove_as_owner": "移除“所有者”身份", "Remove_custom_oauth": "删除自定义 OAuth ", + "Remove_email": "移除邮箱", + "Remove_extension": "移除扩展", "Remove_from_room": "从聊天室中删除", + "Remove_from_team": "从团队中移除", "Remove_last_admin": "删除上个管理员", + "Remove_last_character": "移除最后一个字符", + "Remove_phone": "移除电话", "Remove_someone_from_room": "将某人从聊天室中删除", "Removed": "已移除", "Removed_User": "已移除用户", + "Removed__roomName__from_the_team": "已将 #{{roomName}} 从此团队移除", + "Removed__roomName__from_this_team": "已移除 #{{roomName}} 从此团队", + "Removed__username__from_team": "已移除 @{{user_removed}} 从此团队", + "Removed__username__from_the_team": "已将 @{{user_removed}} 从此团队移除", + "Renews_DATE": "续订于 {{date}}", "Replay": "重放", "Replied_on": "回复于", "Replies": "回复", @@ -2668,29 +4420,51 @@ "Reply_via_Email": "通过电子邮件回复", "Report": "举报", "Report_Abuse": "举报滥用", + "Report_User": "举报用户", + "Report_message": "举报消息", + "Report_has_been_sent": "举报已发送", + "Report_reason": "举报原因", "Report_sent": "已发送举报", + "Reported_Messages": "已举报的消息", + "Reported_Users": "已举报的用户", "Reporting": "报告", + "Reports": "报告", + "Request": "请求", "Request_comment_when_closing_conversation": "在会话结束时请求评价", "Request_comment_when_closing_conversation_description": "在启用时,客服将需要在会话结束时设置备注。", "Request_tag_before_closing_chat": "在结束会话之前请求标签", + "Requested": "已请求", "Requested_At": "请求于", "Requested_By": "请求由", + "Requested_apps_will_appear_here": "请求的应用将显示在此处", + "Requests": "请求", "Require": "要求", + "Require_Two_Factor_Authentication": "要求双因素认证", "Require_all_tokens": "要求所有令牌", "Require_any_token": "需要任何令牌", "Require_password_change": "要求修改密码", "Required": "必需", + "Required_action": "所需操作", + "Required_field": "{{field}} 为必填项", + "Requires_subscription_add-on": "需要订阅附加组件", "Resend_verification_email": "重新发送确认邮件", + "Resend_welcome_email": "重新发送欢迎邮件", "Reset": "重置", "Reset_Connection": "重置连接", + "Reset_E2EE_password": "重置 E2EE 密码", + "Reset_E2EE_password_description": "重置后将注销,并在重新登录时生成新的 E2EE 密码。你将重新获得对有在线成员的加密房间的访问权限,但无法访问没有任何成员在线的房间。", "Reset_TOTP": "重置 TOTP", "Reset_password": "重设密码", + "Reset_priorities": "重置优先级", "Reset_section_settings": "重置区块为默认", + "Resize": "调整大小", "Responding": "回复", "Response_description_post": "空的请求体或空文本请求体将被直接忽略。非-200 响应将以合理次数重试。响应将以上方的别名和头像进行发布。您可以参考上方示例覆盖这些信息。", "Response_description_pre": "如果处理者希望将响应发布回频道,应当返回以下 JSON 作为响应体:", "Restart": "重启", "Restart_the_server": "重启服务器", + "Results": "结果", + "Resume": "继续", "Retail": "零售", "RetentionPolicy": "保留政策", "RetentionPolicyRoom_Enabled": "自动修剪旧消息", @@ -2703,8 +4477,10 @@ "RetentionPolicy_Advanced_Precision_Cron": "使用高级保留策略任务计划", "RetentionPolicy_Advanced_Precision_Cron_Description": "使用 cron 任务表达式定义修剪定时器运行频度,将此设置为更精确的值会使具有快速保留计时器的频道更好地工作,但对大型社区可能会消耗额外的处理能力。", "RetentionPolicy_AppliesToChannels": "适用于频道", + "RetentionPolicy_AppliesToChannels_Description": "包括公共频道、讨论和团队。", "RetentionPolicy_AppliesToDMs": "适用于私聊消息", "RetentionPolicy_AppliesToGroups": "适用于私人组", + "RetentionPolicy_AppliesToGroups_Description": "包括私人频道、讨论和团队。", "RetentionPolicy_Description": "自动修剪 Rocket.Chat 实例中的旧消息。", "RetentionPolicy_DoNotPruneDiscussion": "不修剪讨论消息", "RetentionPolicy_DoNotPrunePinned": "不修剪已固定的消息", @@ -2720,10 +4496,29 @@ "RetentionPolicy_MaxAge_Groups": "私人组中的消息最大年龄", "RetentionPolicy_Precision": "定时精度", "RetentionPolicy_Precision_Description": "修剪计时器应该多久运行一次。将此设置为更精确的值会使具有快速保留计时器的频道更好地工作,但对大型社区可能会消耗额外的处理能力。", + "RetentionPolicy_RoomWarning_FilesOnly_NextRunDate": "早于 {{maxAge}} 的文件将于 {{nextRunDate}} 被修剪。", + "RetentionPolicy_RoomWarning_NextRunDate": "早于 {{maxAge}} 的消息将于 {{nextRunDate}} 被修剪", + "RetentionPolicy_RoomWarning_UnpinnedFilesOnly_NextRunDate": "早于 {{maxAge}} 的未固定文件将于 {{nextRunDate}} 被修剪。", + "RetentionPolicy_RoomWarning_Unpinned_NextRunDate": "早于 {{maxAge}} 的未固定消息将于 {{nextRunDate}} 被修剪。", + "RetentionPolicy_TTL_Channels": "修剪早于以下时间的消息", + "RetentionPolicy_TTL_DMs": "修剪早于以下时间的消息", + "RetentionPolicy_TTL_Groups": "修剪早于以下时间的消息", + "Retention_policy_warning_banner": "保留策略警告横幅", + "Retention_policy_warning_callout": "保留策略警告提示", "Retention_setting_changed_successfully": "保留策略设置已成功更改", + "Retry": "重试", + "Retrying": "正在重试", "Retry_Count": "重试计数", "Return_to_home": "返回主页", "Return_to_previous_page": "返回前一页", + "Return_to_the_queue": "返回队列", + "Review": "审查", + "Review_contact": "审查联系人", + "Review_devices": "查看设备连接的时间与地点", + "Revoke_invitation": "撤销邀请", + "Right": "右侧", + "Ringing": "响铃中", + "Ringtones_and_visual_indicators_notify_people_of_incoming_calls": "铃声和视觉提示会提醒来电。", "Robot_Instructions_File_Content": "Robots.txt 文件内容", "Rocket_Chat_Alert": "Rocket.Chat 提醒", "Role": "角色", @@ -2732,6 +4527,7 @@ "Role_removed": "已移除角色", "Roles": "角色", "Room": "聊天室", + "Room_Edit": "编辑房间", "Room_Info": "聊天室信息", "Room_Status_Open": "打开", "Room_announcement_changed_successfully": "公告已成功修改", @@ -2742,10 +4538,18 @@ "Room_default_change_to_private_will_be_default_no_more": "将默认频道更改为私人组将使其不再是默认频道。您想继续吗?", "Room_description_changed_successfully": "聊天室描述修改成功", "Room_has_been_archived": "聊天室已归档", + "Room_has_been_converted": "房间已转换", + "Room_has_been_created": "房间已创建", + "Room_has_been_removed": "房间已移除", "Room_has_been_unarchived": "已撤销Room归档", + "Room_members_list": "成员列表", "Room_name_changed": " {{user_by}} 将聊天室名称更改为: {{room_name}} ", "Room_name_changed_successfully": "聊天室名称变更成功", + "Room_name_changed_to": "将房间名称更改为 {{room_name}}", + "Room_not_exist_or_not_permission": "房间不存在或你没有访问权限", "Room_not_found": "未找到聊天室", + "Room_notifications_on": "{{roomName}} 通知已开启", + "Room_notifications_off": "{{roomName}} 通知已关闭", "Room_password_changed_successfully": "聊天室密码修改成功", "Room_topic_changed_successfully": "聊天室话题已成功修改", "Room_type_changed_successfully": "聊天室类型已成功修改", @@ -2755,17 +4559,20 @@ "Room_uploaded_file_list": "文件列表", "Room_uploaded_file_list_empty": "没有任何文件。", "Rooms": "聊天室", + "Rooms_added_successfully": "房间添加成功", + "Root": "Root(根)", "Routing": "路由", "Run_only_once_for_each_visitor": "每位访客仅运行一次", "Running_Instances": "正在运行的实例", "Runtime_Environment": "运行环境", - "SAML": "SAML", + "SAML": "SAML(安全断言标记语言)", "SAML_Allowed_Clock_Drift": "允许来自标识符提供者的时间偏移", "SAML_Allowed_Clock_Drift_Description": "标识符提供者的时间可能相对您的系统时间向前偏移。您可以允许少量的时间偏移。值必须以毫秒数提供。值将加到相应验证时的当前时间。", "SAML_AuthnContext_Template": "AuthnContext 模版", "SAML_AuthnContext_Template_Description": "你可以在这里使用 AuthnRequest 模板中的任何变量。 \n \n 要添加额外的 authn 上下文,复制 `AuthnContextClassRef` 标签,并用新的上下文替换 `\\_\\_authnContext\\_\\_` 变量。", "SAML_AuthnRequest_Template": "AuthnRequest 模板", "SAML_AuthnRequest_Template_Description": "下列变量可用: \n- *\\_\\_newId\\_\\_*: 随机生成的字符串 ID \n- *\\_\\_instant\\_\\_*: 当前时间戳 \n- *\\_\\_callbackUrl\\_\\_*: Rocket.Chat 回调 URL \n- *\\_\\_entryPoint\\_\\_*: *Custom Entry Point* 设置值. \n- *\\_\\_issuer\\_\\_*: *Custom Issuer* 设置值 \n- *\\_\\_identifierFormatTag\\_\\_*: 配置了合法 *Identifier Format* 时的 *NameID Policy Template* 内容 \n- *\\_\\_identifierFormat\\_\\_*: *Identifier Format* 设置值. \n- *\\_\\_authnContextTag\\_\\_*: 配置了合法 *Custom Authn Context* 时的 *AuthnContext Template* 内容 \n- *\\_\\_authnContextComparison\\_\\_*: *Authn Context Comparison* 设置值 \n- *\\_\\_authnContext\\_\\_*: *Custom Authn Context* 设置值", + "SAML_Connection": "连接", "SAML_Custom_Authn_Context": "自定义授权上下文", "SAML_Custom_Authn_Context_Comparison": "Authn Context 对比", "SAML_Custom_Authn_Context_description": "留空以省略请求中的 authn上下文。 \n \n要添加多个 authn 上下文,添加额外的上下文到 *AuthnContext Template* 设置。", @@ -2785,6 +4592,8 @@ "SAML_Custom_Private_Key": "私钥内容", "SAML_Custom_Provider": "自定义供应商", "SAML_Custom_Public_Cert": "公共证书内容", + "SAML_Custom_Signature_Algorithm": "签名算法", + "SAML_Custom_Signature_Algorithm_description": "用于对来自 Rocket.Chat 的请求与响应进行签名的算法。", "SAML_Custom_Username_Field": "用户名字段名称", "SAML_Custom_Username_Normalize": "标准化用户名", "SAML_Custom_Username_Normalize_Lowercase": "至小写", @@ -2801,10 +4610,15 @@ "SAML_Custom_signature_validation_response": "验证响应签名", "SAML_Custom_signature_validation_type": "签名验证类型", "SAML_Custom_signature_validation_type_description": "如果没有提供自定义证书,此设置将被忽略。", + "SAML_Custom_user_data_custom_fieldmap": "用户数据自定义字段映射", + "SAML_Custom_user_data_custom_fieldmap_description": "配置从 SAML 记录(找到后)填充用户自定义字段的方式。", "SAML_Custom_user_data_fieldmap": "用户数据字段映射", "SAML_Custom_user_data_fieldmap_description": "设置用户数据字段(比如 E-mail)来自 LDAP 何处(如果能找到)。 例如:`{\"name\":\"cn\", \"email\":\"mail\"}` 会从属性 cn 中取出名字字段,从 mail 属性中取出 email 字段。有效字段包括 `name` 和 `email` 和 `username`,所有其他的字段将被作为 `customFields` 保存。 \n分配不可更改属性到 `{{identifier}}` 键使用它作为用户标识符。 \n您也可以使用正则表达式和模版。除非引用正则表达式结果,模版将先被处理。 \n`{\"email\": \"mail\",\"username\": {\"fieldName\": \"mail\",\"regex\": \"(.*)@.+$\",\"template\": \"user-regex\"}, \"name\": { \"fieldNames\": [\"firstName\", \"lastName\"], \"template\": \"{{firstName}} {{lastName}}\"}, \"{{identifier}}\": \"uid\"}`", "SAML_Default_User_Role": "默认用户角色", "SAML_Default_User_Role_Description": "您可以指定多个角色,用逗号分隔。", + "SAML_Description": "用于交换认证与授权数据的安全断言标记语言。", + "SAML_Enterprise": "高级版", + "SAML_General": "常规", "SAML_Identifier_Format": "标识符格式", "SAML_Identifier_Format_Description": "将此留空以忽略请求的 NameID 策略", "SAML_LogoutRequest_Template": "登出请求模版", @@ -2827,27 +4641,42 @@ "SAML_Section_4_Roles": "角色", "SAML_Section_5_Mapping": "映射", "SAML_Section_6_Advanced": "高级", + "SLA_Policies": "SLA 策略", + "SLA_Policy": "SLA 策略", + "SLA_removed": "SLA 已移除", + "SMS": "SMS(短信)", "SMS_Default_Omnichannel_Department": "Omnichannel 部门(默认)", "SMS_Default_Omnichannel_Department_Description": "当设置时,新初始化的入站聊天将被路由至此部门", + "SMS_Description": "在工作区启用并配置短信网关。", "SMS_Enabled": "短信功能已开启", - "SMTP": "SMTP", + "SMS_Twilio_InvalidCredentials": "Twilio 短信凭据无效,无法发送消息", + "SMS_Twilio_NotConfigured": "Twilio 短信尚未配置。前往 设置 -> SMS 进行配置", + "SMTP": "SMTP(邮件传输)", "SMTP_Host": "SMTP 主机", "SMTP_Password": "SMTP 密码", "SMTP_Port": "SMTP 端口", + "SMTP_Server_Not_Setup_Description": "设置 SMTP 邮件服务器以开始发送邀请或手动添加用户", + "SMTP_Server_Not_Setup_Title": "SMTP 服务器尚未设置", "SMTP_Test_Button": "测试 SMTP 设置", "SMTP_Username": "SMTP 用户名", - "SSL": "SSL", + "SSL": "SSL(安全套接层)", "S_new_messages": "%s 条新消息", "S_new_messages_since_s": "%s 新消息,自从 %s", "Same_As_Token_Sent_Via": "与“通过发送的令牌”相同", + "Same_Origin": "同源", "Same_Style_For_Mentions": "同样的风格提及", "Saturday": "星期六", "Save": "保存", + "Save_E2EE_password": "保存 E2EE 密码", "Save_Mobile_Bandwidth": "节约移动网络带宽", "Save_To_Webdav": "保存到 WebDAV", "Save_changes": "保存修改", "Save_to_enable_this_action": "保存以启用该操作", + "Save_user": "保存用户", + "Save_your_new_E2EE_password": "保存你的新 E2EE 密码", + "Save_your_encryption_password_to_access": "保存你的端到端加密密码以访问", "Saved": "已保存", + "Saved_new_url_site_is__url__": "已保存,新站点 URL 为:{{url}}", "Saving": "保存中", "Scan_QR_code": "使用Google身份验证器、Authy或Duo等身份验证器应用扫描这个二维码。它将显示一个6位数的代码,您需要在下面输入这串代码。", "Scan_QR_code_alternative_s": "如果您无法扫描二维码,可以手动输入代码:", @@ -2855,59 +4684,119 @@ "Score": "评分", "Screen_Lock": "屏幕锁", "Screen_Share": "屏幕共享", + "Script": "脚本", "Script_Enabled": "脚本已启用", + "Script_Engine": "脚本沙箱", + "Script_Engine_Description": "旧脚本可能需要兼容沙箱才能正常运行,但所有新脚本应尽量使用安全沙箱。", + "Script_Engine_isolated_vm": "安全沙箱", "Search": "搜索", "Search_Apps": "搜索应用", "Search_Channels": "搜索频道", + "Search_calls": "搜索通话", "Search_Chat_History": "搜索聊天历史", + "Search_Description": "选择工作区搜索提供商并配置搜索相关设置。", + "Search_Devices_Users": "搜索设备或用户", + "Search_Enterprise_Apps": "搜索企业版应用", "Search_Files": "搜索文件", + "Search_Installed_Apps": "搜索已安装应用", "Search_Integrations": "搜索继承", "Search_Messages": "搜索消息", "Search_Page_Size": "页面大小", + "Search_Premium_Apps": "搜索高级版应用", "Search_Private_Groups": "搜索私人组", + "Search_Private_apps": "搜索私有应用", "Search_Provider": "搜索提供商", + "Search_Requested_Apps": "搜索已请求的应用", "Search_Rooms": "搜索聊天室", "Search_Users": "搜索用户", + "Search_by_category": "按类别搜索", "Search_by_file_name": "按文件名搜索", "Search_by_username": "按用户名搜索", "Search_current_provider_not_active": "当前搜索提供程序不活动", + "Search_for_a_more_general_term": "搜索更泛的关键词", + "Search_for_a_more_specific_term": "搜索更具体的关键词", "Search_message_search_failed": "搜索请求失败", + "Search_on_marketplace": "在应用市场中搜索", + "Search_options": "搜索选项", + "Search_roles": "搜索角色", + "Search_rooms": "搜索聊天室", + "Searchable": "可搜索", + "Seat_limit_reached": "席位上限已达", + "Seat_limit_reached_Description": "你的工作区已达到合同约定的席位上限。购买更多席位以添加更多用户。", + "Seats": "席位", + "Seats_Available": "可用席位:{{seatsLeft, number}}", + "Seats_InfoText": "每个唯一用户占用一个席位。已停用用户不占用席位。席位总数由当前许可证类型决定。", + "Seats_usage": "席位使用情况", "Secret_token": "Secret 令牌", + "Secure_SaaS_solution": "安全的 SaaS 解决方案。", "Security": "安全", + "Security_Log_App": "应用({{appId}})", + "Security_Log_System": "系统({{reason}})", + "Security_and_permissions": "安全与权限", + "Security_and_privacy": "安全与隐私", + "Security_code": "安全码", + "Security_logs": "安全日志", + "See_Paid_Plan": "查看付费方案", + "See_Pricing": "查看定价", + "See_all_themes": "查看所有主题", + "See_conflicts": "查看冲突", + "See_documentation": "查看文档", "See_full_profile": "查看全部资料", + "See_history": "查看历史", "See_on_Engagement_Dashboard": "在合约仪表盘中查看", + "Select": "选择", + "Select__count__messages": "选择 {{count}} 条消息", "Select_a_department": "选择一个部门", "Select_a_room": "选择聊天室", "Select_a_user": "选择一个用户", + "Select_a_webdav_server": "选择一个 WebDAV 服务器", "Select_an_avatar": "请选择一个头像", "Select_an_option": "选择一个选项", "Select_at_least_one_user": "选择至少一个用户", "Select_at_least_two_users": "选择至少两个用户", + "Select_atleast_one_channel_to_forward_the_messsage_to": "选择至少一个频道以转发消息", + "Select_agent": "选择客服", + "Select_channel": "选择频道", "Select_department": "选择一个部门", "Select_file": "选择文件", + "Select_messages_to_hide": "选择要隐藏的消息", + "Select_period": "选择时间段", + "Select_recipient": "选择收件人", "Select_role": "选择一个角色", "Select_service_to_login": "选择服务登录加载您的头像,或直接从您的电脑上传一个", + "Select_someone_to_transfer_the_call_to": "选择要转接通话的人", "Select_tag": "选择一个标签", + "Select_template": "选择模板", + "Select_the_channels_you_want_the_user_to_be_removed_from": "选择要将该用户移除的频道", + "Select_the_teams_channels_you_would_like_to_delete": "选择要删除的团队频道,未选择的将被移动到工作区。", "Select_user": "选择用户", "Select_users": "选择用户", "Selected_agents": "已选择的客服", + "Selected_by_default": "默认选中", "Selected_departments": "已选择的部门", + "Selected_first_reply_unselected_following_replies": "首条回复选中,后续回复不选中", "Selected_monitors": "已选择的监控", "Selecting_users": "正在选择用户", + "Self_managed_hosting": "自托管部署", "Send": "发送", + "Send_Email_SMTP_Warning": "在邮件设置中配置 SMTP 服务器以启用。", "Send_Test": "发送测试", "Send_Test_Email": "发送测试邮件", "Send_Visitor_navigation_history_as_a_message": "将访客导航历史记录作为消息发送", "Send_a_message": "发送一条消息", + "Send_a_message_external_service": "发送消息(外部服务)", "Send_a_test_mail_to_my_user": "向我自己发送测试邮件", "Send_a_test_push_to_my_user": "向我自己推送一条测试信息", "Send_confirmation_email": "已发送确认电子邮件", + "Send_conversation_transcript_via_email": "通过邮件发送对话记录", "Send_data_into_RocketChat_in_realtime": "实时向 Rocket.Chat 发送数据。", "Send_email": "发送邮件", + "Send_file_via_email": "通过邮件发送文件", "Send_invitation_email": "发送邀请电子邮件", "Send_invitation_email_error": "您还没有提供任何有效的电子邮箱地址。", "Send_invitation_email_info": "您可以一次发送多个电子邮件邀请。", "Send_invitation_email_success": "您已成功发送邀请电子邮件至以下地址:", + "Send_it_as_attachment_instead_question": "改为作为附件发送?", "Send_me_the_code_again": "再次向我发送验证码", "Send_request_on": "发送请求于", "Send_request_on_agent_message": "客服消息时发送请求", @@ -2919,79 +4808,135 @@ "Send_request_on_lead_capture": "发送潜在客户请求", "Send_request_on_offline_messages": "当显示离线消息时发送请求", "Send_request_on_visitor_message": "在访问者消息上发送请求", + "Send_transcript": "发送聊天记录", "Send_via_Email_as_attachment": "通过电子邮件作为附件发送", "Send_via_email": "通过邮件发送", "Send_visitor_navigation_history_on_request": "根据要求发送访客导航历史", "Send_welcome_email": "发送欢迎邮件", "Send_your_JSON_payloads_to_this_URL": "将你的 JSON 信息发送到这个 URL。", + "Sender": "发送者", "Sender_Info": "发送者信息", "Sending": "发送中", + "Sending_Invitations": "正在发送邀请", + "Sending_your_mail_to_s": "正在发送邮件至 %s", "Sent_an_attachment": "发送附件", "Sent_from": "发送自", "Separate_multiple_words_with_commas": "用逗号分隔多个词语", "Served_By": "服务于", "Server": "服务器", + "Server_Configuration": "服务器配置", "Server_File_Path": "服务器文件路径", "Server_Folder_Path": "服务器文件夹路径", "Server_Info": "服务器信息", + "Server_logs_access_has_changed_callout_title": "Server logs access has changed in Rocket.Chat 8.0", + "Server_logs_access_has_changed_callout_description": "The logs viewer was removed. We recommend configuring an observability stack and using the supported log access methods described in the [logging guide]({{docsUrl}}).", "Server_Type": "服务器类型", + "Server_already_added": "服务器已添加", + "Server_doesnt_exist": "服务器不存在", + "Server_name": "服务器名称", + "Servers": "服务器", "Service": "服务", "Service_account_key": "服务帐户密钥", + "Service_disabled": "服务已禁用", + "Service_disabled_description": "只有同时活跃连接少于 200 个时才能再次启用。", + "Service_fallback_message_hint": "外部服务当前处于活动状态。若不希望超时结束后发送消息,请将此字段留空。", + "Service_level_agreements": "服务级别协议", + "Service_status": "服务状态", "Set_as_favorite": "设为收藏", "Set_as_leader": "设为领导", "Set_as_moderator": "设为主持", "Set_as_owner": "设为所有者", + "Set_manually": "手动设置", "Set_random_password_and_send_by_email": "设置随机密码并用邮件发送", + "Set_randomly_and_send_by_email": "随机设置并通过邮件发送", + "Set_up_2FA": "设置 2FA", + "Setting": "设置", + "Setting_change": "设置更改", "Settings": "设置", "Settings_updated": "设置已更新", + "Setup_SMTP": "设置 SMTP", "Setup_Wizard": "安装向导", + "Setup_Wizard_Description": "工作区的基本信息,例如组织名称和国家/地区。", "Setup_Wizard_Info": "我们将指导您设置第一位管理员用户,配置您的组织并注册您的服务器以接收免费推送通知等。", + "Share": "分享", "Share_Location_Title": "分享位置信息?", + "Share_screen": "共享屏幕", "Shared_Location": "共享位置", "Shared_Secret": "共享的秘密", + "Sharing": "共享中", "Shortcut": "快捷方式", "Should_be_a_URL_of_an_image": "应当是一个图片的地址。", "Should_exists_a_user_with_this_username": "用户必须已存在。", "Show_Avatars": "显示头像", + "Show_Only_This_Content": "仅显示此内容", "Show_Setup_Wizard": "显示安装向导", + "Show_To_Workspace": "显示到工作区", + "Show_additional_fields": "显示额外字段", "Show_agent_email": "显示代理邮件", "Show_agent_info": "显示代理信息", "Show_all": "显示全部", "Show_counter": "显示柜台", + "Show_default_content": "显示默认内容", "Show_email_field": "显示电子邮件字段", + "Show_mentions": "显示提及徽章", "Show_more": "显示更多", "Show_name_field": "显示名称字段", "Show_on_offline_page": "显示离线页面", "Show_on_registration_page": "在注册页面显示", "Show_only_online": "只显示在线用户", + "Show_or_hide_the_user_roles_of_message_authors": "显示或隐藏消息作者的用户角色。", + "Show_or_hide_the_username_of_message_authors": "显示或隐藏消息作者的用户名。", "Show_preregistration_form": "显示预注册表单", "Show_queue_list_to_all_agents": "将队列列表显示给所有代理", + "Show_roles": "显示角色", "Show_room_counter_on_sidebar": "在侧边栏上显示房间计数器", "Show_the_keyboard_shortcut_list": "显示键盘快捷键列表", + "Show_usernames": "显示用户名", + "Show_video": "显示视频", + "Showing": "显示中", "Showing_archived_results": "

显示 %s 归档结果

", + "Showing_current_of_total": "显示 {{current}} / {{total}}", "Showing_online_users": "显示:{{total_showing}},在线:{{online}}, 总计:{{total}} 用户", "Showing_results": "

显示%s条结果

", "Showing_results_of": "展示结果 %s-%s 共 %s", "Sidebar": "侧边栏", + "Sidebar_Sections_Order": "侧边栏分区顺序", + "Sidebar_Sections_Order_Description": "按你的偏好顺序选择分类。", + "Sidebar_actions": "侧边栏操作", "Sidebar_list_mode": "边栏频道列表模式", + "Side_panel": "侧边面板", "Sign_in_to_start_talking": "登录以便开始聊天", + "Sign_in_with__provider__": "使用 {{provider}} 登录", "Site_Name": "网站名称", "Site_Url": "网站地址", "Site_Url_Description": "例如:`https://chat.domain.com/`", "Size": "尺寸", + "Skin_tone": "肤色", "Skip": "跳过", + "Skip_to_main_content": "跳转到主要内容", + "Slack": "Slack(协作平台)", "SlackBridge_APIToken": "API 令牌", "SlackBridge_APIToken_Description": "你可以通过每行添加一个 API 令牌来配置多个 slack 服务器", + "SlackBridge_AppToken": "应用令牌", + "SlackBridge_AppToken_Description": "你可以每行添加一个 App Token 来配置多个 Slack 服务器。", + "SlackBridge_BotToken": "机器人令牌", + "SlackBridge_BotToken_Description": "你可以每行添加一个 Bot Token 来配置多个 Slack 服务器。", + "SlackBridge_Description": "使 Rocket.Chat 能直接与 Slack 通信。", "SlackBridge_Out_All": "SlackBridge 输出所有", "SlackBridge_Out_All_Description": "向 Slack 中所有机器人已加入的频道发送消息", "SlackBridge_Out_Channels": "SlackBridge 输出频道", "SlackBridge_Out_Channels_Description": "选择哪些频道将消息发回 Slack", "SlackBridge_Out_Enabled": "SlackBridge 输出已启用", "SlackBridge_Out_Enabled_Description": "选择 SlackBridge 是否也应该将您的消息发送回 Slack", + "SlackBridge_Remove_Channel_Links_Description": "移除 Rocket.Chat 频道与 Slack 频道之间的内部链接。链接随后将根据频道名称重新创建。", + "SlackBridge_SigningSecret": "签名密钥", + "SlackBridge_SigningSecret_Description": "你可以每行添加一个签名密钥来配置多个 Slack 服务器。", + "SlackBridge_UseLegacy": "使用旧版 API 令牌", "SlackBridge_error": "SlackBridge 导入消息出错于 %s :%s", "SlackBridge_finish": "SlackBridge 已完成导入消息于 %s 。请重新加载以查看所有消息。", "SlackBridge_start": "@%s 已于`#%s`启动了一个 SlackBridge 导入。完成后我们会通知您。", "Slack_Users": "Slack 的用户 CSV", + "Slackbridge_channel_links_removed_successfully": "Slackbridge 频道链接已成功移除。", "Slash_Gimme_Description": "在您的消息前显示 ༼ つ ◕_◕ ༽つ", "Slash_LennyFace_Description": "在您的消息后显示 ( ͡° ͜ʖ ͡°)", "Slash_Shrug_Description": "在您的消息后显示 ¯\\ _(ツ)_ /¯", @@ -3001,6 +4946,8 @@ "Slash_Tableflip_Description": "显示 (╯°□°)╯︵ ┻━┻", "Slash_Topic_Description": "设置主题", "Slash_Topic_Params": "主题消息", + "Smarsh": "Smarsh(合规存档)", + "Smarsh_Description": "用于保存电子邮件通信的配置。", "Smarsh_Email": "Smarsh 电子邮件", "Smarsh_Email_Description": "Smarsh 电子邮件地址发送 .eml 文件。", "Smarsh_Enabled": "Smarsh 已启用", @@ -3016,18 +4963,53 @@ "Snippet_name": "片段名称", "Snippeted_a_message": "创建了一个片段 {{snippetLink}}", "Social_Network": "社交网络", + "Solve_issues": "解决问题", + "Some_ideas_to_get_you_started": "一些入门建议", + "Something_Went_Wrong": "发生错误", + "Something_went_wrong": "发生错误", + "Something_went_wrong_try_again_later": "发生错误,请稍后再试。", + "Something_went_wrong_while_executing_command": "执行命令时发生错误:`/{{command}}`", "Sorry_page_you_requested_does_not_exist_or_was_deleted": "对不起,您请求的网页不存在或被删除!", "Sort": "排序", "Sort_By": "排序方式", "Sort_by_activity": "按活动排序", + "Sorting_mechanism": "排序机制", "Sound": "声音", + "Sound File": "声音文件", + "Sound_Beep": "哔声", + "Sound_Call_Ended": "通话结束", + "Sound_Chelle": "Chelle(铃声)", + "Sound_Chime": "钟声", + "Sound_Dialtone": "拨号音", + "Sound_Ding": "叮声", + "Sound_Door": "门铃", + "Sound_Droplet": "水滴", "Sound_File_mp3": "声音文件 (mp3)", + "Sound_Highbell": "高铃", + "Sound_Outbound_Call_Ringing": "外呼振铃", + "Sound_Ringtone": "铃声", + "Sound_Seasons": "四季", + "Sound_Telephone": "电话", + "Sounds": "声音", + "Source": "来源", + "Speaker": "扬声器", + "Speakers": "扬声器", "Star": "标星", "Star_Message": "星标消息", "Starred_Messages": "星标消息", + "Starred_messages_are_only_visible_to_you": "星标消息仅对你可见", "Start": "开始", + "Start_Time": "开始时间", + "Start_Date": "开始日期", "Start_Chat": "开始聊天", + "Start_a_call": "发起通话", + "Start_a_call_in__roomName__": "在 {{roomName}} 中发起通话", + "Start_a_call_with__roomName__": "与 {{roomName}} 发起通话", + "Start_a_free_trial": "开始免费试用", "Start_audio_call": "开始音频对话", + "Start_call": "开始通话", + "Start_conference_call": "开始会议通话", + "Start_free_trial": "开始免费试用", "Start_of_conversation": "会话的开始", "Start_video_call": "开始视频对话", "Start_video_conference": "开始视频会议?", @@ -3062,6 +5044,7 @@ "Stats_Total_Messages": "总计消息", "Stats_Total_Messages_Channel": "总计频道消息", "Stats_Total_Messages_Direct": "总计私聊消息", + "Stats_Total_Messages_Discussions": "总计讨论串消息", "Stats_Total_Messages_Livechat": "总计 Omnichannel 消息", "Stats_Total_Messages_PrivateGroup": "总计私人组消息", "Stats_Total_Outgoing_Integrations": "总计出站集成", @@ -3078,14 +5061,21 @@ "StatusMessage_Too_Long": "状态消息必须少于 120 字。", "Step": "步", "Stop_Recording": "停止录制", + "Stop_call": "结束通话", "Store_Last_Message": "存储最后消息", "Store_Last_Message_Sent_per_Room": "存储每个聊天室发送的最后一条消息。", "Stream_Cast": "流演员", "Stream_Cast_Address": "流投射地址", "Stream_Cast_Address_Description": "知识产权或您的Rocket.Chat中央流铸主机。例如。 `192.168.1.1:3000`或`localhost:4000`", + "Strict_Origin": "严格同源", + "Strict_Origin_When_Cross_Origin": "跨域时严格同源", + "Strikethrough": "删除线", "Style": "样式", "Subject": "标题", "Submit": "提交", + "Subscribe": "订阅", + "Subscription": "订阅", + "Subscription_add-on_required": "需要订阅附加组件", "Success": "成功", "Success_message": "成功消息", "Successfully_downloaded_file_from_external_URL_should_start_preparing_soon": "从外部 URL 成功下载文件,即将开始准备", @@ -3100,34 +5090,117 @@ "Sync_Interval": "同步间隔", "Sync_Users": "同步用户", "Sync_in_progress": "同步正在进行中", + "Sync_license_update": "同步许可证更新", + "Sync_license_update_Callout": "如果几分钟内工作区没有变化,请同步许可证更新。", + "Sync_license_update_Callout_Title": "我们正在更新你的许可证", "Sync_success": "同步成功", + "System": "系统", "System_messages": "系统消息", "TOTP Invalid [totp-invalid]": "代码或密码无效", "TOTP_Reset_Other_Key_Warning": "重置当前的两步验证 TOTP 将使用户登出。用户稍后能再次设置两步验证。", "TOTP_reset_email": "两步验证 TOTP 重置通知", "Tag": "标签", + "Tag_already_exists": "标签已存在", "Tag_removed": "已移除标签", + "Tags": "标签", "Take_it": "拿去!", + "Take_rocket_chat_with_you_with_mobile_applications": "使用移动应用随身携带 Rocket.Chat。", + "Taken_at": "记录于", + "Talk_to_an_expert": "咨询专家", + "Talk_to_sales": "联系销售", + "Talk_to_your_workspace_admin_to_address_this_issue": "请联系工作区管理员解决此问题。", + "Talk_to_your_workspace_administrator_about_enabling_video_conferencing": "请联系工作区管理员以启用视频会议", "Target user not allowed to receive messages": "目标用户不允许接收消息", "TargetRoom": "目标Room", "TargetRoom_Description": "事件被触发所产生的结果将被发送到哪个聊天室?仅允许设置一个目标聊天室,且该聊天室必须是存在的。", "Team": "团队", + "Team_Add_existing": "添加现有", + "Team_Add_existing_channels": "添加现有频道", + "Team_Auto-join": "自动加入", + "Team_Auto-join_exceeded_user_limit": "自动加入最多 {{limit}} 名成员,#{{channelName}} 现有 {{numberOfMembers}} 名成员", + "Team_Auto-join_updated": "#{{channelName}} 现有 {{numberOfMembers}} 名成员", + "Team_collaboration_filters": "团队协作筛选器", + "Team_Channels": "团队频道", + "Team_Delete_Channel_modal_content": "是否要删除此频道?", + "Team_Delete_Channel_modal_content_danger": "此操作无法撤销。", + "Team_Info": "团队信息", + "Team_Mapping": "团队映射", + "Team_Name": "团队名称", + "Team_Remove_from_team": "从团队移除", + "Team_Remove_from_team_modal_content": "是否要将此频道从 {{teamName}} 中移除?该频道将移回工作区。", + "Team_has_been_created": "团队已创建", + "Team_has_been_deleted": "团队已删除", + "Team_voice_call": "团队语音通话", + "Team_what_is_this_team_about": "这个团队是做什么的", + "Teams": "团队", + "Teams_Errors_Already_exists": "团队 `{{name}}` 已存在。", + "Teams_Errors_team_name": "不能使用 \"{{name}}\" 作为团队名称。", + "Teams_Info": "团队信息", "Teams_New_Add_members_Label": "成员", "Teams_New_Broadcast_Description": "只有授权用户才能发送新消息,但其他用户可以回复消息", + "Teams_New_Broadcast_Label": "广播", "Teams_New_Description_Label": "话题", + "Teams_New_Description_Placeholder": "这个团队是做什么的", + "Teams_New_Encrypted_Description_Disabled": "仅私有团队可用", + "Teams_New_Encrypted_Description_Enabled": "端到端加密的团队。搜索在加密团队中不可用,通知可能不会显示消息内容。", "Teams_New_Encrypted_Label": "加密的", "Teams_New_Name_Label": "姓名", + "Teams_New_Private_Description_Disabled": "任何人都可以访问", + "Teams_New_Private_Description_Enabled": "仅受邀者可加入", "Teams_New_Private_Label": "私人", + "Teams_New_Read_only_Description": "团队中的所有用户都可以发送消息", "Teams_New_Read_only_Label": "只读", + "Teams_New_Title": "创建团队", "Teams_Private_Team": "私人团队", + "Teams_Public_Team": "公开团队", + "Teams_Search_teams": "搜索团队", + "Teams_Select_a_team": "选择一个团队", + "Teams_about_the_channels": "以及这些频道呢?", + "Teams_channels": "团队频道", + "Teams_channels_didnt_leave": "你未选择以下频道,因此不会离开它们:", + "Teams_channels_last_owner_delete_channel_warning": "你是该频道的最后一个所有者。一旦将团队转换为频道,该频道将被移到工作区。", + "Teams_channels_last_owner_leave_channel_warning": "你是该频道的最后一个所有者。一旦离开团队,该频道将保留在团队内,但你将从外部管理它。", + "Teams_convert_channel_to_team": "转换为团队", + "Teams_delete_team": "你即将删除此团队。", + "Teams_delete_team_Warning": "一旦删除团队,所有聊天内容和配置将被删除。", + "Teams_delete_team_choose_channels": "选择要删除的频道。你决定保留的频道将在工作区可用。", + "Teams_delete_team_public_notice": "注意:公共频道仍会保持公开并对所有人可见。", + "Teams_deleted_channels": "以下频道将被删除:", + "Teams_kept__username__channels": "你未选择以下频道,因此 {{username}} 将保留在这些频道中:", + "Teams_kept_channels": "你未选择以下频道,因此它们将被移到工作区:", + "Teams_leave": "离开团队", + "Teams_leave_channels": "选择你要离开的团队频道。", + "Teams_leaving_team": "你正在离开此团队。", + "Teams_left_team_successfully": "已成功离开团队", + "Teams_members": "团队成员", + "Teams_move_channel_to_team": "移动到团队", + "Teams_move_channel_to_team_confirm_description": "阅读上述说明后,你要继续执行此操作吗?", + "Teams_move_channel_to_team_description_first": "将频道移动到团队意味着该频道会被加入团队上下文,但所有非该团队成员的频道成员仍可访问该频道,不过不会成为团队成员。", + "Teams_move_channel_to_team_description_fourth": "请注意,团队所有者可以从该频道移除成员。", + "Teams_move_channel_to_team_description_second": "该频道的管理仍由该频道的所有者负责。", + "Teams_move_channel_to_team_description_third": "团队成员甚至团队所有者,如果不是该频道成员,将无法访问该频道内容。", + "Teams_new_description": "团队允许一组人协作,并可包含多个频道。", + "Teams_removing__username__from_team": "你正在将 {{username}} 从此团队移除", + "Teams_removing__username__from_team_and_channels": "你正在将 {{username}} 从此团队及其所有频道中移除。", + "Teams_removing_member": "正在移除成员", + "Technology_Provider": "技术提供商", "Technology_Services": "技术服务", + "Temporarily_unavailable": "暂时不可用", + "Template": "模板", + "template": "模板", + "Template_message": "模板消息", "Terms": "条款", + "Terms_of_use": "使用条款", "Test_Connection": "测试连接", "Test_Desktop_Notifications": "测试桌面通知", + "Test_LDAP_Search": "测试 LDAP 搜索", + "Text": "文本", "Texts": "文字", + "Thank_You_For_Choosing_RocketChat": "感谢选择 Rocket.Chat!", "Thank_you_exclamation_mark": "谢谢!", "Thank_you_for_your_feedback": "感谢您的反馈", "The_application_name_is_required": "应用名称必填", + "The_application_will_be_able_to": "<1>{{appName}} 将能够:", "The_channel_name_is_required": "频道名称为必填", "The_emails_are_being_sent": "邮件已发送。", "The_empty_room__roomName__will_be_removed_automatically": "空房间 {{roomName}} 将被自动移除。", @@ -3144,7 +5217,18 @@ "The_user_s_will_be_removed_from_role_s": "用户 %s 将被移除 %s 角色", "The_user_will_be_removed_from_s": "将从 %s 中移除该用户", "The_user_wont_be_able_to_type_in_s": "该用户将被禁止在 %s 房间中发言", + "The_workspace_has_exceeded_the_monthly_limit_of_active_contacts": "工作区已超过月度活跃联系人上限。", "Theme": "主题", + "Theme_Appearence": "主题外观", + "Theme_dark": "深色", + "Theme_dark_description": "在低光环境下通过减少屏幕发光量来减轻眼疲劳。", + "Theme_high_contrast": "高对比度", + "Theme_high_contrast_description": "通过醒目颜色和强对比提供最大色调区分度,提升可访问性。", + "Theme_light": "浅色", + "Theme_light_description": "更适合视力障碍者,也适用于光线充足的环境。", + "Theme_match_system": "跟随系统", + "Theme_match_system_description": "自动匹配系统外观。", + "Themes": "主题", "There_are_no_agents_added_to_this_department_yet": "该部门尚未分配任何客服。", "There_are_no_applications": "尚未添加 oAuth 应用程序。", "There_are_no_applications_installed": "目前还没有安装 Rocket.Chat 应用程序。", @@ -3155,28 +5239,55 @@ "There_are_no_integrations": "没有集成", "There_are_no_monitors_added_to_this_unit_yet": "尚未有监控添加至此单元", "There_are_no_personal_access_tokens_created_yet": "尚未创建个人访问令牌。", + "There_are_no_rooms_for_the_given_search_criteria": "没有符合该搜索条件的房间", "There_are_no_users_in_this_role": "该角色没有对应用户。", + "There_has_been_an_error_installing_the_app": "安装应用时发生错误", + "There_is_no_video_conference_history_in_this_room": "此房间没有视频会议历史", "There_is_one_or_more_apps_in_an_invalid_state_Click_here_to_review": "一个或多个应用程序处于无效状态。点击此处查看。", + "These_options_affect_this_conversation_only_To_set_default_selections_go_to_My_Account_Omnichannel": "这些选项仅影响此会话。要设置默认选项,请前往 我的账户 > Omnichannel。", + "Third_party_applications_table": "第三方应用表", + "Third_party_login": "第三方登录", + "This_action_cannot_be_undone": "此操作无法撤销", "This_agent_was_already_selected": "该代理已被选中", + "This_attachment_is_not_supported": "不支持该附件格式", + "This_cant_be_undone": "此操作无法撤销。", "This_conversation_is_already_closed": "该会话已关闭。", "This_email_has_already_been_used_and_has_not_been_verified__Please_change_your_password": "此电子邮箱地址已被使用但未经验证。请修改您的密码。", + "This_feature_is_currently_in_alpha": "此功能当前处于 Alpha 阶段!", + "This_is_a_deprecated_feature_alert": "此功能已弃用,可能无法按预期工作,且不会再更新。", "This_is_a_desktop_notification": "这是一条桌面通知", "This_is_a_push_test_messsage": "这是一条测试消息", "This_message_was_rejected_by__peer__peer": "此消息被对等端 {{peer}} 拒绝。", "This_monitor_was_already_selected": "已选中监控", "This_month": "这个月", + "This_option_affect_this_conversation_only_To_set_default_selection_go_to_My_Account_Omnichannel": "此选项仅影响此会话。要设置默认选项,请前往 我的账户 > Omnichannel。", + "This_page_is_deprecated_will_be_removed_soon": "此页面已弃用,将很快移除", "This_room_encryption_has_been_disabled_by__username_": "{{username}} 已禁用聊天室加密", "This_room_encryption_has_been_enabled_by__username_": "{{username}} 已启用聊天室加密", + "This_room_has_been_archived": "房间已归档", "This_room_has_been_archived_by__username_": "此聊天室已被 {{username}} 归档", + "This_room_has_been_unarchived": "房间已取消归档", "This_room_has_been_unarchived_by__username_": "此聊天室已被 {{username}} 取消归档", "This_room_is_read_only": "这个聊天室是只读的", + "This_server_will_be_available_while_your_session_is_active": "此服务器在会话有效期间可用", "This_week": "这个星期", + "This_year": "今年", "Thread_message": "评论 * {{username}} * 的消息: _ {{msg}} _", + "Thread_message_list": "讨论串消息列表", "Threads": "讨论串", + "Threads_Description": "讨论串允许围绕特定消息进行有序讨论。", + "Threads_unavailable_for_federation": "联邦房间不可使用讨论串", "Thursday": "星期四", + "Time": "时间", + "Time_in_minutes": "时间(分钟)", "Time_in_seconds": "时间(秒)", + "Time_slash_Date": "时间 / 日期", "Timeout": "超时", + "Timeout_in_miliseconds": "超时(毫秒)", + "Timeout_in_miliseconds_cant_be_negative_number": "超时(毫秒)不能为负数", + "Timeout_in_miliseconds_hint": "等待外部服务响应的毫秒数,超时后将取消请求。", "Timeouts": "超时", + "Timestamp": "时间戳", "Timezone": "时区", "Title": "标题", "Title_bar_color": "标题栏颜色", @@ -3185,12 +5296,15 @@ "To": "到", "To_additional_emails": "额外的电子邮件", "To_install_RocketChat_Livechat_in_your_website_copy_paste_this_code_above_the_last_body_tag_on_your_site": "要在您的网站中安装 Rocket.Chat 即时聊天,请将以下代码复制粘贴到您的网页代码中最后一个 </body> 标签之上。", + "To_prevent_seeing_this_message_again_allow_popups_from_workspace_URL": "若不想再次看到此消息,请确保浏览器设置允许从工作区 URL 打开弹窗:", "To_users": "给用户", "Today": "今天", "Toggle_original_translated": "切换地区/语言", "Token": "令牌", "Token_Access": "令牌访问", "Token_Controlled_Access": "令牌受控访问", + "Token_Not_Recognized": "令牌无法识别", + "Token_has_been_removed": "令牌已移除", "Token_required": "需要令牌", "Tokens_Minimum_Needed_Balance": "最低需要的令牌余额", "Tokens_Minimum_Needed_Balance_Description": "在每个令牌上设置最低需要的余额。空白或“0”不限制。", @@ -3199,23 +5313,38 @@ "Tokens_Required_Input_Description": "输入一个或多个以逗号分隔的令牌资产名称。", "Tokens_Required_Input_Error": "无效带类型令牌。", "Tokens_Required_Input_Placeholder": "令牌资产名称", + "Toolbox_room_actions": "房间主要操作", + "Top_5_agents_with_the_most_conversations": "会话最多的前 5 位客服", "Topic": "话题", "Total": "总计", + "Total_time": "总时长", "Total_Discussions": "总计讨论", "Total_Threads": "总计讨论串", "Total_abandoned_chats": "总计放弃的聊天", "Total_conversations": "总计对话", "Total_messages": "总计消息", + "Total_rooms": "房间总数", "Total_visitors": "总计访客", "Transcript": "聊天记录", "Transcript_Enabled": "询问访问者是否会在聊天结束后收到抄本", "Transcript_Request": "聊天记录请求", "Transcript_message": "查询历史记录时要显示的消息", "Transcript_of_your_livechat_conversation": "您的 Omnichannel 会话记录。", + "Transfer_call": "转接通话", + "Transfer_to": "转接到", + "Transferred": "已转接", + "Transferred_call__from__to": "{{from}} 已将通话转接给", + "Transferring_call": "正在转接通话", + "Transferring_call_incoming": "来电转接中", + "Transferring_call_incoming__from_": "来自 {{from}}", "Translate": "翻译", + "Translate_to": "翻译为", "Translated": "翻译", "Translations": "翻译", "Travel_and_Places": "旅行与地点", + "Trial_active": "试用中", + "Trial_period: ": "试用期", + "Trigger": "触发器", "Trigger_Words": "触发关键词", "Trigger_removed": "已移除的触发器", "Triggers": "触发器", @@ -3233,17 +5362,29 @@ "Troubleshoot_Disable_Presence_Broadcast_Alert": "这个设置可以防止所有的实例将用户的状态变化发送给他们的客户端,使所有的用户保持他们第一次加载时的存在状态。", "Troubleshoot_Disable_Sessions_Monitor": "禁用会话监控", "Troubleshoot_Disable_Sessions_Monitor_Alert": "这个设置停止了对用户会话的处理,将导致统计工作无法正常进行!", + "Troubleshoot_Disable_Teams_Mention": "禁用团队提及", + "Troubleshoot_Disable_Teams_Mention_Alert": "此设置将禁用团队提及功能。用户将无法在消息中通过名称提及团队并通知其成员。", + "Troubleshoot_Force_Caching_Version": "基于版本变更强制浏览器清理网络缓存", + "Troubleshoot_Force_Caching_Version_Alert": "如果提供的值非空且不同于上一次,浏览器将尝试清理缓存。此设置不应长期启用,因为会影响浏览器性能,请尽快清除。", "True": "是", + "Try_different_filters": "尝试不同的筛选条件", + "Try_entering_a_different_search_term": "尝试输入其他搜索词。", "Try_now": "立即尝试", + "Try_searching_in_the_marketplace_instead": "尝试改在应用市场中搜索", "Tuesday": "星期二", "Turn_OFF": "关掉", "Turn_ON": "打开", + "Turn_off_answer_chats": "关闭应答聊天", + "Turn_off_video": "关闭视频", + "Turn_on_answer_chats": "开启应答聊天", + "Turn_on_video": "开启视频", "Two Factor Authentication": "两步验证", "Two-factor_authentication": "基于 TOTP 的两步验证", "Two-factor_authentication_disabled": "两步验证被禁用", "Two-factor_authentication_email": "基于邮件的两步验证", "Two-factor_authentication_enabled": "启用两步验证", "Two-factor_authentication_native_mobile_app_warning": "警告:一旦启用此功能,您将无法使用密码登录原生移动应用(Rocket.Chat +),直到他们实施2FA。", + "Two-factor_authentication_required": "需要双因素认证", "Two-factor_authentication_via_TOTP": "基于 TOTP 的两步验证", "Type": "类型", "Type_your_email": "输入您的电子邮件地址", @@ -3252,7 +5393,9 @@ "Type_your_name": "输入您的姓名", "Type_your_password": "输入您的密码", "Type_your_username": "输入您的用户名", + "Types": "类型", "Types_and_Distribution": "类型和分发", + "UIKit_Interaction_Timeout": "应用未能响应。请重试或联系管理员", "UI_Allow_room_names_with_special_chars": "允许Room名称中的特殊字符", "UI_DisplayRoles": "显示角色", "UI_Group_Channels_By_Type": "按类型分组频道", @@ -3261,57 +5404,131 @@ "UI_Unread_Counter_Style": "未读计数器风格", "UI_Use_Name_Avatar": "使用全名缩写来生成默认头像", "UI_Use_Real_Name": "使用真实姓名", - "URL": "URL", + "URL": "URL(网址)", + "URLs": "URL(网址)", + "UTC_Timezone": "UTC 时区", + "UTF8_Channel_Names_Validation": "UTF-8 频道名称验证", + "UTF8_Channel_Names_Validation_Description": "用于验证频道名称的正则表达式", "UTF8_Names_Slugify": "着重显示 UTF-8 名字", + "UTF8_User_Names_Validation": "UTF-8 用户名验证", + "UTF8_User_Names_Validation_Description": "用于验证用户名的正则表达式", + "Unable_to_complete_call": "无法完成通话", + "Unable_to_complete_call__code": "无法完成通话。错误代码 [{{statusCode}}]", + "Unable_to_load_active_connections": "无法加载活跃连接", + "Unable_to_make_calls_while_another_is_ongoing": "无法在另一通话进行时拨打电话", + "Unable_to_negotiate_call_params": "无法协商通话参数。", "Unarchive": "取消归档", + "Unassign_extension": "取消分机分配", + "Unassigned": "未分配", "Unavailable": "不可用", + "Unavailable_in_encrypted_channels": "加密频道中不可用", + "Unblock": "取消屏蔽", "Unblock_User": "取消阻止用户", "Uncheck_All": "取消全部选择", "Uncollapse": "展开", "Undefined": "未定义", + "Undo_request": "撤销请求", "Unfavorite": "取消收藏", "Unfollow_message": "取消关注消息", "Unignore": "屏蔽", "Uninstall": "卸载", + "Uninstall_grandfathered_app": "卸载 {{appName}}?", + "Unique_ID_change_detected": "检测到唯一 ID 变更", + "Unique_ID_change_detected_description": "用于标识此工作区的信息已发生变化。这可能发生在站点 URL 或数据库连接字符串更改时,或从现有数据库副本创建新工作区时。

你希望对现有工作区进行配置更新,还是创建新的工作区和唯一 ID?", + "Unique_ID_change_detected_learn_more_link": "了解更多", + "Unit": "单位", "Unit_removed": "已移除的单位", + "Units": "单位", "Unknown_Import_State": "未知导入状态", + "Unknown_User": "未知用户", + "Unknown_contact_callout_description": "未知联系人。此联系人不在联系人列表中。", + "unknown": "未知", + "Unknown": "未知", "Unlimited": "无限", + "Unlimited_MACs": "无限 MACs", + "Unlimited_push_notifications": "无限推送通知", + "Unlimited_seats": "无限席位", + "Unlimited_seats_MACs": "无限席位和 MACs", + "Unlock_premium_capabilities": "解锁高级功能", + "Unmute": "取消静音", + "Unmute_microphone": "取消麦克风静音", "Unmute_someone_in_room": "取消某人在聊天室中的禁言", "Unmute_user": "取消禁言", "Unnamed": "未命名", "Unpin": "取消固定", "Unpin_Message": "取消消息固定", + "Unprioritized": "未设优先级", "Unread": "未读", "Unread_Count": "未读数", "Unread_Count_DM": "未读私聊消息数", + "Unread_Count_Omni": "Omnichannel 会话未读计数", "Unread_Messages": "未读消息", + "Unread_Requested_First": "未读优先", + "Unread_Requested_Last": "未读置后", "Unread_Rooms": "未读房间", "Unread_Rooms_Mode": "未读房间模式", "Unread_Tray_Icon_Alert": "未读托盘图标提醒", "Unread_on_top": "未读在最上面", + "Unsafe_Url": "不安全的 URL", + "Unseen_features": "未查看的功能", + "Unselected_by_default": "默认不选中", "Unstar_Message": "取消星标", + "Unverified": "未验证", "Update": "更新", "Update_EnableChecker": "启用更新检查", "Update_EnableChecker_Description": "自动检查来自 Rocket.Chat 开发者的新更新/重要消息,并在可用时接收通知。每一个新版本的通知都会以可点击的横幅和 Rocket.Ca t机器人的消息的形式出现一次,两者均为仅管理员可见。", "Update_LatestAvailableVersion": "更新至最新版本", + "Update_anyway": "仍然更新", "Update_every": "更新于每", + "Update_to_access_marketplace": "更新以访问应用市场", + "Update_to_access_marketplace_description": "此工作区无法访问应用市场,因为运行的 Rocket.Chat 版本不受支持。", "Update_to_version": "更新到 {{version}}", + "Update_version": "更新版本", "Update_your_RocketChat": "更新你的 Rocket.Chat", "Updated_at": "更新于", + "Updated": "已更新", + "Upgrade": "升级", + "UpgradeToGetMore_Headline": "升级以获得更多", + "UpgradeToGetMore_Subtitle": "用高级功能增强你的工作区。", + "UpgradeToGetMore_accessibility-certification_Body": "通过 Rocket.Chat 无障碍计划遵循 WCAG 和 BITV 标准。", + "UpgradeToGetMore_accessibility-certification_Title": "WCAG 2.1 和 BITV 2.0", + "UpgradeToGetMore_auditing_Body": "在一个地方审计会话,确保与客户、供应商和内部团队的沟通质量。", "UpgradeToGetMore_auditing_Title": "消息审计", + "UpgradeToGetMore_custom-roles_Body": "通过为工作区成员设置特定角色和权限,确保安全高效的工作环境。", + "UpgradeToGetMore_custom-roles_Title": "自定义角色", + "UpgradeToGetMore_engagement-dashboard_Body": "通过参与度仪表盘洞察用户、消息和频道使用情况。", "UpgradeToGetMore_engagement-dashboard_Title": "分析", + "UpgradeToGetMore_oauth-enterprise_Body": "通过 LDAP/SAML/OAuth 的角色映射、频道订阅、自动登出等确保访问权限。", + "UpgradeToGetMore_oauth-enterprise_Title": "高级认证", + "UpgradeToGetMore_scalability_Body": "通过从单体切换到微服务或多实例提高效率、降低成本并增加并发用户容量。", + "UpgradeToGetMore_scalability_Title": "高可扩展性", + "Upgrade_subscription_to_enable_private_apps": "升级订阅以启用私有应用。", + "Upgrade_tab_connection_error_description": "似乎没有网络连接。这可能因为工作区安装在完全隔离的离线服务器上。", + "Upgrade_tab_connection_error_restore": "恢复连接以了解你错过的功能。", + "Upgrade_tab_go_fully_featured": "解锁完整功能", + "Upgrade_tab_trial_guide": "试用指南", + "Upgrade_tab_upgrade_your_plan": "升级你的方案", + "Upgrade_to_Pro": "升级到 Pro", "Upload": "上传", "Upload_Folder_Path": "上传文件夹路径", "Upload_From": "从 {{name}} 上传", + "Upload_anyway": "仍然上传", "Upload_app": "上传应用", + "Upload_file": "上传文件", "Upload_file_description": "文件描述", "Upload_file_name": "文件名", "Upload_file_question": "上传文件?", + "Upload_private_app": "上传私有应用", "Upload_user_avatar": "上传头像", "Uploading_file": "文件上传中……", + "Uploading_file__fileName__": "正在上传文件 {{fileName}}", + "Uploads": "上传", "Uptime": "运行时间", + "Usage": "使用情况", + "Use": "使用", "Use_Emojis": "使用表情", "Use_Global_Settings": "使用全局设置", + "Use_Legacy_Message_Template": "使用旧版消息模板", "Use_Room_configuration": "使用聊天室配置并覆盖服务器配置", "Use_Server_configuration": "使用服务器配置", "Use_User_Preferences_or_Global_Settings": "使用用户偏好设置或全局设置", @@ -3322,6 +5539,7 @@ "Use_service_avatar": "使用 %s 头像", "Use_this_response": "使用这个回复", "Use_this_username": "使用此用户名", + "Use_token": "使用令牌", "Use_uploaded_avatar": "使用上传头像", "Use_url_for_avatar": "为头像使用链接", "User": "用户", @@ -3330,6 +5548,7 @@ "UserDataDownload": "用户数据下载", "UserDataDownload_CompletedRequestExistedWithLink_Text": "你的数据已生成。点击此处以下载。", "UserDataDownload_CompletedRequestExisted_Text": "您的数据文件已经生成。检查您的电子邮件帐户的下载链接。", + "UserDataDownload_Description": "用于允许或禁止工作区成员下载工作区数据的配置。", "UserDataDownload_EmailBody": "您的数据文件现在已准备好下载。点击这里下载它。", "UserDataDownload_EmailSubject": "您的数据文件已准备好下载", "UserDataDownload_RequestExisted_Text": "您的数据文件已经生成。准备好后,下载它的链接将发送到您的电子邮件地址。在您之前,队列中有{{pending_operations}} 个操作要运行。", @@ -3341,9 +5560,12 @@ "UserData_MessageLimitPerRequest": "每个请求的消息限制", "UserData_ProcessingFrequency": "处理频率(分钟)", "User_Info": "用户信息", + "User_info": "用户信息", "User_Interface": "用户界面", "User_Presence": "在线状态", "User_Settings": "用户设置", + "User_Status": "用户状态", + "User_Without_Extensions": "无分机用户", "User__username__is_now_a_leader_of__room_name_": "用户 {{username}} 现在是 {{room_name}} 的领导者", "User__username__is_now_a_moderator_of__room_name_": "用户 {{username}} 现在是 {{room_name}} 的主持", "User__username__is_now_an_owner_of__room_name_": "用户 {{username}} 现在是 {{room_name}} 的拥有者", @@ -3355,43 +5577,68 @@ "User_added": "用户已添加", "User_added_by": "{{user_by}}添加了{{user_added}}。", "User_added_successfully": "添加新用户成功", + "User_added_to": "已添加 {{user_added}}", "User_and_group_mentions_only": "仅用户和组提及", + "User_cant_be_empty": "用户不能为空", + "User_card_actions": "用户卡片操作", "User_created_successfully!": "用户创建成功!", "User_default": "用户默认", "User_doesnt_exist": "不存在名为 `@%s` 的用户。", "User_e2e_key_was_reset": "用户端到端密钥已成功重置。", + "User_first_log_in": "用户首次登录", "User_has_been_activated": "用户已经激活", "User_has_been_deactivated": "用户已停用", "User_has_been_deleted": "用户已被删除", "User_has_been_ignored": "用户已被忽略", + "User_has_been_muted": "已禁言 {{user_muted}}", "User_has_been_muted_in_s": "该用户已在 %s 中被禁言", + "User_has_been_removed": "已移除 {{user_removed}}", "User_has_been_removed_from_s": "已从移除 %s 中用户", + "User_has_been_removed_from_team": "用户已从团队移除", "User_has_been_unignored": "用户不再被忽略", + "User_has_been_unmuted": "已解除禁言 {{user_unmuted}}", "User_is_blocked": "用户被阻止", "User_is_no_longer_an_admin": "用户已不再是管理员", "User_is_now_an_admin": "用户已成为管理员", "User_is_unblocked": "用户被解除封锁", "User_joined_channel": "加入了频道。", "User_joined_conversation": "加入了会话", + "User_joined_team": "加入此团队", + "User_joined_the_channel": "加入了频道", + "User_joined_the_conversation": "加入了会话", + "User_joined_the_team": "加入了此团队", "User_left": "已离开频道。", + "User_left_team": "离开此团队", + "User_left_this_channel": "离开了频道", + "User_invited_to_room": "已邀请 {{user_invited}} 加入房间", + "User_rejected_invitation_to_room": "拒绝加入房间的邀请", + "User_left_this_team": "离开了此团队", "User_logged_out": "用户已注销", "User_management": "用户管理", "User_mentions_only": "仅用户提及", + "User_menu": "用户菜单", "User_muted": "用户静音", - "User_muted_by": "{{user_by}} 已禁言用户 {{user_muted}}。", + "User_muted_by": "用户 {{user_muted}}{{user_by}} 禁言。", "User_not_found": "找不到用户", "User_not_found_or_incorrect_password": "用户不存在或密码错误", "User_or_channel_name": "用户或者频道名称", "User_removed": "已移除用户", - "User_removed_by": "{{user_by}} 移除了用户 {{user_removed}}", + "User_removed_by": "用户 {{user_removed}}{{user_by}} 移除", "User_sent_a_message_on_channel": "{{username}}{{channel}}中发送了一条消息", "User_sent_a_message_to_you": "{{username}}向您发送了一条消息", "User_started_a_new_conversation": "{{username}} 开始了新的会话", - "User_unmuted_by": "{{user_by}} 已解除用户 {{user_unmuted}}的禁言。", + "User_status_disabled": "为保证性能,用户状态已临时禁用。", + "User_status_disabled_learn_more": "用户状态已禁用", + "User_status_disabled_learn_more_description": "由于活跃连接量较高,处理用户状态的服务已临时禁用。管理员可在工作区设置中手动重新启用。", + "User_status_menu": "用户状态菜单", + "User_status_temporarily_disabled": "用户状态临时禁用", + "User_unmuted_by": "用户 {{user_unmuted}} 的禁言已被 {{user_by}} 解除。", "User_unmuted_in_room": "用户在聊天室中的禁言已被取消", "User_updated_successfully": "用户更新成功", "User_uploaded_a_file_on_channel": "{{username}}{{channel}}中上传了一个文件", "User_uploaded_a_file_to_you": "{{username}}向您发送了一个文件", + "User_uploaded_files_on_channel": "{{username}} uploaded {{count}} files on {{channel}}", + "User_uploaded_files_to_you": "{{username}} sent you {{count}} files", "User_uploaded_file": "上传了一个文件", "User_uploaded_image": "上传了一张图片", "Username": "用户名", @@ -3402,24 +5649,37 @@ "Username_cant_be_empty": "用户名不能为空", "Username_description": "这个用户名用于让别人在聊天消息中通知您。", "Username_doesnt_exist": "用户名 `%s` 不存在。", + "Username_has_been_updated": "用户名已更新", "Username_invalid": "%s 不是一个有效的用户名,
只能使用字母、数字、连字符和下划线", "Username_is_already_in_here": "`@%s` 已存在于此。", + "Username_name_email": "用户名、姓名或邮箱", "Username_title": "注册用户名", "Users": "用户", "Users must use Two Factor Authentication": "用户必须使用两步验证", + "Users_Connected": "已连接用户", "Users_TOTP_has_been_reset": "用户的 TOTP 已被重置", + "Users_Table_Generic_No_users": "没有 %s 用户", + "Users_Table_no_active_users_description": "活跃用户会显示在这里。", + "Users_Table_no_all_users_description": "未找到任何用户。", + "Users_Table_no_deactivated_users_description": "已停用用户会显示在这里。", + "Users_Table_no_pending_users_description": "待激活的用户或已手动创建但尚未登录的用户会显示在这里。", "Users_added": "用户已被添加", + "Users_and_more_reacted_with": "{{users}} 及另外 {{counter}} 人使用 {{emoji}} 作出反应", "Users_and_rooms": "用户和聊天室", "Users_by_time_of_day": "按每日时间段的用户", "Users_in_role": "用户存在于该角色", "Users_key_has_been_reset": "用户的密钥已被重置", "Users_reacted": "回应过的用户", + "Users_reacted_with": "{{users}} 使用 {{emoji}} 作出反应", "Uses": "使用次数", "Uses_left": "剩余使用次数", + "Utilities": "工具", "Validate_email_address": "验证邮箱", "Validation": "验证", + "Value": "值", "Value_messages": "{{value}} 消息", "Value_users": "{{value}} 用户", + "Values": "值", "Verification": "验证", "Verification_Description": "您可以使用以下占位符: \n - [Verification_Url] 代表验证网址。 \n - [姓名],[fname],[lname]分别代表用户的全名,名字或姓氏。用户的电子邮件为 \n - `[email]`。应用程序名称和URL分别为 \n - `[Site_Name]`和[Site_URL]。 ", "Verification_Email": "点击 这里 验证你的垫邮件地址。", @@ -3430,21 +5690,51 @@ "Verify": "验证", "Verify_your_email": "验证您的电子邮件", "Version": "版本", + "Version_not_supported": "版本 <1>不支持", + "Version_supported_until": "版本 <1>支持 至 {{date}}", "Version_version": "版本 {{version}}", + "VideoConf_Default_Provider": "默认提供商", + "VideoConf_Default_Provider_Description": "如果安装了多个提供商应用,请选择用于新会议通话的一个。", + "VideoConf_Enable_Channels": "在公开频道中启用", + "VideoConf_Enable_DMs": "在私信中启用", + "VideoConf_Enable_Groups": "在私有频道中启用", + "VideoConf_Enable_Persistent_Chat": "启用持久聊天", + "VideoConf_Enable_Persistent_Chat_Alert": "如果工作区禁用了讨论,则持久聊天无法工作。若所用提供商应用未明确支持该功能,也无法工作。", + "VideoConf_Enable_Persistent_Chat_description": "启用持久聊天后,每次发起会议通话时 Rocket.Chat 都会创建一个讨论。提供商应用负责将聊天消息发送到该讨论。", + "VideoConf_Enable_Teams": "在团队中启用", + "VideoConf_Mobile_Ringing": "启用移动端响铃", + "VideoConf_Mobile_Ringing_Alert": "该功能目前处于实验阶段,移动端应用可能尚未完全支持。启用后会向用户发送额外的推送通知。", + "VideoConf_Mobile_Ringing_Description": "启用后,向移动端用户发起的直拨通话会像电话一样在其设备上响铃。", + "VideoConf_Persistent_Chat_Discussion_Name": "持久聊天讨论名称", + "VideoConf_Persistent_Chat_Discussion_Name_Description": "使用 [date] 标签指定日期插入位置。若未包含该标签,日期将添加到开头。", + "Video_Call_unavailable_for_this_type_of_room": "此类型房间不支持视频通话", "Video_Chat_Window": "视频聊天", "Video_Conference": "视频会议", + "Video_Conference_Description": "配置工作区的会议通话。", + "Video_Conference_Info": "会议信息", + "Video_Conference_Url": "会议 URL(网址)", + "Video_Conferences": "会议通话", + "Video_and_Audio_Call": "视频与音频通话", + "Video_call": "视频通话", + "Video_call_manager": "视频通话管理", "Video_message": "视频消息", + "Video_record": "视频录制", "Videocall_declined": "视频通话被拒绝。", "Videocall_enabled": "视频通话已启用", "Videos": "视频", "View_All": "查看全部", "View_Logs": "查看日志", + "View_rooms": "查看房间", + "View_channels": "查看频道", + "View_full_conversation": "查看完整对话", "View_mode": "视图", "View_original": "查看原始", "View_the_Logs_for": "查看 \"{{name}}\" 的日志", + "View_thread": "查看线程", "Viewing_room_administration": "正在查看聊天室管理", "Visibility": "可见性", "Visible": "可见", + "Visible_To_Workspace": "对工作区可见", "Visit_Site_Url_and_try_the_best_open_source_chat_solution_available_today": "访问 [Site_URL] 并尝试最好的开源即时消息解决方案!", "Visitor": "访客", "Visitor_Email": "访客电子邮件", @@ -3452,42 +5742,96 @@ "Visitor_Name": "访客名称", "Visitor_Name_Placeholder": "请输入访客名称…", "Visitor_Navigation": "游客导航", + "Visitor_does_not_exist": "访客不存在!", "Visitor_message": "访客消息", + "Visitor_not_found": "未找到访客", "Visitor_page_URL": "访客页面地址", "Visitor_time_on_site": "访客网站停留时间", - "WAU_value": "WAU {{value}}", + "VoIP": "VoIP(语音通话)", + "VoIP_TeamCollab": "团队语音通话(VoIP)", + "VoIP_TeamCollab_Description": "在团队协作中设置 VoIP", + "VoIP_TeamCollab_Feature1": "<0>直接呼叫: 在 Rocket.Chat 工作区内与团队成员即时发起或接听通话。", + "VoIP_TeamCollab_Feature2": "<0>分机管理: 管理员可以为用户分配唯一分机,支持组织内外快速直拨。", + "VoIP_TeamCollab_Feature3": "<0>通话转接: 无缝转接正在进行的通话,确保用户联系到合适的团队成员。", + "VoIP_TeamCollab_Feature4": "<0>可用性设置: 用户可控制其接听通话的可用性,提升灵活性。", + "VoIP_TeamCollab_Ice_Gathering_Timeout": "ICE 收集超时", + "VoIP_TeamCollab_Ice_Gathering_Timeout_Description": "在发送前等待 ICE 收集完成的时间。较低值可能导致 ICE 服务器无法使用,而较高值在指定无效 ICE 服务器时可能延迟 VoIP 通话开始。", + "VoIP_TeamCollab_Ice_Servers": "ICE 服务器", + "VoIP_TeamCollab_Ice_Servers_Description": "ICE 服务器(STUN 和/或 TURN)列表,以逗号分隔。 \n 允许使用 `username:password@stun:host:port` 或 `username:password@turn:host:port` 格式提供用户名、密码和端口。 \n 用户名和密码可以进行 HTML 编码。", + "VoIP_TeamCollab_WebRTC": "WebRTC 设置", + "VoIP_TeamCollab_SIP_Integration": "SIP 集成", + "VoIP_TeamCollab_SIP_Integration_Enabled": "已启用 SIP 集成", + "VoIP_TeamCollab_SIP_Integration_For_Internal_Calls": "通过 SIP 集成路由内部通话", + "VoIP_TeamCollab_Internal_SIP_Beta_Alert": "通过 SIP 路由内部通话仍处于 Beta 阶段,不建议用于生产环境。", + "VoIP_TeamCollab_Drachtio_Host": "Drachtio 主机", + "VoIP_TeamCollab_Drachtio_Port": "Drachtio 端口", + "VoIP_TeamCollab_Drachtio_Password": "Drachtio 密码", + "VoIP_TeamCollab_SIP_Server_Host": "SIP 服务器主机", + "VoIP_TeamCollab_SIP_Server_Port": "SIP 服务器端口", + "VoIP_device_permission_required": "需要麦克风/扬声器权限", + "VoIP_device_permission_required_description": "您的浏览器阻止 {{workspaceUrl}} 使用您的麦克风和/或扬声器。\n\n请在浏览器设置中允许扬声器和麦克风访问,以避免再次看到此消息。", + "VoIP_allow_and_call": "允许并呼叫", + "VoIP_allow_and_accept": "允许并接听", + "VoIP_cancel_and_reject": "取消并拒绝", + "Voice_Call": "语音通话", + "Voice_Call_Extension": "语音通话分机", + "Voice_and_omnichannel": "语音与 Omnichannel", + "Voice_call": "语音通话", + "Voice_call__user_": "与 {{user}} 语音通话", + "Voice_call__user__hangup": "结束与 {{user}} 的通话", + "Voice_call__user__cancel": "取消与 {{user}} 的通话", + "Voice_call__user__reject": "拒绝 {{user}} 的来电", + "Voice_call_extension": "语音通话分机", + "Voice_calling_disabled": "语音通话已禁用", + "Voice_calling_enabled": "语音通话已启用", + "Voice_calling_registration_failed": "语音通话注册失败", + "WAU_value": "WAU(周活跃用户) {{value}}", "Wait_activation_warning": "您的帐户必须由管理员手工激活后才能登录。", + "Waiting_for_answer": "等待接听", "Waiting_queue": "等待队列", "Waiting_queue_message": "等待队列消息", "Waiting_queue_message_description": "访客进入等待队列时显示的消息", "Warning": "警告", "Warnings": "警告", + "We_Could_not_retrive_any_data": "无法检索到任何数据", "We_appreciate_your_feedback": "我们很感谢您的反馈", "We_are_offline_Sorry_for_the_inconvenience": "现在没有人在线,带来不便请您谅解。", "We_have_sent_password_email": "我们已经向您发送密码重置的电子邮件。如果您没有收到邮件,请重试。", "We_have_sent_registration_email": "我们已经向您发出一封电子邮件,以确认您的注册。如果您没有收到邮件,请重试。", "WebDAV_Accounts": "WebDAV帐户", + "WebDAV_Integration_Not_Allowed": "不允许 WebDAV 集成", + "WebRTC": "WebRTC(实时通信)", + "WebRTC_Call": "WebRTC 通话", + "WebRTC_Call_unavailable_for_federation": "联邦房间不支持 WebRTC 通话", + "WebRTC_Description": "在无需中间人的情况下广播音频和/或视频,并在浏览器之间传输任意数据。", "WebRTC_Enable_Channel": "为公共频道启用", "WebRTC_Enable_Direct": "为私聊启用", "WebRTC_Enable_Private": "为私人频道启用", + "WebRTC_call_declined_message": " 联系人已拒绝通话。", + "WebRTC_call_ended_message": " 通话于 {{endTime}} 结束 - 持续 {{callDuration}}", "WebRTC_direct_audio_call_from_%s": "来自 %s 的直接音频通话", "WebRTC_direct_video_call_from_%s": "来自 %s 的直接视频通话", "WebRTC_group_audio_call_from_%s": "来自 %s 的群音频通话", "WebRTC_group_video_call_from_%s": "来自 %s 的群视频通话", "WebRTC_monitor_call_from_%s": "监控来自 %s 的通话", "Webdav Integration": "WebDAV集成", + "Webdav Integration_Description": "一个允许用户在服务器上创建、修改和移动文档的框架,用于连接 Nextcloud 等 WebDAV 服务器。", "Webdav_Integration_Enabled": "Webdav 集成已启用", "Webdav_Password": "WebDAV 密码", "Webdav_Server_URL": "WebDAV 服务器访问 URL", "Webdav_Username": "WebDAV 用户名", + "Webdav_account_removed": "WebDAV 帐户已移除", "Webdav_add_new_account": "添加新的 WebDAV 帐户", "Webhook_Details": "WebHook 详情", "Webhook_URL": "WebHook 地址", - "Webhooks": "Webhooks", + "Webhook_URL_not_set": "WebHook URL(网址) 未设置", + "Webhooks": "WebHook", "Website": "网站", "Wednesday": "星期三", "Weekly_Active_Users": "每周活跃用户", "Welcome": "欢迎 %s", + "Welcome_email_failed": "重新发送欢迎邮件失败", + "Welcome_email_resent": "欢迎邮件已重新发送", "Welcome_to": "欢迎来到 [Site_Name]", "Welcome_to_the": "欢迎来到", "Welcome_to_workspace": "欢迎来到 {{Site_Name}}", @@ -3496,53 +5840,93 @@ "When_is_the_chat_busier?": "什么时候聊天比较忙?", "Where_are_the_messages_being_sent?": "信息发送至哪里?", "Why_did_you_chose__score__": "您选择 {{score}} 的原因是?", + "Why_has_a_trial_been_applied_to_this_workspace": "<0>为何此工作区启用了试用?", "Will_Appear_In_From": "将显示在您发送的电子邮件 From: 标头中。", "Will_be_available_here_after_saving": "将在保存后可用", + "Without_SLA": "无 SLA", "Without_priority": "没有优先级", + "Workspace": "工作区", + "Workspace_detail": "工作区详情", + "Workspace_and_user_preferences": "工作区与用户偏好设置", + "Workspace_exceeded_MAC_limit_disclaimer": "该工作区已超过月活跃联系人(MAC)上限,请联系工作区管理员处理此问题。", + "Workspace_instance": "工作区实例", + "Workspace_not_connected": "工作区未连接", + "Workspace_not_registered": "工作区未注册", + "Workspace_now_using_device_management": "工作区现在使用设备管理", + "Workspace_registered": "工作区已注册", + "Workspaces_on_Community_edition_install_app": "社区版工作区最多可启用 {{limit}} 个 {{context}} 应用。升级到 Premium 计划以启用无限应用。", + "Workspaces_on_community_edition_trial_off": "社区版工作区最多可启用 5 个市场应用。私有应用仅可在 Premium 计划中启用。升级到 Premium 以移除限制并提升工作区能力。", + "Workspaces_on_community_edition_trial_on": "社区版工作区最多可启用 5 个市场应用。私有应用仅可在 Premium 计划中启用。立即开始免费 Premium 试用以移除这些限制!", "Worldwide": "全世界", + "Would_you_like_to_place_chat_on_hold": "是否要将此聊天挂起?", "Would_you_like_to_return_the_inquiry": "你想返回调查吗?", + "Would_you_like_to_return_the_queue": "是否要将此房间移回队列?所有对话历史都会保留在该房间中。", + "Wrap_up_conversation": "结束会话", "Yes": "是", "Yes_archive_it": "是的,存档!", "Yes_clear_all": "是,清除所有!", + "Yes_continue": "是,继续!", "Yes_deactivate_it": "是,停用它!", "Yes_delete_it": "是的,删除它!", "Yes_hide_it": "确定,隐藏!", "Yes_leave_it": "确定,离开!", "Yes_mute_user": "确定,禁言该用户!", + "Yes_pin_message": "是,置顶消息", "Yes_prune_them": "是的,修剪它们!", "Yes_remove_user": "确定,删除用户!", "Yes_unarchive_it": "是的,解除它的归档!", "Yesterday": "昨天", "You": "您", + "You_and_users_Reacted_with": "你和 {{users}} 使用 {{emoji}} 作出反应", + "You_are_converting_team_to_channel": "你正在将此团队转换为频道。", "You_are_logged_in_as": "您已登录为", "You_are_not_authorized_to_view_this_page": "您无权查看此页面。", + "You_are_not_authorized_to_access_this_feature": "你无权访问此功能。", "You_can_change_a_different_avatar_too": "您可以覆盖从这次整合后的分身。", "You_can_close_this_window_now": "您现在可以关闭这个窗口了。", + "You_can_do_from_account_preferences": "你可以稍后在账户偏好设置中完成此操作", "You_can_search_using_RegExp_eg": "您可以使用正则表达式(Regular Expression)搜索。例如:/^text$/i", + "You_can_try_to": "你可以尝试", "You_can_use_an_emoji_as_avatar": "您也可以使用表情符号作为头像。", "You_can_use_webhooks_to_easily_integrate_livechat_with_your_CRM": "您可以使用网页钩子方便地将 Omnichannel 集成到您的客户关系管理系统。", "You_cant_leave_a_livechat_room_Please_use_the_close_button": "你不能离开 Omnichannel 聊天室。请使用关闭按钮。", + "You_cant_take_chats_offline": "你处于离线状态,无法接收新对话", + "You_cant_take_chats_unavailable": "你处于不可用状态,无法接收新对话", + "You_do_not_have_permission_to_execute_this_command": "你没有足够权限执行命令:`/{{command}}`", + "You_followed_this_message": "你已关注此消息。", "You_have_a_new_message": "您有新的消息", "You_have_been_muted": "你已被禁言,因此不能在此聊天室内发言", + "You_have_been_removed_from__roomName_": "你已被从房间 {{roomName}} 中移除", + "You_have_created_user": "你已创建 1 个用户", "You_have_n_codes_remaining": "你有 {{number}} 条剩余代码。", "You_have_not_verified_your_email": "您尚未验证您的电子邮箱地址。", + "You_have_reached_the_limit_active_costumers_this_month": "你已达到本月活跃客户上限", "You_have_successfully_unsubscribed": "您已成功从我们的邮件列表中取消订阅。", "You_have_to_set_an_API_token_first_in_order_to_use_the_integration": "您必须先设置 API 令牌才能使用集成。", + "You_mentioned___mentions__but_theyre_not_in_this_room": "你提及了 {{mentions}},但他们不在此房间。", + "You_mentioned___mentions__but_theyre_not_in_this_room_You_can_ask_a_room_admin_to_add_them": "你提及了 {{mentions}},但他们不在此房间。你可以让房间管理员添加他们。", + "You_mentioned___mentions__but_theyre_not_in_this_room_You_let_them_know_via_dm": "你提及了 {{mentions}},但他们不在此房间。你已通过私信通知他们。", "You_must_join_to_view_messages_in_this_channel": "你必须先加入此频道才能查看消息", "You_need_confirm_email": "登录前请验证您的电子邮件!", "You_need_install_an_extension_to_allow_screen_sharing": "你需要安装扩展程序才能支持屏幕共享", "You_need_to_change_your_password": "你需要修改密码", + "You_need_to_join_this_channel": "你需要加入此频道才能查看其历史记录", "You_need_to_type_in_your_password_in_order_to_do_this": "您需要输入您的密码以便执行此操作!", "You_need_to_type_in_your_username_in_order_to_do_this": "您需要输入您的用户名以便完成此操作!", "You_need_to_verifiy_your_email_address_to_get_notications": "您需要验证您的电子邮箱地址才能收到通知", "You_reached_the_maximum_number_of_guest_users_allowed_by_your_license": "您的访客用户数已经达到当前许可证允许的最大值。", + "You_reacted_with": "你使用 {{emoji}} 作出反应", "You_should_inform_one_url_at_least": "您应定义至少一个 URL 。", "You_should_name_it_to_easily_manage_your_integrations": "为了方便管理您的集成,请给它命名。", + "You_unfollowed_this_message": "你已取消关注此消息。", + "You_users_and_more_Reacted_with": "你、{{users}} 及另外 {{counter}} 人使用 {{emoji}} 作出反应", "You_will_be_asked_for_permissions": "您将被要求提供权限", "You_will_not_be_able_to_recover": "您将无法恢复此消息!", "You_will_not_be_able_to_recover_email_inbox": "您将不能恢复此电子邮件收件箱", "You_will_not_be_able_to_recover_file": "您将不能恢复此文件!", "You_wont_receive_email_notifications_because_you_have_not_verified_your_email": "您将不会收到电子邮件通知,因为您还没有验证您的电子邮箱地址。", + "Your_E2EE_password_is": "你的 E2EE 密码是:", + "Your_E2EE_password_is_incorrect": "你的 E2EE 密码不正确", "Your_TOTP_has_been_reset": "您的两步验证 TOTP 已重置。", "Your_e2e_key_has_been_reset": "您的端到端加密密钥已被重置", "Your_email_address_has_changed": "您的电子邮箱已更改。", @@ -3558,17 +5942,37 @@ "Your_password_was_changed_by_an_admin": "您的密码已被管理员更改。", "Your_push_was_sent_to_s_devices": "您的推送已被送到 %s 台设备", "Your_question": "你的问题", + "Your_request_to_join__roomName__has_been_made_it_could_take_up_to_15_minutes_to_be_processed": "你加入 {{roomName}} 的请求已提交,处理可能需要最多 15 分钟。完成后会通知你。", "Your_server_link": "您的服务器链接", "Your_temporary_password_is_password": "您的暂时密码为 [password]。", + "Your_web_browser_blocked_Rocket_Chat_from_opening_tab": "你的浏览器阻止 Rocket.Chat 打开新标签页。", "Your_workspace_is_ready": "您的工作区已准备好使用🎉", + "Youre_not_a_part_of__channel__and_I_mentioned_you_there": "你不是 {{channel}} 的成员,但我在那里提到了你", + "Zapier": "Zapier(自动化)", + "Zapier_integration_has_been_deprecated": "Zapier 集成已弃用,可能无法按预期工作,且不再更新", + "Zapier_integration_is_not_available": "Zapier 集成已弃用,新的 Rocket.Chat 工作区不再可用", + "Zoom_in": "放大", + "Zoom_out": "缩小", "access-mailer": "访问邮件程序屏幕", "access-mailer_description": "向所有用户发送大量电子邮件的权限。", + "access-marketplace": "访问应用市场", + "access-marketplace_description": "浏览并从应用市场获取应用的权限", "access-permissions": "访问权限屏幕", "access-permissions_description": "修改各种角色的权限。", "access-setting-permissions": "修改基于设置的权限", + "access-setting-permissions_description": "允许修改基于设置的权限。", + "access-federation": "访问联邦", + "access-federation_description": "访问联邦功能、创建并加入联邦房间的权限", + "active": "活跃", + "add-all-to-room": "将所有用户添加到房间", + "add-all-to-room_description": "将所有用户添加到房间的权限", "add-livechat-department-agents": "将 omnichannel 客服添加到部门", + "add-livechat-department-agents_description": "将全渠道客服添加到部门的权限", "add-oauth-service": "添加 Oauth 服务", "add-oauth-service_description": "添加新 Oauth 服务的权限", + "add-team-member": "添加团队成员", + "add-team-member_description": "添加团队成员的权限", + "add-to-room": "添加到房间", "add-user": "添加用户", "add-user-to-any-c-room": "将用户添加到任何公共频道", "add-user-to-any-c-room_description": "将用户添加到任何公共频道的权限", @@ -3577,42 +5981,77 @@ "add-user-to-joined-room": "将用户添加到任何加入的频道", "add-user-to-joined-room_description": "将用户添加到当前已加入的频道的权限", "add-user_description": "通过用户筛选将新用户添加到服务器的权限", + "added__roomName__to_team": "已添加 #{{roomName}} 到该团队", + "added__roomName__to_this_team": "已将 #{{roomName}} 添加到该团队", "additional_integrations_Bots": "如果你正在研究如何整合你自己的机器人,那么可以看看我们的 Hubot 适配器。 https://github.com/RocketChat/hubot-rocketchat", + "admin-no-active-video-conf-provider": "**会议通话未启用**:请配置会议通话以在该工作区使用。", + "admin-no-videoconf-provider-app": "**会议通话未启用**:可在 Rocket.Chat 应用市场获取会议通话应用。", + "admin-video-conf-provider-not-configured": "**会议通话未启用**:请配置会议通话以在该工作区使用。", + "all": "全部", "and": "和", "api-bypass-rate-limit": "绕过 REST API 速率限制", + "api-bypass-rate-limit_description": "允许调用 API 时不受速率限制。", "archive-room": "归档聊天室", "archive-room_description": "归档频道的权限", + "are_playing": "正在播放", + "are_recording": "正在录制", "are_typing": "正在输入", + "are_uploading": "正在上传", "assign-admin-role": "分配管理员角色", "assign-admin-role_description": "将管理员角色分配给其他用户的权限", "assign-roles": "分配角色", + "assign-roles_description": "将角色分配给其他用户的权限", "at": "于", "auto-translate": "自动翻译", "auto-translate_description": "使用自动翻译工具的权限", "away": "离开", "ban-user": "禁止用户", "ban-user_description": "在频道中屏蔽用户的权限", + "block-ip-device-management": "设备管理:屏蔽 IP", + "block-ip-device-management_description": "屏蔽 IP 地址的权限", + "block-livechat-contact": "屏蔽 Omnichannel 联系人频道", "bold": "粗体", "bot_request": "机器人请求", "bulk-register-user": "批量创建频道", "bulk-register-user_description": "批量创建频道的权限", "busy": "忙碌", "by": "通过", + "bypass-time-limit-edit-and-delete": "绕过编辑/删除时间限制", + "bypass-time-limit-edit-and-delete_description": "绕过编辑和删除消息时间限制的权限", "cache_cleared": "缓存已清理", "call-management": "呼叫管理", + "call-management_description": "发起会议的权限", + "can-audit": "允许审计", + "can-audit-log": "可访问审计日志", + "can-audit-log_description": "访问审计日志的权限", + "can-audit_description": "访问审计的权限", + "Change_E2EE_password": "更改端到端加密(E2EE)密码", + "change-livechat-room-visitor": "更改 Livechat 房间访客", + "change-livechat-room-visitor_description": "向 Livechat 房间访客添加额外信息的权限", + "changed_room_announcement_to__room_announcement_": "将房间公告更改为:{{room_announcement}}", + "changed_room_description_to__room_description_": "将房间描述更改为:{{room_description}}", "channel": "频道", + "chat_on_hold_due_to_inactivity": "由于无活动,此聊天已被挂起", "clean-channel-history": "清理频道历史记录", "clean-channel-history_description": "清除频道历史记录的权限", "clear": "清理", + "clear-oembed-cache": "清除 OEmbed 缓存", + "clear-oembed-cache_description": "清除 OEmbed 缓存的权限", "clear_cache_now": "立即清理缓存", "clear_history": "清理历史记录", "close": "关闭", + "close-blocked-room-comment": "此频道已被屏蔽", "close-livechat-room": "关闭 omnichannel ", "close-livechat-room_description": "关闭当前 omnichannel 聊天室的权限", "close-others-livechat-room": "关闭其他 omnichannel 聊天室", "close-others-livechat-room_description": "关闭其他 omnichannel 聊天室的权限", + "cloud.RegisterWorkspace_Setup_Email_Confirmation": "确认链接已发送到 <1>email。", + "cloud.RegisterWorkspace_Setup_Terms_Privacy": "我同意 <1>条款与条件 和 <3>隐私政策", + "cloud.RegisterWorkspace_Token_Step_One": "1. 前往:<1>cloud.rocket.chat > Workspaces 并点击 <3>'Register self-managed'。", "color": "颜色", "conversation_with_s": "与 %s 的会话", + "convert-team": "转换团队", + "convert-team_description": "将团队转换为频道的权限", "could-not-access-webdav": "不能访问 WebDAV", "create-c": "创建公共频道", "create-c_description": "创建公共频道的权限", @@ -3620,26 +6059,51 @@ "create-d_description": "开启私聊的权限", "create-invite-links": "创建邀请链接", "create-invite-links_description": "创建频道邀请链接的权限", + "create-livechat-contact": "创建 Omnichannel 联系人", "create-p": "创建私人频道", "create-p_description": "创建私人频道的权限", "create-personal-access-tokens": "创建个人访问令牌", + "create-personal-access-tokens_description": "创建个人访问令牌的权限", + "create-team": "创建团队", + "create-team-channel": "在团队内创建频道", + "create-team-channel_description": "在团队中创建频道的权限(覆盖全局权限)", + "create-team-group": "在团队内创建群组", + "create-team-group_description": "在团队中创建群组的权限(覆盖全局权限)", + "create-team_description": "创建团队的权限", "create-user": "创建用户", "create-user_description": "创建用户的权限", "days": "天", + "deactivated": "已停用", + "default": "默认", "delete-c": "删除公共频道", "delete-c_description": "删除公共频道的权限", "delete-d": "删除私聊消息", "delete-d_description": "删除私聊消息的权限", "delete-message": "删除消息", "delete-message_description": "允许删除聊天室内的消息", + "delete-own-message": "删除自己的消息", + "delete-own-message_description": "删除自己消息的权限", "delete-p": "删除私人频道", "delete-p_description": "允许删除私人频道", + "delete-team": "删除团队", + "delete-team-channel": "删除团队内频道", + "delete-team-channel_description": "在已授予删除公共频道权限时,删除团队内频道的权限", + "delete-team-group": "删除团队内群组", + "delete-team-group_description": "在已授予删除群组权限时,删除团队内群组的权限", + "delete-team_description": "删除团队的权限", "delete-user": "删除用户", "delete-user_description": "删除用户的权限", + "delete-livechat-contact": "删除 Omnichannel 联系人", + "different_values_found": "找到 {{number}} 个不同的值", + "disabled": "已禁用", "discussion-created": "{{message}}", "duplicated-account": "重复的账号", + "edit-livechat-room-customfields": "编辑 Livechat 房间自定义字段", + "edit-livechat-room-customfields_description": "编辑 Livechat 房间自定义字段的权限", "edit-message": "编辑信息", "edit-message_description": "编辑聊天室内消息的权限", + "edit-omnichannel-contact": "编辑 Omnichannel 联系人", + "edit-omnichannel-contact_description": "编辑 Omnichannel 联系人的权限", "edit-other-user-active-status": "编辑其他用户活动状态", "edit-other-user-active-status_description": "启用或禁用其他帐户的权限", "edit-other-user-avatar": "编辑其他用户头像", @@ -3651,6 +6115,7 @@ "edit-other-user-password": "编辑其他用户密码", "edit-other-user-password_description": "修改其他用户密码的权限。需要编辑其他用户信息的权限。", "edit-other-user-totp": "编辑其他用户的两步验证 TOTP", + "edit-other-user-totp_description": "编辑其他用户双因素 TOTP 的权限", "edit-privileged-setting": "编辑特权设置", "edit-privileged-setting_description": "编辑设置的权限", "edit-room": "编辑Room", @@ -3659,6 +6124,12 @@ "edit-room-retention-policy": "编辑聊天室的保留政策", "edit-room-retention-policy_description": "编辑聊天室保留策略,自动删除其中的消息的权限", "edit-room_description": "编辑聊天室的名称,主题,类型(私人或公共状态)和状态(活动或归档)的权限", + "edit-team": "编辑团队", + "edit-team-channel": "编辑团队频道", + "edit-team-channel_description": "编辑团队频道的权限", + "edit-team-member": "编辑团队成员", + "edit-team-member_description": "编辑团队成员的权限", + "edit-team_description": "编辑团队的权限", "edited": "已编辑", "email_plain_text_only": "仅发送纯文本邮件", "email_style_description": "避免嵌套选择器", @@ -3666,30 +6137,52 @@ "ensure_email_address_valid": "无效电子邮件地址", "error-action-not-allowed": "{{action}} 不允许", "error-agent-offline": "客服离线", + "error-agent-status-service-offline": "客服状态离线或 Omnichannel 服务未启用", "error-application-not-found": "应用程序未找到", "error-archived-duplicate-name": "有一个名为 '{{room_name}}' 的已归档频道", "error-avatar-invalid-url": "无效头像地址:{{url}}", "error-avatar-url-handling": "为用户 {{username}} 从网址 ({{url}}) 处理头像时出错", + "error-blocked-username": "**{{field}}** 已被屏蔽,无法使用!", + "error-business-hour-finish-time-before-start-time": "结束时间必须晚于开始时间", + "error-business-hour-finish-time-equals-start-time": "开始时间和结束时间不能相同", "error-business-hours-are-closed": "营业时间已结束", "error-canned-response-not-found": "未找到自动回复", "error-cannot-delete-app-user": "已禁用不允许直接删除应用用户,请卸载相应的应用来移除它。", + "error-cannot-place-chat-on-hold": "无法将聊天挂起", + "error-cant-add-federated-users": "无法将联邦用户添加到非联邦房间", "error-cant-invite-for-direct-room": "不能邀请用户加入私聊", "error-channels-setdefault-is-same": "频道的默认设置与要更改的设置相同。", "error-channels-setdefault-missing-default-param": "bodyParam 'default' 是必需的", + "error-comment-is-required": "必须填写备注", + "error-contact-sent-last-message-so-cannot-place-on-hold": "联系人发送了最后一条消息,无法将聊天挂起", + "error-contact-not-found": "未找到联系人。", + "error-contact-has-open-rooms": "联系人仍有打开的房间,无法删除。", + "error-contact-something-went-wrong": "删除联系人时发生错误。", "error-could-not-change-email": "无法更改电子邮箱地址", "error-could-not-change-name": "无法更改名称", "error-could-not-change-username": "无法更改用户名", "error-custom-field-name-already-exists": "自定义字段名称已存在", + "error-custom-field-not-allowed": "不允许自定义字段 {{key}}", "error-delete-protected-role": "无法删除受保护的角色", "error-department-not-found": "找不到该部门", + "error-department-removal-disabled": "管理员已禁用部门删除,请联系管理员", "error-direct-message-file-upload-not-allowed": "私聊中不允许文件共享", + "error-direct-message-max-user-exceeded": "私信中最多只能添加 {{maxUsers}} 位用户(包括你自己)", "error-duplicate-channel-name": "频道 '{{channel_name}}' 已存在", + "error-duplicate-priority-name": "已存在同名优先级", + "error-duplicated-sla": "已存在同名或相同截止时间的 SLA", "error-edit-permissions-not-allowed": "不允许编辑权限", + "error-email-body-not-initialized": "邮件正文未初始化。发送富文本邮件前请在邮件设置中配置页眉和页脚", "error-email-domain-blacklisted": "电子邮箱域名被列入黑名单", + "error-email-inbox-not-found": "未找到邮件收件箱", "error-email-send-failed": "尝试发送邮件出错:{{message}}", "error-essential-app-disabled": "错误:为此所需的 Rocket.Chat 应用已禁用。请联系您的系统管理员", + "error-extension-not-assigned": "未分配分机", + "error-extension-not-available": "分机不可用", + "error-failed-to-delete-department": "删除部门失败", "error-field-unavailable": "{{field}} 已被使用 :(", "error-file-too-large": "文件太大", + "error-forwarding-chat": "转接聊天时发生错误,请稍后重试。", "error-forwarding-chat-same-department": "所选部门和当前聊天室部门小童", "error-forwarding-department-target-not-allowed": "不允许转发到目标部门", "error-guests-cant-have-other-roles": "访客用户无法拥有其他角色", @@ -3698,25 +6191,31 @@ "error-import-file-missing": "在指定的路径上找不到要导入的文件。", "error-importer-not-defined": "导入程序没有正确定义,它缺少导入类。", "error-input-is-not-a-valid-field": "{{input}} 不是一个有效的 {{field}}", + "error-inquiry-taken": "问询已被接手", + "error-insufficient-permission": "错误!你没有执行此操作所需的 ' {{permission}} ' 权限", "error-invalid-account": "无效账号", "error-invalid-actionlink": "无效操作链接", "error-invalid-arguments": "无效参数", "error-invalid-asset": "无效资产", "error-invalid-channel": "无效频道。", "error-invalid-channel-start-with-chars": "无效频道。应该以 @ 或 # 开头", + "error-invalid-contact": "联系人无效。", "error-invalid-custom-field": "无效自定义字段", "error-invalid-custom-field-name": "无效自定义字段名称。只能使用字母、数字、连字符和下划线。", "error-invalid-custom-field-value": "无效 {{field}} 字段值", "error-invalid-date": "提供的日期无效。", + "error-invalid-dates": "开始日期不能晚于结束日期", "error-invalid-description": "无效描述", "error-invalid-domain": "无效域名", "error-invalid-email": "无效电子邮件 {{email}}", "error-invalid-email-address": "无效电子邮件地址", "error-invalid-email-inbox": "非法电子邮件收件箱", + "error-invalid-external-service-response": "外部服务响应无效", "error-invalid-file-height": "无效文件高度", "error-invalid-file-type": "无效文件类型", "error-invalid-file-width": "无效文件宽度", "error-invalid-from-address": "您提供了一个无效 FROM 地址。", + "error-invalid-image-url": "图片 URL 无效", "error-invalid-inquiry": "无效查询", "error-invalid-integration": "无效集成", "error-invalid-message": "无效消息", @@ -3741,18 +6240,31 @@ "error-invalid-user": "无效用户", "error-invalid-username": "无效用户名", "error-invalid-value": "无效值", + "error-removing-tag": "移除标签时出错", "error-invalid-webhook-response": "WebHook 地址响应的状态码不是200", + "error-license-user-limit-reached": "已达到最大用户数限制。", + "error-loading-extension-list": "加载分机列表失败", "error-logged-user-not-in-room": "你不在 `%s` 聊天室里", + "error-mac-limit-reached": "已达到此工作区月活跃联系人数量上限。", + "error-max-departments-number-reached": "已达到许可证允许的部门上限。请联系 sale@rocket.chat 获取新许可证。", "error-max-guests-number-reached": "您的访客用户数已经达到当前许可的最大值。请联系 sale@rocket.chat 以获取一个新的许可。", "error-max-number-simultaneous-chats-reached": "已达到每客服最大同时聊天数量", + "error-max-rooms-per-guest-reached": "已达到每位访客允许的房间上限。", "error-message-deleting-blocked": "不允许删除消息", "error-message-editing-blocked": "不允许编辑消息", "error-message-size-exceeded": "消息大小超过 Message_MaxAllowedSize", "error-missing-unsubscribe-link": "您必须提供 [unsubscribe] 链接。", + "error-no-agents-available-for-service-on-department": "该部门没有可服务的客服。", + "error-no-message-for-unread": "没有可标记为未读的消息", + "error-no-owner-channel": "只有所有者才能将此频道添加到团队", + "error-no-permission-team-channel": "你没有权限将此频道添加到团队", "error-no-tokens-for-this-user": "该用户没有令牌", "error-not-allowed": "不允许", "error-not-authorized": "未经授权", + "error-not-authorized-federation": "无权访问联邦", "error-office-hours-are-closed": "当前非上班时间。", + "error-only-compliant-users-can-be-added-to-abac-rooms": "只有符合条件的用户才能加入 ABAC 管理的房间。", + "error-password-in-history": "输入的密码曾被使用过", "error-password-policy-not-met": "密码不符合服务器的政策", "error-password-policy-not-met-maxLength": "密码不符合服务器的最大长度限制(密码过长)", "error-password-policy-not-met-minLength": "密码不符合服务器的最小长度限制(密码太短)", @@ -3765,21 +6277,40 @@ "error-personal-access-tokens-are-current-disabled": "个人访问令牌已禁用", "error-pinning-message": "消息不应被固定", "error-push-disabled": "推送被禁用", + "error-registration-not-found": "未找到注册信息", "error-remove-last-owner": "这是最后一个所有者。要删除此用户,请先设置一个新的所有者。", "error-returning-inquiry": "询价队列返回错误", + "error-role-already-present": "已存在同名角色", "error-role-in-use": "无法删除使用中的角色", "error-role-name-required": "角色名称是必需的", + "error-room-already-closed": "房间已关闭", + "error-room-already-hidden": "房间已隐藏", + "error-room-does-not-exist": "该房间不存在", + "error-room-is-already-on-hold": "错误!房间已挂起", "error-room-is-not-closed": "聊天室没有关闭", + "error-room-not-on-hold": "错误!房间未挂起", + "error-room-onHold": "错误!房间处于挂起状态", + "error-room-is-abac-managed": "该房间由 ABAC 管理,无法直接添加新用户", + "error-adding-monitor": "添加监控时出错", + "error-saving-sla": "保存 SLA 时发生错误", "error-selected-agent-room-agent-are-same": "所选客服和聊天室客服相同", "error-starring-message": "不能开始消息", "error-tags-must-be-assigned-before-closing-chat": "必须在聊天结束前分配标签", "error-the-field-is-required": "字段 {{field}} 必填。", + "error-this-is-a-premium-feature": "仅高级方案可用", + "error-this-is-an-ee-feature": "这是企业版功能", "error-this-is-not-a-livechat-room": "这不是一个 omnichannel 聊天室", + "error-timeout": "请求超时", "error-token-already-exists": "已存在具有此名称的令牌", "error-token-does-not-exists": "令牌不存在", "error-too-many-requests": "错误,请求过多。请放慢速度。你必须等待 {{seconds}} 秒后再次重试。", "error-transcript-already-requested": "已请求聊天记录", + "error-unable-to-update-priority": "无法更新优先级", + "error-unknown-contact": "联系人未知。", "error-unpinning-message": "不能取消固定消息", + "error-unserved-rooms-cannot-be-placed-onhold": "房间在被服务前无法挂起", + "error-unverified-contact": "联系人未验证。", + "error-user-deactivated": "用户未激活", "error-user-has-no-roles": "用户没有角色", "error-user-is-not-activated": "用户未激活", "error-user-is-not-agent": "用户不是 omnichannel 客服", @@ -3791,9 +6322,22 @@ "error-user-registration-disabled": "用户注册被禁用", "error-user-registration-secret": "用户注册只能通过私密地址", "error-validating-department-chat-closing-tags": "当部门要求标签时关闭对话需要至少一个关闭标签", + "error-videoconf-cant-start-call-with-manager-busy": "由于其他通话的当前状态,无法发起新通话。", + "error-videoconf-direct-call-accept-canceled": "远端用户在我们接受通话前已挂断。", + "error-videoconf-direct-call-accept-ended": "服务器在我们接受通话前结束了通话。", + "error-videoconf-direct-call-accept-timeout": "通知通话已接受后,远端用户无响应。", + "error-videoconf-join-failed": "加入通话时发生意外服务器错误。", + "error-videoconf-missing-url": "获取会议 URL 失败。", + "error-videoconf-unexpected": "发生意外的会议通话错误", + "error-voip-disaled": "团队语音通话(VoIP)已禁用", "error-you-are-last-owner": "你是聊天室当前的所有者。请先重设所有者再离开聊天室。", + "every_10_minutes": "每 10 分钟一次", "every_10_seconds": "每10秒一次", + "every_12_hours": "每 12 小时一次", + "every_24_hours": "每 24 小时一次", "every_30_minutes": "每30分钟一次", + "every_30_seconds": "每 30 秒一次", + "every_48_hours": "每 48 小时一次", "every_5_minutes": "每5分钟一次", "every_day": "每天一次", "every_hour": "每小时一次", @@ -3802,18 +6346,33 @@ "every_six_hours": "每6小时一次", "except_pinned": "(固定的除外)", "expression": "表达式", + "featured": "精选", "file_pruned": "文件已修剪", "force-delete-message": "强制删除消息", "force-delete-message_description": "绕过所有限制删除消息的权限", + "free_per_month_user": "$0/每月/用户", "get-password-policy-forbidRepeatingCharacters": "密码不应包含重复的字符", "get-password-policy-forbidRepeatingCharactersCount": "密码不应包含多于 {{forbidRepeatingCharactersCount}} 个重复的字符", + "get-password-policy-forbidRepeatingCharactersCount-label": "最多 {{limit}} 个重复字符", "get-password-policy-maxLength": "密码长度最大不应超过 {{maxLength}} ", + "get-password-policy-maxLength-label": "最多 {{limit}} 个字符", "get-password-policy-minLength": "密码长度最小不应超过 {{minLength}} ", + "get-password-policy-minLength-label": "至少 {{limit}} 个字符", "get-password-policy-mustContainAtLeastOneLowercase": "密码应包含至少一个小写字符", + "get-password-policy-mustContainAtLeastOneLowercase-label": "至少包含一个小写字母", "get-password-policy-mustContainAtLeastOneNumber": "密码应包含至少一个数字", + "get-password-policy-mustContainAtLeastOneNumber-label": "至少包含一个数字", "get-password-policy-mustContainAtLeastOneSpecialCharacter": "密码应包含至少一个特殊字符", + "get-password-policy-mustContainAtLeastOneSpecialCharacter-label": "至少包含一个符号", "get-password-policy-mustContainAtLeastOneUppercase": "密码应包含至少一个大写字符", + "get-password-policy-mustContainAtLeastOneUppercase-label": "至少包含一个大写字母", + "get-server-info": "获取服务器信息", + "get-server-info_description": "获取服务器信息的权限", + "github_HEAD": "HEAD(指针)", "github_no_public_email": "在您的 GitHub 帐户中没有设置任何电子邮件作为公共电子邮件地址。", + "group_mentions_counter": { + "other": "{{count}} group mentions" + }, "hours": "小时", "if_they_are_from": "(如果他们来自 %s)", "importer_status_done": "成功完成", @@ -3834,21 +6393,45 @@ "importer_status_preparing_users": "读取用户文件", "importer_status_uploading": "上传文件", "importer_status_user_selection": "已可选择要导入的内容", + "Inbound": "入站", + "increments-of-two": "以 2 为增量", "initials_avatar": "首字母头像", "inline_code": "内联代码", + "integration-scripts-disabled": "已禁用集成脚本", + "integration-scripts-isolated-vm-disabled": "“安全沙箱”可能无法用于新建或修改的脚本。", + "integration-scripts-unknown-engine": "未知的集成脚本引擎", "invisible": "隐身", + "is_playing": "正在播放", + "is_recording": "正在录制", "is_typing": "正在输入", + "is_uploading": "正在上传", "italics": "斜体", "join-without-join-code": "无加入码加入", "join-without-join-code_description": "绕过频道加入码的权限", + "joined": "已加入", + "_joined.comment": "用户 a 和用户 b 加入了视频通话", + "kick-user-from-any-c-room": "从任意公共频道踢出用户", + "kick-user-from-any-c-room_description": "从任意公共频道踢出用户的权限", + "kick-user-from-any-p-room": "从任意私有频道踢出用户", + "kick-user-from-any-p-room_description": "从任意私有频道踢出用户的权限", "leave-c": "保留频道", + "leave-c_description": "离开频道的权限", "leave-p": "离开私人组", + "leave-p_description": "离开私有群组的权限", + "Limit_number_of_logs": "限制数量", "line": "行", + "link": "链接", + "logout-device-management": "设备管理:注销设备", + "logout-device-management_description": "从设备管理面板注销其他用户的权限", + "logout-other-user": "注销其他用户", + "logout-other-user_description": "注销其他用户的权限", "mail-messages": "邮件消息", "mail-messages_description": "使用邮件消息选项的权限", "manage-apps": "管理应用", + "manage-apps_description": "管理所有应用的权限", "manage-assets": "管理资产", "manage-assets_description": "管理服务器资产的权限", + "manage-cloud": "管理云服务", "manage-cloud_description": "管理云", "manage-email-inbox": "管理电子邮件收件箱", "manage-email-inbox_description": "管理电子邮件收件箱的权限", @@ -3859,8 +6442,25 @@ "manage-integrations": "管理集成", "manage-integrations_description": "管理服务器集成的权限", "manage-livechat-agents": "管理 Omnichannel 客服", + "manage-livechat-agents_description": "管理 Omnichannel 客服的权限", + "manage-livechat-canned-responses": "管理 Omnichannel 自动回复", + "manage-livechat-canned-responses_description": "管理 Omnichannel 自动回复的权限", "manage-livechat-departments": "管理 Omnichannel 部门", + "manage-livechat-departments_description": "管理 Omnichannel 部门的权限", "manage-livechat-managers": "管理 Omnichannel 管理员", + "manage-livechat-managers_description": "管理 Omnichannel 管理员的权限", + "manage-livechat-monitors": "管理 Omnichannel 监控", + "manage-livechat-monitors_description": "管理 Omnichannel 监控的权限", + "manage-livechat-priorities": "管理 Omnichannel 优先级", + "manage-livechat-priorities_description": "管理 Omnichannel 优先级的权限", + "manage-livechat-sla": "管理 Omnichannel SLA", + "manage-livechat-sla_description": "管理 Omnichannel SLA 的权限", + "manage-livechat-tags": "管理 Omnichannel 标签", + "manage-livechat-tags_description": "管理 Omnichannel 标签的权限", + "manage-livechat-units": "管理 Omnichannel 单位", + "manage-livechat-units_description": "管理 Omnichannel 单位的权限", + "manage-moderation-actions": "管理内容审核操作", + "manage-moderation-actions_description": "管理审核操作并对被举报用户执行操作的权限", "manage-oauth-apps": "管理Oauth应用程序", "manage-oauth-apps_description": "管理服务器 Oauth 应用程序的权限", "manage-outgoing-integrations": "管理外向集成", @@ -3878,17 +6478,35 @@ "manage-the-app": "管理应用程序", "manage-user-status": "管理用户状态", "manage-user-status_description": "管理服务器自定义用户状态的权限", + "marketplace_featured_section_community_featured": "精选社区应用", + "marketplace_featured_section_community_supported": "社区支持应用", + "marketplace_featured_section_enterprise": "精选企业版应用", + "marketplace_featured_section_featured": "精选应用", + "marketplace_featured_section_most_popular": "最受欢迎应用", + "marketplace_featured_section_new_arrivals": "新上架", + "marketplace_featured_section_omnichannel": "Omnichannel 应用", + "marketplace_featured_section_popular_this_month": "本月热门应用", + "marketplace_featured_section_recommended": "推荐应用", + "marketplace_featured_section_social": "社交应用", + "marketplace_featured_section_trending": "趋势应用", + "marketplace_featured_section_video_conferencing": "视频会议应用", "mention-all": "提及所有", "mention-all_description": "使用 @all 提及的权限", "mention-here": "在此提及", "mention-here_description": "使用 @here 提及的权限", + "mentions_counter": { + "other": "{{count}} mentions" + }, "message": "消息", + "message-impersonate": "冒充其他用户", + "message-impersonate_description": "使用消息别名冒充其他用户的权限", "message_counter": { "other": "{{count}} 消息" }, "message_pruned": "已修剪消息", "messages": "消息", "messages_pruned": "消息被修剪", + "Message_preview": "消息预览", "meteor_status_connected": "已连接", "meteor_status_connecting": "连接中...", "meteor_status_failed": "服务器连接失败", @@ -3896,83 +6514,268 @@ "meteor_status_reconnect_in": { "other": "在 {{count}} 秒钟后重试..." }, + "meteor_status_try_again_later": "请稍后再试或联系工作区管理员协助", "meteor_status_try_now_offline": "重新连接", "meteor_status_try_now_waiting": "立即尝试", "meteor_status_waiting": "等待服务器连接,", "minute": "分钟", "minutes": "分钟", + "mobile-upload-file": "允许移动设备上传文件", + "mobile-upload-file_description": "允许移动设备上传文件的权限", + "move-room-to-team": "在团队内移动房间", + "move-room-to-team_description": "将现有房间添加到团队的权限", "multi": "多", "multi_line": "多线", + "multiple_instance_solutions": "多实例方案", "mute-user": "静音用户", "mute-user_description": "禁言同一频道中其他用户的权限", + "n_days_left": "剩余 {{n}} 天", "n_messages": "%s 条消息", + "no-active-video-conf-provider": "**会议通话未启用**:工作区管理员需先启用会议通话功能。", + "no-videoconf-provider-app": "**会议通话不可用**:工作区管理员可在 Rocket.Chat 应用市场安装会议通话应用。", + "offline": "离线", + "omnichannel_contacts_importer": "Omnichannel 联系人 (*.csv)", + "omnichannel_priority_change_history": "优先级已更改:{{user}} 将优先级更改为 {{priority}}", + "omnichannel_sla_change_history": "SLA 策略已更改:{{user}} 将 SLA 策略更改为 {{sla}}", + "on-hold-livechat-room": "挂起 Omnichannel 房间", + "on-hold-livechat-room_description": "挂起 Omnichannel 房间的权限", + "on-hold-others-livechat-room": "挂起他人的 Omnichannel 房间", + "on-hold-others-livechat-room_description": "挂起他人的 Omnichannel 房间的权限", + "onboarding.component.emailCodeFallback": "未收到邮件?<1>重新发送 或 <3>更换邮箱。", "onboarding.component.form.action.back": "返回", + "onboarding.component.form.action.completeRegistration": "完成注册", + "onboarding.component.form.action.confirm": "确认", + "onboarding.component.form.action.next": "下一步", "onboarding.component.form.action.pasteHere": "在此粘贴", + "onboarding.component.form.action.register": "注册", + "onboarding.component.form.action.registerNow": "立即注册", + "onboarding.component.form.action.registerOffline": "离线注册", + "onboarding.component.form.action.registerWorkspace": "注册工作区", + "onboarding.component.form.action.skip": "跳过此步骤", + "onboarding.component.form.requiredField": "此字段为必填项", + "onboarding.component.form.steps": "第 {{currentStep}} 步,共 {{stepCount}} 步", + "onboarding.component.form.termsAndConditions": "我同意 <1>条款与条件 和 <3>隐私政策", "onboarding.form.adminInfoForm.fields.email.label": "电子邮箱", "onboarding.form.adminInfoForm.fields.email.placeholder": "电子邮箱", + "onboarding.form.adminInfoForm.fields.fullName.label": "姓名", + "onboarding.form.adminInfoForm.fields.fullName.placeholder": "名和姓", + "onboarding.form.adminInfoForm.fields.keepPosted.label": "及时了解 Rocket.Chat 更新", "onboarding.form.adminInfoForm.fields.password.label": "密码", + "onboarding.form.adminInfoForm.fields.password.placeholder": "创建密码", + "onboarding.form.adminInfoForm.fields.username.label": "用户名", + "onboarding.form.adminInfoForm.fields.username.placeholder": "@用户名", + "onboarding.form.adminInfoForm.subtitle": "我们需要这些信息来为你的工作区创建管理员资料。", + "onboarding.form.adminInfoForm.title": "管理员信息", + "onboarding.form.awaitConfirmationForm.content.securityCode": "安全码", + "onboarding.form.awaitConfirmationForm.content.sentEmail": "确认链接已发送至 <1>{{emailAddress}}。请验证下方安全码与邮件中的一致。", + "onboarding.form.awaitConfirmationForm.title": "等待确认", + "onboarding.form.organizationInfoForm.fields.country.label": "国家", + "onboarding.form.organizationInfoForm.fields.country.placeholder": "请选择", + "onboarding.form.organizationInfoForm.fields.organizationIndustry.label": "行业", + "onboarding.form.organizationInfoForm.fields.organizationIndustry.placeholder": "请选择", + "onboarding.form.organizationInfoForm.fields.organizationName.label": "组织名称", + "onboarding.form.organizationInfoForm.fields.organizationName.placeholder": "组织名称", + "onboarding.form.organizationInfoForm.fields.organizationSize.label": "组织规模", + "onboarding.form.organizationInfoForm.fields.organizationSize.placeholder": "请选择", + "onboarding.form.organizationInfoForm.fields.organizationType.label": "组织类型", + "onboarding.form.organizationInfoForm.fields.organizationType.placeholder": "请选择", + "onboarding.form.organizationInfoForm.subtitle": "我们需要了解你的身份。", + "onboarding.form.organizationInfoForm.title": "组织信息", + "onboarding.form.registerOfflineForm.copyStep.description": "如因任何原因无法将工作区连接到互联网,请按以下步骤操作:<1>1. 前往:<2>cloud.rocket.chat > Workspaces 并点击“<3>Register self-managed”<4>2. 点击“<5>Continue offline”<6>3. 在 cloud.rocket.chat 的 <7>Register offline workspace 对话框中,将令牌粘贴到下方输入框", + "onboarding.form.registerOfflineForm.fields.registrationToken.inputLabel": "注册令牌", + "onboarding.form.registerOfflineForm.pasteStep.description": "1. 在 <1>cloud.rocket.chat 获取生成的文本并粘贴到下方以完成注册流程", "onboarding.form.registerOfflineForm.title": "离线注册", + "onboarding.form.registeredServerForm.continueStandalone": "继续作为独立部署", + "onboarding.form.registeredServerForm.fields.accountEmail.inputLabel": "管理员邮箱", + "onboarding.form.registeredServerForm.fields.accountEmail.inputPlaceholder": "请输入邮箱以继续", + "onboarding.form.registeredServerForm.included.apps": "访问应用市场应用", + "onboarding.form.registeredServerForm.included.externalProviders": "与外部提供商集成(WhatsApp、Facebook、Telegram、Twitter)", + "onboarding.form.registeredServerForm.included.push": "移动推送通知", + "onboarding.form.registeredServerForm.keepInformed": "让我了解新闻和活动", + "onboarding.form.registeredServerForm.notConnectedToInternet": "服务器未连接互联网,因此需要为该工作区进行离线注册。", + "onboarding.form.registeredServerForm.registerLater": "稍后注册", + "onboarding.form.registeredServerForm.registrationEngagement": "注册可启用自动许可证更新、关键漏洞通知以及访问 Rocket.Chat 云服务。不会共享敏感的工作区数据;发送给 Rocket.Chat 的统计信息会在管理区域对你可见。", + "onboarding.form.registeredServerForm.registrationKeepInformed": "提交此表单即表示你同意根据我们的 <1>隐私政策 接收 Rocket.Chat 产品、活动和更新信息。你可随时取消订阅。", + "onboarding.form.registeredServerForm.title": "注册你的工作区", + "onboarding.form.standaloneServerForm.manuallyIntegrate": "需要手动集成外部服务", + "onboarding.form.standaloneServerForm.publishOwnApp": "要发送推送通知,需要编译并发布你自己的应用到 Google Play 和 App Store", + "onboarding.form.standaloneServerForm.servicesUnavailable": "部分服务不可用或需要手动设置", + "onboarding.form.standaloneServerForm.title": "独立服务器确认", + "onboarding.page.alreadyHaveAccount": "已有账号?<1>管理你的工作区。", + "onboarding.page.awaitingConfirmation.subtitle": "我们已向 {{emailAddress}} 发送包含确认链接的邮件。请验证下方安全码与邮件中的一致。", + "onboarding.page.checkYourEmail.subtitle": "你的请求已成功发送。<1>请查看邮箱以启动高级方案试用。<1>链接将在 30 分钟后过期。", + "onboarding.page.checkYourEmail.title": "查看邮箱", + "onboarding.page.cloudDescription.auditing": "消息审计面板 / 审计日志", + "onboarding.page.cloudDescription.availability": "高可用", + "onboarding.page.cloudDescription.engagement": "参与度仪表板", + "onboarding.page.cloudDescription.goldIncludes": "* 黄金方案包含其他方案的全部功能", + "onboarding.page.cloudDescription.ldap": "LDAP 增强同步", + "onboarding.page.cloudDescription.numberOfIntegrations": "1,000 个集成", + "onboarding.page.cloudDescription.omnichannel": "Omnichannel 高级版", + "onboarding.page.cloudDescription.push": "安全的推送通知", + "onboarding.page.cloudDescription.sla": "SLA:高级版", + "onboarding.page.cloudDescription.title": "让我们启动你的工作区并开启 <1>14 天试用", + "onboarding.page.cloudDescription.tryGold": "免费试用 14 天我们最佳的黄金方案", + "onboarding.page.confirmationProcess.title": "确认处理中", + "onboarding.page.emailConfirmed.subtitle": "你可以返回 Rocket.Chat 应用——我们已启动你的工作区。", + "onboarding.page.emailConfirmed.title": "邮箱已确认!", + "onboarding.page.form.title": "让我们启动你的工作区", + "onboarding.page.invalidLink.button.text": "请求新链接", + "onboarding.page.invalidLink.content": "似乎你已经使用了邀请链接。该链接仅可用于一次登录。请请求新的链接以加入你的工作区。", + "onboarding.page.invalidLink.title": "链接已失效", + "onboarding.page.magicLinkEmail.subtitle": "点击我们刚发送给你的邮件中的链接以登录工作区。<1>链接将在 30 分钟后过期。", + "onboarding.page.magicLinkEmail.title": "我们已向你发送登录链接", + "onboarding.page.requestTrial.subtitle": "免费试用 30 天我们最佳的高级方案", + "onboarding.page.requestTrial.title": "申请 <1>30 天试用", "online": "在线", "optional": "可选", "or": "或", "others": "其它", + "Outbound": "外发", + "Outbound_message_upsell_title": "迈出对话的第一步", + "Outbound_message_upsell_description": "通过 WhatsApp 等渠道发送个性化外发消息——适用于提醒、告警和跟进。", + "Outbound_message_upsell_annotation": "联系工作区管理员以启用外发聊天", + "Outbound_message_upsell_alt": "一部智能手机收到新消息通知的插图。", + "outbound.send-messages": "发送外发消息", + "outbound.send-messages_description": "发送外发消息的权限", + "outbound.can-assign-queues": "可分配部门处理外发消息回复", + "outbound.can-assign-queues_description": "分配部门处理外发消息回复的权限", + "outbound.can-assign-any-agent": "可分配任意客服处理外发消息回复", + "outbound.can-assign-any-agent_description": "分配任意客服处理外发消息回复的权限", + "outbound.can-assign-self-only": "仅可分配自己处理外发消息回复", + "outbound.can-assign-self-only_description": "仅分配自己处理外发消息回复的权限", + "pdf_error_message": "生成 PDF 聊天记录时出错", + "pdf_success_message": "PDF 聊天记录生成成功", + "pending": "待处理", "pin-message": "固定信息", "pin-message_description": "在频道中固定消息的权限", "pinning-not-allowed": "不允许固定", "please_enter_valid_domain": "请输入有效的域名", + "plus__usersCount__joined": "+ {{count}} 已加入", "post-readonly": "发送只读", "post-readonly_description": "在只读频道发布消息的权限", + "powers-of-ten": "10 的幂", + "powers-of-two": "2 的幂", "preview-c-room": "预览公共频道", "preview-c-room_description": "在加入之前查看公共频道内容的权限", + "public": "公开", "quote": "引用", + "recording": "录制中", + "register-on-cloud": "在云端注册", + "register-on-cloud_description": "允许在云端注册", "registration.component.form.confirmPassword": "确认密码", + "registration.component.form.confirmation": "确认", + "registration.component.form.createAnAccount": "创建账户", "registration.component.form.divider": "或", "registration.component.form.email": "电子邮箱", "registration.component.form.emailAlreadyExists": "邮箱已存在", "registration.component.form.emailOrUsername": "电子邮件或用户名", + "registration.component.form.emailPlaceholder": "示例:example@example.com", "registration.component.form.invalidConfirmPass": "两次输入的密码不一致", "registration.component.form.invalidEmail": "输入的电子邮件地址无效", + "registration.component.form.joinYourTeam": "加入你的团队", "registration.component.form.name": "姓名", + "registration.component.form.nameContainsInvalidChars": "姓名包含无效字符", + "registration.component.form.nameOptional": "姓名可选", "registration.component.form.password": "密码", "registration.component.form.reasonToJoin": "加入的理由", "registration.component.form.register": "注册一个新帐号", + "registration.component.form.requiredField": "此字段为必填项", "registration.component.form.sendConfirmationEmail": "已发送确认电子邮件", "registration.component.form.submit": "提交", "registration.component.form.userAlreadyExist": "此用户名已存在。请尝试其他用户名。", "registration.component.form.username": "用户名", "registration.component.form.usernameAlreadyExists": "此用户名已存在。请尝试其他用户名。", + "registration.component.form.usernameContainsInvalidChars": "用户名包含无效字符", "registration.component.login": "登录", + "registration.component.login.incorrectPassword": "密码不正确", "registration.component.login.userNotFound": "找不到用户", "registration.component.resetPassword": "重设密码", + "registration.component.switchLanguage": "切换到 <2>{{name}}", + "registration.component.welcome": "欢迎来到 <1>Rocket.Chat 工作区", + "registration.page.emailVerification.sent": "验证邮件已发送,请检查收件箱。", + "registration.page.emailVerification.subTitle": "此服务器要求验证邮箱地址。请查看收件箱中的验证链接。", + "registration.page.guest.chooseHowToJoin": "选择加入方式", + "registration.page.guest.continueAsGuest": "以访客身份继续", + "registration.page.guest.loginWithRocketChat": "使用 Rocket.Chat 登录", "registration.page.login.errors.AppUserNotAllowedToLogin": "应用用户不被允许直接登录。", + "registration.page.login.errors.invalidEmail": "无效的邮箱地址", + "registration.page.login.errors.licenseUserLimitReached": "已达到用户数量上限。", "registration.page.login.errors.loginBlockedForIp": "此 IP 的登录被临时禁用了", "registration.page.login.errors.loginBlockedForUser": "此用户的登录被临时禁用了", "registration.page.login.errors.wrongCredentials": "用户不存在或密码错误", "registration.page.login.forgot": "忘记密码", + "registration.page.login.register": "新用户?<1>创建账户", + "registration.page.poweredBy": "由 <1>Rocket.Chat 提供支持", + "registration.page.register.back": "返回登录", "registration.page.registration.waitActivationWarning": "您的帐户必须由管理员手工激活后才能登录。", + "registration.page.resetPassword.errors.invalidEmail": "无效的邮箱地址", + "registration.page.resetPassword.sendInstructions": "发送指引", "registration.page.resetPassword.sent": "如果此电子邮件已注册,我们将发送有关如何重置密码的说明。如果您很短时间内没有收到电子邮件,请返回并重试。", + "remove-canned-responses": "移除自动回复", + "remove-canned-responses_description": "移除自动回复的权限", + "remove-closed-livechat-room": "移除已关闭的 Omnichannel 聊天室", + "remove-closed-livechat-room_description": "移除已关闭的 Omnichannel 聊天室的权限", "remove-closed-livechat-rooms": "删除已关闭的 Omnichannel 聊天室", + "remove-closed-livechat-rooms_description": "移除所有已关闭的 Omnichannel 聊天室的权限", + "remove-livechat-department": "移除 Omnichannel 部门", + "remove-livechat-department_description": "移除 Omnichannel 部门的权限", + "remove-slackbridge-links": "移除 Slackbridge 链接", + "remove-slackbridge-links_description": "移除 Slackbridge 链接的权限", + "remove-team-channel": "移除团队频道", + "remove-team-channel_description": "移除团队频道的权限", "remove-user": "删除用户", "remove-user_description": "从聊天室移除用户的权限", + "removed__username__as__role_": "已移除 {{username}} 的 {{role}} 角色", + "request": "请求", + "request-pdf-transcript": "请求 PDF 聊天记录", + "request-pdf-transcript_description": "请求指定 Omnichannel 聊天室的 PDF 聊天记录的权限", + "requests": "请求", + "required": "必填", "reset-other-user-e2e-key": "重置其他用户端到端密钥", + "restart-server": "重启服务器", + "restart-server_description": "重启服务器的权限", + "room_account_deactivated": "此帐户已停用", + "room_allowed_reacting": "房间允许回应,由 {{user_by}}", + "room_allowed_reactions": "允许的反应", + "room_avatar_changed": "更改了房间头像", "room_changed_announcement": "{{user_by}} 将公告修改为:{{room_announcement}}", "room_changed_avatar": "聊天室头像被 {{user_by}} 更改", "room_changed_description": "聊天室描述由 {{user_by}} 修改为:{{room_description}}", "room_changed_privacy": "{{user_by}} 将聊天室类型修改为:{{room_type}}", "room_changed_topic": "{{user_by}} 将聊天室主题修改为:{{room_topic}}", + "room_changed_topic_to": "将房间主题更改为 {{room_topic}}", + "room_changed_type": "将房间更改为 {{room_type}}", + "room_disallowed_reacting": "房间禁止回应,由 {{user_by}}", + "room_disallowed_reactions": "禁止的反应", "room_is_blocked": "这个聊天室已被屏蔽", "room_is_read_only": "这个聊天室是只读的", "room_name": "聊天室名称", + "room_removed_read_only": "房间已添加写入权限,由 {{user_by}}", + "room_removed_read_only_permission": "已移除只读权限", + "room_set_read_only": "房间设为只读,由 {{user_by}}", + "room_set_read_only_permission": "将房间设为只读", "run-import": "运行导入", "run-import_description": "运行导入的权限", "run-migration": "运行迁移", "run-migration_description": "运行迁移的权限", + "save-all-canned-responses": "保存所有自动回复", + "save-all-canned-responses_description": "保存所有自动回复的权限", + "save-canned-responses": "保存自动回复", + "save-canned-responses_description": "保存自动回复的权限", + "save-department-canned-responses": "保存部门自动回复", + "save-department-canned-responses_description": "保存部门自动回复的权限", "save-others-livechat-room-info": "保存其他 Omnichannel 聊天室信息", "save-others-livechat-room-info_description": "保存其他 Omnichannel 聊天室信息的权限", "seconds": "秒", + "send-mail": "发送邮件", + "send-mail_description": "发送邮件的权限", "send-many-messages": "发送多条消息", + "send-many-messages_description": "绕过每秒 5 条消息速率限制的权限", "send-omnichannel-chat-transcript": "发送 Omnichannel 会话记录", + "send-omnichannel-chat-transcript_description": "发送 Omnichannel 对话记录的权限", "set-leader": "设置领导", + "set-leader_description": "将其他用户设置为频道领导的权限", "set-moderator": "设置主持", "set-moderator_description": "将其他用户设置为频道主持的权限", "set-owner": "设置所有者", @@ -3981,13 +6784,40 @@ "set-react-when-readonly_description": "对只读通道中的消息作出回应的权限", "set-readonly": "设置只读", "set-readonly_description": "将频道设置为只读频道的权限", + "set__username__as__role_": "将 {{username}} 设为 {{role}}", + "shortcut_name": "快捷方式名称", "show_offline_users": "显示离线用户", "since_creation": "自从 %s", "snippet-message": "片段消息", "snippet-message_description": "创建代码片段消息的权限", + "start-discussion": "开始讨论", + "start-discussion-other-user": "开始讨论(其他用户)", "start-discussion-other-user_description": "开始讨论", "start-discussion_description": "开始讨论", "strike": "划线", + "subscription.callout.activeUsers": "席位", + "subscription.callout.allPremiumCapabilitiesDisabled": "所有高级功能已禁用", + "subscription.callout.capabilitiesDisabled": "功能已禁用", + "subscription.callout.description.limitsExceeded": { + "other": "Your workspace exceeded the <1>{{val, list}} license limits. <3>Manage your subscription to increase limits." + }, + "subscription.callout.description.limitsReached": { + "other": "Your workspace reached the <1>{{val, list}} license limits. <3>Manage your subscription to increase limits." + }, + "subscription.callout.guestUsers": "访客", + "subscription.callout.marketplaceApps": "已安装的应用市场应用", + "subscription.callout.monthlyActiveContacts": "月活跃联系人", + "subscription.callout.privateApps": "已安装的私有应用", + "subscription.callout.roomsPerGuest": "每位访客最大房间数", + "subscription.callout.servicesDisruptionsMayOccur": "可能发生服务中断", + "subscription.callout.servicesDisruptionsOccurring": "正在发生服务中断", + "sync-auth-services-users": "同步认证服务用户", + "sync-auth-services-users_description": "同步认证服务用户的权限", + "system_message": "系统消息", + "test-admin-options": "测试管理面板选项", + "test-admin-options_description": "测试管理面板选项(如 LDAP 登录)的权限。", + "test-push-notifications": "测试推送通知", + "test-push-notifications_description": "测试推送通知的权限", "theme-color-attention-color": "关注颜色", "theme-color-component-color": "组件颜色", "theme-color-content-background-color": "内容的背景色", @@ -4007,6 +6837,10 @@ "theme-color-rc-color-alert-message-secondary-background": "提醒消息次背景色", "theme-color-rc-color-alert-message-warning": "提醒消息警告颜色", "theme-color-rc-color-alert-message-warning-background": "提醒消息警告背景色", + "theme-color-rc-color-announcement-background": "公告背景色", + "theme-color-rc-color-announcement-background-hover": "公告背景色(悬停)", + "theme-color-rc-color-announcement-text": "公告文字颜色", + "theme-color-rc-color-announcement-text-hover": "公告文字颜色(悬停)", "theme-color-rc-color-button-primary": "主要按钮", "theme-color-rc-color-button-primary-light": "按钮主灯", "theme-color-rc-color-content": "内容", @@ -4037,29 +6871,81 @@ "theme-color-unread-notification-color": "未读消息色", "theme-custom-css": "自定义 CSS", "theme-font-body-font-family": "正文字体", + "this_app_is_included_with_subscription": "此应用包含在 {{bundleName}} 方案中", "thread": "讨论串", + "thread_message": "讨论串消息", + "thread_messages": "讨论串消息", + "thread_message_preview": "讨论串消息预览", + "threads_counter": { + "other": "{{count}} unread threaded messages" + }, "to_see_more_details_on_how_to_integrate": "以便查看更多关于集成的细节。", "toggle-room-e2e-encryption": "开关聊天室端到端加密", + "toggle-room-e2e-encryption_description": "切换端到端加密房间的权限", "totp-disabled": "您没有为您的用户启用两步验证登录。", "totp-invalid": "代码或密码无效", + "totp-max-attempts": "已达到 OTP 失败尝试次数上限。将生成新验证码。", "totp-required": "需要 TOTP", "transfer-livechat-guest": "转移 LiveChat 访客", + "transfer-livechat-guest_description": "转移 Omnichannel 访客的权限", + "trial": "试用", + "typing": "正在输入", "unable-to-get-file": "无法取得文件", "unarchive-room": "解除Room的归档", "unarchive-room_description": "取消频道归档的权限", "unauthorized": "未经授权", + "unblock-livechat-contact": "解除屏蔽 Omnichannel 联系人频道", "unpinning-not-allowed": "不允许取消固定", + "unread_messages_counter": { + "other": "{{count}} unread messages" + }, + "update-livechat-contact": "更新 Omnichannel 联系人", + "used_limit": "已用 {{used, number}} / {{limit, number}}", + "used_limit_infinite": "已用 {{used, number}} / ∞", "user-generate-access-token": "用户生成访问令牌", "user-generate-access-token_description": "生成用户访问令牌的权限", + "user_key_refreshed_successfully": "密钥刷新成功", "user_sent_an_attachment": "{{user}}发送了一个附件", + "video-conf-provider-not-configured": "**会议通话未启用**:需要由工作区管理员先启用会议通话功能。", + "video_conference_ended": "_通话已结束。_", + "video_conference_ended_by": "**{{username}}** _结束了通话。_", + "video_conference_started": "_开始了通话。_", + "video_conference_started_by": "**{{username}}** _开始了通话。_", + "video_direct_calling": "_正在呼叫。_", + "video_direct_ended": "_通话已结束。_", + "video_direct_ended_by": "**{{username}}** _结束了通话。_", + "video_direct_missed": "_发起了无人接听的通话。_", + "video_direct_started": "_开始了通话。_", + "video_livechat_missed": "_发起了无人接听的视频通话。_", + "video_livechat_started": "_开始了视频通话。_", + "videoconf-ring-users": "通话时让其他用户响铃", + "videoconf-ring-users_description": "通话时让其他用户响铃的权限", + "view-agent-canned-responses": "查看坐席预设回复", + "view-agent-canned-responses_description": "查看坐席预设回复的权限", + "view-all-canned-responses": "查看所有预设回复", + "view-all-canned-responses_description": "查看所有预设回复的权限", + "view-all-team-channels": "查看所有团队频道", + "view-all-team-channels_description": "查看所有团队频道的权限", + "view-all-teams": "查看所有团队", + "view-all-teams_description": "查看所有团队的权限", "view-broadcast-member-list": "在广播室中查看会员列表", + "view-broadcast-member-list_description": "查看广播频道用户列表的权限", "view-c-room": "查看公共频道", "view-c-room_description": "查看公共频道的权限", "view-canned-responses": "查看自动回复", + "view-canned-responses_description": "查看预设回复的权限", "view-d-room": "查看私聊消息", "view-d-room_description": "查看私聊信息的权限", + "view-device-management": "查看设备管理", + "view-device-management_description": "查看设备管理仪表板的权限", + "view-engagement-dashboard": "查看参与度仪表板", + "view-engagement-dashboard_description": "查看参与度仪表板的权限", + "view-federation-data": "查看联邦数据", + "view-federation-data_description": "查看联邦数据的权限", "view-full-other-user-info": "查看完整的其他用户信息", "view-full-other-user-info_description": "查看其他用户的完整个人资料的权限,包括帐户创建日期,上次登录等。", + "view-import-operations": "查看导入操作", + "view-import-operations_description": "查看导入操作的权限", "view-join-code": "查看加入代码", "view-join-code_description": "查看频道加入码的权限", "view-joined-room": "查看加入的Room", @@ -4067,18 +6953,48 @@ "view-l-room": "查看 Omnichannel 聊天室", "view-l-room_description": "查看 Omnichannel 聊天室的权限", "view-livechat-analytics": "查看 Omnichannel 分析", + "view-livechat-analytics_description": "查看 Omnichannel 分析的权限", + "view-livechat-appearance": "查看 Omnichannel 外观", + "view-livechat-appearance_description": "查看 Omnichannel 外观的权限", + "view-livechat-business-hours": "查看 Omnichannel 营业时间", + "view-livechat-business-hours_description": "查看 Omnichannel 营业时间的权限", + "view-livechat-contact": "查看 Omnichannel 联系人", + "view-livechat-contact-history": "查看 Omnichannel 联系人历史记录", + "view-livechat-customfields": "查看 Omnichannel 自定义字段", + "view-livechat-customfields_description": "查看 Omnichannel 自定义字段的权限", "view-livechat-departments": "查看 Omnichannel 部门", + "view-livechat-departments_description": "查看 Omnichannel 部门的权限", + "view-livechat-facebook": "查看 Omnichannel Facebook", + "view-livechat-facebook_description": "查看 Omnichannel Facebook 的权限", + "view-livechat-installation": "查看 Omnichannel 安装", + "view-livechat-installation_description": "查看 Omnichannel 安装的权限", "view-livechat-manager": "查看 Omnichannel 管理员", "view-livechat-manager_description": "查看其他 Omnichannel 管理员的权限", "view-livechat-monitor": "查看 Livechat 监控", "view-livechat-queue": "查看 Omnichannel 队列", + "view-livechat-queue_description": "查看 Omnichannel 队列的权限", + "view-livechat-real-time-monitoring": "查看 Omnichannel 实时监控", "view-livechat-room-closed-by-another-agent": "查看其他客服关闭的 Omnichannel 聊天室", + "view-livechat-room-closed-by-another-agent_description": "查看被其他坐席关闭的 Omnichannel 聊天室的权限", "view-livechat-room-closed-same-department": "查看其他同部门客服关闭的 Omnichannel 聊天室", + "view-livechat-room-closed-same-department_description": "查看同部门其他坐席关闭的 Omnichannel 聊天室的权限", + "view-livechat-room-customfields": "查看 Omnichannel 聊天室自定义字段", + "view-livechat-room-customfields_description": "查看 Omnichannel 聊天室自定义字段的权限", "view-livechat-rooms": "查看 Omnichannel 聊天室", "view-livechat-rooms_description": "查看其他 Omnichannel 聊天室的权限", + "view-livechat-triggers": "查看 Omnichannel 触发器", + "view-livechat-triggers_description": "查看 Omnichannel 触发器的权限", "view-livechat-unit": "查看 Livechat 单位", + "view-livechat-webhooks": "查看 Omnichannel WebHook", + "view-livechat-webhooks_description": "查看 Omnichannel WebHook 的权限", "view-logs": "查看日志", "view-logs_description": "查看服务器日志的权限", + "view-members-list-all-rooms": "可查看所有房间的成员", + "view-members-list-all-rooms_description": "允许查看所有房间的成员列表,即使用户不在其中", + "view-moderation-console": "查看审核控制台", + "view-moderation-console_description": "查看服务器审核控制台的权限", + "view-omnichannel-contact-center": "查看 Omnichannel 联系中心", + "view-omnichannel-contact-center_description": "查看并与 Omnichannel 联系中心交互的权限", "view-other-user-channels": "查看其他用户频道", "view-other-user-channels_description": "查看其他用户拥有频道的权限", "view-outside-room": "查看外部聊天室", @@ -4095,14 +7011,71 @@ "view-user-administration_description": "访问当前登录的其他用户的部分只读列表视图的权限。使用此权限不能访问用户帐户信息", "webdav-account-saved": "WebDAV 帐户已保存", "webdav-account-updated": "WebDAV 账户已更新", + "webdav-server-not-found": "未找到 WebDAV 服务器", "will_be_able_to": "将能", "yesterday": "昨天", + "you_are_in_preview": "你正在预览模式", "you_are_in_preview_mode_of": "您正在预览频道 #{{room_name}}", "you_are_in_preview_mode_of_incoming_livechat": "您正在此聊天的预览模式中", + "you_are_in_preview_please_insert_the_password": "请输入密码", "your_message": "你的消息", "your_message_optional": "你的消息(可选)", + "__agents__agents_and__count__conversations__period__": "{{agents}} 位客服与 {{count}} 个会话,{{period}}", + "__count__conversations__period__": "{{count}} 个会话,{{period}}", "__count__empty_rooms_will_be_removed_automatically": "将会自动移除 {{count}} 个空聊天室。", "__count__empty_rooms_will_be_removed_automatically__rooms__": "将会自动移除以下 {{count}} 个空聊天室:
{{rooms}}", + "__count__file_pruned": { + "other": "已清理 {{count}} 个文件" + }, + "__count__follower": { + "other": "+{{count}} 位关注者" + }, + "__count__message_pruned": { + "other": "已清理 {{count}} 条消息" + }, + "__count__messages_selected": "已选择 {{count}} 条消息", + "__count__replies": "{{count}} 条回复", + "__count__replies__date__": "{{count}} 条回复,{{date}}", + "__count__tags__and__count__conversations__period__": "{{count}} 个标签与 {{conversations}} 个会话,{{period}}", + "__count__without__assignee__": "{{count}} 个未分配", + "__count__without__department__": "{{count}} 个未分配部门", + "__count__without__tags__": "{{count}} 个无标签", + "__departments__departments_and__count__conversations__period__": "{{departments}} 个部门与 {{count}} 个会话,{{period}}", + "__roomName__encryption_keys_need_to_be_updated": "{{roomName}} 的加密密钥需要更新才能让您访问。需要另一位房间成员在线以完成此操作。", + "__roomName__is_encrypted": "{{roomName}} 已加密", + "__roomName__was_added_to_favorites": "{{roomName}} 已添加到收藏", + "__roomName__was_removed_from_favorites": "{{roomName}} 已从收藏中移除", + "__unreadTitle__from__roomTitle__": "来自 {{roomTitle}} 的 {{unreadTitle}}", + "Insert_timestamp": "插入时间戳", + "Insert": "插入", "__username__is_no_longer__role__defined_by__user_by_": "{{user_by}} 移除了 {{username}} 的 {{role}} 角色", - "__username__was_set__role__by__user_by_": "{{user_by}} 设置 {{username}} 为 {{role}} 角色" + "__username__was_set__role__by__user_by_": "{{user_by}} 设置 {{username}} 为 {{role}} 角色", + "__usernames__and__count__more_joined": "{{usernames}} 以及另外 {{count}} 人加入", + "__usernames__joined": "{{usernames}} 已加入", + "__usersCount__joined": "{{count}} 人已加入", + "__usersCount__people_will_be_invited": "将邀请 {{usersCount}} 人", + "You_cannot_add_external_users_to_non_federated_room": "无法向非联邦房间添加外部用户", + "VERIFIED": "用户已验证", + "UNVERIFIED": "用户未验证", + "UNABLE_TO_VERIFY": "无法验证用户", + "Users_invited": "用户已被邀请", + "timestamps.shortTime": "短时间", + "timestamps.longTime": "长时间", + "timestamps.shortDate": "短日期", + "timestamps.longDate": "长日期", + "timestamps.fullDateTime": "完整日期和时间", + "timestamps.fullDateTimeLong": "完整日期和时间(长格式)", + "timestamps.relativeTime": "相对时间", + "timestamps.shortTimeDescription": "上午12:00", + "timestamps.longTimeDescription": "上午12:00:00", + "timestamps.shortDateDescription": "2020/12/31", + "timestamps.longDateDescription": "2020/12/31 上午12:00", + "timestamps.fullDateTimeDescription": "2020年12月31日 上午12:00", + "timestamps.fullDateTimeLongDescription": "星期四,2020年12月31日 上午12:00:00", + "__username__profile_picture": "{{username}} 的头像", + "User_card": "用户卡片", + "timestamps.relativeTimeDescription": "1 年前", + "Message_composer": "消息输入框", + "Thread_composer": "讨论串编辑器", + "Room_composer": "房间编辑器" } \ No newline at end of file diff --git a/packages/livechat/src/i18n/zh-HK.json b/packages/livechat/src/i18n/zh-HK.json index 1e632d6b1927b..20751b99b5a84 100644 --- a/packages/livechat/src/i18n/zh-HK.json +++ b/packages/livechat/src/i18n/zh-HK.json @@ -1,14 +1,107 @@ { "translation": { + "accept": "接受", + "are_you_sure_you_want_to_finish_this_chat": "您确定要离开聊天吗?", + "are_you_sure_you_want_to_remove_all_of_your_person": "您确定要删除所有个人资料吗?", + "are_you_sure_you_want_to_switch_the_department": "你确定要切换部门吗?", + "call_end_time": "通话于 {{time, datetime}} 结束 - 持续 {{callDuration, datetime}}", "cancel": "取消", + "change_department": "变更部门", + "change_department_1": "变更部门", + "chat_finished": "聊天结束", + "chat_now": "立即聊天", + "chat_started": "聊天已开始", + "choose_a_department": "选择部门...", + "choose_a_department_1": "选择部门", + "choose_an_option": "请选择...", "conversation_finished": "對話已結束", + "count_new_messages_since_since_one": "有一个来自 {{val, datetime}} 的新信息 ", + "count_new_messages_since_since_other": "{{count}} 个来自 {{val, datetime}} 的新信息", + "decline": "拒绝", "department_switched": "部门切换", + "departments": "部门", + "disable_notifications": "关闭通知", + "dismiss_this_alert": "取消此提醒", + "drop_here_to_upload_a_file": "拽文件到这里上传", + "email": "电子邮件", + "enable_notifications": "开启通知", + "error_closing_chat": "关闭聊天窗口错误.", + "error_getting_call_alert": "获取来电提醒时出错。", + "error_removing_user_data": "移除用户资料错误.", + "error_starting_a_new_conversation_reason": "开始新对话时错误: {{reason}}", + "expand_chat": "展开聊天窗口", + "field_required": "此字段必填", + "file_exceeds_allowed_size_of_size": "文件大小超过允许的限制 {{size}}.", + "file_upload_disabled": "文件上传已禁用", + "fileupload_error": "文件上传错误", + "finish_this_chat": "结束聊天", + "forget_remove_my_data": "移除我的资料", + "from_returned_the_chat_to_the_queue": "{{from}} 已将聊天退回队列", + "the_agent_transferred_the_chat_to_the_department_to": "客服将聊天转移到部门 {{to}}", + "from_transferred_the_chat_to_the_department_to": "{{from}} 已将聊天转移到部门 {{to}}", + "from_transferred_the_chat_to_to": "{{from}} 已将聊天转移至 {{to}}", + "gdpr": "GDPR", + "go_to_menu_options_forget_remove_my_personal_data": "前往 <1>选项 → 移除我的资料 请求立即删除您的数据.", + "hiddenelementscount_more": "+ {{hiddenElementsCount}} 更多", + "i_agree": "我同意", + "i_need_help_with": "我需要帮助...", + "if_you_have_any_other_questions_just_press_the_but": "如有其他疑问,请按下面的按钮开始新的聊天.", + "incoming_video_call": "来电视频通话", + "insert_your_field_here": "请在此输入 {{field}} ...", + "invalid_email": "无效的电子邮件", + "invalid_value": "无效的内容", + "join_call": "加入通话", + "join_my_room_to_start_the_video_call": "加入我的房间以开始视频通话", + "leave_a_message": "留言", + "livechat_connected": "聊天已连线", + "livechat_is_not_connected": "聊天未连线", + "media_types_not_accepted": "不允许的档案类型", + "message": "信息", + "messages": "消息", + "message_separator_date": "{{val, datetime}}", + "message_time": "{{val, datetime}}", + "minimize_chat": "最小化聊天窗口", + "name": "名字", + "need_help": "需要帮忙吗?", + "new_chat": "开启新聊天", "no": "否", + "no_available_agents_to_transfer": "目前没有其他客服在线上,请稍等", + "offline_form_not_available": "离线表单不可用", + "ok": "好", "options": "選項", + "please_tell_us_some_information_to_start_the_chat": "请输入信息来开启聊天", + "please_wait_for_the_next_available_agent": "请稍等其他客服..", + "powered_by_rocket_chat": "由 Rocket.Chat 提供技术支持", + "restore_chat": "恢复聊天", + "room_name_changed": "聊天房间名称改变", "send": "傳送", + "sound_is_off": "声音关闭", + "sound_is_on": "声音开启", + "start_chat": "开始聊天", + "thanks_for_talking_with_us": "谢谢与我们联系", + "the_chat_was_moved_back_to_queue": "聊天已被移回队列", + "the_chat_was_moved_back_to_queue_due_to_unanswered": "聊天在 {{duration}} 秒未接到应答后已被移回队列", + "the_chat_was_transferred_to_another_agent": "聊天已被转移给另一位客服", + "the_chat_was_transferred_to_another_agent_due_to_unanswered": "聊天在 {{duration}} 秒未接到应答后已被转移给另一位客服", + "the_controller_of_your_personal_data_is_company_na": "您的个人数据的由 [Company Name] 使用, 注册办公室位于 [Company Address]. 要开始聊天,您同意应按照通用数据保护条例(GDPR)处理和传输您的个人数据.", + "transcript_success": "聊天记录已发送", + "type_your_message_here": "请在此输入您的信息", + "unread_messages_count_one": "{{count}} 条未读消息", + "unread_messages_count_other": "{{count}} 条未读消息", + "unread_messages": "未读信息", + "user_added_by": "用户由其他人添加", "user_joined": "用户加入", "user_left": "用户离开了", + "user_removed_by": "用户已被移除", + "waiting_queue": "等待中", "we_are_not_online_right_now_please_leave_a_message": "我们现在不在线。请留言。", - "yes": "对" + "welcome": "欢迎", + "would_you_like_a_copy_of_this_chat_emailed": "是否要将此聊天记录发送到邮箱?", + "write_your_message": "请输入您的信息...", + "yes": "对", + "you_browser_doesn_t_support_audio_element": "您的浏览器不支持音频", + "you_browser_doesn_t_support_video_element": "您的浏览器不支持视频", + "your_spot_is_spot": "您的等待位置是 #{{spot}}", + "your_spot_is_spot_estimated_wait_time_estimatedwai": "您的等待位置是 #{{spot}} (预估等待时间: {{estimatedWaitTime}})" } -} \ No newline at end of file +} diff --git a/packages/livechat/src/i18n/zh-TW.json b/packages/livechat/src/i18n/zh-TW.json index 368868a897e46..d4f1265322f42 100644 --- a/packages/livechat/src/i18n/zh-TW.json +++ b/packages/livechat/src/i18n/zh-TW.json @@ -1,14 +1,107 @@ { "translation": { + "accept": "接受", + "are_you_sure_you_want_to_finish_this_chat": "您確定要離開聊天嗎?", + "are_you_sure_you_want_to_remove_all_of_your_person": "您確定要刪除所有個人資料嗎?", + "are_you_sure_you_want_to_switch_the_department": "你確定要切換部門嗎?", + "call_end_time": "通話於 {{time, datetime}} 結束 - 持續 {{callDuration, datetime}}", "cancel": "取消", - "conversation_finished": "對話已結束", - "department_switched": "部門已更換", + "change_department": "變更部門", + "change_department_1": "變更部門", + "chat_finished": "聊天結束", + "chat_now": "立即聊天", + "chat_started": "聊天已開始", + "choose_a_department": "選擇部門...", + "choose_a_department_1": "選擇部門", + "choose_an_option": "請選擇...", + "conversation_finished": "會話已結束", + "count_new_messages_since_since_one": "有一個來自 {{val, datetime}} 的新信息 ", + "count_new_messages_since_since_other": "{{count}} 個來自 {{val, datetime}} 的新信息", + "decline": "拒絕", + "department_switched": "部門切換", + "departments": "部門", + "disable_notifications": "關閉通知", + "dismiss_this_alert": "取消此提醒", + "drop_here_to_upload_a_file": "拽文件到這裡上傳", + "email": "電子郵件", + "enable_notifications": "開啟通知", + "error_closing_chat": "關閉聊天窗口錯誤.", + "error_getting_call_alert": "獲取來電提醒時出錯。", + "error_removing_user_data": "移除用戶資料錯誤.", + "error_starting_a_new_conversation_reason": "開始新對話時錯誤: {{reason}}", + "expand_chat": "展開聊天窗口", + "field_required": "此字段必填", + "file_exceeds_allowed_size_of_size": "文件大小超過允許的限制 {{size}}.", + "file_upload_disabled": "文件上傳已禁用", + "fileupload_error": "文件上傳錯誤", + "finish_this_chat": "結束聊天", + "forget_remove_my_data": "移除我的資料", + "from_returned_the_chat_to_the_queue": "{{from}} 已將聊天退回隊列", + "the_agent_transferred_the_chat_to_the_department_to": "客服將聊天轉移到部門 {{to}}", + "from_transferred_the_chat_to_the_department_to": "{{from}} 已將聊天轉移到部門 {{to}}", + "from_transferred_the_chat_to_to": "{{from}} 已將聊天轉移至 {{to}}", + "gdpr": "GDPR", + "go_to_menu_options_forget_remove_my_personal_data": "前往 <1>選項 → 移除我的資料 請求立即刪除您的數據.", + "hiddenelementscount_more": "+ {{hiddenElementsCount}} 更多", + "i_agree": "我同意", + "i_need_help_with": "我需要幫助...", + "if_you_have_any_other_questions_just_press_the_but": "如有其他疑問,請按下面的按鈕開始新的聊天.", + "incoming_video_call": "來電視頻通話", + "insert_your_field_here": "請在此輸入 {{field}} ...", + "invalid_email": "無效的電子郵件", + "invalid_value": "無效的內容", + "join_call": "加入通話", + "join_my_room_to_start_the_video_call": "加入我的房間以開始視頻通話", + "leave_a_message": "留言", + "livechat_connected": "聊天已連線", + "livechat_is_not_connected": "聊天未連線", + "media_types_not_accepted": "不允許的檔案類型", + "message": "信息", + "messages": "消息", + "message_separator_date": "{{val, datetime}}", + "message_time": "{{val, datetime}}", + "minimize_chat": "最小化聊天窗口", + "name": "名字", + "need_help": "需要幫忙嗎?", + "new_chat": "開啟新聊天", "no": "否", + "no_available_agents_to_transfer": "目前沒有其他客服在線上,請稍等", + "offline_form_not_available": "離線表單不可用", + "ok": "好", "options": "選項", - "send": "傳送", - "user_joined": "使用者已加入", - "user_left": "使用者已離開", + "please_tell_us_some_information_to_start_the_chat": "請輸入信息來開啟聊天", + "please_wait_for_the_next_available_agent": "請稍等其他客服..", + "powered_by_rocket_chat": "由 Rocket.Chat 提供技術支持", + "restore_chat": "恢復聊天", + "room_name_changed": "聊天房間名稱改變", + "send": "發送", + "sound_is_off": "聲音關閉", + "sound_is_on": "聲音開啟", + "start_chat": "開始聊天", + "thanks_for_talking_with_us": "謝謝與我們聯繫", + "the_chat_was_moved_back_to_queue": "聊天已被移回隊列", + "the_chat_was_moved_back_to_queue_due_to_unanswered": "聊天在 {{duration}} 秒未接到應答後已被移回隊列", + "the_chat_was_transferred_to_another_agent": "聊天已被轉移給另一位客服", + "the_chat_was_transferred_to_another_agent_due_to_unanswered": "聊天在 {{duration}} 秒未接到應答後已被轉移給另一位客服", + "the_controller_of_your_personal_data_is_company_na": "您的個人數據的由 [Company Name] 使用, 註冊辦公室位於 [Company Address]. 要開始聊天,您同意應按照通用數據保護條例(GDPR)處理和傳輸您的個人數據.", + "transcript_success": "聊天記錄已發送", + "type_your_message_here": "請在此輸入您的信息", + "unread_messages_count_one": "{{count}} 條未讀消息", + "unread_messages_count_other": "{{count}} 條未讀消息", + "unread_messages": "未讀信息", + "user_added_by": "用戶由其他人添加", + "user_joined": "用戶已加入", + "user_left": "用戶已離開", + "user_removed_by": "用戶已被移除", + "waiting_queue": "等待中", "we_are_not_online_right_now_please_leave_a_message": "我們現在不在線。請留言。", - "yes": "是" + "welcome": "歡迎", + "would_you_like_a_copy_of_this_chat_emailed": "是否要將此聊天記錄發送到郵箱?", + "write_your_message": "請輸入您的信息...", + "yes": "是", + "you_browser_doesn_t_support_audio_element": "您的瀏覽器不支持音頻", + "you_browser_doesn_t_support_video_element": "您的瀏覽器不支持視頻", + "your_spot_is_spot": "您的等待位置是 #{{spot}}", + "your_spot_is_spot_estimated_wait_time_estimatedwai": "您的等待位置是 #{{spot}} (預估等待時間: {{estimatedWaitTime}})" } -} \ No newline at end of file +} diff --git a/packages/livechat/src/i18n/zh.json b/packages/livechat/src/i18n/zh.json index 0ce60b9cfbbe9..4ae0201dc5f92 100644 --- a/packages/livechat/src/i18n/zh.json +++ b/packages/livechat/src/i18n/zh.json @@ -1,18 +1,23 @@ { "translation": { + "accept": "接受", "are_you_sure_you_want_to_finish_this_chat": "您确定要离开聊天吗?", "are_you_sure_you_want_to_remove_all_of_your_person": "您确定要删除所有个人资料吗?", "are_you_sure_you_want_to_switch_the_department": "你确定要切换部门吗?", + "call_end_time": "通话于 {{time, datetime}} 结束 - 持续 {{callDuration, datetime}}", "cancel": "取消", "change_department": "变更部门", "change_department_1": "变更部门", "chat_finished": "聊天结束", + "chat_now": "立即聊天", + "chat_started": "聊天已开始", "choose_a_department": "选择部门...", "choose_a_department_1": "选择部门", "choose_an_option": "请选择...", "conversation_finished": "会话已结束", "count_new_messages_since_since_one": "有一个来自 {{val, datetime}} 的新信息 ", "count_new_messages_since_since_other": "{{count}} 个来自 {{val, datetime}} 的新信息", + "decline": "拒绝", "department_switched": "部门切换", "departments": "部门", "disable_notifications": "关闭通知", @@ -21,37 +26,52 @@ "email": "电子邮件", "enable_notifications": "开启通知", "error_closing_chat": "关闭聊天窗口错误.", + "error_getting_call_alert": "获取来电提醒时出错。", "error_removing_user_data": "移除用户资料错误.", "error_starting_a_new_conversation_reason": "开始新对话时错误: {{reason}}", "expand_chat": "展开聊天窗口", "field_required": "此字段必填", "file_exceeds_allowed_size_of_size": "文件大小超过允许的限制 {{size}}.", + "file_upload_disabled": "文件上传已禁用", "fileupload_error": "文件上传错误", "finish_this_chat": "结束聊天", "forget_remove_my_data": "移除我的资料", + "from_returned_the_chat_to_the_queue": "{{from}} 已将聊天退回队列", + "the_agent_transferred_the_chat_to_the_department_to": "客服将聊天转移到部门 {{to}}", + "from_transferred_the_chat_to_the_department_to": "{{from}} 已将聊天转移到部门 {{to}}", + "from_transferred_the_chat_to_to": "{{from}} 已将聊天转移至 {{to}}", + "gdpr": "GDPR", "go_to_menu_options_forget_remove_my_personal_data": "前往 <1>选项 → 移除我的资料 请求立即删除您的数据.", "hiddenelementscount_more": "+ {{hiddenElementsCount}} 更多", "i_agree": "我同意", "i_need_help_with": "我需要帮助...", "if_you_have_any_other_questions_just_press_the_but": "如有其他疑问,请按下面的按钮开始新的聊天.", + "incoming_video_call": "来电视频通话", "insert_your_field_here": "请在此输入 {{field}} ...", "invalid_email": "无效的电子邮件", "invalid_value": "无效的内容", + "join_call": "加入通话", + "join_my_room_to_start_the_video_call": "加入我的房间以开始视频通话", "leave_a_message": "留言", "livechat_connected": "聊天已连线", "livechat_is_not_connected": "聊天未连线", - "media_types_not_accepted": "不允许的挡案类型", + "media_types_not_accepted": "不允许的档案类型", "message": "信息", + "messages": "消息", + "message_separator_date": "{{val, datetime}}", + "message_time": "{{val, datetime}}", "minimize_chat": "最小化聊天窗口", "name": "名字", "need_help": "需要帮忙吗?", "new_chat": "开启新聊天", "no": "否", "no_available_agents_to_transfer": "目前没有其他客服在线上,请稍等", + "offline_form_not_available": "离线表单不可用", "ok": "好", "options": "选项", "please_tell_us_some_information_to_start_the_chat": "请输入信息来开启聊天", "please_wait_for_the_next_available_agent": "请稍等其他客服..", + "powered_by_rocket_chat": "由 Rocket.Chat 提供技术支持", "restore_chat": "恢复聊天", "room_name_changed": "聊天房间名称改变", "send": "发送", @@ -59,20 +79,29 @@ "sound_is_on": "声音开启", "start_chat": "开始聊天", "thanks_for_talking_with_us": "谢谢与我们联系", + "the_chat_was_moved_back_to_queue": "聊天已被移回队列", + "the_chat_was_moved_back_to_queue_due_to_unanswered": "聊天在 {{duration}} 秒未接到应答后已被移回队列", + "the_chat_was_transferred_to_another_agent": "聊天已被转移给另一位客服", + "the_chat_was_transferred_to_another_agent_due_to_unanswered": "聊天在 {{duration}} 秒未接到应答后已被转移给另一位客服", "the_controller_of_your_personal_data_is_company_na": "您的个人数据的由 [Company Name] 使用, 注册办公室位于 [Company Address]. 要开始聊天,您同意应按照通用数据保护条例(GDPR)处理和传输您的个人数据.", + "transcript_success": "聊天记录已发送", "type_your_message_here": "请在此输入您的信息", + "unread_messages_count_one": "{{count}} 条未读消息", + "unread_messages_count_other": "{{count}} 条未读消息", "unread_messages": "未读信息", + "user_added_by": "用户由其他人添加", "user_joined": "用户已加入", "user_left": "用户已离开", "user_removed_by": "用户已被移除", "waiting_queue": "等待中", "we_are_not_online_right_now_please_leave_a_message": "我们现在不在线。请留言。", "welcome": "欢迎", + "would_you_like_a_copy_of_this_chat_emailed": "是否要将此聊天记录发送到邮箱?", "write_your_message": "请输入您的信息...", "yes": "是", - "you_browser_doesn_t_support_audio_element": "您的浏览器不支援音频", - "you_browser_doesn_t_support_video_element": "您的浏览器不支援视频", + "you_browser_doesn_t_support_audio_element": "您的浏览器不支持音频", + "you_browser_doesn_t_support_video_element": "您的浏览器不支持视频", "your_spot_is_spot": "您的等待位置是 #{{spot}}", "your_spot_is_spot_estimated_wait_time_estimatedwai": "您的等待位置是 #{{spot}} (预估等待时间: {{estimatedWaitTime}})" } -} \ No newline at end of file +} From 706c08639e063c7ca5c1a5b93f0e3d3681205ac4 Mon Sep 17 00:00:00 2001 From: Yash Rajpal <58601732+yash-rajpal@users.noreply.github.com> Date: Wed, 21 Jan 2026 19:24:39 +0530 Subject: [PATCH 002/131] chore: Move Github OAuth to `CustomOAuth` (#37604) --- apps/meteor/.meteor/packages | 1 - apps/meteor/.meteor/versions | 2 - apps/meteor/app/github/server/index.ts | 1 + apps/meteor/app/github/server/lib.ts | 19 ++++++++ apps/meteor/client/meteor/login/github.ts | 44 ------------------- apps/meteor/client/meteor/login/index.ts | 1 - apps/meteor/client/views/root/AppLayout.tsx | 2 + .../root/hooks/customOAuth/useGithubOAuth.ts | 33 ++++++++++++++ .../externals/meteor/github-oauth.d.ts | 3 -- apps/meteor/server/importPackages.ts | 1 + 10 files changed, 56 insertions(+), 51 deletions(-) create mode 100644 apps/meteor/app/github/server/index.ts create mode 100644 apps/meteor/app/github/server/lib.ts delete mode 100644 apps/meteor/client/meteor/login/github.ts create mode 100644 apps/meteor/client/views/root/hooks/customOAuth/useGithubOAuth.ts delete mode 100644 apps/meteor/definition/externals/meteor/github-oauth.d.ts diff --git a/apps/meteor/.meteor/packages b/apps/meteor/.meteor/packages index 4242b98f6bed3..000e897aa4ce5 100644 --- a/apps/meteor/.meteor/packages +++ b/apps/meteor/.meteor/packages @@ -10,7 +10,6 @@ rocketchat:version accounts-base@3.1.2 accounts-facebook@1.3.4 -accounts-github@1.5.1 accounts-google@1.4.1 accounts-meteor-developer@1.5.1 accounts-oauth@1.4.6 diff --git a/apps/meteor/.meteor/versions b/apps/meteor/.meteor/versions index 04fcb4d920bdd..29f3113919760 100644 --- a/apps/meteor/.meteor/versions +++ b/apps/meteor/.meteor/versions @@ -1,6 +1,5 @@ accounts-base@3.1.2 accounts-facebook@1.3.4 -accounts-github@1.5.1 accounts-google@1.4.1 accounts-meteor-developer@1.5.1 accounts-oauth@1.4.6 @@ -35,7 +34,6 @@ facebook-oauth@1.11.6 facts-base@1.0.2 fetch@0.1.6 geojson-utils@1.0.12 -github-oauth@1.4.2 google-oauth@1.4.5 hot-code-push@1.0.5 http@3.0.0 diff --git a/apps/meteor/app/github/server/index.ts b/apps/meteor/app/github/server/index.ts new file mode 100644 index 0000000000000..cf327e4971bb2 --- /dev/null +++ b/apps/meteor/app/github/server/index.ts @@ -0,0 +1 @@ +import './lib'; diff --git a/apps/meteor/app/github/server/lib.ts b/apps/meteor/app/github/server/lib.ts new file mode 100644 index 0000000000000..abdc87419956a --- /dev/null +++ b/apps/meteor/app/github/server/lib.ts @@ -0,0 +1,19 @@ +import type { OauthConfig } from '@rocket.chat/core-typings'; + +import { CustomOAuth } from '../../custom-oauth/server/custom_oauth_server'; + +const config: OauthConfig = { + serverURL: 'https://github.com', + identityPath: 'https://api.github.com/user', + tokenPath: 'https://github.com/login/oauth/access_token', + scope: 'user:email', + mergeUsers: false, + addAutopublishFields: { + forLoggedInUser: ['services.github'], + forOtherUsers: ['services.github.username'], + }, + accessTokenParam: 'access_token', + identityTokenSentVia: 'header', +}; + +export const Github = new CustomOAuth('github', config); diff --git a/apps/meteor/client/meteor/login/github.ts b/apps/meteor/client/meteor/login/github.ts deleted file mode 100644 index 98c8fb8fea76d..0000000000000 --- a/apps/meteor/client/meteor/login/github.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Random } from '@rocket.chat/random'; -import { Accounts } from 'meteor/accounts-base'; -// eslint-disable-next-line import/no-duplicates -import { Github } from 'meteor/github-oauth'; -import { Meteor } from 'meteor/meteor'; -// eslint-disable-next-line import/no-duplicates -import { OAuth } from 'meteor/oauth'; - -import { createOAuthTotpLoginMethod } from './oauth'; -import { overrideLoginMethod } from '../../lib/2fa/overrideLoginMethod'; -import { wrapRequestCredentialFn } from '../../lib/wrapRequestCredentialFn'; - -const { loginWithGithub } = Meteor; -const loginWithGithubAndTOTP = createOAuthTotpLoginMethod(Github); -Meteor.loginWithGithub = (options, callback) => { - overrideLoginMethod(loginWithGithub, [options], callback, loginWithGithubAndTOTP); -}; - -Github.requestCredential = wrapRequestCredentialFn('github', ({ config, loginStyle, options, credentialRequestCompleteCallback }) => { - const credentialToken = Random.secret(); - const scope = options?.requestPermissions || ['user:email']; - const flatScope = scope.map(encodeURIComponent).join('+'); - - let allowSignup = ''; - if (Accounts._options?.forbidClientAccountCreation) { - allowSignup = '&allow_signup=false'; - } - - const loginUrl = - `https://github.com/login/oauth/authorize` + - `?client_id=${config.clientId}` + - `&scope=${flatScope}` + - `&redirect_uri=${OAuth._redirectUri('github', config)}` + - `&state=${OAuth._stateParam(loginStyle, credentialToken, options.redirectUrl)}${allowSignup}`; - - OAuth.launchLogin({ - loginService: 'github', - loginStyle, - loginUrl, - credentialRequestCompleteCallback, - credentialToken, - popupOptions: { width: 900, height: 450 }, - }); -}); diff --git a/apps/meteor/client/meteor/login/index.ts b/apps/meteor/client/meteor/login/index.ts index cef3570085f43..d49f3653de683 100644 --- a/apps/meteor/client/meteor/login/index.ts +++ b/apps/meteor/client/meteor/login/index.ts @@ -1,7 +1,6 @@ import './cas'; import './crowd'; import './facebook'; -import './github'; import './google'; import './ldap'; import './meteorDeveloperAccount'; diff --git a/apps/meteor/client/views/root/AppLayout.tsx b/apps/meteor/client/views/root/AppLayout.tsx index 17c8561ca489b..7a62fff7b9c5b 100644 --- a/apps/meteor/client/views/root/AppLayout.tsx +++ b/apps/meteor/client/views/root/AppLayout.tsx @@ -8,6 +8,7 @@ import { useDolphinOAuth } from './hooks/customOAuth/useDolphinOAuth'; import { useDrupalOAuth } from './hooks/customOAuth/useDrupalOAuth'; import { useGitHubEnterpriseOAuth } from './hooks/customOAuth/useGitHubEnterpriseOAuth'; import { useGitLabOAuth } from './hooks/customOAuth/useGitLabOAuth'; +import { useGithubOAuth } from './hooks/customOAuth/useGithubOAuth'; import { useNextcloudOAuth } from './hooks/customOAuth/useNextcloudOAuth'; import { useWordPressOAuth } from './hooks/customOAuth/useWordPressOAuth'; import { useAnalytics } from './hooks/useAnalytics'; @@ -56,6 +57,7 @@ const AppLayout = () => { useLivechatEnterprise(); useNextcloudOAuth(); useGitLabOAuth(); + useGithubOAuth(); useGitHubEnterpriseOAuth(); useDrupalOAuth(); useDolphinOAuth(); diff --git a/apps/meteor/client/views/root/hooks/customOAuth/useGithubOAuth.ts b/apps/meteor/client/views/root/hooks/customOAuth/useGithubOAuth.ts new file mode 100644 index 0000000000000..acf799e27344f --- /dev/null +++ b/apps/meteor/client/views/root/hooks/customOAuth/useGithubOAuth.ts @@ -0,0 +1,33 @@ +import type { OauthConfig } from '@rocket.chat/core-typings'; +import { useSetting } from '@rocket.chat/ui-contexts'; +import { useEffect } from 'react'; + +import { CustomOAuth } from '../../../../lib/customOAuth/CustomOAuth'; + +const config: OauthConfig = { + authorizePath: 'https://github.com/login/oauth/authorize', + serverURL: 'https://github.com', + identityPath: 'https://api.github.com/user', + tokenPath: 'https://github.com/login/oauth/access_token', + scope: 'user:email', + mergeUsers: false, + addAutopublishFields: { + forLoggedInUser: ['services.github'], + forOtherUsers: ['services.github.username'], + }, + accessTokenParam: 'access_token', +} as const satisfies OauthConfig; + +const Github = CustomOAuth.configureOAuthService('github', config); + +export const useGithubOAuth = () => { + const enabled = useSetting('Accounts_OAuth_Github'); + + useEffect(() => { + if (enabled) { + Github.configure({ + ...config, + }); + } + }, [enabled]); +}; diff --git a/apps/meteor/definition/externals/meteor/github-oauth.d.ts b/apps/meteor/definition/externals/meteor/github-oauth.d.ts deleted file mode 100644 index ac67405bd4e4c..0000000000000 --- a/apps/meteor/definition/externals/meteor/github-oauth.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -declare module 'meteor/github-oauth' { - export const Github: any; -} diff --git a/apps/meteor/server/importPackages.ts b/apps/meteor/server/importPackages.ts index 61cb32f15958d..49af9b1ca237a 100644 --- a/apps/meteor/server/importPackages.ts +++ b/apps/meteor/server/importPackages.ts @@ -79,3 +79,4 @@ import '../app/ui-utils/server'; import '../app/reactions/server'; import '../app/livechat/server'; import '../app/authentication/server'; +import '../app/github/server'; From e0969b3b1d818b62a7296d271b3c831e0f09c643 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Wed, 21 Jan 2026 11:17:29 -0300 Subject: [PATCH 003/131] test: refactor integration tests to use before/after hooks (#38255) --- .../end-to-end/api/incoming-integrations.ts | 1015 +++++++++-------- 1 file changed, 564 insertions(+), 451 deletions(-) diff --git a/apps/meteor/tests/end-to-end/api/incoming-integrations.ts b/apps/meteor/tests/end-to-end/api/incoming-integrations.ts index 1aa98bd560bf3..a8986b5bddcdc 100644 --- a/apps/meteor/tests/end-to-end/api/incoming-integrations.ts +++ b/apps/meteor/tests/end-to-end/api/incoming-integrations.ts @@ -49,8 +49,19 @@ describe('[Incoming Integrations]', () => { }); describe('[/integrations.create]', () => { - it('should return an error when the user DOES NOT have the permission "manage-incoming-integrations" to add an incoming integration', (done) => { - void updatePermission('manage-incoming-integrations', []).then(() => { + describe('Permission checks', () => { + before(async () => { + await Promise.all([updatePermission('manage-incoming-integrations', []), updatePermission('manage-own-incoming-integrations', [])]); + }); + + after(async () => { + await Promise.all([ + updatePermission('manage-incoming-integrations', ['admin']), + updatePermission('manage-own-incoming-integrations', ['admin']), + ]); + }); + + it('should return an error when the user DOES NOT have the permission "manage-incoming-integrations" to add an incoming integration', (done) => { void request .post(api('integrations.create')) .set(credentials) @@ -72,10 +83,8 @@ describe('[Incoming Integrations]', () => { }) .end(done); }); - }); - it('should return an error when the user DOES NOT have the permission "manage-own-incoming-integrations" to add an incoming integration', (done) => { - void updatePermission('manage-own-incoming-integrations', []).then(() => { + it('should return an error when the user DOES NOT have the permission "manage-own-incoming-integrations" to add an incoming integration', (done) => { void request .post(api('integrations.create')) .set(credentials) @@ -99,33 +108,21 @@ describe('[Incoming Integrations]', () => { }); }); - it('should return an error when the user sends an invalid type of integration', (done) => { - void request - .post(api('integrations.create')) - .set(credentials) - .send({ - type: 'webhook-incoming-invalid', - name: 'Incoming test', - enabled: true, - alias: 'test', - username: 'rocket.cat', - scriptEnabled: false, - overrideDestinationChannelEnabled: true, - channel: '#general', - }) - .expect('Content-Type', 'application/json') - .expect(400) - .expect((res) => { - expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('error', 'Invalid integration type.'); - }) - .end(done); - }); + describe('With manage-incoming-integrations permission', () => { + let tempIntegrationId: IIntegration['_id']; - it('should add the integration successfully when the user ONLY has the permission "manage-incoming-integrations" to add an incoming integration', (done) => { - let integrationId: IIntegration['_id']; - void updatePermission('manage-own-incoming-integrations', ['admin']).then(() => { - void request + before(async () => { + await updatePermission('manage-incoming-integrations', ['admin']); + }); + + after(async () => { + if (tempIntegrationId) { + await removeIntegration(tempIntegrationId, 'incoming'); + } + }); + + it('should add the integration successfully when the user ONLY has the permission "manage-incoming-integrations" to add an incoming integration', async () => { + const res = await request .post(api('integrations.create')) .set(credentials) .send({ @@ -139,339 +136,361 @@ describe('[Incoming Integrations]', () => { channel: '#general', }) .expect('Content-Type', 'application/json') + .expect(200); + + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('integration').and.to.be.an('object'); + tempIntegrationId = res.body.integration._id; + }); + + it('should set overrideDestinationChannelEnabled setting to false when it is not provided', async () => { + const res = await request + .post(api('integrations.create')) + .set(credentials) + .send({ + type: 'webhook-incoming', + name: 'Incoming test', + enabled: true, + alias: 'test', + username: 'rocket.cat', + scriptEnabled: false, + channel: '#general', + }) + .expect('Content-Type', 'application/json') + .expect(200); + + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('integration').and.to.be.an('object'); + expect(res.body.integration).to.have.property('overrideDestinationChannelEnabled', false); + const integrationId = res.body.integration._id; + await removeIntegration(integrationId, 'incoming'); + }); + }); + + describe('With manage-own-incoming-integrations permission', () => { + before(async () => { + await Promise.all([ + updatePermission('manage-incoming-integrations', []), + updatePermission('manage-own-incoming-integrations', ['admin']), + ]); + }); + + after(async () => { + await Promise.all([ + updatePermission('manage-incoming-integrations', ['admin']), + updatePermission('manage-own-incoming-integrations', ['admin']), + ]); + }); + + it('should add the integration successfully when the user ONLY has the permission "manage-own-incoming-integrations" to add an incoming integration', (done) => { + void request + .post(api('integrations.create')) + .set(credentials) + .send({ + type: 'webhook-incoming', + name: 'Incoming test 2', + enabled: true, + alias: 'test2', + username: 'rocket.cat', + scriptEnabled: false, + overrideDestinationChannelEnabled: false, + channel: '#general', + }) + .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('integration').and.to.be.an('object'); - integrationId = res.body.integration._id; + integration = res.body.integration; }) - .end(() => removeIntegration(integrationId, 'incoming').then(done)); + .end(done); }); }); - it('should set overrideDestinationChannelEnabled setting to false when it is not provided', async () => { - const res = await request - .post(api('integrations.create')) - .set(credentials) - .send({ - type: 'webhook-incoming', - name: 'Incoming test', - enabled: true, - alias: 'test', - username: 'rocket.cat', - scriptEnabled: false, - channel: '#general', - }) - .expect('Content-Type', 'application/json') - .expect(200); + describe('Incoming Integration execution', () => { + it('should return an error when the user sends an invalid type of integration', (done) => { + void request + .post(api('integrations.create')) + .set(credentials) + .send({ + type: 'webhook-incoming-invalid', + name: 'Incoming test', + enabled: true, + alias: 'test', + username: 'rocket.cat', + scriptEnabled: false, + overrideDestinationChannelEnabled: true, + channel: '#general', + }) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error', 'Invalid integration type.'); + }) + .end(done); + }); - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.property('integration').and.to.be.an('object'); - expect(res.body.integration).to.have.property('overrideDestinationChannelEnabled', false); - const integrationId = res.body.integration._id; - await removeIntegration(integrationId, 'incoming'); - }); + it('should execute the incoming integration', (done) => { + void request + .post(`/hooks/${integration._id}/${integration.token}`) + .send({ + text: 'Example message', + }) + .expect(200) + .end(done); + }); - it('should add the integration successfully when the user ONLY has the permission "manage-own-incoming-integrations" to add an incoming integration', (done) => { - void updatePermission('manage-incoming-integrations', []).then(() => { - void updatePermission('manage-own-incoming-integrations', ['admin']).then(() => { - void request - .post(api('integrations.create')) - .set(credentials) - .send({ - type: 'webhook-incoming', - name: 'Incoming test 2', - enabled: true, - alias: 'test2', - username: 'rocket.cat', - scriptEnabled: false, - overrideDestinationChannelEnabled: false, - channel: '#general', - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.property('integration').and.to.be.an('object'); - integration = res.body.integration; - }) - .end(done); - }); + it("should return an error when sending 'channel' field telling its configuration is disabled", (done) => { + void request + .post(`/hooks/${integration._id}/${integration.token}`) + .send({ + text: 'Example message', + channel: [testChannelName], + }) + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error', 'overriding destination channel is disabled for this integration'); + }) + .end(done); }); - }); - it('should execute the incoming integration', (done) => { - void request - .post(`/hooks/${integration._id}/${integration.token}`) - .send({ - text: 'Example message', - }) - .expect(200) - .end(done); - }); + it("should return an error when sending 'roomId' field telling its configuration is disabled", (done) => { + void request + .post(`/hooks/${integration._id}/${integration.token}`) + .send({ + text: 'Example message', + roomId: channel._id, + }) + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error', 'overriding destination channel is disabled for this integration'); + }) + .end(done); + }); + it('should send a message for a channel that is specified in the webhooks configuration', (done) => { + const successfulMessage = `Message sent successfully at #${Date.now()}`; + void request + .post(`/hooks/${integration._id}/${integration.token}`) + .send({ + text: successfulMessage, + }) + .expect(200) + .end(() => { + return request + .get(api('channels.messages')) + .set(credentials) + .query({ + roomId: 'GENERAL', + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('messages').and.to.be.an('array'); + expect(!!(res.body.messages as IMessage[]).find((m) => m.msg === successfulMessage)).to.be.true; + }) + .end(done); + }); + }); + it('should send a message for a channel that is not specified in the webhooks configuration', async () => { + await request + .put(api('integrations.update')) + .set(credentials) + .send({ + type: 'webhook-incoming', + overrideDestinationChannelEnabled: true, + integrationId: integration._id, + username: 'rocket.cat', + channel: '#general', + scriptEnabled: true, + enabled: true, + name: integration.name, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('integration'); + expect(res.body.integration.overrideDestinationChannelEnabled).to.be.equal(true); + }); + const successfulMessage = `Message sent successfully at #${Date.now()}`; + await request + .post(`/hooks/${integration._id}/${integration.token}`) + .send({ + text: successfulMessage, + channel: [testChannelName], + }) + .expect(200); - it("should return an error when sending 'channel' field telling its configuration is disabled", (done) => { - void request - .post(`/hooks/${integration._id}/${integration.token}`) - .send({ - text: 'Example message', - channel: [testChannelName], - }) - .expect(400) - .expect((res) => { - expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('error', 'overriding destination channel is disabled for this integration'); - }) - .end(done); - }); + return request + .get(api('channels.messages')) + .set(credentials) + .query({ + roomId: channel._id, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('messages').and.to.be.an('array'); + expect(!!(res.body.messages as IMessage[]).find((m) => m.msg === successfulMessage)).to.be.true; + }); + }); - it("should return an error when sending 'roomId' field telling its configuration is disabled", (done) => { - void request - .post(`/hooks/${integration._id}/${integration.token}`) - .send({ - text: 'Example message', - roomId: channel._id, - }) - .expect(400) - .expect((res) => { - expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('error', 'overriding destination channel is disabled for this integration'); - }) - .end(done); - }); - it('should send a message for a channel that is specified in the webhooks configuration', (done) => { - const successfulMesssage = `Message sent successfully at #${Date.now()}`; - void request - .post(`/hooks/${integration._id}/${integration.token}`) - .send({ - text: successfulMesssage, - }) - .expect(200) - .end(() => { - return request - .get(api('channels.messages')) - .set(credentials) - .query({ - roomId: 'GENERAL', - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.property('messages').and.to.be.an('array'); - expect(!!(res.body.messages as IMessage[]).find((m) => m.msg === successfulMesssage)).to.be.true; - }) - .end(done); - }); - }); - it('should send a message for a channel that is not specified in the webhooks configuration', async () => { - await request - .put(api('integrations.update')) - .set(credentials) - .send({ - type: 'webhook-incoming', - overrideDestinationChannelEnabled: true, - integrationId: integration._id, - username: 'rocket.cat', - channel: '#general', - scriptEnabled: true, - enabled: true, - name: integration.name, - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.property('integration'); - expect(res.body.integration.overrideDestinationChannelEnabled).to.be.equal(true); - }); - const successfulMesssage = `Message sent successfully at #${Date.now()}`; - await request - .post(`/hooks/${integration._id}/${integration.token}`) - .send({ - text: successfulMesssage, - channel: [testChannelName], - }) - .expect(200); + it('should send a message if the payload is a application/x-www-form-urlencoded JSON', async () => { + const payload = { msg: `Message as x-www-form-urlencoded JSON sent successfully at #${Date.now()}` }; - return request - .get(api('channels.messages')) - .set(credentials) - .query({ - roomId: channel._id, - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.property('messages').and.to.be.an('array'); - expect(!!(res.body.messages as IMessage[]).find((m) => m.msg === successfulMesssage)).to.be.true; - }); + await request + .post(`/hooks/${integration._id}/${integration.token}`) + .set('Content-Type', 'application/x-www-form-urlencoded') + .send(`payload=${JSON.stringify(payload)}`) + .expect(200) + .expect(async () => { + return request + .get(api('channels.messages')) + .set(credentials) + .query({ + roomId: 'GENERAL', + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('messages').and.to.be.an('array'); + expect(!!(res.body.messages as IMessage[]).find((m) => m.msg === payload.msg)).to.be.true; + }); + }); + }); }); - it('should send a message if the payload is a application/x-www-form-urlencoded JSON', async () => { - const payload = { msg: `Message as x-www-form-urlencoded JSON sent successfully at #${Date.now()}` }; + describe('Script integration tests', () => { + let withScript: IIntegration; + let withScriptDefaultContentType: IIntegration; - await request - .post(`/hooks/${integration._id}/${integration.token}`) - .set('Content-Type', 'application/x-www-form-urlencoded') - .send(`payload=${JSON.stringify(payload)}`) - .expect(200) - .expect(async () => { - return request - .get(api('channels.messages')) - .set(credentials) - .query({ - roomId: 'GENERAL', - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.property('messages').and.to.be.an('array'); - expect(!!(res.body.messages as IMessage[]).find((m) => m.msg === payload.msg)).to.be.true; - }); - }); - }); - - it('should send a message if the payload is a application/x-www-form-urlencoded JSON AND the integration has a valid script', async () => { - const payload = { msg: `Message as x-www-form-urlencoded JSON sent successfully at #${Date.now()}` }; - let withScript: IIntegration | undefined; + before(async () => { + await updatePermission('manage-incoming-integrations', ['admin']); - await updatePermission('manage-incoming-integrations', ['admin']); - await request - .post(api('integrations.create')) - .set(credentials) - .send({ - type: 'webhook-incoming', - name: 'Incoming test with script', - enabled: true, - alias: 'test', - username: 'rocket.cat', - scriptEnabled: true, - overrideDestinationChannelEnabled: false, - channel: '#general', - script: ` - class Script { - process_incoming_request({ request }) { - return { - content:{ - text: request.content.text - } - }; + const res1 = await request + .post(api('integrations.create')) + .set(credentials) + .send({ + type: 'webhook-incoming', + name: 'Incoming test with script', + enabled: true, + alias: 'test', + username: 'rocket.cat', + scriptEnabled: true, + overrideDestinationChannelEnabled: false, + channel: '#general', + script: ` + class Script { + process_incoming_request({ request }) { + return { + content:{ + text: request.content.text + } + }; + } } - } - `, - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.property('integration').and.to.be.an('object'); - withScript = res.body.integration; - }); - - if (!withScript) { - throw new Error('Integration not created'); - } - - await request - .post(`/hooks/${withScript._id}/${withScript.token}`) - .set('Content-Type', 'application/x-www-form-urlencoded') - .send(`payload=${JSON.stringify(payload)}`) - .expect(200) - .expect(async () => { - return request - .get(api('channels.messages')) - .set(credentials) - .query({ - roomId: 'GENERAL', - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.property('messages').and.to.be.an('array'); - expect(!!(res.body.messages as IMessage[]).find((m) => m.msg === payload.msg)).to.be.true; - }); - }); + `, + }) + .expect(200); + withScript = res1.body.integration; - await removeIntegration(withScript._id, 'incoming'); - }); + const res2 = await request + .post(api('integrations.create')) + .set(credentials) + .send({ + type: 'webhook-incoming', + name: 'Incoming test with script and default content-type', + enabled: true, + alias: 'test', + username: 'rocket.cat', + scriptEnabled: true, + overrideDestinationChannelEnabled: false, + channel: '#general', + script: + 'const buildMessage = (obj) => {\n' + + ' \n' + + ' const template = `[#VALUE](${ obj.test })`;\n' + + ' \n' + + ' return {\n' + + ' text: template\n' + + ' };\n' + + ' };\n' + + ' \n' + + ' class Script {\n' + + ' process_incoming_request({ request }) {\n' + + ' msg = buildMessage(request.content);\n' + + ' \n' + + ' return {\n' + + ' content:{\n' + + ' text: msg.text\n' + + ' }\n' + + ' };\n' + + ' }\n' + + ' }\n' + + ' \n', + }) + .expect(200); + withScriptDefaultContentType = res2.body.integration; + }); - it('should send a message if the payload is a application/x-www-form-urlencoded JSON(when not set, default one) but theres no "payload" key, its just a string, the integration has a valid script', async () => { - const payload = { test: 'test' }; - let withScript: IIntegration | undefined; + after(async () => { + await Promise.all([removeIntegration(withScript._id, 'incoming'), removeIntegration(withScriptDefaultContentType._id, 'incoming')]); + }); - await updatePermission('manage-incoming-integrations', ['admin']); - await request - .post(api('integrations.create')) - .set(credentials) - .send({ - type: 'webhook-incoming', - name: 'Incoming test with script and default content-type', - enabled: true, - alias: 'test', - username: 'rocket.cat', - scriptEnabled: true, - overrideDestinationChannelEnabled: false, - channel: '#general', - script: - 'const buildMessage = (obj) => {\n' + - ' \n' + - ' const template = `[#VALUE](${ obj.test })`;\n' + - ' \n' + - ' return {\n' + - ' text: template\n' + - ' };\n' + - ' };\n' + - ' \n' + - ' class Script {\n' + - ' process_incoming_request({ request }) {\n' + - ' msg = buildMessage(request.content);\n' + - ' \n' + - ' return {\n' + - ' content:{\n' + - ' text: msg.text\n' + - ' }\n' + - ' };\n' + - ' }\n' + - ' }\n' + - ' \n', - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.property('integration').and.to.be.an('object'); - withScript = res.body.integration; - }); + it('should send a message if the payload is a application/x-www-form-urlencoded JSON AND the integration has a valid script', async () => { + const payload = { msg: `Message as x-www-form-urlencoded JSON sent successfully at #${Date.now()}` }; - if (!withScript) { - throw new Error('Integration not created'); - } + await request + .post(`/hooks/${withScript._id}/${withScript.token}`) + .set('Content-Type', 'application/x-www-form-urlencoded') + .send(`payload=${JSON.stringify(payload)}`) + .expect(200) + .expect(async () => { + return request + .get(api('channels.messages')) + .set(credentials) + .query({ + roomId: 'GENERAL', + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('messages').and.to.be.an('array'); + expect(!!(res.body.messages as IMessage[]).find((m) => m.msg === payload.msg)).to.be.true; + }); + }); + }); - await request - .post(`/hooks/${withScript._id}/${withScript.token}`) - .send(JSON.stringify(payload)) - .expect(200) - .expect(async () => { - return request - .get(api('channels.messages')) - .set(credentials) - .query({ - roomId: 'GENERAL', - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.property('messages').and.to.be.an('array'); - expect(!!(res.body.messages as IMessage[]).find((m) => m.msg === '[#VALUE](test)')).to.be.true; - }); - }); + it('should send a message if the payload is a application/x-www-form-urlencoded JSON(when not set, default one) but theres no "payload" key, its just a string, the integration has a valid script', async () => { + const payload = { test: 'test' }; - await removeIntegration(withScript._id, 'incoming'); + await request + .post(`/hooks/${withScriptDefaultContentType._id}/${withScriptDefaultContentType.token}`) + .send(JSON.stringify(payload)) + .expect(200) + .expect(async () => { + return request + .get(api('channels.messages')) + .set(credentials) + .query({ + roomId: 'GENERAL', + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('messages').and.to.be.an('array'); + expect(!!(res.body.messages as IMessage[]).find((m) => m.msg === '[#VALUE](test)')).to.be.true; + }); + }); + }); }); describe('With manage-own-incoming-integrations permission', () => { @@ -523,6 +542,17 @@ describe('[Incoming Integrations]', () => { }); describe('[/integrations.history]', () => { + before(async () => { + await Promise.all([updatePermission('manage-incoming-integrations', []), updatePermission('manage-own-incoming-integrations', [])]); + }); + + after(async () => { + await Promise.all([ + updatePermission('manage-incoming-integrations', ['admin']), + updatePermission('manage-own-incoming-integrations', ['admin']), + ]); + }); + it('should return an error when trying to get history of incoming integrations if user does NOT have enough permissions', (done) => { void request .get(api('integrations.history')) @@ -589,83 +619,120 @@ describe('[Incoming Integrations]', () => { .end(done); }); - it('should return the list of integrations created by the user only', (done) => { - void updatePermission('manage-incoming-integrations', []).then(() => { - void updatePermission('manage-own-incoming-integrations', ['user']).then(() => { - void request - .get(api('integrations.list')) - .set(userCredentials) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - const integrationCreatedByAdmin = (res.body.integrations as IIntegration[]).find( - (createdIntegration) => createdIntegration._id === integration._id, - ); - expect(integrationCreatedByAdmin).to.be.equal(undefined); - expect(res.body).to.have.property('offset'); - expect(res.body).to.have.property('items'); - expect(res.body).to.have.property('total'); - }) - .end(done); - }); + describe('With manage-own-incoming-integrations permission', () => { + before(async () => { + await Promise.all([ + updatePermission('manage-incoming-integrations', []), + updatePermission('manage-own-incoming-integrations', ['user']), + ]); + }); + + after(async () => { + await Promise.all([ + updatePermission('manage-incoming-integrations', ['admin']), + updatePermission('manage-own-incoming-integrations', ['admin']), + ]); + }); + + it('should return the list of integrations created by the user only', (done) => { + void request + .get(api('integrations.list')) + .set(userCredentials) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + const integrationCreatedByAdmin = (res.body.integrations as IIntegration[]).find( + (createdIntegration) => createdIntegration._id === integration._id, + ); + expect(integrationCreatedByAdmin).to.be.equal(undefined); + expect(res.body).to.have.property('offset'); + expect(res.body).to.have.property('items'); + expect(res.body).to.have.property('total'); + }) + .end(done); }); }); - it('should return unauthorized error when the user does not have any integrations permissions', async () => { - await Promise.all([ - updatePermission('manage-incoming-integrations', []), - updatePermission('manage-own-incoming-integrations', []), - updatePermission('manage-outgoing-integrations', []), - updatePermission('manage-outgoing-integrations', []), - ]); - await request - .get(api('integrations.list')) - .set(credentials) - .expect('Content-Type', 'application/json') - .expect(403) - .expect((res) => { - expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('error', 'User does not have the permissions required for this action [error-unauthorized]'); - }); + describe('Without any permissions', () => { + before(async () => { + await Promise.all([ + updatePermission('manage-incoming-integrations', []), + updatePermission('manage-own-incoming-integrations', []), + updatePermission('manage-outgoing-integrations', []), + updatePermission('manage-own-outgoing-integrations', []), + ]); + }); + + after(async () => { + await Promise.all([ + updatePermission('manage-incoming-integrations', ['admin']), + updatePermission('manage-own-incoming-integrations', ['admin']), + updatePermission('manage-outgoing-integrations', ['admin']), + updatePermission('manage-own-outgoing-integrations', ['admin']), + ]); + }); + + it('should return unauthorized error when the user does not have any integrations permissions', async () => { + await request + .get(api('integrations.list')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(403) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error', 'User does not have the permissions required for this action [error-unauthorized]'); + }); + }); }); }); describe('[/integrations.get]', () => { - it('should return an error when the required "integrationId" query parameters is not sent', (done) => { - void request - .get(api('integrations.get')) - .set(credentials) - .expect('Content-Type', 'application/json') - .expect(400) - .expect((res) => { - expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('error', `must have required property 'integrationId' [invalid-params]`); - }) - .end(done); - }); + describe('Invalid params', () => { + it('should return an error when the required "integrationId" query parameters is not sent', (done) => { + void request + .get(api('integrations.get')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error', `must have required property 'integrationId' [invalid-params]`); + }) + .end(done); + }); - it('should return an error when the user DOES NOT have the permission "manage-incoming-integrations" to get an incoming integration', (done) => { - void updatePermission('manage-incoming-integrations', []).then(() => { + it('should return an error when the user sends an invalid integration', (done) => { void request .get(api('integrations.get')) - .query({ integrationId: integration._id }) + .query({ integrationId: 'invalid' }) .set(credentials) .expect('Content-Type', 'application/json') .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('error', 'not-authorized'); + expect(res.body).to.have.property('error', 'The integration does not exists.'); }) .end(done); }); }); - it('should return an error when the user DOES NOT have the permission "manage-incoming-integrations" to get an incoming integration created by another user', (done) => { - void updatePermission('manage-incoming-integrations', []).then(() => { + describe('Without permissions', () => { + before(async () => { + await Promise.all([updatePermission('manage-incoming-integrations', []), updatePermission('manage-own-incoming-integrations', [])]); + }); + + after(async () => { + await Promise.all([ + updatePermission('manage-incoming-integrations', ['admin']), + updatePermission('manage-own-incoming-integrations', ['admin']), + ]); + }); + + it('should return an error when the user DOES NOT have the permission "manage-incoming-integrations" to get an incoming integration', (done) => { void request .get(api('integrations.get')) - .query({ integrationId: integrationCreatedByAnUser._id }) + .query({ integrationId: integration._id }) .set(credentials) .expect('Content-Type', 'application/json') .expect(400) @@ -675,45 +742,28 @@ describe('[Incoming Integrations]', () => { }) .end(done); }); - }); - it('should return an error when the user sends an invalid integration', (done) => { - void updatePermission('manage-incoming-integrations', ['admin']).then(() => { + it('should return an error when the user DOES NOT have the permission "manage-incoming-integrations" to get an incoming integration created by another user', (done) => { void request .get(api('integrations.get')) - .query({ integrationId: 'invalid' }) + .query({ integrationId: integrationCreatedByAnUser._id }) .set(credentials) .expect('Content-Type', 'application/json') .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('error', 'The integration does not exists.'); + expect(res.body).to.have.property('error', 'not-authorized'); }) .end(done); }); }); - it('should return the integration successfully when the user is able to see only your own integrations', (done) => { - void updatePermission('manage-incoming-integrations', []) - .then(() => updatePermission('manage-own-incoming-integrations', ['user'])) - .then(() => { - void request - .get(api('integrations.get')) - .query({ integrationId: integrationCreatedByAnUser._id }) - .set(userCredentials) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.property('integration'); - expect(res.body.integration._id).to.be.equal(integrationCreatedByAnUser._id); - }) - .end(done); - }); - }); + describe('With manage-incoming-integrations permission', () => { + before(async () => { + await updatePermission('manage-incoming-integrations', ['admin']); + }); - it('should return the integration successfully', (done) => { - void updatePermission('manage-incoming-integrations', ['admin']).then(() => { + it('should return the integration successfully', (done) => { void request .get(api('integrations.get')) .query({ integrationId: integration._id }) @@ -728,6 +778,37 @@ describe('[Incoming Integrations]', () => { .end(done); }); }); + + describe('With manage-own-incoming-integrations permission', () => { + before(async () => { + await Promise.all([ + updatePermission('manage-incoming-integrations', []), + updatePermission('manage-own-incoming-integrations', ['user']), + ]); + }); + + after(async () => { + await Promise.all([ + updatePermission('manage-incoming-integrations', ['admin']), + updatePermission('manage-own-incoming-integrations', ['admin']), + ]); + }); + + it('should return the integration successfully when the user is able to see only your own integrations', (done) => { + void request + .get(api('integrations.get')) + .query({ integrationId: integrationCreatedByAnUser._id }) + .set(userCredentials) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('integration'); + expect(res.body.integration._id).to.be.equal(integrationCreatedByAnUser._id); + }) + .end(done); + }); + }); }); describe('[/integrations.update]', () => { @@ -816,11 +897,11 @@ describe('[Incoming Integrations]', () => { }); it('should send messages to the channel under the updated username', async () => { - const successfulMesssage = `Message sent successfully at #${Random.id()}`; + const successfulMessage = `Message sent successfully at #${Random.id()}`; await request .post(`/hooks/${integration._id}/${integration.token}`) .send({ - text: successfulMesssage, + text: successfulMessage, }) .expect(200); @@ -835,15 +916,33 @@ describe('[Incoming Integrations]', () => { .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('messages').and.to.be.an('array'); - const message = (res.body.messages as IMessage[]).find((m) => m.msg === successfulMesssage); + const message = (res.body.messages as IMessage[]).find((m) => m.msg === successfulMessage); expect(message?.u).have.property('username', senderUser.username); }); }); }); describe('[/integrations.remove]', () => { - it('should return an error when the user DOES NOT have the permission "manage-incoming-integrations" to remove an incoming integration', (done) => { - void updatePermission('manage-incoming-integrations', []).then(() => { + describe('Without permissions', () => { + before(async () => { + await Promise.all([ + updatePermission('manage-incoming-integrations', []), + updatePermission('manage-own-incoming-integrations', []), + updatePermission('manage-outgoing-integrations', []), + updatePermission('manage-own-outgoing-integrations', []), + ]); + }); + + after(async () => { + await Promise.all([ + updatePermission('manage-incoming-integrations', ['admin']), + updatePermission('manage-own-incoming-integrations', ['admin']), + updatePermission('manage-outgoing-integrations', ['admin']), + updatePermission('manage-own-outgoing-integrations', ['admin']), + ]); + }); + + it('should return an error when the user DOES NOT have the permission "manage-incoming-integrations" to remove an incoming integration', (done) => { void request .post(api('integrations.remove')) .set(credentials) @@ -859,10 +958,8 @@ describe('[Incoming Integrations]', () => { }) .end(done); }); - }); - it('should return an error when the user DOES NOT have the permission "manage-own-incoming-integrations" to remove an incoming integration', (done) => { - void updatePermission('manage-own-incoming-integrations', []).then(() => { + it('should return an error when the user DOES NOT have the permission "manage-own-incoming-integrations" to remove an incoming integration', (done) => { void request .post(api('integrations.remove')) .set(credentials) @@ -880,8 +977,12 @@ describe('[Incoming Integrations]', () => { }); }); - it('should return an error when the user sends an invalid type of integration', (done) => { - void updatePermission('manage-own-incoming-integrations', ['admin']).then(() => { + describe('Invalid params', () => { + before(async () => { + await updatePermission('manage-own-incoming-integrations', ['admin']); + }); + + it('should return an error when the user sends an invalid type of integration', (done) => { void request .post(api('integrations.remove')) .set(credentials) @@ -899,8 +1000,12 @@ describe('[Incoming Integrations]', () => { }); }); - it('should remove the integration successfully when the user at least one of the necessary permission to remove an incoming integration', (done) => { - void updatePermission('manage-incoming-integrations', ['admin']).then(() => { + describe('With manage-incoming-integrations permission', () => { + before(async () => { + await updatePermission('manage-incoming-integrations', ['admin']); + }); + + it('should remove the integration successfully when the user at least one of the necessary permission to remove an incoming integration', (done) => { void request .post(api('integrations.remove')) .set(credentials) @@ -917,8 +1022,16 @@ describe('[Incoming Integrations]', () => { }); }); - it('the normal user should remove the integration successfully when the user have the "manage-own-incoming-integrations" to remove an incoming integration', (done) => { - void updatePermission('manage-own-incoming-integrations', ['user']).then(() => { + describe('Normal user with manage-own-incoming-integrations', () => { + before(async () => { + await updatePermission('manage-own-incoming-integrations', ['user']); + }); + + after(async () => { + await updatePermission('manage-own-incoming-integrations', ['admin']); + }); + + it('the normal user should remove the integration successfully when the user have the "manage-own-incoming-integrations" to remove an incoming integration', (done) => { void request .post(api('integrations.remove')) .set(userCredentials) @@ -1102,11 +1215,11 @@ describe('[Incoming Integrations]', () => { }); it('should not send a message in public room if token is invalid', async () => { - const successfulMesssage = `Message sent successfully at #${Random.id()}`; + const successfulMessage = `Message sent successfully at #${Random.id()}`; await request .post(`/hooks/${integration4._id}/invalid-token`) .send({ - text: successfulMesssage, + text: successfulMessage, }) .expect(500) .expect((res) => { @@ -1123,16 +1236,16 @@ describe('[Incoming Integrations]', () => { .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('messages').and.to.be.an('array'); - expect((res.body.messages as IMessage[]).find((m) => m.msg === successfulMesssage)).to.be.undefined; + expect((res.body.messages as IMessage[]).find((m) => m.msg === successfulMessage)).to.be.undefined; }); }); it('should not send a message in private room if token is invalid', async () => { - const successfulMesssage = `Message sent successfully at #${Random.id()}`; + const successfulMessage = `Message sent successfully at #${Random.id()}`; await request .post(`/hooks/${integration2._id}/invalid-token`) .send({ - text: successfulMesssage, + text: successfulMessage, }) .expect(500) .expect((res) => { @@ -1149,16 +1262,16 @@ describe('[Incoming Integrations]', () => { .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('messages').and.to.be.an('array'); - expect((res.body.messages as IMessage[]).find((m) => m.msg === successfulMesssage)).to.be.undefined; + expect((res.body.messages as IMessage[]).find((m) => m.msg === successfulMessage)).to.be.undefined; }); }); it('should not send a message to a private rooms on behalf of a non member', async () => { - const successfulMesssage = `Message sent successfully at #${Random.id()}`; + const successfulMessage = `Message sent successfully at #${Random.id()}`; await request .post(`/hooks/${integration2._id}/${integration2.token}`) .send({ - text: successfulMesssage, + text: successfulMessage, }) .expect(400) .expect((res) => { @@ -1175,16 +1288,16 @@ describe('[Incoming Integrations]', () => { .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('messages').and.to.be.an('array'); - expect((res.body.messages as IMessage[]).find((m) => m.msg === successfulMesssage)).to.be.undefined; + expect((res.body.messages as IMessage[]).find((m) => m.msg === successfulMessage)).to.be.undefined; }); }); it('should not add non member to private rooms when sending message', async () => { - const successfulMesssage = `Message sent successfully at #${Random.id()}`; + const successfulMessage = `Message sent successfully at #${Random.id()}`; await request .post(`/hooks/${integration2._id}/${integration2.token}`) .send({ - text: successfulMesssage, + text: successfulMessage, }) .expect(400) .expect((res) => { @@ -1206,11 +1319,11 @@ describe('[Incoming Integrations]', () => { }); it('should not send a message to public channel of a private team on behalf of a non team member', async () => { - const successfulMesssage = `Message sent successfully at #${Random.id()}`; + const successfulMessage = `Message sent successfully at #${Random.id()}`; await request .post(`/hooks/${integration3._id}/${integration3.token}`) .send({ - text: successfulMesssage, + text: successfulMessage, }) .expect(400) .expect((res) => { @@ -1227,16 +1340,16 @@ describe('[Incoming Integrations]', () => { .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('messages').and.to.be.an('array'); - expect((res.body.messages as IMessage[]).find((m) => m.msg === successfulMesssage)).to.be.undefined; + expect((res.body.messages as IMessage[]).find((m) => m.msg === successfulMessage)).to.be.undefined; }); }); it('should not add non team member to the public channel in a private team when sending message', async () => { - const successfulMesssage = `Message sent successfully at #${Random.id()}`; + const successfulMessage = `Message sent successfully at #${Random.id()}`; await request .post(`/hooks/${integration3._id}/${integration3.token}`) .send({ - text: successfulMesssage, + text: successfulMessage, }) .expect(400) .expect((res) => { @@ -1258,11 +1371,11 @@ describe('[Incoming Integrations]', () => { }); it('should send messages from non-members to public rooms and add them as room members', async () => { - const successfulMesssage = `Message sent successfully at #${Random.id()}`; + const successfulMessage = `Message sent successfully at #${Random.id()}`; await request .post(`/hooks/${integration4._id}/${integration4.token}`) .send({ - text: successfulMesssage, + text: successfulMessage, }) .expect(200); @@ -1277,7 +1390,7 @@ describe('[Incoming Integrations]', () => { .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('messages').and.to.be.an('array'); - expect((res.body.messages as IMessage[]).find((m) => m.msg === successfulMesssage)).not.to.be.undefined; + expect((res.body.messages as IMessage[]).find((m) => m.msg === successfulMessage)).not.to.be.undefined; }); await request From 88da141f3c2af6f91980c7ca8b8777161f99a068 Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Wed, 21 Jan 2026 11:18:21 -0300 Subject: [PATCH 004/131] fix: slash command list is incomplete during startup (#38267) --- .changeset/green-dragons-boil.md | 6 ++ apps/meteor/app/api/server/v1/commands.ts | 15 +++++ .../client/hooks/useAppSlashCommands.spec.tsx | 56 ++++++++++++++++++- .../client/hooks/useAppSlashCommands.ts | 9 ++- packages/rest-typings/src/v1/commands.ts | 1 + 5 files changed, 83 insertions(+), 4 deletions(-) create mode 100644 .changeset/green-dragons-boil.md diff --git a/.changeset/green-dragons-boil.md b/.changeset/green-dragons-boil.md new file mode 100644 index 0000000000000..47e9914c8dc19 --- /dev/null +++ b/.changeset/green-dragons-boil.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/rest-typings': patch +'@rocket.chat/meteor': patch +--- + +Fixes an issue where web clients could remain with a stale slashcommand list during a rolling workspace update diff --git a/apps/meteor/app/api/server/v1/commands.ts b/apps/meteor/app/api/server/v1/commands.ts index fda27be1d0dd9..59b90baafa8a1 100644 --- a/apps/meteor/app/api/server/v1/commands.ts +++ b/apps/meteor/app/api/server/v1/commands.ts @@ -1,3 +1,4 @@ +import { Apps } from '@rocket.chat/apps'; import { Messages } from '@rocket.chat/models'; import { Random } from '@rocket.chat/random'; import objectPath from 'object-path'; @@ -143,6 +144,19 @@ API.v1.addRoute( { authRequired: true }, { async get() { + if (!Apps.self?.isLoaded()) { + return { + statusCode: 202, // Accepted - apps are not ready, so the list is incomplete. Retry later + body: { + commands: [], + appsLoaded: false, + offset: 0, + count: 0, + total: 0, + }, + }; + } + const params = this.queryParams as Record; const { offset, count } = await getPaginationItems(params); const { sort, query } = await this.parseJsonQuery(); @@ -161,6 +175,7 @@ API.v1.addRoute( skip: offset, limit: count, }), + appsLoaded: true, offset, count: commands.length, total: totalCount, diff --git a/apps/meteor/client/hooks/useAppSlashCommands.spec.tsx b/apps/meteor/client/hooks/useAppSlashCommands.spec.tsx index 058e2c8dc4d3f..0da074aa1de3d 100644 --- a/apps/meteor/client/hooks/useAppSlashCommands.spec.tsx +++ b/apps/meteor/client/hooks/useAppSlashCommands.spec.tsx @@ -5,6 +5,7 @@ import { renderHook, waitFor } from '@testing-library/react'; import { useAppSlashCommands } from './useAppSlashCommands'; import { slashCommands } from '../../app/utils/client/slashCommand'; +import { appsQueryKeys } from '../lib/queryKeys'; const mockSlashCommands: SlashCommand[] = [ { @@ -30,6 +31,7 @@ const mockSlashCommands: SlashCommand[] = [ const mockApiResponse = { commands: mockSlashCommands, total: mockSlashCommands.length, + appsLoaded: true, }; describe('useAppSlashCommands', () => { @@ -96,7 +98,7 @@ describe('useAppSlashCommands', () => { expect(slashCommands.commands['/test']).toBeUndefined(); await waitFor(() => { - expect(queryClient.invalidateQueries).toHaveBeenCalledWith(expect.objectContaining({ queryKey: ['apps', 'slashCommands'] })); + expect(queryClient.invalidateQueries).toHaveBeenCalledWith(expect.objectContaining({ queryKey: appsQueryKeys.slashCommands() })); }); }); @@ -123,7 +125,7 @@ describe('useAppSlashCommands', () => { expect(slashCommands.commands['/test']).toBeUndefined(); await waitFor(() => { - expect(queryClient.invalidateQueries).toHaveBeenCalledWith(expect.objectContaining({ queryKey: ['apps', 'slashCommands'] })); + expect(queryClient.invalidateQueries).toHaveBeenCalledWith(expect.objectContaining({ queryKey: appsQueryKeys.slashCommands() })); }); }); @@ -155,6 +157,7 @@ describe('useAppSlashCommands', () => { }, ], total: mockSlashCommands.length + 1, + appsLoaded: true, }); streamRef.controller?.emit('apps', [['command/added', ['/newcommand']]]); @@ -188,13 +191,39 @@ describe('useAppSlashCommands', () => { streamRef.controller?.emit('apps', [['command/updated', ['/test']]]); await waitFor(() => { - expect(queryClient.invalidateQueries).toHaveBeenCalledWith(expect.objectContaining({ queryKey: ['apps', 'slashCommands'] })); + expect(queryClient.invalidateQueries).toHaveBeenCalledWith(expect.objectContaining({ queryKey: appsQueryKeys.slashCommands() })); }); expect(slashCommands.commands['/test']).toBeDefined(); expect(slashCommands.commands['/weather']).toBeDefined(); }); + it('should ignore events that do not start with command/', async () => { + const streamRef: StreamControllerRef<'apps'> = {}; + + renderHook(() => useAppSlashCommands(), { + wrapper: mockAppRoot() + .withJohnDoe() + .withQueryClient(queryClient) + .withStream('apps', streamRef) + .withEndpoint('GET', '/v1/commands.list', mockGetSlashCommands) + .build(), + }); + + expect(streamRef.controller).toBeDefined(); + + await waitFor(() => { + expect(Object.keys(slashCommands.commands)).toHaveLength(mockSlashCommands.length); + }); + + // @ts-expect-error - testing invalid event + streamRef.controller?.emit('apps', [['some/random/event', ['/test']]]); + + await new Promise((resolve) => setTimeout(resolve, 200)); + + expect(queryClient.invalidateQueries).not.toHaveBeenCalled(); + }); + it('should not set up stream listener when user ID is not available', () => { const streamRef: StreamControllerRef<'apps'> = {}; @@ -221,6 +250,7 @@ describe('useAppSlashCommands', () => { return Promise.resolve({ commands: largeMockCommands.slice(offset, offset + count), total: largeMockCommands.length, + appsLoaded: true, }); }); @@ -232,4 +262,24 @@ describe('useAppSlashCommands', () => { expect(Object.keys(slashCommands.commands)).toHaveLength(largeMockCommands.length); }); }); + + it('should not load commands when apps are not loaded', async () => { + mockGetSlashCommands.mockResolvedValue({ + commands: [], + total: 0, + appsLoaded: false, + }); + + expect(Object.keys(slashCommands.commands)).toHaveLength(0); + + renderHook(() => useAppSlashCommands(), { + wrapper: mockAppRoot().withJohnDoe().withEndpoint('GET', '/v1/commands.list', mockGetSlashCommands).build(), + }); + + await waitFor(() => { + expect(mockGetSlashCommands).toHaveBeenCalled(); + }); + + expect(Object.keys(slashCommands.commands)).toHaveLength(0); + }); }); diff --git a/apps/meteor/client/hooks/useAppSlashCommands.ts b/apps/meteor/client/hooks/useAppSlashCommands.ts index 0f2a1123c0b93..c1a6d5e33b7f8 100644 --- a/apps/meteor/client/hooks/useAppSlashCommands.ts +++ b/apps/meteor/client/hooks/useAppSlashCommands.ts @@ -48,10 +48,17 @@ export const useAppSlashCommands = () => { queryKey: appsQueryKeys.slashCommands(), enabled: !!uid, structuralSharing: false, + retry: true, + // Add a bit of randomness to avoid thundering herd problem + retryDelay: (attemptIndex) => Math.min(500 * Math.random() * 10 * 2 ** attemptIndex, 30000), queryFn: async () => { const fetchBatch = async (currentOffset: number, accumulator: SlashCommandBasicInfo[] = []): Promise => { const count = 50; - const { commands, total } = await getSlashCommands({ offset: currentOffset, count }); + const { commands, appsLoaded, total } = await getSlashCommands({ offset: currentOffset, count }); + + if (!appsLoaded) { + throw new Error('Apps not loaded, retry later'); + } const newAccumulator = [...accumulator, ...commands]; diff --git a/packages/rest-typings/src/v1/commands.ts b/packages/rest-typings/src/v1/commands.ts index 41a08435bade5..bbf3788b2b475 100644 --- a/packages/rest-typings/src/v1/commands.ts +++ b/packages/rest-typings/src/v1/commands.ts @@ -15,6 +15,7 @@ export type CommandsEndpoints = { fields?: string; }>, ) => PaginatedResult<{ + appsLoaded: boolean; commands: Pick[]; }>; }; From eb5a1efa422d5cbbdcc805e6d080c5a7df26e469 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Wed, 21 Jan 2026 11:24:30 -0300 Subject: [PATCH 005/131] fix(federation): set room topic on creation of federated rooms (#38264) --- .../federation-matrix/src/FederationMatrix.ts | 4 + .../tests/end-to-end/room.spec.ts | 98 +++++++++++++++++++ 2 files changed, 102 insertions(+) diff --git a/ee/packages/federation-matrix/src/FederationMatrix.ts b/ee/packages/federation-matrix/src/FederationMatrix.ts index 332d393c1d18c..e2a975bd22194 100644 --- a/ee/packages/federation-matrix/src/FederationMatrix.ts +++ b/ee/packages/federation-matrix/src/FederationMatrix.ts @@ -114,6 +114,10 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS this.logger.debug({ msg: 'Matrix room created', response: matrixRoomResult }); + if (room.topic) { + await federationSDK.setRoomTopic(matrixRoomResult.room_id, matrixUserId, room.topic); + } + await Rooms.setAsFederated(room._id, { mrid: matrixRoomResult.room_id, origin: this.serverName }); // Members are NOT invited here - invites are sent via beforeAddUserToRoom callback. diff --git a/ee/packages/federation-matrix/tests/end-to-end/room.spec.ts b/ee/packages/federation-matrix/tests/end-to-end/room.spec.ts index ff120a10f5764..4b51395eb8d84 100644 --- a/ee/packages/federation-matrix/tests/end-to-end/room.spec.ts +++ b/ee/packages/federation-matrix/tests/end-to-end/room.spec.ts @@ -1,4 +1,6 @@ import type { IMessage, IUser } from '@rocket.chat/core-typings'; +import type { Room } from 'matrix-js-sdk'; +import { EventTimeline } from 'matrix-js-sdk'; import { createRoom, @@ -713,6 +715,102 @@ import { SynapseClient } from '../helper/synapse-client'; }); }); + describe('Create a room with a topic', () => { + describe('Create a federated room with a topic', () => { + let channelName: string; + let channelTopic: string; + let federatedChannel: any; + + beforeAll(async () => { + channelName = `federated-channel-with-topic-${Date.now()}`; + channelTopic = 'This is a test topic for federation'; + + const createResponse = await createRoom({ + type: 'p', + name: channelName, + members: [federationConfig.hs1.adminMatrixUserId], + extraData: { + federated: true, + topic: channelTopic, + }, + config: rc1AdminRequestConfig, + }); + + federatedChannel = createResponse.body.group; + + expect(federatedChannel).toHaveProperty('_id'); + expect(federatedChannel).toHaveProperty('name', channelName); + expect(federatedChannel).toHaveProperty('t', 'p'); + expect(federatedChannel).toHaveProperty('federated', true); + + const acceptedRoomId = await hs1AdminApp.acceptInvitationForRoomName(channelName); + expect(acceptedRoomId).not.toBe(''); + }, 10000); + + it('should set the topic on the Rocket.Chat side', async () => { + // RC view: Verify the topic is set in Rocket.Chat + const roomInfo = await getRoomInfo(federatedChannel._id, rc1AdminRequestConfig); + expect(roomInfo.room).toHaveProperty('topic', channelTopic); + }); + + it('should set the topic on the Matrix side', async () => { + const hs1Room1 = (await hs1AdminApp.matrixClient.getRoom(federatedChannel.federation.mrid)) as Room; + + expect(hs1Room1).toBeDefined(); + + const [topic] = hs1Room1.getLiveTimeline().getState(EventTimeline.FORWARDS)?.getStateEvents('m.room.topic') || []; + + expect(topic.getContent().topic).toBe(channelTopic); + }); + }); + + describe('Create a federated room without a topic', () => { + let channelName: string; + let federatedChannel: any; + + beforeAll(async () => { + channelName = `federated-channel-no-topic-${Date.now()}`; + + const createResponse = await createRoom({ + type: 'p', + name: channelName, + members: [federationConfig.hs1.adminMatrixUserId], + extraData: { + federated: true, + }, + config: rc1AdminRequestConfig, + }); + + federatedChannel = createResponse.body.group; + + expect(federatedChannel).toHaveProperty('_id'); + expect(federatedChannel).toHaveProperty('name', channelName); + expect(federatedChannel).toHaveProperty('t', 'p'); + expect(federatedChannel).toHaveProperty('federated', true); + expect(federatedChannel).toHaveProperty('federation.mrid'); + + const acceptedRoomId = await hs1AdminApp.acceptInvitationForRoomName(channelName); + expect(acceptedRoomId).not.toBe(''); + }, 10000); + + it('should not set a topic on the Rocket.Chat side', async () => { + // RC view: Verify no topic is set in Rocket.Chat + const roomInfo = await getRoomInfo(federatedChannel._id, rc1AdminRequestConfig); + expect(roomInfo.room?.topic).toBeUndefined(); + }); + + it('should not set a topic on the Matrix side', async () => { + const hs1Room1 = (await hs1AdminApp.matrixClient.getRoom(federatedChannel.federation.mrid)) as Room; + + expect(hs1Room1).toBeDefined(); + + const [topic] = hs1Room1.getLiveTimeline().getState(EventTimeline.FORWARDS)?.getStateEvents('m.room.topic') || []; + + expect(topic).toBeUndefined(); + }); + }); + }); + describe('Create a room on RC as private and federated, then invite users', () => { describe('Go to the members list and', () => { describe('Add a federated user', () => { From 25271b2c35a9e5025b9b407483827d831ad79deb Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Wed, 21 Jan 2026 12:04:24 -0600 Subject: [PATCH 006/131] chore: Adapt logs to object format (almost) (#38280) --- apps/meteor/app/api/server/v1/emoji-custom.ts | 4 ++-- apps/meteor/app/api/server/v1/ldap.ts | 4 ++-- apps/meteor/app/api/server/v1/misc.ts | 4 ++-- .../server/lib/logLoginAttempts.ts | 11 +++++++--- apps/meteor/app/cloud/server/index.ts | 20 +++++++++---------- apps/meteor/app/crowd/server/crowd.ts | 4 ++-- .../server/methods/sendFileMessage.ts | 4 ++-- .../app/file-upload/ufs/AmazonS3/server.ts | 8 ++++---- .../file-upload/ufs/GoogleStorage/server.ts | 2 +- .../app/file-upload/ufs/Webdav/server.ts | 2 +- .../classes/converters/MessageConverter.ts | 12 +++++------ .../integrations/server/lib/triggerHandler.ts | 7 ++++--- .../app/settings/server/CachedSettings.ts | 8 ++++---- .../app/settings/server/SettingsRegistry.ts | 6 +++--- .../server/hooks/afterOnHold.ts | 2 +- .../server/hooks/afterOnHoldChatResumed.ts | 2 +- .../hooks/checkAgentBeforeTakeInquiry.ts | 4 ++-- .../server/hooks/resumeOnHold.ts | 8 ++++---- apps/meteor/server/lib/ldap/Manager.ts | 4 ++-- .../modules/streamer/streamer.module.ts | 2 +- .../services/omnichannel-analytics/service.ts | 6 +++--- .../providers/twilio.ts | 8 ++++---- .../server/services/omnichannel/queue.ts | 12 +++++------ apps/meteor/server/startup/migrations/v317.ts | 18 ++++++++++------- .../services/omnichannel/queue.tests.ts | 1 - ee/packages/federation-matrix/src/setup.ts | 4 ++-- ee/packages/license/src/license.ts | 6 ++---- .../omnichannel-services/src/QueueWorker.ts | 16 +++++++-------- .../runtime/deno/AppsEngineDenoRuntime.ts | 10 +++++----- 29 files changed, 102 insertions(+), 97 deletions(-) diff --git a/apps/meteor/app/api/server/v1/emoji-custom.ts b/apps/meteor/app/api/server/v1/emoji-custom.ts index fa84d3cc6b995..15764b7f74e7d 100644 --- a/apps/meteor/app/api/server/v1/emoji-custom.ts +++ b/apps/meteor/app/api/server/v1/emoji-custom.ts @@ -135,8 +135,8 @@ API.v1.addRoute( }); await uploadEmojiCustomWithBuffer(this.userId, fileBuffer, mimetype, emojiData); - } catch (e) { - SystemLogger.error(e); + } catch (err) { + SystemLogger.error({ err }); return API.v1.failure(); } diff --git a/apps/meteor/app/api/server/v1/ldap.ts b/apps/meteor/app/api/server/v1/ldap.ts index 4b112cfbf3351..3f9a2c29deded 100644 --- a/apps/meteor/app/api/server/v1/ldap.ts +++ b/apps/meteor/app/api/server/v1/ldap.ts @@ -20,8 +20,8 @@ API.v1.addRoute( try { await LDAP.testConnection(); - } catch (error) { - SystemLogger.error(error); + } catch (err) { + SystemLogger.error({ err }); throw new Error('Connection_failed'); } diff --git a/apps/meteor/app/api/server/v1/misc.ts b/apps/meteor/app/api/server/v1/misc.ts index 403ffcc29b4ce..192a3e3579dc0 100644 --- a/apps/meteor/app/api/server/v1/misc.ts +++ b/apps/meteor/app/api/server/v1/misc.ts @@ -519,7 +519,7 @@ API.v1.addRoute( return API.v1.success(mountResult({ id, result })); } catch (err) { if (!(err as any).isClientSafe && !(err as any).meteorError) { - SystemLogger.error({ msg: `Exception while invoking method ${method}`, err }); + SystemLogger.error({ msg: 'Exception while invoking method', err, method }); } if (settings.get('Log_Level') === '2') { @@ -576,7 +576,7 @@ API.v1.addRoute( return API.v1.success(mountResult({ id, result })); } catch (err) { if (!(err as any).isClientSafe && !(err as any).meteorError) { - SystemLogger.error({ msg: `Exception while invoking method ${method}`, err }); + SystemLogger.error({ msg: 'Exception while invoking method', err, method }); } if (settings.get('Log_Level') === '2') { Meteor._debug(`Exception while invoking method ${method}`, err); diff --git a/apps/meteor/app/authentication/server/lib/logLoginAttempts.ts b/apps/meteor/app/authentication/server/lib/logLoginAttempts.ts index 0f97796aa4a75..3c50937599063 100644 --- a/apps/meteor/app/authentication/server/lib/logLoginAttempts.ts +++ b/apps/meteor/app/authentication/server/lib/logLoginAttempts.ts @@ -26,7 +26,12 @@ export const logFailedLoginAttempts = (login: ILoginAttempt): void => { if (!settings.get('Login_Logs_UserAgent')) { userAgent = '-'; } - SystemLogger.info( - `Failed login detected - Username[${user}] ClientAddress[${clientAddress}] ForwardedFor[${forwardedFor}] XRealIp[${realIp}] UserAgent[${userAgent}]`, - ); + SystemLogger.info({ + msg: 'Failed login detected', + user, + clientAddress, + forwardedFor, + realIp, + userAgent, + }); }; diff --git a/apps/meteor/app/cloud/server/index.ts b/apps/meteor/app/cloud/server/index.ts index e7214295225b1..7bc7696d5b0dc 100644 --- a/apps/meteor/app/cloud/server/index.ts +++ b/apps/meteor/app/cloud/server/index.ts @@ -23,36 +23,36 @@ Meteor.startup(async () => { } console.log('Successfully registered with token provided by REG_TOKEN!'); - } catch (e: any) { - SystemLogger.error('An error occurred registering with token.', e.message); + } catch (err: any) { + SystemLogger.error({ msg: 'An error occurred registering with token.', err }); } } setImmediate(async () => { try { await syncWorkspace(); - } catch (e: any) { - if (e instanceof CloudWorkspaceAccessTokenEmptyError) { + } catch (err: any) { + if (err instanceof CloudWorkspaceAccessTokenEmptyError) { return; } - if (e.type && e.type === 'AbortError') { + if (err.type && err.type === 'AbortError') { return; } - SystemLogger.error('An error occurred syncing workspace.', e.message); + SystemLogger.error({ msg: 'An error occurred syncing workspace.', err }); } }); const minute = Math.floor(Math.random() * 60); await cronJobs.add(licenseCronName, `${minute} */12 * * *`, async () => { try { await syncWorkspace(); - } catch (e: any) { - if (e instanceof CloudWorkspaceAccessTokenEmptyError) { + } catch (err: any) { + if (err instanceof CloudWorkspaceAccessTokenEmptyError) { return; } - if (e.type && e.type === 'AbortError') { + if (err.type && err.type === 'AbortError') { return; } - SystemLogger.error('An error occurred syncing workspace.', e.message); + SystemLogger.error({ msg: 'An error occurred syncing workspace.', err }); } }); }); diff --git a/apps/meteor/app/crowd/server/crowd.ts b/apps/meteor/app/crowd/server/crowd.ts index ac1467dedbe00..8e499b06f77c6 100644 --- a/apps/meteor/app/crowd/server/crowd.ts +++ b/apps/meteor/app/crowd/server/crowd.ts @@ -392,8 +392,8 @@ Accounts.registerLoginHandler('crowd', async function (this: typeof Accounts, lo return result; } catch (err: any) { - logger.debug({ err }); - logger.error('Crowd user not authenticated due to an error'); + logger.error({ msg: 'Crowd user not authenticated due to an error', err }); + throw new Meteor.Error('user-not-found', err.message); } }); diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index fd503b31644ce..fbad271ed0438 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -101,8 +101,8 @@ export const parseFileIntoMessageAttachments = async ( typeGroup: thumbnail.typeGroup || '', }); } - } catch (e) { - SystemLogger.error(e); + } catch (err) { + SystemLogger.error({ err }); } attachments.push(attachment); } else if (/^audio\/.+/.test(file.type as string)) { diff --git a/apps/meteor/app/file-upload/ufs/AmazonS3/server.ts b/apps/meteor/app/file-upload/ufs/AmazonS3/server.ts index 3cc3f04f9ccb3..9e00e4ea497f3 100644 --- a/apps/meteor/app/file-upload/ufs/AmazonS3/server.ts +++ b/apps/meteor/app/file-upload/ufs/AmazonS3/server.ts @@ -128,7 +128,7 @@ class AmazonS3Store extends UploadFS.Store { try { return s3.deleteObject(params).promise(); } catch (err: any) { - SystemLogger.error(err); + SystemLogger.error({ err }); } }; @@ -184,9 +184,9 @@ class AmazonS3Store extends UploadFS.Store { ContentType: file.type, Bucket: classOptions.connection.params.Bucket, }, - (error) => { - if (error) { - SystemLogger.error(error); + (err) => { + if (err) { + SystemLogger.error({ err }); } writeStream.emit('real_finish'); diff --git a/apps/meteor/app/file-upload/ufs/GoogleStorage/server.ts b/apps/meteor/app/file-upload/ufs/GoogleStorage/server.ts index 2034ea2135706..e2b71ac8052d7 100644 --- a/apps/meteor/app/file-upload/ufs/GoogleStorage/server.ts +++ b/apps/meteor/app/file-upload/ufs/GoogleStorage/server.ts @@ -103,7 +103,7 @@ class GoogleStorageStore extends UploadFS.Store { try { return bucket.file(this.getPath(file)).delete(); } catch (err: any) { - SystemLogger.error(err); + SystemLogger.error({ err }); } }; diff --git a/apps/meteor/app/file-upload/ufs/Webdav/server.ts b/apps/meteor/app/file-upload/ufs/Webdav/server.ts index 69ab18a4ebb08..e5a8a62b5d059 100644 --- a/apps/meteor/app/file-upload/ufs/Webdav/server.ts +++ b/apps/meteor/app/file-upload/ufs/Webdav/server.ts @@ -94,7 +94,7 @@ class WebdavStore extends UploadFS.Store { try { return client.deleteFile(this.getPath(file)); } catch (err: any) { - SystemLogger.error(err); + SystemLogger.error({ err }); } }; diff --git a/apps/meteor/app/importer/server/classes/converters/MessageConverter.ts b/apps/meteor/app/importer/server/classes/converters/MessageConverter.ts index 8fa9eaba04534..825090147be8a 100644 --- a/apps/meteor/app/importer/server/classes/converters/MessageConverter.ts +++ b/apps/meteor/app/importer/server/classes/converters/MessageConverter.ts @@ -41,9 +41,8 @@ export class MessageConverter extends RecordConverter { for await (const rid of this.rids) { try { await Rooms.resetLastMessageById(rid, null); - } catch (e) { - this._logger.warn({ msg: 'Failed to update last message of room', roomId: rid }); - this._logger.error(e); + } catch (err) { + this._logger.error({ msg: 'Failed to update last message of room', roomId: rid, err }); } } } @@ -70,9 +69,8 @@ export class MessageConverter extends RecordConverter { try { await insertMessage(creator, msgObj as unknown as IDBMessage, rid, true); - } catch (e) { - this._logger.warn({ msg: 'Failed to import message', timestamp: msgObj.ts, roomId: rid }); - this._logger.error(e); + } catch (err) { + this._logger.error({ msg: 'Failed to import message', timestamp: msgObj.ts, roomId: rid, err }); } } @@ -167,7 +165,7 @@ export class MessageConverter extends RecordConverter { } if (!data.username) { - this._logger.debug(importId); + this._logger.debug({ msg: 'Mentioned user has no username', importId }); throw new Error('importer-message-mentioned-username-not-found'); } diff --git a/apps/meteor/app/integrations/server/lib/triggerHandler.ts b/apps/meteor/app/integrations/server/lib/triggerHandler.ts index 0a29396ec2c32..abf3437f1bed9 100644 --- a/apps/meteor/app/integrations/server/lib/triggerHandler.ts +++ b/apps/meteor/app/integrations/server/lib/triggerHandler.ts @@ -158,9 +158,10 @@ class RocketChatIntegrationHandler { // If no room could be found, we won't be sending any messages but we'll warn in the logs if (!tmpRoom) { - outgoingLogger.warn( - `The Integration "${trigger.name}" doesn't have a room configured nor did it provide a room to send the message to.`, - ); + outgoingLogger.warn({ + msg: 'The Integration doesnt have a room configured nor did it provide a room to send the message to.', + integrationName: trigger.name, + }); return; } diff --git a/apps/meteor/app/settings/server/CachedSettings.ts b/apps/meteor/app/settings/server/CachedSettings.ts index 6a16a4c761313..3c46dd05a6806 100644 --- a/apps/meteor/app/settings/server/CachedSettings.ts +++ b/apps/meteor/app/settings/server/CachedSettings.ts @@ -108,7 +108,7 @@ export class CachedSettings */ public override has(_id: ISetting['_id']): boolean { if (!this.ready && warn) { - SystemLogger.warn(`Settings not initialized yet. getting: ${_id}`); + SystemLogger.warn({ msg: 'Settings not initialized yet. getting', _id }); } return this.store.has(_id); } @@ -120,7 +120,7 @@ export class CachedSettings */ public getSetting(_id: ISetting['_id']): ISetting | undefined { if (!this.ready && warn) { - SystemLogger.warn(`Settings not initialized yet. getting: ${_id}`); + SystemLogger.warn({ msg: 'Settings not initialized yet. getting', _id }); } return this.store.get(_id); } @@ -134,7 +134,7 @@ export class CachedSettings */ public get(_id: ISetting['_id']): T { if (!this.ready && warn) { - SystemLogger.warn(`Settings not initialized yet. getting: ${_id}`); + SystemLogger.warn({ msg: 'Settings not initialized yet. getting', _id }); } return this.store.get(_id)?.value as T; } @@ -148,7 +148,7 @@ export class CachedSettings */ public getByRegexp(_id: RegExp): [string, T][] { if (!this.ready && warn) { - SystemLogger.warn(`Settings not initialized yet. getting: ${_id}`); + SystemLogger.warn({ msg: 'Settings not initialized yet. getting', _id }); } return [...this.store.entries()].filter(([key]) => _id.test(key)).map(([key, setting]) => [key, setting.value]) as [string, T][]; diff --git a/apps/meteor/app/settings/server/SettingsRegistry.ts b/apps/meteor/app/settings/server/SettingsRegistry.ts index cb6f9da20ab97..3b93ca5c0bfb6 100644 --- a/apps/meteor/app/settings/server/SettingsRegistry.ts +++ b/apps/meteor/app/settings/server/SettingsRegistry.ts @@ -132,7 +132,7 @@ export class SettingsRegistry { ); if (isSettingEnterprise(settingFromCode) && !('invalidValue' in settingFromCode)) { - SystemLogger.error(`Enterprise setting ${_id} is missing the invalidValue option`); + SystemLogger.error({ msg: 'Enterprise setting is missing the invalidValue option', _id }); throw new Error(`Enterprise setting ${_id} is missing the invalidValue option`); } @@ -145,7 +145,7 @@ export class SettingsRegistry { try { validateSetting(settingFromCode._id, settingFromCode.type, settingFromCode.value); } catch (e) { - IS_DEVELOPMENT && SystemLogger.error(`Invalid setting code ${_id}: ${(e as Error).message}`); + IS_DEVELOPMENT && SystemLogger.error({ msg: 'Invalid setting code', _id, err: e as Error }); } const isOverwritten = settingFromCode !== settingFromCodeOverwritten || (settingStored && settingStored !== settingStoredOverwritten); @@ -189,7 +189,7 @@ export class SettingsRegistry { try { validateSetting(settingFromCode._id, settingFromCode.type, settingStored?.value); } catch (e) { - IS_DEVELOPMENT && SystemLogger.error(`Invalid setting stored ${_id}: ${(e as Error).message}`); + IS_DEVELOPMENT && SystemLogger.error({ msg: 'Invalid setting stored', _id, err: e as Error }); } return; } diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterOnHold.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterOnHold.ts index bb38b53773602..89a28af413420 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterOnHold.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterOnHold.ts @@ -18,7 +18,7 @@ const handleAfterOnHold = async (room: Pick): Promise('Livechat_auto_close_on_hold_chats_custom_message') || i18n.t('Closed_automatically_because_chat_was_onhold_for_seconds', { diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterOnHoldChatResumed.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterOnHoldChatResumed.ts index 9ebdfcacb539a..e114fd89ec435 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterOnHoldChatResumed.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterOnHoldChatResumed.ts @@ -13,7 +13,7 @@ const handleAfterOnHoldChatResumed = async (room: IRoom): Promise => { const { _id: roomId } = room; - cbLogger.debug(`Removing current on hold timers for room ${roomId}`); + cbLogger.debug({ msg: 'Removing current on hold timers for room', roomId }); await AutoCloseOnHoldScheduler.unscheduleRoom(roomId); return room; diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/checkAgentBeforeTakeInquiry.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/checkAgentBeforeTakeInquiry.ts index a76cc7871f45b..349ad44652bf1 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/checkAgentBeforeTakeInquiry.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/checkAgentBeforeTakeInquiry.ts @@ -38,12 +38,12 @@ const validateMaxChats = async ({ } if (!settings.get('Livechat_waiting_queue')) { - cbLogger.info(`Chat can be taken by Agent ${agentId}: waiting queue is disabled`); + cbLogger.info({ msg: 'Chat can be taken by Agent: waiting queue is disabled', agentId }); return agent; } if (await allowAgentSkipQueue(agent)) { - cbLogger.info(`Chat can be taken by Agent ${agentId}: agent can skip queue`); + cbLogger.info({ msg: 'Chat can be taken by Agent: agent can skip queue', agentId }); return agent; } diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/resumeOnHold.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/resumeOnHold.ts index 85239c4fe1fc9..b35b9e8acce5f 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/resumeOnHold.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/resumeOnHold.ts @@ -16,7 +16,7 @@ const resumeOnHoldCommentAndUser = async (room: IOmnichannelRoom): Promise<{ com projection: { name: 1, username: 1 }, }); if (!visitor) { - callbackLogger.error(`[afterOmnichannelSaveMessage] Visitor Not found for room ${rid} while trying to resume on hold`); + callbackLogger.error({ msg: '[afterOmnichannelSaveMessage] Visitor Not found for room while trying to resume on hold', rid }); throw new Error('Visitor not found while trying to resume on hold'); } @@ -26,7 +26,7 @@ const resumeOnHoldCommentAndUser = async (room: IOmnichannelRoom): Promise<{ com const resumedBy = await Users.findOneById('rocket.cat'); if (!resumedBy) { - callbackLogger.error(`[afterOmnichannelSaveMessage] User Not found for room ${rid} while trying to resume on hold`); + callbackLogger.error({ msg: '[afterOmnichannelSaveMessage] User Not found for room while trying to resume on hold', rid }); throw new Error(`User not found while trying to resume on hold`); } @@ -53,13 +53,13 @@ callbacks.add( } if (isMessageFromVisitor(message) && room.onHold) { - callbackLogger.debug(`[afterOmnichannelSaveMessage] Room ${rid} is on hold, resuming it now since visitor sent a message`); + callbackLogger.debug({ msg: '[afterOmnichannelSaveMessage] Room is on hold, resuming it now since visitor sent a message', rid }); try { const { comment: resumeChatComment, resumedBy } = await resumeOnHoldCommentAndUser(updatedRoom); await OmnichannelEEService.resumeRoomOnHold(updatedRoom, resumeChatComment, resumedBy); } catch (error) { - callbackLogger.error(`[afterOmnichannelSaveMessage] Error while resuming room ${rid} on hold: Error: `, error); + callbackLogger.error({ msg: '[afterOmnichannelSaveMessage] Error while resuming room on hold', rid, err: error }); return message; } } diff --git a/apps/meteor/server/lib/ldap/Manager.ts b/apps/meteor/server/lib/ldap/Manager.ts index 19dd6a516e789..9b747c0f78fd9 100644 --- a/apps/meteor/server/lib/ldap/Manager.ts +++ b/apps/meteor/server/lib/ldap/Manager.ts @@ -241,7 +241,7 @@ export class LDAPManager { // Do a search as the user and check if they have any result authLogger.debug('User authenticated successfully, performing additional search.'); if ((await ldap.searchAndCount(ldapUser.dn, {})) === 0) { - authLogger.debug(`Bind successful but user ${ldapUser.dn} was not found via search`); + authLogger.debug({ msg: 'Bind successful but user was not found via search', dn: ldapUser.dn }); } } return ldapUser; @@ -267,7 +267,7 @@ export class LDAPManager { // Do a search as the user and check if they have any result authLogger.debug('User authenticated successfully, performing additional search.'); if ((await ldap.searchAndCount(ldapUser.dn, {})) === 0) { - authLogger.debug(`Bind successful but user ${ldapUser.dn} was not found via search`); + authLogger.debug({ msg: 'Bind successful but user was not found via search', dn: ldapUser.dn }); } } diff --git a/apps/meteor/server/modules/streamer/streamer.module.ts b/apps/meteor/server/modules/streamer/streamer.module.ts index 01cdd6861600d..e55592fb2e907 100644 --- a/apps/meteor/server/modules/streamer/streamer.module.ts +++ b/apps/meteor/server/modules/streamer/streamer.module.ts @@ -78,7 +78,7 @@ export abstract class Streamer extends EventEmit } if (typeof fn === 'string' && ['all', 'none', 'logged'].indexOf(fn) === -1) { - SystemLogger.error(`${name} shortcut '${fn}' is invalid`); + SystemLogger.error({ msg: 'shortcut is invalid', name, fn }); } if (fn === 'all' || fn === true) { diff --git a/apps/meteor/server/services/omnichannel-analytics/service.ts b/apps/meteor/server/services/omnichannel-analytics/service.ts index 9ad567feaa188..24ce8ded79f2a 100644 --- a/apps/meteor/server/services/omnichannel-analytics/service.ts +++ b/apps/meteor/server/services/omnichannel-analytics/service.ts @@ -55,7 +55,7 @@ export class OmnichannelAnalyticsService extends ServiceClassInternal implements } if (!this.agentOverview.isActionAllowed(name)) { - serviceLogger.error(`AgentOverview.${name} is not a valid action`); + serviceLogger.error({ msg: 'AgentOverview action is not valid', name }); return; } @@ -74,7 +74,7 @@ export class OmnichannelAnalyticsService extends ServiceClassInternal implements // Check if function exists, prevent server error in case property altered if (!this.chart.isActionAllowed(chartLabel)) { - serviceLogger.error(`ChartData.${chartLabel} is not a valid action`); + serviceLogger.error({ msg: 'ChartData action is not valid', chartLabel }); return; } @@ -164,7 +164,7 @@ export class OmnichannelAnalyticsService extends ServiceClassInternal implements } if (!this.overview.isActionAllowed(name)) { - serviceLogger.error(`OverviewData.${name} is not a valid action`); + serviceLogger.error({ msg: 'OverviewData action is not valid', name }); return; } diff --git a/apps/meteor/server/services/omnichannel-integrations/providers/twilio.ts b/apps/meteor/server/services/omnichannel-integrations/providers/twilio.ts index 26b6dbaea1cf9..fe4f5e30241b0 100644 --- a/apps/meteor/server/services/omnichannel-integrations/providers/twilio.ts +++ b/apps/meteor/server/services/omnichannel-integrations/providers/twilio.ts @@ -78,7 +78,7 @@ export class Twilio implements ISMSProvider { } if (isNaN(numMedia)) { - SystemLogger.error(`Error parsing NumMedia ${data.NumMedia}`); + SystemLogger.error({ msg: 'Error parsing NumMedia', numMedia: data.NumMedia }); return returnData; } @@ -114,7 +114,7 @@ export class Twilio implements ISMSProvider { return twilio(sid, token); } catch (error) { await notifyAgent(userId, rid, i18n.t('SMS_Twilio_InvalidCredentials')); - SystemLogger.error(`(Twilio) -> ${error}`); + SystemLogger.error({ msg: '(Twilio) ->', err: error }); } } @@ -157,7 +157,7 @@ export class Twilio implements ISMSProvider { if (reason) { await notifyAgent(userId, rid, reason); - SystemLogger.error(`(Twilio) -> ${reason}`); + SystemLogger.error({ msg: '(Twilio) ->', reason }); return ''; } @@ -219,7 +219,7 @@ export class Twilio implements ISMSProvider { if (result.errorCode) { await notifyAgent(userId, rid, result.errorMessage); - SystemLogger.error(`(Twilio) -> ${result.errorCode}`); + SystemLogger.error({ msg: '(Twilio) ->', errorCode: result.errorCode }); } return { diff --git a/apps/meteor/server/services/omnichannel/queue.ts b/apps/meteor/server/services/omnichannel/queue.ts index 89acb5434702f..d522c78d7daaa 100644 --- a/apps/meteor/server/services/omnichannel/queue.ts +++ b/apps/meteor/server/services/omnichannel/queue.ts @@ -46,7 +46,7 @@ export class OmnichannelQueue implements IOmnichannelQueue { } const activeQueues = await this.getActiveQueues(); - queueLogger.debug(`Active queues: ${activeQueues.length}`); + queueLogger.debug({ msg: 'Active queues', count: activeQueues.length }); this.running = true; queueLogger.info('Service started'); @@ -118,22 +118,22 @@ export class OmnichannelQueue implements IOmnichannelQueue { } private async checkQueue(queue: string | null) { - queueLogger.debug(`Processing items for queue ${queue || 'Public'}`); + queueLogger.debug({ msg: 'Processing items for queue', queue: queue || 'Public' }); try { const nextInquiry = await LivechatInquiry.findNextAndLock(getOmniChatSortQuery(getInquirySortMechanismSetting()), queue); if (!nextInquiry) { - queueLogger.debug(`No more items for queue ${queue || 'Public'}`); + queueLogger.debug({ msg: 'No more items for queue', queue: queue || 'Public' }); return; } const result = await this.processWaitingQueue(queue, nextInquiry as InquiryWithAgentInfo); if (!result) { - queueLogger.debug(`Inquiry ${nextInquiry._id} not taken. Unlocking and re-queueing`); + queueLogger.debug({ msg: 'Inquiry not taken. Unlocking and re-queueing', inquiry: nextInquiry._id }); return await LivechatInquiry.unlock(nextInquiry._id); } - queueLogger.debug(`Inquiry ${nextInquiry._id} taken successfully. Unlocking`); + queueLogger.debug({ msg: 'Inquiry taken successfully. Unlocking', inquiry: nextInquiry._id }); await LivechatInquiry.unlock(nextInquiry._id); queueLogger.debug({ msg: 'Inquiry processed', @@ -264,7 +264,7 @@ export class OmnichannelQueue implements IOmnichannelQueue { _id: rid, servedBy: { _id: agentId }, } = room; - queueLogger.debug(`Inquiry ${inquiry._id} taken successfully by agent ${agentId}. Notifying`); + queueLogger.debug({ msg: 'Inquiry taken successfully by agent. Notifying', inquiry: inquiry._id, agentId }); setTimeout(() => { void dispatchAgentDelegated(rid, agentId); }, 1000); diff --git a/apps/meteor/server/startup/migrations/v317.ts b/apps/meteor/server/startup/migrations/v317.ts index 02867fcdb5cc7..383f2a47adb0b 100644 --- a/apps/meteor/server/startup/migrations/v317.ts +++ b/apps/meteor/server/startup/migrations/v317.ts @@ -70,7 +70,7 @@ addMigration({ return; } - SystemLogger.warn(`The default value of the setting ${key} has changed to ${newValue}. Please review your settings.`); + SystemLogger.warn({ msg: 'The default value of the setting has changed. Please review your settings.', key, newValue }); return Settings.updateOne({ _id: key }, { $set: { value: newValue } }); }) @@ -91,9 +91,11 @@ addMigration({ return; } - SystemLogger.warn( - `The default value of the custom setting ${_id} has changed to ${newDefaultButtonColor}. Please review your settings.`, - ); + SystemLogger.warn({ + msg: 'The default value of the custom setting has changed. Please review your settings.', + _id, + newValue: newDefaultButtonColor, + }); return Settings.updateOne({ _id }, { $set: { value: newDefaultButtonColor } }); }) @@ -105,9 +107,11 @@ addMigration({ return; } - SystemLogger.warn( - `The default value of the custom setting ${_id} has changed to ${newDefaultButtonLabelColor}. Please review your settings.`, - ); + SystemLogger.warn({ + msg: 'The default value of the custom setting has changed. Please review your settings.', + _id, + newValue: newDefaultButtonLabelColor, + }); return Settings.updateOne({ _id }, { $set: { value: newDefaultButtonLabelColor } }); }) diff --git a/apps/meteor/tests/unit/server/services/omnichannel/queue.tests.ts b/apps/meteor/tests/unit/server/services/omnichannel/queue.tests.ts index 4d498a204307f..1580620ae509f 100644 --- a/apps/meteor/tests/unit/server/services/omnichannel/queue.tests.ts +++ b/apps/meteor/tests/unit/server/services/omnichannel/queue.tests.ts @@ -384,7 +384,6 @@ describe('Omnichannel Queue processor', () => { await queue.execute(); expect(queue.getActiveQueues.calledOnce).to.be.true; - expect(queueLogger.debug.calledWith('Processing items for queue Public')).to.be.true; }); }); describe('start', () => { diff --git a/ee/packages/federation-matrix/src/setup.ts b/ee/packages/federation-matrix/src/setup.ts index f307d9c1e5327..ccfffb3ad0da0 100644 --- a/ee/packages/federation-matrix/src/setup.ts +++ b/ee/packages/federation-matrix/src/setup.ts @@ -14,7 +14,7 @@ function validateDomain(domain: string): boolean { } if (value.toLowerCase() !== value) { - logger.error(`The Federation domain "${value}" cannot have uppercase letters`); + logger.error({ msg: 'The Federation domain cannot have uppercase letters', domain: value }); return false; } @@ -25,7 +25,7 @@ function validateDomain(domain: string): boolean { throw new Error(); } } catch { - logger.error(`The configured Federation domain "${value}" is not valid`); + logger.error({ msg: 'The configured Federation domain is not valid', domain: value }); return false; } diff --git a/ee/packages/license/src/license.ts b/ee/packages/license/src/license.ts index 3e94af592066a..0e92af730b1cf 100644 --- a/ee/packages/license/src/license.ts +++ b/ee/packages/license/src/license.ts @@ -355,10 +355,8 @@ export abstract class LicenseManager extends Emitter { this.emit('installed'); return true; - } catch (e) { - logger.error('Invalid license'); - - logger.error({ msg: 'Invalid raw license', encryptedLicense, e }); + } catch (err) { + logger.error({ msg: 'Invalid raw license', encryptedLicense, err }); throw new InvalidLicenseError(); } diff --git a/ee/packages/omnichannel-services/src/QueueWorker.ts b/ee/packages/omnichannel-services/src/QueueWorker.ts index 3e18e4fea4b7a..69855f14071c2 100644 --- a/ee/packages/omnichannel-services/src/QueueWorker.ts +++ b/ee/packages/omnichannel-services/src/QueueWorker.ts @@ -46,7 +46,7 @@ export class QueueWorker extends ServiceClass implements IQueueWorkerService { await this.createIndexes(); this.registerWorkers(); } catch (e) { - this.logger.fatal(e, 'Fatal error occurred when registering workers'); + this.logger.fatal({ msg: 'Fatal error occurred when registering workers', err: e }); process.exit(1); } } @@ -75,24 +75,24 @@ export class QueueWorker extends ServiceClass implements IQueueWorkerService { } private async workerCallback(queueItem: Work<{ to: string; data: any }>): Promise { - this.logger.info(`Processing queue item ${queueItem._id} for work`); - this.logger.info(`Queue item is trying to call ${queueItem.message.to}`); + this.logger.info({ msg: 'Processing queue item for work', queueItemId: queueItem._id }); + this.logger.info({ msg: 'Queue item is trying to call', to: queueItem.message.to }); try { await api.call(queueItem.message.to, [queueItem.message]); - this.logger.info(`Queue item ${queueItem._id} completed`); + this.logger.info({ msg: 'Queue item completed', queueItemId: queueItem._id }); return 'Completed' as const; } catch (err: unknown) { const e = err as Error; - this.logger.error(`Queue item ${queueItem._id} errored: ${e.message}`); + this.logger.error({ msg: 'Queue item errored', queueItemId: queueItem._id, err: e }); queueItem.releasedReason = e.message; // Let's only retry for X times when the error is "service not found" // For any other error, we'll just reject the item if ((queueItem.retryCount || 0) < this.retryCount && this.isRetryableError(e.message)) { - this.logger.info(`Queue item ${queueItem._id} will be retried in 10 seconds`); + this.logger.info({ msg: 'Queue item will be retried', queueItemId: queueItem._id, retry: this.retryDelay }); queueItem.nextReceivableTime = new Date(Date.now() + this.retryDelay); return 'Retry' as const; } - this.logger.info(`Queue item ${queueItem._id} will be rejected`); + this.logger.info({ msg: 'Queue item will be rejected', queueItemId: queueItem._id }); return 'Rejected' as const; } } @@ -119,7 +119,7 @@ export class QueueWorker extends ServiceClass implements IQueueWorkerService { // `to` is a service name that will be called, including namespace + action // This is a "generic" job that allows you to call any service async queueWork>(queue: Actions, to: string, data: T): Promise { - this.logger.info(`Queueing work for ${to}`); + this.logger.info({ msg: 'Queueing work for', to }); if (!this.matchServiceCall(to)) { // We don't want to queue calls to invalid service names throw new Error(`Invalid service name ${to}`); diff --git a/packages/apps-engine/src/server/runtime/deno/AppsEngineDenoRuntime.ts b/packages/apps-engine/src/server/runtime/deno/AppsEngineDenoRuntime.ts index 8b4eb385a88f8..d445eeac0ccd8 100644 --- a/packages/apps-engine/src/server/runtime/deno/AppsEngineDenoRuntime.ts +++ b/packages/apps-engine/src/server/runtime/deno/AppsEngineDenoRuntime.ts @@ -287,7 +287,7 @@ export class DenoRuntimeSubprocessController extends EventEmitter implements IRu this.debug('Restarting app subprocess'); const logger = new AppConsole('runtime:restart'); - logger.info('Starting restart procedure for app subprocess...', this.livenessManager.getRuntimeData()); + logger.info({ msg: 'Starting restart procedure for app subprocess...', runtimeData: this.livenessManager.getRuntimeData() }); this.state = 'restarting'; @@ -297,13 +297,13 @@ export class DenoRuntimeSubprocessController extends EventEmitter implements IRu const hasKilled = await this.killProcess(); if (hasKilled) { - logger.debug('Process successfully terminated', { pid }); + logger.debug({ msg: 'Process successfully terminated', pid }); } else { - logger.warn('Could not terminate process. Maybe it was already dead?', { pid }); + logger.warn({ msg: 'Could not terminate process. Maybe it was already dead?', pid }); } await this.setupApp(); - logger.info('New subprocess successfully spawned', { pid: this.deno.pid }); + logger.info({ msg: 'New subprocess successfully spawned', pid: this.deno.pid }); // setupApp() changes the state to 'ready' - we'll need to workaround that for now this.state = 'restarting'; @@ -319,7 +319,7 @@ export class DenoRuntimeSubprocessController extends EventEmitter implements IRu logger.info('Successfully restarted app subprocess'); } catch (e) { - logger.error("Failed to restart app's subprocess", { error: e.message || e }); + logger.error({ msg: "Failed to restart app's subprocess", err: e }); throw e; } finally { await this.logStorage.storeEntries(AppConsole.toStorageEntry(this.getAppId(), logger)); From 647dca1411f8b21ad6ff88a417caf2379c1bf214 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Wed, 21 Jan 2026 13:02:31 -0600 Subject: [PATCH 007/131] chore: Remove pdf and tests from transcript build (#38285) --- ee/packages/omnichannel-services/package.json | 6 +++--- .../omnichannel-services/tsconfig.build.json | 7 +++++++ ee/packages/pdf-worker/test.pdf | Bin 23767 -> 0 bytes 3 files changed, 10 insertions(+), 3 deletions(-) create mode 100644 ee/packages/omnichannel-services/tsconfig.build.json delete mode 100644 ee/packages/pdf-worker/test.pdf diff --git a/ee/packages/omnichannel-services/package.json b/ee/packages/omnichannel-services/package.json index c5f6fe2d87fa9..b5da676a41ed7 100644 --- a/ee/packages/omnichannel-services/package.json +++ b/ee/packages/omnichannel-services/package.json @@ -8,13 +8,13 @@ "/dist" ], "scripts": { - "build": "rm -rf dist && tsc -p tsconfig.json", - "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput", + "build": "rm -rf dist && tsc -p tsconfig.build.json", + "dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput", "lint": "eslint --ext .js,.jsx,.ts,.tsx .", "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", "test": "jest", "testunit": "jest", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit --skipLibCheck" }, "dependencies": { "@rocket.chat/core-services": "workspace:^", diff --git a/ee/packages/omnichannel-services/tsconfig.build.json b/ee/packages/omnichannel-services/tsconfig.build.json new file mode 100644 index 0000000000000..ca6860f736abc --- /dev/null +++ b/ee/packages/omnichannel-services/tsconfig.build.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": true, + }, + "exclude": ["./dist", "./src/**/*.spec.ts", "./src/**/*.fixtures.ts"], +} diff --git a/ee/packages/pdf-worker/test.pdf b/ee/packages/pdf-worker/test.pdf deleted file mode 100644 index a4cb0d90fce46a60fba9142fc7772e10f9d19300..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23767 zcmdSB1z1&E*C@Q{?v&i5glu5bBHfLIlt_a}H%OOsmjVLPDGkyMf^?^}C<20{EG3S_LjvjNZv6MkoQksnu!iCOo18nF5U=FaI@iX)X575~) zJnYTD>{9N|GV0Do&SvQBCPrXRbYWq1Gh0&_3@6v$fVEYOpP89BqqD2K7(3q%m$Nal z0EUWNIy=Jg?c8==y zMkc`Il4h=!CT40f;^^$sme$T@jzE)yhNPK^ohg7+(ahEYz`)1BbxW3$v!j`j4f@UO z%?b!tURq8X1P2EK!2y4un+1>r2oV7RwgVdy5+V``1_}x?G72_2Iw}S(HXa@>HZBf6 zAsI0~0Vx3vE-^I;DLFYMB_$p)n3kG?mW+au0)_+*2?+@W83hXk1&ab77oXyPdAsQZ z;h_RFH6y^`f#C7r5b)q`xOvew;$>gsQ8@)boe|tRk(AJC#{LQ=hdnp5K>%!!@(g5=I=m+EI!jv zcs~%e7@zlp8<5W)ARY=wb1$#{F7VOOJawE1aqHW7yf{4AUMguSDGB_5qp2h?t8kU$ zu{{WcW#EmwyTAib!M`zh_9Nki*dAJAP_VJ=BU-|spwM6=8X8*QkA@cD zfLiCrb1DP#C3~zn54a1`3Ab<9OLNX35bAmY*ZL>rJ1BYi?=Q2j`H#8ZRfI-{Mj6Xa zLusMF4jh0V2M7u7I5XwVh)=F$F0#XC{Oc3$mW!TxSN*@J5C#3V30FHF0j%YJ`$*YT z^%(@h>!bYCH_-6i?>89L;-h9ozZMxPNsl31m)4Ot^}F-WK_EnjK94HL46mFLx|6sy zS9i0Pz|BVH2_^% zLLw;NeA$){Z$Pic_KR^!4J5X$TL49(u18m!ZYute8Kq`l>=S&RUSv0B$GnbQK?h%< zh=5#yNr2eSfr)_F0XwvpBA@~!fRINr4obiH{T+)a(E0mDt=$?jIrlsZCM0@`zCjA; z@4c{V;auDE1$K>FQ8%CcNO>W4-es7XsgHL7HSQemyxne3zpepPuu)lmjrvzJ;SK_} zFe6$TSy|a+e0+Rb;13@k42*%hzzM9^sZNggVu7%Y{H?6&SQG@Ji&h+odBtx^HJOHR zu$9jKjv-f)UXm3J4_&+>4mUP74)^v53km#PE?f;3B&{&0nB-f;5Pk>e@1}+^uP0;wRgTi{geNnoHTJ^$-U*< z=*i2=!$2oazTFbpVB0UG-l9v#bx#Um*3|EDaLA4sxG-_=^3A7+Jxe?^H2h9+$yk8! zd-U}5lCWJ83=o1~Rl+}IHi|NdwXA3MJK;hNTsZw1Pg_3&d;)u^92&5}aEJrDq&SQT zzNV_}s?)x$9h^e)*18W<; zI{}>k#{FNzP=GMbi3@^5K!8Jm<)L4p9u6J>5eb4G8~V?(qKkZU+YW zv#;{kxW7Y2|B3VOegCuhr0u*zZb1Bu69;A4w{=gm0xiaQ-Y z=f_oh%#N;ZL@wjlcB>#e;Mq(#<45vy@Lb%@s6S(EtiT?98&w7+j z&Ai;)4#9Id1@E|Gjd)tmasR5ptlKiU>sUF4D?cq-W@_Uu4tT zEBAc40cE2m`rm+xT3&e(emvdm_O@v|EE3`cnvl>{PO=E?SGz*6DfoY#9(ILbT+3Zk zdoZ`oPnbU)Dwuo944HEK+Pc7LrRg1pzEogL{8hhkOVepynZdj`C;x3PZ-VK-cdKg= zQB&5=RQQB;4g*SK}eJBAbF2x+`0W>l!5L5OV`0zEMp%`>vOV|C@gY^zQKwa+WQ40+LA! zzIt-pUmnD+uR1gpG_Krr?J23>_4RzoiE%#XfL&jWP`1+!fC!hUyUC6}Gxte$H9O!N zKkU46wP@@~Jr38iyTdN$j#lp-FmBzuEc3ithCDB>vZ|v|SpX^jmEFja1u}ILHok~! zI)~35zx&n4N_-`MrngK(lT}7X zMQwq2(&1jl1X6GG<9aHFfgQvKrO=eSRrniqYLCN*6=HSK?54BzmMW4DZ5Wys`fY0v z>se^^g72`oFth0kuqo14cQ?B$u%bO4y8&fvu7lsU-jmbRkJVb-J(|?a_(g|Z-qs6W zJ-dD7$yUZQRj8%?4QM9YZ%p)Q7Pl%H6BZ$IID!=1|H%g8TmDzx?z4t<;3>d3+R@Cw>18~`r@TxJHd(xxZ*=EILX8xB& zInR3@xTsb=eGEhh834r}h-5Gp&1bmGdB{>O*J%crbMl*?FI*DT@`bgW0(h__Coeq= z)Jm!8HlMd_Xgalr&&IE7Gjx{DXr^xG=&3Q7$(0tVYY7ymPYyCD_GX)8G7raodhbN# zMB!6FU8KxrL%WJQTyp+EAJeHZB3tQZ)`SK8_0PPC{L#ZtpJsEPuh<=py?pT*vyhO}8N!FVx4<}Cp zTsH?8Qmn23?!*J_$E-xy)RpyTcc0l2@wNVNpw?4Y6Eq{xwL!wOR@BUYCY;S!9DU0> z`fZ>@X98qF(%{j@x}!i)TyijQq1l*GS3SMF0??^(8)dD(*I8*2%-Wz!*Y%SVzy3mT zAmE;p8V+Fj*MMu^@`2dExkB=cxnz$>OTK}n#M|Z?Z9c6VhDGQp6yx<VVM1yuRP37X7DZxThuIh`Y4;>Y<8i)o@k-E>1+x3xEDYPK$FY2;48Wh#0~4pj`GRk_Yu>@Fn2&R6W*(E4=bsEM7UvYc zNk~#t*Rkb^qp$FE-1dC_*&F_oH}Xcv=$_1&Evh$J^SPA6QSq2fQpiN{=%^+cS%Q?R zB_sC)MVPYs=oR+0ul)7EtvG(FZ3uyRm-}I_4Lb4tA;J;_e7SYw+NiK7lV9nFX|*$& zg8e%no6d*}a^G;@6kvfN30q*m!y%(1qroF1z*a55vmvm+z{8J=PZ*m(#wFn36f@1~ z8YiStRm-k~iau0#NPM%pMyp9j&!u5hC1L!>ngdM)w&s|r9JssdF%G-M^>Igamux717{lG@>C1^K&QO%om=>b*Hl;Y zBdfi)us#?2Z$zm-N`4!+_UE|UzKh@bK1==UxLW!%zE450sh>*x0k+KJU`LdC zQ4#@=H3WbSdBdjm{B%1t_bpks6#h2TPq2SuMCkBO>c_mNB$_qXbc{eBlvouQBMvNo zM9K&-hdyESNUwj()XtFU&u&*u^(oa-hGo=uGBlm4>nz%-&Rw{BC23yXa-N#}io)+x zM!-92*GUMfel11-iH-P*_k!v>YPU%U%2F+c|Ec$FLyi;Lg$&=q11f&R!-4iwfsGYT z(iQmn&R6~L`+e2;GNlx#gN={v(O*@+M>&@LF2|rMUTCbr$->fg{X#n*}jY&XxUh0_hQTx|4^BWL4iuw!Vk1^Zy z&2Ry$SeVWvV0{JNC$y@Z$2>Vq%z6aZq=$6x`1r?X3=|vL+@~3JX4bIgx`h zen4PU&epjBfzq-09lSsPpt7rTf8hA?Zm@q4aXh787q0kQvicp#_wDV1M67c01bseLlLY&I--unKiieLH*B_m z?5^=ka~7kQMZV&>cfy}PcYLr(8EZelGA6iZ*akl$5A}W!={ru-CZ##{eSbe=)yYpd z)#FPL(L>}LP^5{G6BSfQ2A)&31Bs!r^-%S!AZ)+&kw!9VCWYj4|NgruxT2aG1ksIxhUpn;5I<}6rCUzPpLX>oBfBvjp)UN8SQ<>x}U@1s{8grR%CS4 z#zrh!zZ2B14ii+Fb8ytuW2Ps3mPJ&( zP+orcQ8`QcmHZYTvsNj6b@gW%4odWz+INgS5Cbqo5pl=;ofaiTDa{_+axE#qrK`*L ziqCjQAq5qcQ@^r{Q*fK9f1lU*bzwtIpJ(?wGvxWn4~-NyPp@Ky=yx6(N$0DZDsQJ_ zSiEg(Jk-DP|bXleL!Cr#E;_VxfIl{Q={Hft zOKVcl&cE-fHQmi-?RpdE%G6-b_y+XugosE;MZ&yMpFFl-?grFDuCg#%^o*_kUTJEC zS5!~r4QMXrR7GUL3tMmqi_M$zp2igS6jvQz?aot6etb?8Ofjtstu4Ak)*zze-m+RQ zGK;V~7-Tb%7$G_kQ4$F{Q_yj&gVr`O)EwR@<4xQLysuWN%HUw{+G|^(;vd$Ri=LX> z3qSI0w9?!@>D21)0k& z;Cf-j)6lnhUqeii@PBU?7@k=kDFpP-s<;#*P`0`!E*iUxA*l_vt?$lM6i5G^VGMoc<%<}qQ395owIT^ zc>_Y(%RlpVVeia4)5y&B{hS^%<7pAtcY5!KgZG7x@{Yjw?}iNm z*OUF)$(HLW>P<#YMo^+wyW*{sDJ3yc*W5RA6Z!edK{#d3ob3f6-3Jkm@1vgJpgybS zGj3GULR8r#?eq6@MKsx_xB*%3yKlxncb$UUYfQqAMbpgfa#P#F7 znUxC@Alf@5mhV470jNAD#{iA3ZNGkJ?)aG=kCf%jx|;hzYM`rP_E}3B~9}!TkqL8syDkuoeFI4grBq7-@VTtI4V#Q}s7kF9T?luu=xh{nltl0Lb@ADZKo{?9o0O!nA+aA77 zUS=P>N`?L4yPX6#AkoZo#0{U8g$LBut|KVX@s`@yNJExg_v=Tp-^AZtjmEJ=jAmip z2t9hhv7(81&Jy5FBq1E0ohJ2g=#`qn?cSeVls8zkF<7+xO7wDoc-QpVORN|NgP!&8XVMq5YdB-VeT>TKzN})i zjo2fl4Y^O%WCICCe;W=%g;zAzR1m1WFD&UEcwXRaTQ%#@L9oGf80}wo!XL%{R45-g z6*IL(($=H_4nMCL z{%HHHm*FQc00&(C26uZ49OQ31{sEu7HZ84I@5Vrla!jOn6w8d`v)ReQq>kMoJ#w~f zp@tIIWLkqsPp3x<6G-ga3#oOyushz~t{~ZkS9RQVpKL39 zZs!ix;{Y0X1b84&ehv<(0GOYf6T&aR!NJL+4=W1VIs=28z`Q?4shK(1xj34bIf40p zHfTDTIcnNk0%(7$P{R8DsAj4<+L@@EIfM1sfr=-XUBk@X8O(lLofQ9dmiTp+1Lpg) z>IsHbLS>vd!LZ7x8VonPG(gL3vosXU2_qWdLYfQA`HLZGZZIeGPvBn_(OY;qPVjBb z^cKx;byOHne~|gJ==y&^P@LR9Df>%OoIF2U{!vu7oqrJV|BbAElgJ;Y!vs`xO`O^6 zP0gAAtUCWuqW*_IC`=nLCYU7eLph+_9DJO-oC4eeJiNMpjsG{JFm*8j%F3Lu838>? z024af0rx-E{qx?^&Q=mAfP-(*-zx8yg+pO_{zGU#H3u^eNi!!CM@xGE&rf4fHnIU~ z*=kaX(yC&ta)2Q_vWeSSn*yEIMix$Bp#BXTC4TD!Y=41l%E&*PC2p&2%*)C3hs1gK`5;_8d;&Z?U><%>2rnPd#|s1{J_rB{ zfO7HkL%0RFxd3y40*yTUJb-e*d^}JHCl?1VKNQRh>S8;e_Gk24H~z$H4^v|BejG1+?-DKsk88e<|m8SRP(Z2;a{s1UNY%FsuMx zZVo`-x1@u40o8Lsx%dGK0u&43;N;`r29R;_KsbKV3y=*A;{!N{3P1$-IAJ!$%`X7q z;=VO6E)FOJz%0N8umkW5<%C%vn1>gD0xt<7-h>;D#QZ)4%#z2P@IfEfT_ zOaD^?kg^3n?tm$m{SW1`$^K!+rbd7f0+!SrmFDEJpr3@u{MIa@Bfn?`ZLh~CWzl--d}O*H_iN49{SHx`M>9+ zzta0JCj}M)U~XP+2sb}J5D>v!fWPu^aRXrw%*(+8;RJjW=AGP72+U1kmw?N`LL`R( zV6I#cz=^nTJsDV2z(O1lMY(`*2iQ9R01Wuk>jeP3xA7DV1-u^cb3RU3^aSuhA%MZd zVlgilH{{mwpIV4nA9j0GaeAAuXp z1-RxdC7fU&i2U*=U>aTsj1_>B!SyJ4w;BtQ@Tp+3la02TgE+D4^ zDVqy$RlwhYoX&OYXkZ>7UGnn+nGTlufb0$BzghcVjlVwglrRF8k#-j7?65}< zr#~LfU@KQA@K2QNk1WmH%p6r6&CJb!2RPWX*T3hPzuV3&HrV5v3$QK&)_w|>ra24s`ai;mq@|O+wULLUor#8}Gq4hN zc62fOZEbS9HuwYf*LR~_9KcfekIzbhMDJ|&>%&rDhF?#z?&)S~A8Z3TaGSSSPhL`G zBjRTz;i@G);Xisao>5HE{)AScN4r;G|CEP{)l!q1)Rmjd%9UoG>T7{6tIPtQVE7Bl zV|j%S6YiA(MeEh$tl9JoML|u|a6O}BH?5M~ha2vNE+T3=ki{kv#o^%^pG1>BH;mI~ z40?GZHP;V#{VgHAUWe`TW5Y9=Qt=nKKNeF{Tn*=uFY=S0;n_u&1Bv z`@hrvT0nh5{h~Ee&$9KJ-kjThO|Iq~&JUDB_V;%H&_AB!U}5zSxkCm16S*td1#)1v zoNArTQWn1z?P8?KEo!o#4e51Fb(IY(qK(20K5B0>cpovZ^Z{;t>(u%#@7T%c172y* z`PRltt2y=ZD8~m;7BifbKXQk{pQgl(zivAla}BIn^_3eNPS|?Gc_ph^YiQ@s;lVd? zE*2%zs#8HyFl*_QecbmZO4hUPBn^c(!#2=hCih(ob7ro0GQtG!0ACb3#qO4p>h-7Q zkz{6t_mEm1mWT|rKdV4 z7~dd!3P7^>=8WWrmm_}oc|@uEq(#vH2kwaK{cMS6F% zEVQlIT+7VLa1uj83;k*Hh*(Q%@`!kzNftjP%8_Qp!7G-k!Nar6W2@%iaT&*6FcOSR zD=Ly|Fm#-~S@g@cu^nzWUED}N6luM=xJgK9qM__pg*P6p89XAge1F3nM4X1k{-V!i zl(uT7AUd)wq|S6V9KPB&H0KQl-&TH1q(Pu^9m~b*$khW8JoLz+$7xzC)U0meS}`0) z_fAcGs!!`T{kitC(M<9T^xYteS*O$nD(9_j3worwq;^?4e$a{Dij`WzO+Owb@((a1<2x6$V#oa(NZ@a{DZS|943M|IDSOYif9f7mn zHhhv9>--Y_kVUT|zu4q$o7_~40H1h&&JIzwvTjP(rld-tspLJ{(;lJzEnZF!>o4HW z#TPkgoVG@S&1ffeJy9UW53vyjw1s1)gc!UpO*vi~KQUfwhc|?;@Vob&p_f!BZmjT# z@rs`@UfzTAgb>wW@_2c3PEF@d0!UhMkFPoF? zM`h$#3c~sh5Myr(or6lVkr)j0jQjewwK^n>NmS`YBds38C7(lwm9}-YUaw(3mnV47 zs#ro|w@Cj54L>Q+TDQ$Nip<2ilv9@+v9pl=3*zG1-ow7eVpdxl2fjJSVbcntkKAw` zygQ4EFLawHv}L-Elax@8YP!g>AHd<_q6#BRR3?aOHd4Hs{xY1Fka`~_)ejj?`Gfh8 zt~B|e0+nMm@vA$ZEDe#Oh6xpI6r(6eXmyqhjSriJTJ}g%dL^Y%lri+VS$97u!9TO= z`qa@~qoEI4Ai`}fJVEH6UV?}BhG!l=lfwg5<2iFuyySgP*ri(AUMHr@TN(Jcq^M&; z@c#Z$Dw$*1y6VM1(r!qua5GWC)pp07vNF@QEiJXNdCjoRi_>*ZUh;-|UlBakqG8$~ zY4|J~%q0B+^)H3G$ zr8~}i4czOag)vMR0bP8|o2KKD2Jtg}RV5 zt9-zcb#d^Ryyr|c1{dcZacIVOt6a771F6?kca-e1NWDwUhC~v~FXSFnH<6D#u)I4Y z;uazBdO8z%=(W++8FEI<)+#vy!p0pnmF@RjSCGdMZ0?xDb~Nk>6pk^8yy`zv{jAm< z3Ud<_4G~`n^YSu5qQkU`KMi7)`5F%x^fKE&b%h*ULj_ltUtWzWTKK0|DaJZMF^|-B zuIQr&y6@!pdql3c=dFgeimn)!JxXSNHbeDv;3@rUi{X_mGOkqY(y4HxMtvpMp?>Ej(i;N&?7VKt zLkZJ#LRrxM`~Jj?FXt50^Lajb?o%fiQVc@NJ}ohe0v96V*NSdubNYR6Umm_t-I@}T zND_LvwZoBcEtk>D14rP-E7nKJ|R@bF^W=Oq0*GCu4-OzpKa zmk$T*$9*>uzpxijO#34-K5-{OkGk6-oWrk2srq%IK*75p~;qnoS0>{2+yWj3;thyh4dkkrL}s)+p`@RahXC zp-I5y(Hkth!A2fWvKe3H*7CBh3CPsi-IJlir0Q_;jV|FenlCp?+Z|zAtOp4qx?N}4 zM|2HbRK~8<)7;GZ5b7Ui6{aXj(UXp2 zHoRW=q_(_pK`VC;iAsld)=zyB!FIlIkD>hy!*we2tmN@@i9F&4E1?-4B;|9w6d0M+ zJ|CA(J2zFSn1kao;q~V7vs5JQ!TW>ZjEVyVtASL9 z$%FNc{4U)T=#wF{nw^R#3Mws+%q`^(1vb#UAEv3L$@RPzc3hF){>1zdBK#)x=$lYz z-#iQFO+RE!;<{11v-;#9l0saGOYNPIu6~ff`=Rwd2Ms5kq49%fe5eU(DfKxFbY(qi zu53J4x@Sb`AKg$2cnWO8jn5@FGO7e>t=O>(g`?*5Gh;83cpXo(mdD>AotR%KqSwe( zlQw5L{=hzj^kXnGyJE#T;FrJD0LNY92`H&*ip6`&a@Vv@R#l5MxjE~qpAC%E@qBz3 zW8jlt8h5$>ivIZ3=DLLEaJ+IK(<|?g<=G642WGng&rzZ?s~kMol)t|_`2HbFd|6cM zYdf;*$K+P^zOXzmR85N3CM>zZEq}~p%uDf-Wf3~UO{P6VRoWosL_!f0OQqm1NMSlT zU5BA52Zr75p)*=3?Y`$X&|C4-Tgg7@^nC_@`7(t+hHTN8a<{mG%3$DW z66H?y%BzRN+kxt8%hVK7C1Qob7BN{t@as6FQ-~rj9*&jyP~;fJ7!u-VuRRQH)Hy;4 zGDehn(kdED%I7y`v~#nDZ~a06Eh!bneGEPY(sla@yPqO%+pdamK3D)j*BhJ)`;XNbW2YB+5(*?jSOSw_DA++RzrdFV7@=h%D`! z-luF4l1BX~JY1p_A=%jH*#gDmeH}5PbqlIj9V-0RmGx5hto2WO z8_}u*&B^#mvr&)=R~o~u61oU?#Z`UwMCVCy@PBYPr@G`Ic0@P2<87i@zGv|%sS`h5`hmnk$z(qwakd>UtS~s@cQX=S#ffKQg!MjC@S`z|Te!1-r_ung zj~YFa47))&><&BnhexD8N{qifv;y^z|Kt%V(NlF&ld$jGmC)W>zX}{z5=8%CB=cOc zSu@|4>%a?a0*)M2%{X$GK4?fVPv zp-kA^(>vvEMXt-@_D!3kR0#-v`C+EXn3Ob@n@!U5ZG5`k}PsJ54c#nJt^j^xRzgXrk+;x$DjDumL^RxF{jEKW{1) z#wKY}c>vdEf|u?e(^fmhog*LTk6Iq5>(W?eEwdav=dr zMZo-5wV+9-;u^<5FzL~=VDu;Jw4LH;9lXzpWLSa^{Bv-=ex90;k#GDyi{M&KC?LDp zp}S2{$8O%Fg4r@3mBq<3?9dv17C;LJ4Uv{4T^Ch=H{ouqlItWO4HORbKkDVMV^m{D zmCoT~JwRT1W`5d%;)$;Ogn7yH+2$KJ-NxFZjBD15)}3!78l&V-Fj$3^3U@xlnQ2<1 zk5~`zQ|Uf0%9}PGA4w@PMwDCfKTjbbZ&J@0@U56_U{x4mPRKtgEjNRcr~b$V5h=5- z*48g}3fN6~`6h*Bv}BwtNw=X~*Jc#sfJ~eoalj0aln=Q~PhQ_PBDGP+d~O+?CZdwE z@uBr|bMp!m!QD5VgWJ^?qZC5Za@=c*q#L?Z6oOF?I+2ta>W#~+$ENiCStrAHKF8zC z;G33MuW3II$!yCnYi}JGx}n{FK45Tsa~YV_o7uELH3@$+ogGaPCn8q8koJ}7O09d@ z1$*B_<6H9(iFlnl71v?WDVV6SB~3E@e9!KN~mPlC#h! zow3loIm9E!c(Y|vPPr^pWk$F?i{|2m;1U$dm1Pp?fPMh3mHx`e)%Q4b2EVh?lJCn8 z^%nkv0;*Denj8}!YQ7d84GsPpIunBo{E+_WvAzxxht(Fe=2h{bD?0M!a!;>CV=L`e zo~xxWty z6;GmyRUlS0hcl*m0a34~b2r;<^+aVyr zl#@!H`=STiWuH0x#-*R9@9ZFn#{}{U`V}J=NzJ;8b2`d=b(gGHgzKAT!cYSKp$t^9 zgS|pTVPRcq@=3E~`4H$LMj<89)^(3uh8s?PEz(~yDcWmo?^O({R5-P+92I3U0)wyT zN0JcLa{1b1GX~{ojH5+N+2`uxhEdraq1kaa_pEBPe7Vq`BWG$oZ&tE*%X}kmf1k0i zdQ)RdXwVv~bU(Hy-g{H+tyXX2_Xyjih&j(^4}`q8`CN;!Y;V>}KlDF9*zo~vGlppV zU6!|>N&LGkxw(LM;Q!q#XO~A98VNGQ8G<*NB6zs6eumy)PQRtPDpY#svNbCrv4=Ud z!VeMPkTf=odQ21&zdvONRV`r^FPqiU#jVtx&)5wp%hqe!i<7TsJ=V6dn#-@V)kKW% z|0p;5Y+*=%>s9}aMXTqx(Z-Q=#9%PmB<`?>sCk0d#I65+4#k)`0_t4 zxUIgd@)=5|I3=FsN7~v*-P=9p@8_z*m_RvAKs}hPbM?L^KK*zoM1s6>FVr9z*?0en zB{QtPA5~>$V(-R>Bf!`96?a?7X}I^odqwsK0kThXzH&VHiq(fNF8#{0e6f)SI^i-L zm^sGZ*-uuLl76?IhD^mZ4(?UBvesRo5b^E zq%<<7d~k4{lDbW>5J9ny+;ID|VJ)9^X&nMSI(uUBli9|kkzi8k=*=kY4jek9XFg(~ z;@zZCL8B!$r3YtC6aTr5VN3Z=|j%1p4zch z$YA)oxR>f{sxrvMUA@5mrY1BA*OEb6m6ocT{MA83=Tl1onpB!ZVYo`eKq{H!{y5}E z#pVP?^ku=P9dheCt|5hBH?*=q2UnpxHQx8;sZL^dpJ|GMGI&X8e|Y1V#?zyJ5vi!> z&^~;-hw!5a>q-Ru(~smNtCM>Hxd=KAX(UCf!C9xXDPp^^=Z844KITJmu3y&SRZ9!G z?=4k(flq7AVumw0nyacoI^ixpbVmbDqXnS)&P#52zDJSNYw|`$oSh7w+ej5J6uloj z&Hi?@av6M4o+d_Hij^;_d^2B=k9|rwD?>$x$h>*wH*z+s1#?HtTc72dzmHK(e~H+YN$wyVZ_TqcT@OB$o|$rX8LDSs4jJA`rc!o) z?){`8)du_7i}pu{r{B!0tjkAjl)@8>k*>Lj*{6qx!`R4@4*G?i)bl@1T*SRU7%#5P z7w90$v`s(TDcVA@)laYW!4DeZB`14<%oH;4fDV%&AtlFqq7r#gEXKoyh79ZJV|eMA zBI<}N>M{H0@lyQht1IRC!YV+oz^GDv_+!Fq(q_n4w zPTvb9i*SYc8LTR@FxTtUDZdaMZ#kml2|X0M2O%Gx9j>jpE3*t-m|N;2IZ>?V>OPTH zE7F^(dNO%T*QQpte*w)r*K3IwBeLv$Ef_U1qjIOTyWxER%7`#lD(SuA#8wSNCDTxO z5+#WmvNwwdq`SzJUhapCX**9rZM?B5i51tp=IXHviX-znIg%QA6JYk-is=5z?gL{t=W@l=lWc8tMtnO35y ziW^if=ojCx}>h_xz)d{@jtr{jFCxT;E%pMG2=cS1@b zg^-9%N4FdAU8)*h$3)Nlrvk{5%$r3!NSM09k8esLMTKXJ zbepygw~g3lRbsfnpfkL=i;Ki!*ygfJRYWJcdpz&Gl6gz*M-+So6A^vRT|@CG33J^hW6g09EI~}7xA#j3e<#7d%->8A*SePu%wt>g%0-helP#nw1cz5 zB-q(IOM?(;xC3_u=Nig0J6m+$_M67mOx3fasO7JZdE#e=NTW)kw_Q5x_fHmu)D1E{@d3G;F zQkGA)c2L$8O)gjYiPF7sOsFQ6qV6?HVD{^`e_zSneo6grD>(rU&i{TPx2rW|yTF0j z`jz@9odF3F3enqEVaZ}R!`01DGwapuFj)!`>I)Kv*@R`AZLiCy z!9xyCvG<8FLviyEKctbnhKR@p?zRxzvHr+GE>;N63!hyw)WRU3edni7ZbOBznuCcg z|7sPBRkN9dSLtDB_C7Qqy7?t3e3tRS>Z8#tDI2S<^O0J3oniVWb{@rSH1cMg`35n% zcd93^y*KN3K3ij4E+kX77+jr?mA0n8-Z%e{8w%myj7s8^rA%65`aY1#M05X3;FxGO zH)@?aa;uH)tMvAS!OI3?E7vD77tuNe)s1&IYO}ZUJ(Evgn4(Uaf4}!lNx)$I&MD3@ zc59k78E}U7PD}}zzmSk66c%g&XC{3d6aFz+{1nv8^<*A5{;4Y1jm5{|{LYu-H2sPX ztj+^GjYZCcK3F#w#%8#FM9jJQiUI!AY_CRNTpn1LG0sgw*3|O1^o`UQB2_*G)AWo| z4E-qN=NC4p{$LW;c_&8PlBjHRQp^3zomMGIu622|^8JE@_O;FZ?>clF$iwU;4eJ#S zJzJTxAp-NS*us>x%LBRC9ua-dbW_DaAWq~Ejx9*65X23%FGjC=*m%TkwAg2h(#D_@ z#t~4SiD^0{b__oCGk13D@COGVj_y&}=Ywd70v9@@Fq%+a`VI>#F6NsWz+rN}rF&(m zW6IGFZ+??2v7W&{GZq#4uH9-5T8mtq*y*1%EfU}u$|TFivdQrv-IwDN(G)Td3l^$y zKO?RC0GRr$zMH?-dIR2&A})*OCD+MJNY|r@G_XX(%!enR(aT+2h?Jj}=ufgc6XiGq z|AzZH$!>oGmo1V2k@)uM54Tt38(Ux1D&|k+MJ^h1r5M6w5XaSP-Q6XbLzcNME6aI; zM9sB{ZL%U&YcvZJ`%+DEoY)hcZVWaEpQJj3(jpc(G>OrvN{hYi%VG?}I})y~G*!Vn z#U5eXcIfb5&~ncc6H6jBM9H;uX6kfF9nX4FY14k(+%xFZtN508w5gX!3fb$S4dbze zMpAN~$>ILqg5M5^J-0Gn(Ba;;3o;_5{Gh?~D=l*McX~?Lq%w+m?+huU@dg*_cIN8L zyahg+&=FhOJ=MsQ@Vdt$Sno3-)lfCly+pnO8MG2}!Hju9|JY2kklKUeU81~2{sm5a zQ!Q$?or8IGn7c>nL-{QEb-zdnC}^78%%&$zpfhoU#?@NT_1S?BVu;b4cWF?|Rq zO&=T*jI*?DDJU8fI0WDSwLmsbBQ864@3^~3K6360u>!r+^fxVrVeyguE!K7%R-=k& zPSvZkaqo6&RK9`{*?9LK{m^gWeprh6m+2$=n zEN|`8R9eWIRXbj|p0sT_uWow13EHZq@i<}CjCeud_Fi^9 zr{{{eheufb#Sqn0kRL-`I->;hyTa7cHDr@$M_u1H_fOj_1SI8Egx+=NVit}QfC`D| z{ScZRBf-;+Nr_2dTkcP6mz?XOXXq$A=+wX)b;aVcK+o1`2-ZBFp3dUPH{L_NdBt$v zSg9PjSU8DnB_iMOLg)MdrTUZ{xLSltNP>KCpjBK&cXqE*3l!TRz@n z4s1b`?{3;BX&qG**i|ym{Znd|a`8{Inlzp*Q>E$Jgi3P(SI4!CH@vH0a*y7t?rt2oE)Y>rfjYdhl6^U0e{udwRS zeigXK%nrc6=jRKaa|_)3xf%h3>ul;jc11zv0wFlEZQQwMBjG(cXK3)HoI&nj#;TUuV8Enm(0ll3CM zij)4S{Qbt!A~oy|ZRNkM4;#NUCqvoveX5+?M=IIkl`0i8^~&7dll>A#?Y`9!O{y%V z>yJuqd8U!;^xot|i`+evSBlk6x$nTS)ogN9SgtqvAV(dNLhd55qfcjh^)hw=jY;WG z$-d>j6ZW1oPnBySK?Z}dL)x>~GjxB)#-*ZgI4!J$HQ!4opA+sQfp!Yi@*%*Kp6*#9 zkL*yY&&sxZJ=@U^d>na8@XTnmUAfPj!Plf1pJIP>gC3_@{M0vqA5hfE(sFdxo+vta zPF^l*mVXF4htjoWp%o8bXD{Ha2F%=OuTGYYL2(pICWKcOII}Zbe4Cc*vx||XbF0<# zTZFv{dl-bOQD=3@e$|}U%_f&(d_@Ws>EyaP7$R&*5ciG^t#Gab4-m^W0 zEA@5?xENyT-W#bi<{4O#_HaXEL+ameUdo# z9D0-YWm&^(m^;R9U0%?5pkj!=eb0c%`v(u*!&3t0Sl+xhD*A2ZwBVnwvhtiBv5qAa z0Hmi}ZTDJbCw$#MN20B2jC~&^2W*(&<}_!O!f|}_(3*3rpEHpSRxPs9A+W+o10Ssy zZJH~P(ewyCOR*du+ixR*q9)ts_s5YBF+(oz!2W_dx;NdiSE)RpP8Qj~YeI^P6vyjsui>e4HMH|QUV7K;A=0)nc&w`EPC9>F`ixX}rxR4~fE5*Kz z+lDVCTd1~Q!Shxm)_2lbrG!F;%iT14i#PlPbMrQi+`T!QtA!$?X?hm;V9r`*f1b^U zw_&HNYWW#bFGYY8l@Cd|BJKgyDTpk#nb$SCShG|NXiw?(FkP#I{up>QKx?yVFx5QY z`VKW-bYnxSpCESPkwIcpMz{lWx4mJwhJI>vuZtK`7}10adDC3h352unxo)J>hqnqiRe#yj{SXTM)H z>9t?Iw<{+fI&0s`pcUsP|Iwe)&50KT%vOy~dX>$SmRfaIZM=%&+$rhLd@ffEebXO! zvXE~>lHBVV=e*`}!0zd`qe-jX3xz&o`;YQtuJQby(!^(qrNZtc){8;2mWNzT9g96a z;~EPLIM05)HZgS=(bdkrqy}F1u->zMKgsJ2Q9T=PP!0}@)y^M0x8JU7)C}&eE7Pq< zUW?}r79|~*8e?Q+&C=+X6bQ3AhR-d@W*?TUe*Gy!MDJ1v1LJWuwCtnR%P`1f$zzHo zQSGs9Ql@@`hWhkZNLrA*{~Aif#VGH$#q=ruz)fmb*I1=rok!n|l7eq7nd@XOi|-^{)6}zGHwxvs@qX7?$q>(M~p&xF1EBmjR-W{g=3v|iOYhJ_*e3~ zsT|kdZZqvkhrM6dh<9N4(p0f;3Z<^Mqv>p7O8T0L==r#|?$mjuhqT4;iJHiIK%nV& z$oPGH6#r9Ys!AWz$+tA*OYGOa{FMWAcD2$RFz-w8;H&7r`x38 zhX>tE2dc0v8pjz%33{IAbe^3{xp+EapVJqt3N4u$zOSextb%ub)8t~IM`BK7O!Y1;1g91o`_ZRN=%PT zib|xVDasdWmjo%S^a<2D6vx-Ib}c{le*V~U4K0C`V3^5V&S`W7&qbe&b&%J&BjpeD z%Qa&72bU)<#YnxELLSrgcT8Md+q*O&D7-K@T-;e)e%w?b%DiEfBz$U(1pm61hC>Vt zq~U)Ci28t(0)zzlkA{GQybx zg?s@JLhAvtW5Qmc5&xqW0EG7jcrEvqz6e7QTm4H5rJVuMVA}Uv!=1^|ew+LB? Date: Thu, 22 Jan 2026 11:18:13 -0600 Subject: [PATCH 008/131] test: Fix ignored unit tests (#38288) --- apps/meteor/.mocharc.js | 1 + .../lib/contacts/mapVisitorToContact.spec.ts | 18 +++-- .../contacts/resolveContactConflicts.spec.ts | 74 ++----------------- .../server/lib/contacts/updateContact.spec.ts | 2 +- .../lib/contacts/validateCustomFields.spec.ts | 2 +- 5 files changed, 24 insertions(+), 73 deletions(-) diff --git a/apps/meteor/.mocharc.js b/apps/meteor/.mocharc.js index cf4b306318cfc..a2230bb7dd5ff 100644 --- a/apps/meteor/.mocharc.js +++ b/apps/meteor/.mocharc.js @@ -29,5 +29,6 @@ module.exports = { 'app/api/server/lib/**/*.spec.ts', 'app/file-upload/server/**/*.spec.ts', 'app/statistics/server/**/*.spec.ts', + 'app/livechat/server/lib/**/*.spec.ts', ], }; diff --git a/apps/meteor/app/livechat/server/lib/contacts/mapVisitorToContact.spec.ts b/apps/meteor/app/livechat/server/lib/contacts/mapVisitorToContact.spec.ts index c8c15fe0d0d9c..07a10f6f5236a 100644 --- a/apps/meteor/app/livechat/server/lib/contacts/mapVisitorToContact.spec.ts +++ b/apps/meteor/app/livechat/server/lib/contacts/mapVisitorToContact.spec.ts @@ -27,6 +27,7 @@ const dataMap: [Partial, IOmnichannelSource, CreateContactPara contactManager: { username: 'user1', }, + lastChat: { _id: 'afdsfdasf', ts: testDate }, }, { type: OmnichannelSourceType.WIDGET, @@ -50,8 +51,10 @@ const dataMap: [Partial, IOmnichannelSource, CreateContactPara details: { type: OmnichannelSourceType.WIDGET, }, + lastChat: { _id: 'afdsfdasf', ts: testDate }, }, ], + lastChat: { _id: 'afdsfdasf', ts: testDate }, customFields: undefined, shouldValidateCustomFields: false, contactManager: 'manager1', @@ -62,6 +65,7 @@ const dataMap: [Partial, IOmnichannelSource, CreateContactPara { _id: 'visitor1', username: 'Username', + lastChat: { _id: 'afdsfdasf', ts: testDate }, }, { type: OmnichannelSourceType.SMS, @@ -85,11 +89,13 @@ const dataMap: [Partial, IOmnichannelSource, CreateContactPara details: { type: OmnichannelSourceType.SMS, }, + lastChat: { _id: 'afdsfdasf', ts: testDate }, }, ], customFields: undefined, shouldValidateCustomFields: false, contactManager: undefined, + lastChat: { _id: 'afdsfdasf', ts: testDate }, }, ], @@ -113,7 +119,7 @@ const dataMap: [Partial, IOmnichannelSource, CreateContactPara unknown: false, channels: [ { - name: 'sms', + name: 'widget', visitor: { visitorId: 'visitor1', source: { @@ -150,6 +156,7 @@ const dataMap: [Partial, IOmnichannelSource, CreateContactPara invalidCustomFieldId: 'invalidCustomFieldValue', }, activity: [], + lastChat: { _id: 'afdsfdasf', ts: testDate }, }, { type: OmnichannelSourceType.WIDGET, @@ -161,7 +168,7 @@ const dataMap: [Partial, IOmnichannelSource, CreateContactPara unknown: true, channels: [ { - name: 'sms', + name: 'widget', visitor: { visitorId: 'visitor1', source: { @@ -173,6 +180,7 @@ const dataMap: [Partial, IOmnichannelSource, CreateContactPara details: { type: OmnichannelSourceType.WIDGET, }, + lastChat: { _id: 'afdsfdasf', ts: testDate }, }, ], customFields: { @@ -180,6 +188,7 @@ const dataMap: [Partial, IOmnichannelSource, CreateContactPara }, shouldValidateCustomFields: false, contactManager: undefined, + lastChat: { _id: 'afdsfdasf', ts: testDate }, }, ], ]; @@ -197,10 +206,9 @@ describe('mapVisitorToContact', () => { getAllowedCustomFields.resolves([{ _id: 'customFieldId', label: 'custom-field-label' }]); }); - const index = 0; - for (const [visitor, source, contact] of dataMap) { + dataMap.forEach(([visitor, source, contact], index) => { it(`should map an ILivechatVisitor + IOmnichannelSource to an ILivechatContact [${index}]`, async () => { expect(await mapVisitorToContact(visitor, source)).to.be.deep.equal(contact); }); - } + }); }); diff --git a/apps/meteor/app/livechat/server/lib/contacts/resolveContactConflicts.spec.ts b/apps/meteor/app/livechat/server/lib/contacts/resolveContactConflicts.spec.ts index 9694c8f7e932a..4b527016359bb 100644 --- a/apps/meteor/app/livechat/server/lib/contacts/resolveContactConflicts.spec.ts +++ b/apps/meteor/app/livechat/server/lib/contacts/resolveContactConflicts.spec.ts @@ -36,25 +36,14 @@ describe('resolveContactConflicts', () => { conflictingFields: [{ field: 'customFields.customField', value: 'oldValue' }], }); modelsMock.Settings.incrementValueById.resolves(1); - modelsMock.LivechatContacts.updateContact.resolves({ - _id: 'contactId', - customField: { customField: 'newValue' }, - conflictingFields: [{ field: 'customFields.customField', value: 'oldValue' }], - } as Partial); - const result = await resolveContactConflicts({ contactId: 'contactId', customField: { customField: 'newValue' } }); + await resolveContactConflicts({ contactId: 'contactId', customFields: { customField: 'newestValue' } }); expect(modelsMock.LivechatContacts.findOneEnabledById.getCall(0).args[0]).to.be.equal('contactId'); - expect(modelsMock.Settings.incrementValueById.getCall(0).args[0]).to.be.equal('Livechat_conflicting_fields_counter'); - expect(modelsMock.Settings.incrementValueById.getCall(0).args[1]).to.be.equal(1); - expect(modelsMock.LivechatContacts.updateContact.getCall(0).args[0]).to.be.equal('contactId'); - expect(modelsMock.LivechatContacts.updateContact.getCall(0).args[1]).to.be.deep.contain({ customFields: { customField: 'newValue' } }); - expect(result).to.be.deep.equal({ - _id: 'contactId', - customField: { customField: 'newValue' }, - conflictingFields: [], + expect(modelsMock.LivechatContacts.updateContact.getCall(0).args[1]).to.be.deep.contain({ + customFields: { customField: 'newestValue' }, }); }); @@ -66,28 +55,13 @@ describe('resolveContactConflicts', () => { conflictingFields: [{ field: 'name', value: 'Old Name' }], }); modelsMock.Settings.incrementValueById.resolves(1); - modelsMock.LivechatContacts.updateContact.resolves({ - _id: 'contactId', - name: 'New Name', - customField: { customField: 'newValue' }, - conflictingFields: [], - } as Partial); - const result = await resolveContactConflicts({ contactId: 'contactId', name: 'New Name' }); + await resolveContactConflicts({ contactId: 'contactId', name: 'New Name' }); expect(modelsMock.LivechatContacts.findOneEnabledById.getCall(0).args[0]).to.be.equal('contactId'); - expect(modelsMock.Settings.incrementValueById.getCall(0).args[0]).to.be.equal('Livechat_conflicting_fields_counter'); - expect(modelsMock.Settings.incrementValueById.getCall(0).args[1]).to.be.equal(1); - expect(modelsMock.LivechatContacts.updateContact.getCall(0).args[0]).to.be.equal('contactId'); expect(modelsMock.LivechatContacts.updateContact.getCall(0).args[1]).to.be.deep.contain({ name: 'New Name' }); - expect(result).to.be.deep.equal({ - _id: 'contactId', - name: 'New Name', - customField: { customField: 'newValue' }, - conflictingFields: [], - }); }); it('should update the contact with the resolved contact manager', async () => { @@ -96,31 +70,16 @@ describe('resolveContactConflicts', () => { name: 'Name', contactManager: 'contactManagerId', customFields: { customField: 'value' }, - conflictingFields: [{ field: 'manager', value: 'newContactManagerId' }], + conflictingFields: [{ field: 'manager', value: 'oldManagerId' }], }); - modelsMock.Settings.incrementValueById.resolves(1); - modelsMock.LivechatContacts.updateContact.resolves({ - _id: 'contactId', - name: 'Name', - contactManager: 'newContactManagerId', - customField: { customField: 'value' }, - conflictingFields: [], - } as Partial); - const result = await resolveContactConflicts({ contactId: 'contactId', name: 'New Name' }); + await resolveContactConflicts({ contactId: 'contactId', name: 'New Name', customFields: { manager: 'newContactManagerId' } }); expect(modelsMock.LivechatContacts.findOneEnabledById.getCall(0).args[0]).to.be.equal('contactId'); - expect(modelsMock.Settings.incrementValueById.getCall(0).args[0]).to.be.equal('Livechat_conflicting_fields_counter'); - expect(modelsMock.Settings.incrementValueById.getCall(0).args[1]).to.be.equal(1); - expect(modelsMock.LivechatContacts.updateContact.getCall(0).args[0]).to.be.equal('contactId'); - expect(modelsMock.LivechatContacts.updateContact.getCall(0).args[1]).to.be.deep.contain({ contactManager: 'newContactManagerId' }); - expect(result).to.be.deep.equal({ - _id: 'contactId', - name: 'New Name', - customField: { customField: 'newValue' }, - conflictingFields: [], + expect(modelsMock.LivechatContacts.updateContact.getCall(0).args[1]).to.be.deep.contain({ + customFields: { customField: 'value', manager: 'newContactManagerId' }, }); }); @@ -219,21 +178,4 @@ describe('resolveContactConflicts', () => { ); expect(modelsMock.LivechatContacts.updateContact.getCall(0)).to.be.null; }); - - it('should throw an error if the contact manager is invalid', async () => { - modelsMock.LivechatContacts.findOneEnabledById.resolves({ - _id: 'contactId', - name: 'Name', - contactManager: 'contactManagerId', - customFields: { customField: 'value' }, - conflictingFields: [{ field: 'manager', value: 'newContactManagerId' }], - }); - await expect(resolveContactConflicts({ contactId: 'id', contactManager: 'invalid' })).to.be.rejectedWith( - 'error-contact-manager-not-found', - ); - - expect(validateContactManagerMock.getCall(0).args[0]).to.be.equal('invalid'); - - expect(modelsMock.LivechatContacts.updateContact.getCall(0)).to.be.null; - }); }); diff --git a/apps/meteor/app/livechat/server/lib/contacts/updateContact.spec.ts b/apps/meteor/app/livechat/server/lib/contacts/updateContact.spec.ts index 348154e998353..fafc98fd355e1 100644 --- a/apps/meteor/app/livechat/server/lib/contacts/updateContact.spec.ts +++ b/apps/meteor/app/livechat/server/lib/contacts/updateContact.spec.ts @@ -14,7 +14,7 @@ const modelsMock = { const { updateContact } = proxyquire.noCallThru().load('./updateContact', { './getAllowedCustomFields': { - getAllowedCustomFields: sinon.stub(), + getAllowedCustomFields: sinon.stub().resolves([]), }, './validateContactManager': { validateContactManager: sinon.stub(), diff --git a/apps/meteor/app/livechat/server/lib/contacts/validateCustomFields.spec.ts b/apps/meteor/app/livechat/server/lib/contacts/validateCustomFields.spec.ts index 39684a62fd91c..4bb0d48ef42c0 100644 --- a/apps/meteor/app/livechat/server/lib/contacts/validateCustomFields.spec.ts +++ b/apps/meteor/app/livechat/server/lib/contacts/validateCustomFields.spec.ts @@ -55,7 +55,7 @@ describe('validateCustomFields', () => { const allowedCustomFields = [{ _id: 'field1', label: 'Field 1', required: false }]; const customFields = { field2: 'value' }; - expect(() => validateCustomFields(allowedCustomFields, customFields, { ignoreValidationErrors: true })) + expect(() => validateCustomFields(allowedCustomFields, customFields, { ignoreAdditionalFields: true })) .not.to.throw() .and.to.equal({}); }); From 150f6623e85e996a1405a9c1524ab572ee7061a1 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Thu, 22 Jan 2026 12:30:23 -0600 Subject: [PATCH 009/131] chore: Adapt logs to object format (final!) (#38289) --- apps/meteor/app/api/server/lib/eraseTeam.ts | 4 +- .../app/apps/server/bridges/scheduler.ts | 20 ++-- .../app/file-upload/server/lib/FileUpload.ts | 2 +- .../app/file-upload/server/lib/requests.ts | 4 +- .../server/PendingFileImporter.ts | 12 +- .../importer-slack/server/SlackImporter.ts | 20 ++-- .../integrations/server/lib/ScriptEngine.ts | 4 +- .../server/lib/isolated-vm/isolated-vm.ts | 2 +- .../integrations/server/lib/triggerHandler.ts | 6 +- .../lib/server/functions/saveUserIdentity.ts | 2 +- .../app/lib/server/functions/setUsername.ts | 4 +- apps/meteor/app/livechat/server/lib/rooms.ts | 4 +- .../livechat/server/lib/stream/agentStatus.ts | 3 +- .../app/livechat/server/sendMessageBySMS.ts | 4 +- .../server/functions/unsubscribe.ts | 7 +- .../meteor-accounts-saml/server/lib/SAML.ts | 19 ++-- .../server/lib/ServiceProvider.ts | 3 +- .../meteor-accounts-saml/server/lib/Utils.ts | 17 ++- .../server/lib/generators/LogoutRequest.ts | 3 +- .../server/lib/generators/LogoutResponse.ts | 3 +- .../server/lib/parsers/LogoutRequest.ts | 13 +-- .../server/lib/parsers/LogoutResponse.ts | 8 +- .../server/lib/parsers/Response.ts | 13 +-- .../server/lib/settings.ts | 2 +- .../server/loginHandler.ts | 14 +-- .../server/methods/samlLogout.ts | 5 +- .../app/metrics/server/lib/collectMetrics.ts | 2 +- .../app/nextcloud/server/addWebdavServer.ts | 4 +- .../server/NotificationQueue.ts | 6 +- apps/meteor/app/push/server/push.ts | 21 ++-- .../app/slackbridge/server/RocketAdapter.js | 7 +- .../app/slackbridge/server/SlackAdapter.js | 60 +++++----- .../app/slackbridge/server/slackbridge.js | 24 ++-- .../server/methods/uploadFileToWebdav.ts | 12 +- .../server/business-hour/Multiple.ts | 2 +- .../server/hooks/afterRemoveDepartment.ts | 2 +- .../ee/server/apps/communication/rest.ts | 92 +++++++++------- .../apps/marketplace/fetchMarketplaceApps.ts | 2 +- .../marketplace/fetchMarketplaceCategories.ts | 2 +- apps/meteor/ee/server/configuration/ldap.ts | 8 +- apps/meteor/ee/server/lib/ldap/Manager.ts | 32 +++--- apps/meteor/server/database/utils.ts | 2 +- .../server/features/EmailInbox/EmailInbox.ts | 4 +- apps/meteor/server/lib/ldap/Connection.ts | 78 +++++++------ apps/meteor/server/lib/ldap/Manager.ts | 28 ++--- .../server/lib/sendDirectMessageToUsers.ts | 4 +- .../meteor/server/lib/sendMessagesToAdmins.ts | 4 +- .../server/methods/sendForgotPasswordEmail.ts | 4 +- .../core-apps/cloudAnnouncements.module.ts | 4 +- .../notifications/notifications.module.ts | 4 +- .../modules/streamer/streamer.module.ts | 4 +- .../services/messages/lib/oembed/providers.ts | 4 +- .../services/nps/getAndCreateNpsSurvey.ts | 4 +- .../server/services/nps/sendNpsResults.ts | 4 +- .../providers/twilio.ts | 2 +- .../server/services/omnichannel/queue.ts | 2 +- .../src/internal/SignalProcessor.ts | 2 +- .../media-calls/src/server/CallDirector.ts | 2 +- .../src/OmnichannelTranscript.ts | 103 ++++++++++++++---- packages/omni-core/src/visitor/create.ts | 2 +- .../ui-client/src/lib/callbacks/Callbacks.ts | 2 +- 61 files changed, 403 insertions(+), 334 deletions(-) diff --git a/apps/meteor/app/api/server/lib/eraseTeam.ts b/apps/meteor/app/api/server/lib/eraseTeam.ts index 5fd47f782539a..076064ecbf6d3 100644 --- a/apps/meteor/app/api/server/lib/eraseTeam.ts +++ b/apps/meteor/app/api/server/lib/eraseTeam.ts @@ -83,8 +83,8 @@ export async function eraseRoomLooseValidation(rid: string): Promise { try { await deleteRoom(rid); - } catch (e) { - SystemLogger.error(e); + } catch (err) { + SystemLogger.error({ err }); return false; } diff --git a/apps/meteor/app/apps/server/bridges/scheduler.ts b/apps/meteor/app/apps/server/bridges/scheduler.ts index 6fdd3d69f9531..b08d49182c9bc 100644 --- a/apps/meteor/app/apps/server/bridges/scheduler.ts +++ b/apps/meteor/app/apps/server/bridges/scheduler.ts @@ -84,9 +84,7 @@ export class AppSchedulerBridge extends SchedulerBridge { ); break; default: - this.orch - .getRocketChatLogger() - .error(`Invalid startup setting type (${String((startupSetting as any).type)}) for the processor ${id}`); + this.orch.getRocketChatLogger().error({ msg: 'Unknown startup setting type', type: (startupSetting as any).type }); break; } }); @@ -105,8 +103,8 @@ export class AppSchedulerBridge extends SchedulerBridge { await this.startScheduler(); const job = await this.scheduler.schedule(when, id, this.decorateJobData(data, appId)); return job.attrs._id.toString(); - } catch (e) { - this.orch.getRocketChatLogger().error(e); + } catch (err) { + this.orch.getRocketChatLogger().error({ err }); } } @@ -140,8 +138,8 @@ export class AppSchedulerBridge extends SchedulerBridge { skipImmediate, }); return job.attrs._id.toString(); - } catch (e) { - this.orch.getRocketChatLogger().error(e); + } catch (err) { + this.orch.getRocketChatLogger().error({ err }); } } @@ -167,8 +165,8 @@ export class AppSchedulerBridge extends SchedulerBridge { try { await this.scheduler.cancel(cancelQuery); - } catch (e) { - this.orch.getRocketChatLogger().error(e); + } catch (err) { + this.orch.getRocketChatLogger().error({ err }); } } @@ -185,8 +183,8 @@ export class AppSchedulerBridge extends SchedulerBridge { const matcher = new RegExp(`_${appId}$`); try { await this.scheduler.cancel({ name: { $regex: matcher } }); - } catch (e) { - this.orch.getRocketChatLogger().error(e); + } catch (err) { + this.orch.getRocketChatLogger().error({ err }); } } diff --git a/apps/meteor/app/file-upload/server/lib/FileUpload.ts b/apps/meteor/app/file-upload/server/lib/FileUpload.ts index 8c1aa7b9d0e3e..7402c59f01bde 100644 --- a/apps/meteor/app/file-upload/server/lib/FileUpload.ts +++ b/apps/meteor/app/file-upload/server/lib/FileUpload.ts @@ -269,7 +269,7 @@ export const FileUpload = { try { await writeFile(tempFilePath, data); } catch (err: any) { - SystemLogger.error(err); + SystemLogger.error({ err }); } await this.getCollection().updateOne( diff --git a/apps/meteor/app/file-upload/server/lib/requests.ts b/apps/meteor/app/file-upload/server/lib/requests.ts index f88b2477777c8..b2ae48fbca362 100644 --- a/apps/meteor/app/file-upload/server/lib/requests.ts +++ b/apps/meteor/app/file-upload/server/lib/requests.ts @@ -44,8 +44,8 @@ WebApp.connectHandlers.use(FileUpload.getPath(), async (req, res, next) => { try { url = await store.getStore().getRedirectURL(file, false); expiryTimespan = await store.getStore().getUrlExpiryTimeSpan(); - } catch (e) { - SystemLogger.debug(e); + } catch (err) { + SystemLogger.debug({ err }); } return FileUpload.respondWithRedirectUrlInfo(url, file, req, res, expiryTimespan); } diff --git a/apps/meteor/app/importer-pending-files/server/PendingFileImporter.ts b/apps/meteor/app/importer-pending-files/server/PendingFileImporter.ts index 818031d07916e..56aa0293aedb0 100644 --- a/apps/meteor/app/importer-pending-files/server/PendingFileImporter.ts +++ b/apps/meteor/app/importer-pending-files/server/PendingFileImporter.ts @@ -129,9 +129,9 @@ export class PendingFileImporter extends Importer { // Update progress more often on large files this.reportProgress(); }); - res.on('error', async (error) => { + res.on('error', async (err) => { await completeFile(details); - logError(error); + logError({ err }); }); res.on('end', async () => { @@ -145,14 +145,14 @@ export class PendingFileImporter extends Importer { await Messages.setImportFileRocketChatAttachment(_importFile.id, url, attachment); await completeFile(details); importedRoomIds.add(message.rid); - } catch (error) { + } catch (err) { await completeFile(details); - logError(error); + logError({ err }); } }); }); - } catch (error) { - this.logger.error(error); + } catch (err) { + this.logger.error({ err }); } } diff --git a/apps/meteor/app/importer-slack/server/SlackImporter.ts b/apps/meteor/app/importer-slack/server/SlackImporter.ts index 87098c8b35ec2..30c710df09b7a 100644 --- a/apps/meteor/app/importer-slack/server/SlackImporter.ts +++ b/apps/meteor/app/importer-slack/server/SlackImporter.ts @@ -290,8 +290,8 @@ export class SlackImporter extends Importer { ImporterWebsocket.progressUpdated({ rate }); oldRate = rate; } - } catch (e) { - this.logger.error(e); + } catch (err) { + this.logger.error({ msg: 'Error updating progress', err }); } }; @@ -332,8 +332,8 @@ export class SlackImporter extends Importer { increaseProgress(); continue; } - } catch (e) { - this.logger.error(e); + } catch (err) { + this.logger.error({ msg: 'Error adding missed type', err }); } } @@ -388,19 +388,19 @@ export class SlackImporter extends Importer { this.logger.warn({ msg: 'Entry is not a valid JSON file; unable to import', entryName: entry.entryName, err: error }); } } - } catch (e) { - this.logger.error(e); + } catch (err) { + this.logger.error({ msg: 'Error processing message entry', err }); } increaseProgress(); } if (Object.keys(missedTypes).length > 0) { - this.logger.info('Missed import types:', missedTypes); + this.logger.info({ msg: 'Missed import types', missedTypes }); } - } catch (e) { - this.logger.error(e); - throw e; + } catch (err) { + this.logger.error({ msg: 'Error preparing import using local file', err }); + throw err; } ImporterWebsocket.progressUpdated({ rate: 100 }); diff --git a/apps/meteor/app/integrations/server/lib/ScriptEngine.ts b/apps/meteor/app/integrations/server/lib/ScriptEngine.ts index 906dbcd4024f3..7778d42f20179 100644 --- a/apps/meteor/app/integrations/server/lib/ScriptEngine.ts +++ b/apps/meteor/app/integrations/server/lib/ScriptEngine.ts @@ -296,7 +296,9 @@ export abstract class IntegrationScriptEngine { }); this.logger.debug({ - msg: `Script method "${method}" result of the Integration "${integration.name}" is:`, + msg: 'Script method result of the Integration', + method, + integration: integration.name, result, }); diff --git a/apps/meteor/app/integrations/server/lib/isolated-vm/isolated-vm.ts b/apps/meteor/app/integrations/server/lib/isolated-vm/isolated-vm.ts index 2c78b6d98a7ce..42044697014cb 100644 --- a/apps/meteor/app/integrations/server/lib/isolated-vm/isolated-vm.ts +++ b/apps/meteor/app/integrations/server/lib/isolated-vm/isolated-vm.ts @@ -56,7 +56,7 @@ export class IsolatedVMScriptEngine extends Integrat const script = integration.scriptCompiled; try { this.logger.info({ msg: 'Will evaluate the integration script', integration: pick(integration, 'name', '_id') }); - this.logger.debug(script); + this.logger.debug({ script }); const isolate = new ivm.Isolate({ memoryLimit: 8 }); diff --git a/apps/meteor/app/integrations/server/lib/triggerHandler.ts b/apps/meteor/app/integrations/server/lib/triggerHandler.ts index abf3437f1bed9..4b8351efee71e 100644 --- a/apps/meteor/app/integrations/server/lib/triggerHandler.ts +++ b/apps/meteor/app/integrations/server/lib/triggerHandler.ts @@ -782,12 +782,12 @@ class RocketChatIntegrationHandler { } } }) - .catch(async (error) => { - outgoingLogger.error(error); + .catch(async (err) => { + outgoingLogger.error({ err }); await updateHistory({ historyId, step: 'after-http-call', - httpError: error, + httpError: err, httpResult: null, }); }); diff --git a/apps/meteor/app/lib/server/functions/saveUserIdentity.ts b/apps/meteor/app/lib/server/functions/saveUserIdentity.ts index e943e1b9128ef..95abd94232bac 100644 --- a/apps/meteor/app/lib/server/functions/saveUserIdentity.ts +++ b/apps/meteor/app/lib/server/functions/saveUserIdentity.ts @@ -88,7 +88,7 @@ export async function saveUserIdentity({ try { await updateUsernameReferences(handleUpdateParams); } catch (err) { - SystemLogger.error(err); + SystemLogger.error({ err }); } }); } else { diff --git a/apps/meteor/app/lib/server/functions/setUsername.ts b/apps/meteor/app/lib/server/functions/setUsername.ts index ad0364d5617a0..9eeff3a14a0e3 100644 --- a/apps/meteor/app/lib/server/functions/setUsername.ts +++ b/apps/meteor/app/lib/server/functions/setUsername.ts @@ -123,8 +123,8 @@ export const _setUsername = async function ( setImmediate(() => { Accounts.sendEnrollmentEmail(user._id); }); - } catch (e: any) { - SystemLogger.error(e); + } catch (err: any) { + SystemLogger.error({ err }); } }, session); } diff --git a/apps/meteor/app/livechat/server/lib/rooms.ts b/apps/meteor/app/livechat/server/lib/rooms.ts index a4b6d0dbc22b6..070946089142e 100644 --- a/apps/meteor/app/livechat/server/lib/rooms.ts +++ b/apps/meteor/app/livechat/server/lib/rooms.ts @@ -254,8 +254,8 @@ export async function returnRoomAsInquiry(room: IOmnichannelRoom, departmentId?: try { await saveTransferHistory(room, transferData); await RoutingManager.unassignAgent(inquiry, departmentId); - } catch (e) { - livechatLogger.error(e); + } catch (err) { + livechatLogger.error({ err }); throw new Meteor.Error('error-returning-inquiry'); } diff --git a/apps/meteor/app/livechat/server/lib/stream/agentStatus.ts b/apps/meteor/app/livechat/server/lib/stream/agentStatus.ts index d2862c93847fa..07e4f8c5ce7d8 100644 --- a/apps/meteor/app/livechat/server/lib/stream/agentStatus.ts +++ b/apps/meteor/app/livechat/server/lib/stream/agentStatus.ts @@ -77,7 +77,8 @@ export const onlineAgents = { } } catch (e) { logger.error({ - msg: `Cannot perform action ${action}`, + msg: 'Cannot perform action', + action, err: e, }); } diff --git a/apps/meteor/app/livechat/server/sendMessageBySMS.ts b/apps/meteor/app/livechat/server/sendMessageBySMS.ts index 2d370410fa337..32c96b529e497 100644 --- a/apps/meteor/app/livechat/server/sendMessageBySMS.ts +++ b/apps/meteor/app/livechat/server/sendMessageBySMS.ts @@ -68,8 +68,8 @@ callbacks.add( phoneNumber: visitor.phone[0].phoneNumber, service, }); - } catch (e) { - callbackLogger.error(e); + } catch (err) { + callbackLogger.error({ msg: 'Error sending SMS message', err }); } return message; diff --git a/apps/meteor/app/mail-messages/server/functions/unsubscribe.ts b/apps/meteor/app/mail-messages/server/functions/unsubscribe.ts index c3d61eef0d8f9..3d14f801fdb2f 100644 --- a/apps/meteor/app/mail-messages/server/functions/unsubscribe.ts +++ b/apps/meteor/app/mail-messages/server/functions/unsubscribe.ts @@ -6,7 +6,12 @@ export const unsubscribe = async function (_id: string, createdAt: string): Prom if (_id && createdAt) { const affectedRows = (await Users.rocketMailUnsubscribe(_id, createdAt)) === 1; - SystemLogger.debug('[Mailer:Unsubscribe]', _id, createdAt, new Date(parseInt(createdAt)), affectedRows); + SystemLogger.debug({ + msg: '[Mailer:Unsubscribe]', + _id, + createdAt, + affectedRows, + }); return affectedRows; } diff --git a/apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts b/apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts index dc9b83eb67f7f..fdac8b0b77fa8 100644 --- a/apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts +++ b/apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts @@ -276,7 +276,7 @@ export class SAML { } private static async _logoutRemoveTokens(userId: string): Promise { - SAMLUtils.log(`Found user ${userId}`); + SAMLUtils.log({ msg: 'Found user', userId }); await Users.unsetLoginTokens(userId); await Users.removeSamlServiceSession(userId); @@ -342,8 +342,8 @@ export class SAML { redirect(url); }); - } catch (e: any) { - SystemLogger.error(e); + } catch (err: any) { + SystemLogger.error({ err }); redirect(); } }); @@ -351,7 +351,7 @@ export class SAML { private static async processLogoutResponse(req: IIncomingMessage, res: ServerResponse, service: IServiceProviderOptions): Promise { if (!req.query.SAMLResponse) { - SAMLUtils.error('Invalid LogoutResponse, missing SAMLResponse', req.query); + SAMLUtils.error({ msg: 'Invalid LogoutResponse received: missing SAMLResponse parameter.', query: req.query }); throw new Error('Invalid LogoutResponse received.'); } @@ -366,7 +366,7 @@ export class SAML { } const logOutUser = async (inResponseTo: string): Promise => { - SAMLUtils.log(`Logging Out user via inResponseTo ${inResponseTo}`); + SAMLUtils.log({ msg: 'Processing logout for inResponseTo', inResponseTo }); const loggedOutUsers = await Users.findBySAMLInResponseTo(inResponseTo).toArray(); if (loggedOutUsers.length > 1) { @@ -410,8 +410,7 @@ export class SAML { try { url = await serviceProvider.getAuthorizeUrl(samlObject.credentialToken); } catch (err: any) { - SAMLUtils.error('Unable to generate authorize url'); - SAMLUtils.error(err); + SAMLUtils.error({ err, msg: 'Unable to generate authorize url' }); url = Meteor.absoluteUrl(); } @@ -455,8 +454,8 @@ export class SAML { Location: url, }); res.end(); - } catch (error) { - SAMLUtils.error(error); + } catch (err) { + SAMLUtils.error({ err }); res.writeHead(302, { Location: Meteor.absoluteUrl(), }); @@ -521,7 +520,7 @@ export class SAML { } } } catch (err: any) { - SystemLogger.error(err); + SystemLogger.error({ err }); } } } diff --git a/apps/meteor/app/meteor-accounts-saml/server/lib/ServiceProvider.ts b/apps/meteor/app/meteor-accounts-saml/server/lib/ServiceProvider.ts index bdce2978850d8..f60b65952d67e 100644 --- a/apps/meteor/app/meteor-accounts-saml/server/lib/ServiceProvider.ts +++ b/apps/meteor/app/meteor-accounts-saml/server/lib/ServiceProvider.ts @@ -175,8 +175,7 @@ export class SAMLServiceProvider { public async getAuthorizeUrl(credentialToken: string): Promise { const request = this.generateAuthorizeRequest(credentialToken); - SAMLUtils.log('-----REQUEST------'); - SAMLUtils.log(request); + SAMLUtils.log({ request, msg: 'getAuthorizeUrl' }); return this.requestToUrl(request, 'authorize'); } diff --git a/apps/meteor/app/meteor-accounts-saml/server/lib/Utils.ts b/apps/meteor/app/meteor-accounts-saml/server/lib/Utils.ts index 5d43e122c7a7e..ec8924c69a7f4 100644 --- a/apps/meteor/app/meteor-accounts-saml/server/lib/Utils.ts +++ b/apps/meteor/app/meteor-accounts-saml/server/lib/Utils.ts @@ -51,7 +51,7 @@ export class SAMLUtils { } public static getServiceProviderOptions(providerName: string): IServiceProviderOptions | undefined { - this.log(providerName, providerList); + this.log({ providerName, providerList }); return providerList.find((providerOptions) => providerOptions.provider === providerName); } @@ -133,15 +133,15 @@ export class SAMLUtils { return `saml/${credentialToken}?saml_idp_credentialToken=${credentialToken}`; } - public static log(obj: any, ...args: Array): void { + public static log(obj: object | string): void { if (debug && logger) { - logger.debug(obj, ...args); + logger.debug(obj); } } - public static error(obj: any, ...args: Array): void { + public static error(obj: object | string): void { if (logger) { - logger.error(obj, ...args); + logger.error(obj); } } @@ -219,9 +219,8 @@ export class SAMLUtils { try { map = JSON.parse(userDataFieldMap); - } catch (e) { - SAMLUtils.log(userDataFieldMap); - SAMLUtils.log(e); + } catch (err) { + SAMLUtils.log({ userDataFieldMap, err }); throw new Error('Failed to parse custom user field map'); } @@ -412,7 +411,7 @@ export class SAMLUtils { public static mapProfileToUserObject(profile: Record): ISAMLUser { const userDataMap = this.getUserDataMapping(); - SAMLUtils.log('parsed userDataMap', userDataMap); + SAMLUtils.log({ msg: 'Mapping SAML Profile to User Object', userDataMap }); if (userDataMap.identifier.type === 'custom') { if (!userDataMap.identifier.attribute) { diff --git a/apps/meteor/app/meteor-accounts-saml/server/lib/generators/LogoutRequest.ts b/apps/meteor/app/meteor-accounts-saml/server/lib/generators/LogoutRequest.ts index ebca0b4b45f8e..733ffd46a89ca 100644 --- a/apps/meteor/app/meteor-accounts-saml/server/lib/generators/LogoutRequest.ts +++ b/apps/meteor/app/meteor-accounts-saml/server/lib/generators/LogoutRequest.ts @@ -12,8 +12,7 @@ export class LogoutRequest { const data = this.getDataForNewRequest(serviceProviderOptions, nameID, sessionIndex); const request = SAMLUtils.fillTemplateData(serviceProviderOptions.logoutRequestTemplate || defaultLogoutRequestTemplate, data); - SAMLUtils.log('------- SAML Logout request -----------'); - SAMLUtils.log(request); + SAMLUtils.log({ request, msg: '------- SAML Logout request -----------' }); return { request, diff --git a/apps/meteor/app/meteor-accounts-saml/server/lib/generators/LogoutResponse.ts b/apps/meteor/app/meteor-accounts-saml/server/lib/generators/LogoutResponse.ts index a2563cede90f7..604d395a1bf04 100644 --- a/apps/meteor/app/meteor-accounts-saml/server/lib/generators/LogoutResponse.ts +++ b/apps/meteor/app/meteor-accounts-saml/server/lib/generators/LogoutResponse.ts @@ -17,8 +17,7 @@ export class LogoutResponse { const data = this.getDataForNewResponse(serviceProviderOptions, nameID, sessionIndex, inResponseToId); const response = SAMLUtils.fillTemplateData(serviceProviderOptions.logoutResponseTemplate || defaultLogoutResponseTemplate, data); - SAMLUtils.log('------- SAML Logout response -----------'); - SAMLUtils.log(response); + SAMLUtils.log({ response, msg: '------- SAML Logout response -----------' }); return { response, diff --git a/apps/meteor/app/meteor-accounts-saml/server/lib/parsers/LogoutRequest.ts b/apps/meteor/app/meteor-accounts-saml/server/lib/parsers/LogoutRequest.ts index bbae84556a9a9..c609328ea21bf 100644 --- a/apps/meteor/app/meteor-accounts-saml/server/lib/parsers/LogoutRequest.ts +++ b/apps/meteor/app/meteor-accounts-saml/server/lib/parsers/LogoutRequest.ts @@ -12,7 +12,7 @@ export class LogoutRequestParser { } public async validate(xmlString: string, callback: ILogoutRequestValidateCallback): Promise { - SAMLUtils.log(`LogoutRequest: ${xmlString}`); + SAMLUtils.log({ msg: 'Validating SAML Logout Request', xmlString }); const doc = new xmldom.DOMParser().parseFromString(xmlString, 'text/xml'); if (!doc) { @@ -37,14 +37,13 @@ export class LogoutRequestParser { const id = request.getAttribute('ID'); return callback(null, { idpSession, nameID, id }); - } catch (e) { - SAMLUtils.error(e); - SAMLUtils.log(`Caught error: ${e}`); + } catch (err) { + SAMLUtils.error({ err }); - const msg = doc.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:protocol', 'StatusMessage'); - SAMLUtils.log(`Unexpected msg from IDP. Does your session still exist at IDP? Idp returned: \n ${msg}`); + const statusMessage = doc.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:protocol', 'StatusMessage'); + SAMLUtils.log({ msg: `Unexpected msg from IDP. Does your session still exist at IDP?`, statusMessage }); - return callback(e instanceof Error ? e : String(e), null); + return callback(err instanceof Error ? err : String(err), null); } } } diff --git a/apps/meteor/app/meteor-accounts-saml/server/lib/parsers/LogoutResponse.ts b/apps/meteor/app/meteor-accounts-saml/server/lib/parsers/LogoutResponse.ts index 54db1a675c9ac..af9c176233cdf 100644 --- a/apps/meteor/app/meteor-accounts-saml/server/lib/parsers/LogoutResponse.ts +++ b/apps/meteor/app/meteor-accounts-saml/server/lib/parsers/LogoutResponse.ts @@ -12,7 +12,7 @@ export class LogoutResponseParser { } public async validate(xmlString: string, callback: ILogoutResponseValidateCallback): Promise { - SAMLUtils.log(`LogoutResponse: ${xmlString}`); + SAMLUtils.log({ msg: 'Validating SAML Logout Response', xmlString }); const doc = new xmldom.DOMParser().parseFromString(xmlString, 'text/xml'); if (!doc) { @@ -28,9 +28,9 @@ export class LogoutResponseParser { let inResponseTo; try { inResponseTo = response.getAttribute('InResponseTo'); - SAMLUtils.log(`In Response to: ${inResponseTo}`); - } catch (e) { - SAMLUtils.log(`Caught error: ${e}`); + SAMLUtils.log({ msg: `Found InResponseTo`, inResponseTo }); + } catch (err) { + SAMLUtils.log({ err }); const msg = doc.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:protocol', 'StatusMessage'); SAMLUtils.log(`Unexpected msg from IDP. Does your session still exist at IDP? Idp returned: \n ${msg}`); } diff --git a/apps/meteor/app/meteor-accounts-saml/server/lib/parsers/Response.ts b/apps/meteor/app/meteor-accounts-saml/server/lib/parsers/Response.ts index af052f43b7fe0..87aeb4ad9f6c8 100644 --- a/apps/meteor/app/meteor-accounts-saml/server/lib/parsers/Response.ts +++ b/apps/meteor/app/meteor-accounts-saml/server/lib/parsers/Response.ts @@ -19,7 +19,7 @@ export class ResponseParser { public validate(xml: string, callback: IResponseValidateCallback): void { // We currently use RelayState to save SAML provider - SAMLUtils.log(`Validating response with relay state: ${xml}`); + SAMLUtils.log({ msg: 'Validating SAML Response', xml }); let error: Error | null = null; @@ -145,7 +145,7 @@ export class ResponseParser { if (authnStatement) { if (authnStatement.hasAttribute('SessionIndex')) { profile.sessionIndex = authnStatement.getAttribute('SessionIndex'); - SAMLUtils.log(`Session Index: ${profile.sessionIndex}`); + SAMLUtils.log({ msg: 'Session Index Found', sessionIndex: profile.sessionIndex }); } else { SAMLUtils.log('No Session Index Found'); } @@ -353,7 +353,7 @@ export class ResponseParser { const options = { key: this.serviceProviderOptions.privateKey }; xmlenc.decrypt(encSubject.getElementsByTagNameNS('*', 'EncryptedData')[0], options, (err, result) => { if (err) { - SAMLUtils.error(err); + SAMLUtils.error({ err }); } subject = new xmldom.DOMParser().parseFromString(result, 'text/xml'); }); @@ -418,9 +418,9 @@ export class ResponseParser { } private mapAttributes(attributeStatement: Element, profile: Record): void { - SAMLUtils.log(`Attribute Statement found in SAML response: ${attributeStatement}`); + SAMLUtils.log({ msg: 'Attribute Statement found, mapping attributes to profile.', attributeStatement }); const attributes = attributeStatement.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'Attribute'); - SAMLUtils.log(`Attributes will be processed: ${attributes.length}`); + SAMLUtils.log({ msg: 'Attributes will be processed', count: attributes.length }); if (attributes) { for (let i = 0; i < attributes.length; i++) { @@ -437,8 +437,7 @@ export class ResponseParser { const key = attributes[i].getAttribute('Name'); if (key) { - SAMLUtils.log(`Attribute: ${attributes[i]} has ${values.length} value(s).`); - SAMLUtils.log(`Adding attribute from SAML response to profile: ${key} = ${value}`); + SAMLUtils.log({ msg: 'Mapping attribute to profile', attribute: key, value }); profile[key] = value; } } diff --git a/apps/meteor/app/meteor-accounts-saml/server/lib/settings.ts b/apps/meteor/app/meteor-accounts-saml/server/lib/settings.ts index 3b93fe22c88b4..dacdd014806e4 100644 --- a/apps/meteor/app/meteor-accounts-saml/server/lib/settings.ts +++ b/apps/meteor/app/meteor-accounts-saml/server/lib/settings.ts @@ -124,7 +124,7 @@ export const loadSamlServiceProviders = async function (): Promise { services.map(async ([key, value]) => { if (value === true) { const samlConfigs = getSamlConfigs(key); - SAMLUtils.log(key); + SAMLUtils.log({ key }); await LoginServiceConfiguration.createOrUpdateService(serviceName, samlConfigs); void notifyOnLoginServiceConfigurationChangedByService(serviceName); return configureSamlService(samlConfigs); diff --git a/apps/meteor/app/meteor-accounts-saml/server/loginHandler.ts b/apps/meteor/app/meteor-accounts-saml/server/loginHandler.ts index b95513fef0366..e9b861b4a511a 100644 --- a/apps/meteor/app/meteor-accounts-saml/server/loginHandler.ts +++ b/apps/meteor/app/meteor-accounts-saml/server/loginHandler.ts @@ -33,16 +33,16 @@ Accounts.registerLoginHandler('saml', async (loginRequest) => { SAMLUtils.events.emit('updateCustomFields', loginResult, updatedUser); return updatedUser; - } catch (error: any) { - SystemLogger.error(error); + } catch (err: any) { + SystemLogger.error({ err }); - let message = error.toString(); + let message = err.toString(); let errorCode = ''; - if (error instanceof Meteor.Error) { - errorCode = (error.error || error.message) as string; - } else if (error instanceof Error) { - errorCode = error.message; + if (err instanceof Meteor.Error) { + errorCode = (err.error || err.message) as string; + } else if (err instanceof Error) { + errorCode = err.message; } if (errorCode) { diff --git a/apps/meteor/app/meteor-accounts-saml/server/methods/samlLogout.ts b/apps/meteor/app/meteor-accounts-saml/server/methods/samlLogout.ts index a7f9e87a93de9..0570a7e1914ca 100644 --- a/apps/meteor/app/meteor-accounts-saml/server/methods/samlLogout.ts +++ b/apps/meteor/app/meteor-accounts-saml/server/methods/samlLogout.ts @@ -63,8 +63,7 @@ Meteor.methods({ sessionIndex: idpSession, }); - SAMLUtils.log('----Logout Request----'); - SAMLUtils.log(request); + SAMLUtils.log({ request, msg: '----Logout Request---' }); // request.request: actual XML SAML Request // request.id: comminucation id which will be mentioned in the ResponseTo field of SAMLResponse @@ -72,7 +71,7 @@ Meteor.methods({ await Users.setSamlInResponseTo(userId, request.id); const result = await _saml.requestToUrl(request.request, 'logout'); - SAMLUtils.log(`SAML Logout Request ${result}`); + SAMLUtils.log({ msg: 'SAML Logout Request URL generated', result }); return result; }, diff --git a/apps/meteor/app/metrics/server/lib/collectMetrics.ts b/apps/meteor/app/metrics/server/lib/collectMetrics.ts index a1ad41c9a95cf..8628a52fe4096 100644 --- a/apps/meteor/app/metrics/server/lib/collectMetrics.ts +++ b/apps/meteor/app/metrics/server/lib/collectMetrics.ts @@ -208,7 +208,7 @@ const updatePrometheusConfig = async (): Promise => { gcStats(client.register)(); } } catch (error) { - SystemLogger.error(error); + SystemLogger.error({ err: error }); } Object.assign(was, is); diff --git a/apps/meteor/app/nextcloud/server/addWebdavServer.ts b/apps/meteor/app/nextcloud/server/addWebdavServer.ts index d439389299ae2..6d3ea80f6d178 100644 --- a/apps/meteor/app/nextcloud/server/addWebdavServer.ts +++ b/apps/meteor/app/nextcloud/server/addWebdavServer.ts @@ -28,8 +28,8 @@ Meteor.startup(() => { }; try { await addWebdavAccountByToken(user._id, data); - } catch (error) { - SystemLogger.error(error); + } catch (err) { + SystemLogger.error({ err }); } }, callbacks.priority.MEDIUM, diff --git a/apps/meteor/app/notification-queue/server/NotificationQueue.ts b/apps/meteor/app/notification-queue/server/NotificationQueue.ts index 6690bea41f521..52035313949a9 100644 --- a/apps/meteor/app/notification-queue/server/NotificationQueue.ts +++ b/apps/meteor/app/notification-queue/server/NotificationQueue.ts @@ -93,9 +93,9 @@ class NotificationClass { } await NotificationQueue.removeById(notification._id); - } catch (e) { - SystemLogger.error(e); - await NotificationQueue.setErrorById(notification._id, e instanceof Error ? e.message : String(e)); + } catch (err) { + SystemLogger.error({ err }); + await NotificationQueue.setErrorById(notification._id, err instanceof Error ? err.message : String(err)); } if (counter >= this.maxBatchSize) { diff --git a/apps/meteor/app/push/server/push.ts b/apps/meteor/app/push/server/push.ts index 6d13a34e7669f..15c057389da4b 100644 --- a/apps/meteor/app/push/server/push.ts +++ b/apps/meteor/app/push/server/push.ts @@ -163,7 +163,7 @@ class PushClass { this.isConfigured = true; - logger.debug('Configure', this.options); + logger.debug({ msg: 'Configure', options: this.options }); if (this.options.apn) { initAPN({ options: this.options as RequiredField, absoluteUrl: Meteor.absoluteUrl() }); @@ -188,7 +188,7 @@ class PushClass { countApn: string[], countGcm: string[], ): Promise { - logger.debug('send to token', app.token); + logger.debug({ msg: 'send to token', token: app.token }); if ('apn' in app.token && app.token.apn) { countApn.push(app._id); @@ -248,7 +248,7 @@ class PushClass { projectId: credentials.project_id, }; } catch (error) { - logger.error('Error getting FCM token', error); + logger.error({ msg: 'Error getting FCM token', err: error }); throw new Error('Error getting FCM token'); } } @@ -275,7 +275,7 @@ class PushClass { const response = await result.text(); if (result.status === 406) { - logger.info('removing push token', token); + logger.info({ msg: 'removing push token', token }); await AppsTokens.deleteMany({ $or: [ { @@ -290,12 +290,12 @@ class PushClass { } if (result.status === 422) { - logger.info('gateway rejected push notification. not retrying.', response); + logger.info({ msg: 'gateway rejected push notification. not retrying.', response }); return; } if (result.status === 401) { - logger.warn('Error sending push to gateway (not authorized)', response); + logger.warn({ msg: 'authorization failed when sending push to gateway. not retrying.', response }); return; } @@ -309,7 +309,7 @@ class PushClass { // [1, 2, 4, 8, 16] minutes (total 31) const ms = 60000 * Math.pow(2, tries); - logger.log('Trying sending push to gateway again in', ms, 'milliseconds'); + logger.log({ msg: 'Retrying push to gateway', tries: tries + 1, in: ms }); setTimeout(() => this.sendGatewayPush(gateway, service, token, notification, tries + 1), ms); } @@ -338,7 +338,7 @@ class PushClass { const gatewayNotification = this.getGatewayNotificationData(notification); for (const gateway of this.options.gateways) { - logger.debug('send to token', app.token); + logger.debug({ msg: 'send to token', token: app.token }); if ('apn' in app.token && app.token.apn) { countApn.push(app._id); @@ -353,7 +353,7 @@ class PushClass { } private async sendNotification(notification: PendingPushNotification): Promise<{ apn: string[]; gcm: string[] }> { - logger.debug('Sending notification', notification); + logger.debug({ msg: 'Sending notification', notification }); const countApn: string[] = []; const countGcm: string[] = []; @@ -382,7 +382,7 @@ class PushClass { const appTokens = AppsTokens.find(query); for await (const app of appTokens) { - logger.debug('send to token', app.token); + logger.debug({ msg: 'send to token', token: app.token }); if (this.shouldUseGateway()) { await this.sendNotificationGateway(app, notification, countApn, countGcm); @@ -503,7 +503,6 @@ class PushClass { userId: notification.userId, err: error, }); - logger.debug(error.stack); } } } diff --git a/apps/meteor/app/slackbridge/server/RocketAdapter.js b/apps/meteor/app/slackbridge/server/RocketAdapter.js index 624d4a72de068..b6a0b1ccbcfec 100644 --- a/apps/meteor/app/slackbridge/server/RocketAdapter.js +++ b/apps/meteor/app/slackbridge/server/RocketAdapter.js @@ -343,7 +343,7 @@ export default class RocketAdapter { } async addUser(slackUserID) { - rocketLogger.debug('Adding Rocket.Chat user from Slack', slackUserID); + rocketLogger.debug({ msg: 'Adding Rocket.Chat user from Slack', slackUserID }); let addedUser; for await (const slack of this.slackAdapters) { if (addedUser) { @@ -410,8 +410,8 @@ export default class RocketAdapter { if (url) { try { await setUserAvatar(user, url, null, 'url'); - } catch (error) { - rocketLogger.debug('Error setting user avatar', error.message); + } catch (err) { + rocketLogger.debug({ msg: 'Error setting user avatar from Slack', err }); } } } @@ -482,6 +482,7 @@ export default class RocketAdapter { rocketMsgObj.tmid = tmessage._id; } } + if (slackMessage.subtype === 'bot_message') { rocketUser = await Users.findOneById('rocket.cat', { projection: { username: 1 } }); } diff --git a/apps/meteor/app/slackbridge/server/SlackAdapter.js b/apps/meteor/app/slackbridge/server/SlackAdapter.js index 46a5ab6d35b5e..3783416c8d4d8 100644 --- a/apps/meteor/app/slackbridge/server/SlackAdapter.js +++ b/apps/meteor/app/slackbridge/server/SlackAdapter.js @@ -25,7 +25,7 @@ import { getUserAvatarURL } from '../../utils/server/getUserAvatarURL'; export default class SlackAdapter { constructor(slackBridge) { - slackLogger.debug('constructor'); + slackLogger.debug({ msg: 'constructor' }); this.slackBridge = slackBridge; this.rtm = {}; // slack-client Real Time Messaging API this.apiToken = {}; // Slack API Token passed in via Connect @@ -45,8 +45,8 @@ export default class SlackAdapter { const connectResult = await (appCredential ? this.connectApp(appCredential) : this.connectLegacy(apiToken)); if (connectResult) { - slackLogger.info('Connected to Slack'); - slackLogger.debug('Slack connection result: ', connectResult); + slackLogger.info({ msg: 'Connected to Slack' }); + slackLogger.debug({ msg: 'Slack connection result', connectResult }); Meteor.startup(async () => { try { await this.populateMembershipChannelMap(); // If run outside of Meteor.startup, HTTP is not defined @@ -153,7 +153,7 @@ export default class SlackAdapter { * } */ this.slackApp.message(async ({ message }) => { - slackLogger.debug('OnSlackEvent-MESSAGE: ', message); + slackLogger.debug({ msg: 'OnSlackEvent-MESSAGE', message }); if (message) { try { await this.onMessage(message); @@ -179,9 +179,8 @@ export default class SlackAdapter { * } */ this.slackApp.event('reaction_added', async ({ event }) => { - slackLogger.debug('OnSlackEvent-REACTION_ADDED: ', event); + slackLogger.debug({ msg: 'OnSlackEvent-REACTION_ADDED', event }); try { - slackLogger.error({ event }); await this.onReactionAdded(event); } catch (err) { slackLogger.error({ msg: 'Unhandled error onReactionAdded', err }); @@ -204,7 +203,7 @@ export default class SlackAdapter { * } */ this.slackApp.event('reaction_removed', async ({ event }) => { - slackLogger.debug('OnSlackEvent-REACTION_REMOVED: ', event); + slackLogger.debug({ msg: 'OnSlackEvent-REACTION_REMOVED', event }); try { await this.onReactionRemoved(event); } catch (err) { @@ -225,7 +224,7 @@ export default class SlackAdapter { * } */ this.slackApp.event('member_joined_channel', async ({ event, context }) => { - slackLogger.debug('OnSlackEvent-CHANNEL_LEFT: ', event); + slackLogger.debug({ msg: 'OnSlackEvent-CHANNEL_LEFT', event }); try { await this.processMemberJoinChannel(event, context); } catch (err) { @@ -234,7 +233,7 @@ export default class SlackAdapter { }); this.slackApp.event('channel_left', async ({ event }) => { - slackLogger.debug('OnSlackEvent-CHANNEL_LEFT: ', event); + slackLogger.debug({ msg: 'OnSlackEvent-CHANNEL_LEFT', event }); try { this.onChannelLeft(event); } catch (err) { @@ -251,7 +250,7 @@ export default class SlackAdapter { * @deprecated */ registerForEventsLegacy() { - slackLogger.debug('Register for events'); + slackLogger.debug({ msg: 'Register for events' }); this.rtm.on('authenticated', () => { slackLogger.info('Connected to Slack'); }); @@ -279,7 +278,7 @@ export default class SlackAdapter { * } **/ this.rtm.on('message', async (slackMessage) => { - slackLogger.debug('OnSlackEvent-MESSAGE: ', slackMessage); + slackLogger.debug({ msg: 'OnSlackEvent-MESSAGE', slackMessage }); if (slackMessage) { try { await this.onMessage(slackMessage); @@ -290,7 +289,7 @@ export default class SlackAdapter { }); this.rtm.on('reaction_added', async (reactionMsg) => { - slackLogger.debug('OnSlackEvent-REACTION_ADDED: ', reactionMsg); + slackLogger.debug({ msg: 'OnSlackEvent-REACTION_ADDED', reactionMsg }); if (reactionMsg) { try { await this.onReactionAdded(reactionMsg); @@ -301,7 +300,7 @@ export default class SlackAdapter { }); this.rtm.on('reaction_removed', async (reactionMsg) => { - slackLogger.debug('OnSlackEvent-REACTION_REMOVED: ', reactionMsg); + slackLogger.debug({ msg: 'OnSlackEvent-REACTION_REMOVED', reactionMsg }); if (reactionMsg) { try { await this.onReactionRemoved(reactionMsg); @@ -370,7 +369,7 @@ export default class SlackAdapter { * } **/ this.rtm.on('channel_left', (channelLeftMsg) => { - slackLogger.debug('OnSlackEvent-CHANNEL_LEFT: ', channelLeftMsg); + slackLogger.debug({ msg: 'OnSlackEvent-CHANNEL_LEFT', channelLeftMsg }); if (channelLeftMsg) { try { this.onChannelLeft(channelLeftMsg); @@ -629,7 +628,7 @@ export default class SlackAdapter { } async postFindChannel(rocketChannelName) { - slackLogger.debug('Searching for Slack channel or group', rocketChannelName); + slackLogger.debug({ msg: 'Searching for Slack channel or group', rocketChannelName }); const channels = await this.slackAPI.getChannels(); if (channels && channels.length > 0) { for (const channel of channels) { @@ -680,7 +679,7 @@ export default class SlackAdapter { addSlackChannel(rocketChID, slackChID) { const ch = this.getSlackChannel(rocketChID); if (ch == null) { - slackLogger.debug('Added channel', { rocketChID, slackChID }); + slackLogger.debug({ msg: 'Added channel', rocketChID, slackChID }); this.slackChannelRocketBotMembershipMap.set(rocketChID, { id: slackChID, family: slackChID.charAt(0) === 'C' ? 'channels' : 'groups', @@ -855,7 +854,7 @@ export default class SlackAdapter { data.thread_ts = tmessage.slackTs; } } - slackLogger.debug('Post Message To Slack', data); + slackLogger.debug({ msg: 'Post Message To Slack', data }); // If we don't have the bot id yet and we have multiple slack bridges, we need to keep track of the messages that are being sent if (!this.slackBotId && this.rocket.slackAdapters && this.rocket.slackAdapters.length >= 2) { @@ -871,7 +870,12 @@ export default class SlackAdapter { if (postResult && postResult.message && postResult.message.bot_id && postResult.message.ts) { this.slackBotId = postResult.message.bot_id; await Messages.setSlackBotIdAndSlackTs(rocketMessage._id, postResult.message.bot_id, postResult.message.ts); - slackLogger.debug(`RocketMsgID=${rocketMessage._id} SlackMsgID=${postResult.message.ts} SlackBotID=${postResult.message.bot_id}`); + slackLogger.debug({ + msg: 'Message posted to Slack', + rocketMessageId: rocketMessage._id, + slackMessageId: postResult.message.ts, + slackBotId: postResult.message.bot_id, + }); } } } @@ -887,7 +891,7 @@ export default class SlackAdapter { text: rocketMessage.msg, as_user: true, }; - slackLogger.debug('Post UpdateMessage To Slack', data); + slackLogger.debug({ msg: 'Post UpdateMessage To Slack', data }); const postResult = await this.slackAPI.updateMessage(data); if (postResult) { slackLogger.debug('Message updated on Slack'); @@ -896,7 +900,7 @@ export default class SlackAdapter { } async processMemberJoinChannel(event, context) { - slackLogger.debug('Member join channel', event.channel); + slackLogger.debug({ msg: 'Member join channel', channel: event.channel }); const rocketCh = await this.rocket.getChannel({ channel: event.channel }); if (rocketCh != null) { this.addSlackChannel(rocketCh._id, event.channel); @@ -908,7 +912,7 @@ export default class SlackAdapter { } async processChannelJoin(slackMessage) { - slackLogger.debug('Channel join', slackMessage.channel.id); + slackLogger.debug({ msg: 'Channel join', channelId: slackMessage.channel.id }); const rocketCh = await this.rocket.addChannel(slackMessage.channel); if (rocketCh != null) { this.addSlackChannel(rocketCh._id, slackMessage.channel); @@ -1320,7 +1324,7 @@ export default class SlackAdapter { if (Array.isArray(data.messages) && data.messages.length) { let latest = 0; for await (const message of data.messages.reverse()) { - slackLogger.debug('MESSAGE: ', message); + slackLogger.debug({ msg: 'MESSAGE', message }); if (!latest || message.ts > latest) { latest = message.ts; } @@ -1332,7 +1336,7 @@ export default class SlackAdapter { } async copyChannelInfo(rid, channelMap) { - slackLogger.debug('Copying users from Slack channel to Rocket.Chat', channelMap.id, rid); + slackLogger.debug({ msg: 'Copying users from Slack channel to Rocket.Chat', channelId: channelMap.id, rid }); const channel = await this.slackAPI.getRoomInfo(channelMap.id); if (channel) { const members = await this.slackAPI.getMembers(channelMap.id); @@ -1340,7 +1344,7 @@ export default class SlackAdapter { for await (const member of members) { const user = (await this.rocket.findUser(member)) || (await this.rocket.addUser(member)); if (user) { - slackLogger.debug('Adding user to room', user.username, rid); + slackLogger.debug({ msg: 'Adding user to room', username: user.username, rid }); await addUserToRoom(rid, user, null, { skipSystemMessage: true }); } } @@ -1369,7 +1373,7 @@ export default class SlackAdapter { if (topic) { const creator = (await this.rocket.findUser(topic_creator)) || (await this.rocket.addUser(topic_creator)); - slackLogger.debug('Setting room topic', rid, topic, creator.username); + slackLogger.debug({ msg: 'Setting room topic', rid, topic, username: creator.username }); await saveRoomTopic(rid, topic, creator, false); } } @@ -1411,13 +1415,13 @@ export default class SlackAdapter { } async importMessages(rid, callback) { - slackLogger.info('importMessages: ', rid); + slackLogger.info({ msg: 'importMessages', rid }); const rocketchat_room = await Rooms.findOneById(rid); if (rocketchat_room) { if (this.getSlackChannel(rid)) { await this.copyChannelInfo(rid, this.getSlackChannel(rid)); - slackLogger.debug('Importing messages from Slack to Rocket.Chat', this.getSlackChannel(rid), rid); + slackLogger.debug({ msg: 'Importing messages from Slack to Rocket.Chat', slackChannel: this.getSlackChannel(rid), rid }); let results = await this.importFromHistory({ channel: this.getSlackChannel(rid).id, @@ -1431,7 +1435,7 @@ export default class SlackAdapter { }); } - slackLogger.debug('Pinning Slack channel messages to Rocket.Chat', this.getSlackChannel(rid), rid); + slackLogger.debug({ msg: 'Pinning Slack channel messages to Rocket.Chat', slackChannel: this.getSlackChannel(rid), rid }); await this.copyPins(rid, this.getSlackChannel(rid)); return callback(); diff --git a/apps/meteor/app/slackbridge/server/slackbridge.js b/apps/meteor/app/slackbridge/server/slackbridge.js index 89ff66a13397e..75409f4314ceb 100644 --- a/apps/meteor/app/slackbridge/server/slackbridge.js +++ b/apps/meteor/app/slackbridge/server/slackbridge.js @@ -45,7 +45,7 @@ class SlackBridgeClass { this.rocket.addSlack(slack); this.slackAdapters.push(slack); - slack.connect({ apiToken }).catch((err) => connLogger.error('error connecting to slack', err)); + slack.connect({ apiToken }).catch((err) => connLogger.error({ msg: 'error connecting to slack', err })); }); } else { const botTokenList = this.botTokens.split('\n'); // Bot token list @@ -70,7 +70,7 @@ class SlackBridgeClass { this.rocket.addSlack(slack); this.slackAdapters.push(slack); - slack.connect({ appCredential }).catch((err) => connLogger.error('error connecting to slack', err)); + slack.connect({ appCredential }).catch((err) => connLogger.error({ msg: 'error connecting to slack', err })); }); } @@ -109,7 +109,7 @@ class SlackBridgeClass { connLogger.info('Slack Bridge Disconnected'); } } catch (error) { - connLogger.error('An error occurred during disconnection', error); + connLogger.error({ msg: 'An error occurred during disconnection', err: error }); } } @@ -120,7 +120,7 @@ class SlackBridgeClass { this.isLegacyRTM = value; this.debouncedReconnectIfEnabled(); } - classLogger.debug('Setting: SlackBridge_UseLegacy', value); + classLogger.debug({ msg: 'Setting: SlackBridge_UseLegacy', value }); }); // Slack installtion Bot token @@ -129,7 +129,7 @@ class SlackBridgeClass { this.botTokens = value; this.debouncedReconnectIfEnabled(); } - classLogger.debug('Setting: SlackBridge_BotToken', value); + classLogger.debug({ msg: 'Setting: SlackBridge_BotToken', value }); }); // Slack installtion App token settings.watch('SlackBridge_AppToken', (value) => { @@ -137,7 +137,7 @@ class SlackBridgeClass { this.appTokens = value; this.debouncedReconnectIfEnabled(); } - classLogger.debug('Setting: SlackBridge_AppToken', value); + classLogger.debug({ msg: 'Setting: SlackBridge_AppToken', value }); }); // Slack installtion Signing token settings.watch('SlackBridge_SigningSecret', (value) => { @@ -145,7 +145,7 @@ class SlackBridgeClass { this.signingSecrets = value; this.debouncedReconnectIfEnabled(); } - classLogger.debug('Setting: SlackBridge_SigningSecret', value); + classLogger.debug({ msg: 'Setting: SlackBridge_SigningSecret', value }); }); // Slack installation API token @@ -155,25 +155,25 @@ class SlackBridgeClass { this.debouncedReconnectIfEnabled(); } - classLogger.debug('Setting: SlackBridge_APIToken', value); + classLogger.debug({ msg: 'Setting: SlackBridge_APIToken', value }); }); // Import messages from Slack with an alias; %s is replaced by the username of the user. If empty, no alias will be used. settings.watch('SlackBridge_AliasFormat', (value) => { this.aliasFormat = value; - classLogger.debug('Setting: SlackBridge_AliasFormat', value); + classLogger.debug({ msg: 'Setting: SlackBridge_AliasFormat', value }); }); // Do not propagate messages from bots whose name matches the regular expression above. If left empty, all messages from bots will be propagated. settings.watch('SlackBridge_ExcludeBotnames', (value) => { this.excludeBotnames = value; - classLogger.debug('Setting: SlackBridge_ExcludeBotnames', value); + classLogger.debug({ msg: 'Setting: SlackBridge_ExcludeBotnames', value }); }); // Reactions settings.watch('SlackBridge_Reactions_Enabled', (value) => { this.isReactionsEnabled = value; - classLogger.debug('Setting: SlackBridge_Reactions_Enabled', value); + classLogger.debug({ msg: 'Setting: SlackBridge_Reactions_Enabled', value }); }); // Is this entire SlackBridge enabled @@ -186,7 +186,7 @@ class SlackBridgeClass { this.disconnect(); } } - classLogger.debug('Setting: SlackBridge_Enabled', value); + classLogger.debug({ msg: 'Setting: SlackBridge_Enabled', value }); }); } } diff --git a/apps/meteor/app/webdav/server/methods/uploadFileToWebdav.ts b/apps/meteor/app/webdav/server/methods/uploadFileToWebdav.ts index 97bcf46322161..6d66031aa4173 100644 --- a/apps/meteor/app/webdav/server/methods/uploadFileToWebdav.ts +++ b/apps/meteor/app/webdav/server/methods/uploadFileToWebdav.ts @@ -38,17 +38,17 @@ Meteor.methods({ try { await uploadFileToWebdav(accountId, fileData instanceof ArrayBuffer ? Buffer.from(fileData) : fileData, name); return { success: true }; - } catch (error: any) { - if (typeof error === 'object' && error instanceof Error && error.name === 'error-invalid-account') { - throw new MeteorError(error.name, 'Invalid WebDAV Account', { + } catch (err: any) { + if (typeof err === 'object' && err instanceof Error && err.name === 'error-invalid-account') { + throw new MeteorError(err.name, 'Invalid WebDAV Account', { method: 'uploadFileToWebdav', }); } - logger.error(error); + logger.error({ err }); - if (error.response) { - const { status } = error.response; + if (err.response) { + const { status } = err.response; if (status === 404) { return { success: false, message: 'webdav-server-not-found' }; } diff --git a/apps/meteor/ee/app/livechat-enterprise/server/business-hour/Multiple.ts b/apps/meteor/ee/app/livechat-enterprise/server/business-hour/Multiple.ts index ce5b28b5a2624..aa1d2d47ea779 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/business-hour/Multiple.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/business-hour/Multiple.ts @@ -225,7 +225,7 @@ export class MultipleBusinessHoursBehavior extends AbstractBusinessHourBehavior } async onDepartmentArchived(department: Pick): Promise { - bhLogger.debug('Processing department archived event on multiple business hours', department); + bhLogger.debug({ msg: 'Processing department archived event on multiple business hours', department }); return this.onDepartmentDisabled(department); } diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterRemoveDepartment.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterRemoveDepartment.ts index 4c692c72edbf3..9e7589ddce7c0 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterRemoveDepartment.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/afterRemoveDepartment.ts @@ -9,7 +9,7 @@ const afterRemoveDepartment = async (options: { agentsId: ILivechatAgent['_id'][]; }) => { if (!options?.department) { - cbLogger.warn('No department found in options', options); + cbLogger.warn({ msg: 'No department found in options', options }); return options; } diff --git a/apps/meteor/ee/server/apps/communication/rest.ts b/apps/meteor/ee/server/apps/communication/rest.ts index a886844692f5f..5f579a0838876 100644 --- a/apps/meteor/ee/server/apps/communication/rest.ts +++ b/apps/meteor/ee/server/apps/communication/rest.ts @@ -85,21 +85,21 @@ export class AppsRestApi { const orchestrator = this._orch; const manager = this._manager; - const handleError = (message: string, e: any) => { + const handleError = (message: string, err: any) => { // when there is no `response` field in the error, it means the request // couldn't even make it to the server - if (!e.hasOwnProperty('response')) { - orchestrator.getRocketChatLogger().warn(message, e.message); + if (!err.hasOwnProperty('response')) { + orchestrator.getRocketChatLogger().warn({ msg: message, err }); return API.v1.internalError('Could not reach the Marketplace'); } - orchestrator.getRocketChatLogger().error(message, e.response.data); + orchestrator.getRocketChatLogger().error({ msg: message, err }); - if (e.response.statusCode >= 500 && e.response.statusCode <= 599) { + if (err.response.statusCode >= 500 && err.response.statusCode <= 599) { return API.v1.internalError(); } - if (e.response.statusCode === 404) { + if (err.response.statusCode === 404) { return API.v1.notFound(); } @@ -148,7 +148,7 @@ export class AppsRestApi { } if (err instanceof z.ZodError) { - orchestrator.getRocketChatLogger().error('Error parsing the Marketplace Apps:', err.issues); + orchestrator.getRocketChatLogger().error({ msg: 'Error validating the response from Marketplace:', err }); return API.v1.failure({ error: i18n.t('Marketplace_Failed_To_Fetch_Apps') }); } @@ -167,7 +167,7 @@ export class AppsRestApi { const categories = await fetchMarketplaceCategories(); return API.v1.success(categories); } catch (err) { - orchestrator.getRocketChatLogger().error('Error getting the categories from the Marketplace:', err); + orchestrator.getRocketChatLogger().error({ msg: 'Error fetching categories from Marketplace:', err }); if (err instanceof MarketplaceConnectionError) { return handleError('Unable to access Marketplace. Does the server has access to the internet?', err); } @@ -177,7 +177,7 @@ export class AppsRestApi { } if (err instanceof z.ZodError) { - orchestrator.getRocketChatLogger().error('Error validating the response from the Marketplace:', err.issues); + orchestrator.getRocketChatLogger().error({ msg: 'Error validating the response from Marketplace:', err }); return API.v1.failure({ error: i18n.t('Marketplace_Failed_To_Fetch_Categories') }); } @@ -268,8 +268,8 @@ export class AppsRestApi { } buff = await response.buffer(); - } catch (e: any) { - orchestrator.getRocketChatLogger().error('Error getting the app from url:', e.response.data); + } catch (err: any) { + orchestrator.getRocketChatLogger().error({ msg: 'Error fetching App from URL:', err }); return API.v1.internalError(); } } else if ('appId' in this.bodyParams && this.bodyParams.appId && this.bodyParams.marketplace && this.bodyParams.version) { @@ -308,7 +308,7 @@ export class AppsRestApi { // Note: marketplace responds with an array of the marketplace info on the app, but it is expected // to always have one element since we are fetching a specific app version. if (!Array.isArray(marketplaceInfo) || marketplaceInfo?.length !== 1) { - orchestrator.getRocketChatLogger().error('Error getting the App information from the Marketplace:', marketplaceInfo); + orchestrator.getRocketChatLogger().error({ msg: 'Error getting app information from marketplace', marketplaceInfo }); throw new Error('Invalid response from the Marketplace'); } @@ -317,7 +317,7 @@ export class AppsRestApi { let message; if (err instanceof Error) { - orchestrator.getRocketChatLogger().error('Error installing app from marketplace: ', err.message, err.cause); + orchestrator.getRocketChatLogger().error({ msg: 'Error installing app from marketplace:', err }); message = err.message; } else { message = err; @@ -453,8 +453,8 @@ export class AppsRestApi { nickname: a.nickname, }; }); - } catch (e) { - orchestrator.getRocketChatLogger().error('Error getting the admins to request an app be installed:', e); + } catch (err) { + orchestrator.getRocketChatLogger().error({ msg: 'Error fetching admins for app request', err }); } const queryParams = new URLSearchParams(); @@ -548,8 +548,8 @@ export class AppsRestApi { return API.v1.failure(); } result = await request.json(); - } catch (e: any) { - orchestrator.getRocketChatLogger().error("Error getting the Bundle's Apps from the Marketplace:", e.response.data); + } catch (err: any) { + orchestrator.getRocketChatLogger().error({ msg: "Error getting the Bundle's Apps from the Marketplace:", err }); return API.v1.internalError(); } @@ -573,7 +573,9 @@ export class AppsRestApi { try { const request = await orchestrator.getMarketplaceClient().fetch(`v1/featured-apps`, { headers }); if (request.status !== 200) { - orchestrator.getRocketChatLogger().error('Error getting the Featured Apps from the Marketplace:', await request.json()); + orchestrator + .getRocketChatLogger() + .error({ msg: 'Error getting the Featured Apps from the Marketplace:', response: await request.json() }); return API.v1.failure(); } result = await request.json(); @@ -611,10 +613,10 @@ export class AppsRestApi { throw new Error(result.error); } return API.v1.success(result); - } catch (e: any) { - orchestrator.getRocketChatLogger().error('Error getting all non sent app requests from the Marketplace:', e.message); + } catch (err: any) { + orchestrator.getRocketChatLogger().error({ msg: 'Error getting the app requests from marketplace', err }); - return API.v1.failure(e.message); + return API.v1.failure(err.message); } }, }, @@ -639,10 +641,10 @@ export class AppsRestApi { throw new Error(result.error); } return API.v1.success(result); - } catch (e: any) { - orchestrator.getRocketChatLogger().error('Error getting the app requests stats from marketplace', e.message); + } catch (err: any) { + orchestrator.getRocketChatLogger().error({ msg: 'Error getting app request stats from marketplace', err }); - return API.v1.failure(e.message); + return API.v1.failure(err.message); } }, }, @@ -675,10 +677,10 @@ export class AppsRestApi { } return API.v1.success(result); - } catch (e: any) { - orchestrator.getRocketChatLogger().error('Error marking app requests as seen', e.message); + } catch (err: any) { + orchestrator.getRocketChatLogger().error({ msg: 'Error marking app requests as seen in marketplace', err }); - return API.v1.failure(e.message); + return API.v1.failure(err.message); } }, }, @@ -713,8 +715,8 @@ export class AppsRestApi { await sendMessagesToAdmins({ msgs }); return API.v1.success(); - } catch (e) { - orchestrator.getRocketChatLogger().error('Error when notifying admins that an user requested an app:', e); + } catch (err) { + orchestrator.getRocketChatLogger().error({ msg: 'Error notifying admins about app request', err }); return API.v1.failure(); } }, @@ -739,7 +741,9 @@ export class AppsRestApi { .getMarketplaceClient() .fetch(`v1/apps/${this.urlParams.id}?appVersion=${this.queryParams.version}`, { headers }); if (request.status !== 200) { - orchestrator.getRocketChatLogger().error('Error getting the App information from the Marketplace:', await request.json()); + orchestrator + .getRocketChatLogger() + .error({ msg: 'Error getting the App from the Marketplace:', response: await request.json() }); return API.v1.failure(); } result = await request.json(); @@ -765,7 +769,9 @@ export class AppsRestApi { headers, }); if (request.status !== 200) { - orchestrator.getRocketChatLogger().error('Error getting the App update info from the Marketplace:', await request.json()); + orchestrator + .getRocketChatLogger() + .error({ msg: 'Error getting the App update from the Marketplace:', response: await request.json() }); return API.v1.failure(); } result = await request.json(); @@ -811,7 +817,9 @@ export class AppsRestApi { }); if (response.status !== 200) { - orchestrator.getRocketChatLogger().error('Error getting the App from the Marketplace:', await response.text()); + orchestrator + .getRocketChatLogger() + .error({ msg: 'Error getting the App from the Marketplace:', response: await response.json() }); return API.v1.failure(); } @@ -822,8 +830,8 @@ export class AppsRestApi { } buff = Buffer.from(await response.arrayBuffer()); - } catch (e: any) { - orchestrator.getRocketChatLogger().error('Error getting the App from the Marketplace:', e.response.data); + } catch (err: any) { + orchestrator.getRocketChatLogger().error({ msg: 'Error getting the App from the Marketplace:', err }); return API.v1.internalError(); } @@ -956,7 +964,7 @@ export class AppsRestApi { } if (!result || statusCode !== 200) { - orchestrator.getRocketChatLogger().error('Error getting the App versions from the Marketplace:', result); + orchestrator.getRocketChatLogger().error({ msg: 'Error getting the App versions from the Marketplace:', result }); return API.v1.failure(); } @@ -994,13 +1002,13 @@ export class AppsRestApi { if (!request.ok) { throw new Error(result.error); } - } catch (e: any) { - orchestrator.getRocketChatLogger().error('Error syncing the App from the Marketplace:', e); + } catch (err: any) { + orchestrator.getRocketChatLogger().error({ msg: 'Error syncing the App from the Marketplace:', err }); return API.v1.internalError(); } if (statusCode !== 200) { - orchestrator.getRocketChatLogger().error('Error syncing the App from the Marketplace:', result); + orchestrator.getRocketChatLogger().error({ msg: 'Error getting the App from the Marketplace during sync:', result }); return API.v1.failure(); } @@ -1057,9 +1065,9 @@ export class AppsRestApi { return API.v1.success({ screenshots: data, }); - } catch (e: any) { - orchestrator.getRocketChatLogger().error('Error getting the screenshots from the Marketplace:', e.message); - return API.v1.failure(e.message); + } catch (err: any) { + orchestrator.getRocketChatLogger().error({ msg: 'Error getting the App screenshots from the Marketplace:', err }); + return API.v1.failure(err.message); } }, }, @@ -1208,7 +1216,7 @@ export class AppsRestApi { response.clusterStatus = clusterStatus[app.getID()]; } } catch (e) { - orchestrator.getRocketChatLogger().warn('App status endpoint: could not fetch status across cluster', e); + orchestrator.getRocketChatLogger().warn({ msg: 'Could not fetch cluster status for app', appId: app.getID(), err: e }); } return API.v1.success(response); diff --git a/apps/meteor/ee/server/apps/marketplace/fetchMarketplaceApps.ts b/apps/meteor/ee/server/apps/marketplace/fetchMarketplaceApps.ts index d3993ad6091d0..6a97ca3d494e2 100644 --- a/apps/meteor/ee/server/apps/marketplace/fetchMarketplaceApps.ts +++ b/apps/meteor/ee/server/apps/marketplace/fetchMarketplaceApps.ts @@ -165,7 +165,7 @@ export async function fetchMarketplaceApps({ endUserID }: FetchMarketplaceAppsPa const response = await request.json(); - Apps.getRocketChatLogger().error('Failed to fetch marketplace apps', response); + Apps.getRocketChatLogger().error({ msg: 'Error fetching marketplace apps', status: request.status, response }); // TODO: Refactor cloud to return a proper error code on unsupported version if (request.status === 426 && 'errorMsg' in response && response.errorMsg === 'unsupported version') { diff --git a/apps/meteor/ee/server/apps/marketplace/fetchMarketplaceCategories.ts b/apps/meteor/ee/server/apps/marketplace/fetchMarketplaceCategories.ts index 847767d6baf12..cbbd7b9441fa9 100644 --- a/apps/meteor/ee/server/apps/marketplace/fetchMarketplaceCategories.ts +++ b/apps/meteor/ee/server/apps/marketplace/fetchMarketplaceCategories.ts @@ -47,7 +47,7 @@ export async function fetchMarketplaceCategories(): Promise { const response = await request.json(); - Apps.getRocketChatLogger().error('Failed to fetch marketplace categories', response); + Apps.getRocketChatLogger().error({ msg: 'Error fetching marketplace categories', status: request.status, response }); // TODO: Refactor cloud to return a proper error code on unsupported version if (request.status === 426 && 'errorMsg' in response && response.errorMsg === 'unsupported version') { diff --git a/apps/meteor/ee/server/configuration/ldap.ts b/apps/meteor/ee/server/configuration/ldap.ts index b68a310a424d9..afa2f05e895fc 100644 --- a/apps/meteor/ee/server/configuration/ldap.ts +++ b/apps/meteor/ee/server/configuration/ldap.ts @@ -82,16 +82,16 @@ Meteor.startup(async () => { settings.watch('LDAP_Groups_To_Rocket_Chat_Teams', (value) => { try { LDAPEEManager.validateLDAPTeamsMappingChanges(value); - } catch (error) { - logger.error(error); + } catch (err) { + logger.error({ err }); } }); settings.watch('LDAP_ABAC_AttributeMap', (value) => { try { LDAPEEManager.validateLDAPABACAttributeMap(value); - } catch (error) { - logger.error(error); + } catch (err) { + logger.error({ err }); } }); diff --git a/apps/meteor/ee/server/lib/ldap/Manager.ts b/apps/meteor/ee/server/lib/ldap/Manager.ts index 6c6d42180a284..a8ec3d84d9769 100644 --- a/apps/meteor/ee/server/lib/ldap/Manager.ts +++ b/apps/meteor/ee/server/lib/ldap/Manager.ts @@ -78,8 +78,8 @@ export class LDAPEEManager extends LDAPManager { if (disableMissingUsers) { await this.disableMissingUsers([...touchedUsers]); } - } catch (error) { - logger.error(error); + } catch (err) { + logger.error({ err }); } ldap.disconnect(); @@ -99,8 +99,8 @@ export class LDAPEEManager extends LDAPManager { } finally { ldap.disconnect(); } - } catch (error) { - logger.error(error); + } catch (err) { + logger.error({ err }); } } @@ -123,8 +123,8 @@ export class LDAPEEManager extends LDAPManager { } finally { ldap.disconnect(); } - } catch (error) { - logger.error(error); + } catch (err) { + logger.error({ err }); } } @@ -145,8 +145,8 @@ export class LDAPEEManager extends LDAPManager { } finally { ldap.disconnect(); } - } catch (error) { - logger.error(error); + } catch (err) { + logger.error({ err }); } } @@ -211,8 +211,8 @@ export class LDAPEEManager extends LDAPManager { } finally { ldap.disconnect(); } - } catch (error) { - logger.error(error); + } catch (err) { + logger.error({ err }); } } @@ -443,9 +443,9 @@ export class LDAPEEManager extends LDAPManager { await addUserToRoom(room._id, user); logger.debug({ msg: 'Synced user channel from LDAP', roomId: room._id, username }); } - } catch (e) { + } catch (err) { logger.debug({ msg: 'Failed to sync user room', username, userChannelName }); - logger.error(e); + logger.error({ err }); } } @@ -692,10 +692,10 @@ export class LDAPEEManager extends LDAPManager { converter.addObjectToMemory(userData, { dn: data.dn, username: this.getLdapUsername(data) }); return userData; }, - endCallback: (error: any): void => { - if (error) { - logger.error(error); - reject(error); + endCallback: (err: any): void => { + if (err) { + logger.error({ err }); + reject(err); return; } diff --git a/apps/meteor/server/database/utils.ts b/apps/meteor/server/database/utils.ts index 68b6d05832a95..e4b95085408cc 100644 --- a/apps/meteor/server/database/utils.ts +++ b/apps/meteor/server/database/utils.ts @@ -36,7 +36,7 @@ export const onceTransactionCommitedSuccessfully = async { try { await EmailMessageHistory.create({ _id: email.messageId, email: emailInboxRecord.email }); void onEmailReceived(email, emailInboxRecord.email, emailInboxRecord.department); - } catch (e: any) { + } catch (err: any) { // In case the email message history has been received by other instance.. - logger.error(e); + logger.error({ err }); } }); diff --git a/apps/meteor/server/lib/ldap/Connection.ts b/apps/meteor/server/lib/ldap/Connection.ts index 4d4ae9eb8e682..a6d273717d668 100644 --- a/apps/meteor/server/lib/ldap/Connection.ts +++ b/apps/meteor/server/lib/ldap/Connection.ts @@ -349,16 +349,16 @@ export class LDAPConnection { let realEntries = 0; return new Promise((resolve, reject) => { - this.client.search(baseDN, searchOptions, (error, res: ldapjs.SearchCallbackResponse) => { - if (error) { - searchLogger.error(error); - reject(error); + this.client.search(baseDN, searchOptions, (err, res: ldapjs.SearchCallbackResponse) => { + if (err) { + searchLogger.error({ err }); + reject(err); return; } - res.on('error', (error) => { - searchLogger.error(error); - reject(error); + res.on('error', (err) => { + searchLogger.error({ err }); + reject(err); }); const entries: T[] = []; @@ -370,8 +370,8 @@ export class LDAPConnection { entries.push(result as T); } realEntries++; - } catch (e) { - searchLogger.error(e); + } catch (err) { + searchLogger.error({ err }); } }); @@ -418,7 +418,7 @@ export class LDAPConnection { } if (!this.options.groupFilterGroupMemberFormat) { - searchLogger.debug(`LDAP Group Filter is enabled but no group member format is set.`); + searchLogger.debug('LDAP Group Filter is enabled but no group member format is set.'); return []; } @@ -527,16 +527,16 @@ export class LDAPConnection { searchLogger.debug({ msg: 'searchOptions', searchOptions, baseDN }); - this.client.search(baseDN, searchOptions, (error: ldapjs.Error | null, res: ldapjs.SearchCallbackResponse): void => { - if (error) { - searchLogger.error(error); - callback(error); + this.client.search(baseDN, searchOptions, (err: ldapjs.Error | null, res: ldapjs.SearchCallbackResponse): void => { + if (err) { + searchLogger.error({ err }); + callback(err); return; } - res.on('error', (error) => { - searchLogger.error(error); - callback(error); + res.on('error', (err) => { + searchLogger.error({ err }); + callback(err); }); const entries: T[] = []; @@ -545,8 +545,8 @@ export class LDAPConnection { try { const result = entryCallback ? entryCallback(entry) : entry; entries.push(result as T); - } catch (e) { - searchLogger.error(e); + } catch (err) { + searchLogger.error({ err }); } }); @@ -591,16 +591,16 @@ export class LDAPConnection { searchLogger.debug({ msg: 'searchOptions', searchOptions, baseDN }); - this.client.search(baseDN, searchOptions, (error: ldapjs.Error | null, res: ldapjs.SearchCallbackResponse): void => { - if (error) { - searchLogger.error(error); - callback(error); + this.client.search(baseDN, searchOptions, (err: ldapjs.Error | null, res: ldapjs.SearchCallbackResponse): void => { + if (err) { + searchLogger.error({ err }); + callback(err); return; } - res.on('error', (error) => { - searchLogger.error(error); - callback(error); + res.on('error', (err) => { + searchLogger.error({ err }); + callback(err); }); let entries: T[] = []; @@ -622,8 +622,8 @@ export class LDAPConnection { ); entries = []; } - } catch (e) { - searchLogger.error(e); + } catch (err) { + searchLogger.error({ err }); } }); @@ -754,24 +754,20 @@ export class LDAPConnection { }; } - private handleConnectionResponse(error: any, response?: any): void { + private handleConnectionResponse(err: any, response?: any): void { if (!this._receivedResponse) { this._receivedResponse = true; - this._connectionCallback(error, response); + this._connectionCallback(err, response); return; } - if (this._connectionTimedOut && !error) { + if (this._connectionTimedOut && !err) { connLogger.info('Received a response after the connection timedout.'); } else { logger.debug('Ignored error/response:'); } - if (error) { - connLogger.debug(error); - } else { - connLogger.debug(response); - } + connLogger.debug({ err, response }); } private initializeConnection(callback: ILDAPCallback): void { @@ -787,7 +783,7 @@ export class LDAPConnection { this.client = ldapjs.createClient(clientOptions); this.client.on('error', (error) => { - connLogger.error(error); + connLogger.error({ err: error }); this.handleConnectionResponse(error, null); }); @@ -809,10 +805,10 @@ export class LDAPConnection { connLogger.info('Starting TLS'); connLogger.debug({ msg: 'tlsOptions', tlsOptions }); - this.client.starttls(tlsOptions, null, (error, response) => { - if (error) { - connLogger.error({ msg: 'TLS connection', error }); - return this.handleConnectionResponse(error, null); + this.client.starttls(tlsOptions, null, (err, response) => { + if (err) { + connLogger.error({ msg: 'TLS connection', err }); + return this.handleConnectionResponse(err, null); } connLogger.info('TLS connected'); diff --git a/apps/meteor/server/lib/ldap/Manager.ts b/apps/meteor/server/lib/ldap/Manager.ts index 9b747c0f78fd9..1dac8ad89ed52 100644 --- a/apps/meteor/server/lib/ldap/Manager.ts +++ b/apps/meteor/server/lib/ldap/Manager.ts @@ -36,8 +36,8 @@ export class LDAPManager { try { await ldap.connect(); ldapUser = await this.findUser(ldap, username, password); - } catch (error) { - logger.error(error); + } catch (err) { + logger.error({ err }); } if (ldapUser === undefined) { @@ -78,8 +78,8 @@ export class LDAPManager { try { await ldap.connect(); ldapUser = await this.findAuthenticatedUser(ldap, username); - } catch (error) { - logger.error(error); + } catch (err) { + logger.error({ err }); } if (ldapUser === undefined) { @@ -108,9 +108,9 @@ export class LDAPManager { try { const ldap = new LDAPConnection(); await ldap.testConnection(); - } catch (error) { - connLogger.error(error); - throw error; + } catch (err) { + connLogger.error({ err }); + throw err; } } @@ -126,9 +126,9 @@ export class LDAPManager { logger.debug({ msg: 'Search results', count: users.length, username: escapedUsername }); throw new Error('User not found'); } - } catch (error) { - logger.error(error); - throw error; + } catch (err) { + logger.error({ err }); + throw err; } } @@ -245,8 +245,8 @@ export class LDAPManager { } } return ldapUser; - } catch (error) { - logger.error(error); + } catch (err) { + logger.error({ err }); } } @@ -276,8 +276,8 @@ export class LDAPManager { } return ldapUser; - } catch (error) { - logger.error(error); + } catch (err) { + logger.error({ err }); } } diff --git a/apps/meteor/server/lib/sendDirectMessageToUsers.ts b/apps/meteor/server/lib/sendDirectMessageToUsers.ts index 644e5161ef903..2183331cedfe0 100644 --- a/apps/meteor/server/lib/sendDirectMessageToUsers.ts +++ b/apps/meteor/server/lib/sendDirectMessageToUsers.ts @@ -25,8 +25,8 @@ export async function sendDirectMessageToUsers( await executeSendMessage(fromId, { rid, msg }); success.push(user._id); - } catch (error) { - SystemLogger.error(error); + } catch (err) { + SystemLogger.error({ err }); } } diff --git a/apps/meteor/server/lib/sendMessagesToAdmins.ts b/apps/meteor/server/lib/sendMessagesToAdmins.ts index 386b8fece3f68..96527f765d7de 100644 --- a/apps/meteor/server/lib/sendMessagesToAdmins.ts +++ b/apps/meteor/server/lib/sendMessagesToAdmins.ts @@ -51,8 +51,8 @@ export async function sendMessagesToAdmins({ await Promise.all( (await getData>(msgs, adminUser)).map((msg) => executeSendMessage(fromId, Object.assign({ rid }, msg))), ); - } catch (error) { - SystemLogger.error(error); + } catch (err) { + SystemLogger.error({ err }); } } diff --git a/apps/meteor/server/methods/sendForgotPasswordEmail.ts b/apps/meteor/server/methods/sendForgotPasswordEmail.ts index 6d7706d25c7ba..e534d2febdbab 100644 --- a/apps/meteor/server/methods/sendForgotPasswordEmail.ts +++ b/apps/meteor/server/methods/sendForgotPasswordEmail.ts @@ -32,8 +32,8 @@ export const sendForgotPasswordEmail = async (to: string): Promise extends EventEmit try { this.registerMethod(method); - } catch (e) { - SystemLogger.error(e); + } catch (err) { + SystemLogger.error({ err }); } } diff --git a/apps/meteor/server/services/messages/lib/oembed/providers.ts b/apps/meteor/server/services/messages/lib/oembed/providers.ts index cb6dfc7cdb013..56a93f485cfe1 100644 --- a/apps/meteor/server/services/messages/lib/oembed/providers.ts +++ b/apps/meteor/server/services/messages/lib/oembed/providers.ts @@ -181,8 +181,8 @@ export const afterParseUrlContent = (data: { data.meta[camelCase(`oembed_${key}`)] = value; } }); - } catch (error) { - SystemLogger.error(error); + } catch (err) { + SystemLogger.error({ err }); } return data; }; diff --git a/apps/meteor/server/services/nps/getAndCreateNpsSurvey.ts b/apps/meteor/server/services/nps/getAndCreateNpsSurvey.ts index f6da6e682b11b..47119692795e2 100644 --- a/apps/meteor/server/services/nps/getAndCreateNpsSurvey.ts +++ b/apps/meteor/server/services/nps/getAndCreateNpsSurvey.ts @@ -62,8 +62,8 @@ export const getAndCreateNpsSurvey = async function getNpsSurvey(npsId: string) }; await Banner.create(banner); - } catch (e) { - SystemLogger.error(e); + } catch (err) { + SystemLogger.error({ err }); return false; } }; diff --git a/apps/meteor/server/services/nps/sendNpsResults.ts b/apps/meteor/server/services/nps/sendNpsResults.ts index 258de3d4169ac..93c591b39d91c 100644 --- a/apps/meteor/server/services/nps/sendNpsResults.ts +++ b/apps/meteor/server/services/nps/sendNpsResults.ts @@ -28,8 +28,8 @@ export const sendNpsResults = async function sendNpsResults(npsId: string, data: body: data, }) ).json(); - } catch (e) { - SystemLogger.error(e); + } catch (err) { + SystemLogger.error({ err }); return false; } }; diff --git a/apps/meteor/server/services/omnichannel-integrations/providers/twilio.ts b/apps/meteor/server/services/omnichannel-integrations/providers/twilio.ts index fe4f5e30241b0..18e99f1ed0a85 100644 --- a/apps/meteor/server/services/omnichannel-integrations/providers/twilio.ts +++ b/apps/meteor/server/services/omnichannel-integrations/providers/twilio.ts @@ -261,7 +261,7 @@ export class Twilio implements ISMSProvider { } if (!authToken || !siteUrl) { - SystemLogger.error(`(Twilio) -> URL or Twilio token not configured.`); + SystemLogger.error('(Twilio) -> URL or Twilio token not configured.'); return false; } diff --git a/apps/meteor/server/services/omnichannel/queue.ts b/apps/meteor/server/services/omnichannel/queue.ts index d522c78d7daaa..68acf2401e961 100644 --- a/apps/meteor/server/services/omnichannel/queue.ts +++ b/apps/meteor/server/services/omnichannel/queue.ts @@ -228,7 +228,7 @@ export class OmnichannelQueue implements IOmnichannelQueue { private async processWaitingQueue(department: string | null, inquiry: InquiryWithAgentInfo) { const queue = department || 'Public'; - queueLogger.debug(`Processing inquiry ${inquiry._id} from queue ${queue}`); + queueLogger.debug({ msg: 'Processing inquiry', inquiry: inquiry._id, queue }); const { defaultAgent } = inquiry; const roomFromDb = await LivechatRooms.findOneById(inquiry.rid); diff --git a/ee/packages/media-calls/src/internal/SignalProcessor.ts b/ee/packages/media-calls/src/internal/SignalProcessor.ts index 3ca1e5952b638..9b292f94d2299 100644 --- a/ee/packages/media-calls/src/internal/SignalProcessor.ts +++ b/ee/packages/media-calls/src/internal/SignalProcessor.ts @@ -104,7 +104,7 @@ export class GlobalSignalProcessor { await agent.processSignal(call, signal); } catch (e) { - logger.error(e); + logger.error({ err: e }); throw e; } } diff --git a/ee/packages/media-calls/src/server/CallDirector.ts b/ee/packages/media-calls/src/server/CallDirector.ts index 1c762d0c480bf..705a5df1a528d 100644 --- a/ee/packages/media-calls/src/server/CallDirector.ts +++ b/ee/packages/media-calls/src/server/CallDirector.ts @@ -240,7 +240,7 @@ class MediaCallDirector { agent: IMediaCallAgent, ): Promise { if (!agent.oppositeAgent) { - logger.error('Unable to transfer calls without a reference to the opposite agent.'); + logger.error({ msg: 'Unable to transfer calls without a reference to the opposite agent.' }); return; } diff --git a/ee/packages/omnichannel-services/src/OmnichannelTranscript.ts b/ee/packages/omnichannel-services/src/OmnichannelTranscript.ts index bd8ba3ace1f47..f24ad369c42e2 100644 --- a/ee/packages/omnichannel-services/src/OmnichannelTranscript.ts +++ b/ee/packages/omnichannel-services/src/OmnichannelTranscript.ts @@ -162,33 +162,54 @@ export class OmnichannelTranscript extends ServiceClass implements IOmnichannelT } if (!isFileAttachment(attachment)) { - this.log.error( - `Invalid attachment type ${(attachment as { type?: string }).type} for file ${attachment.title} in room ${message.rid}!`, - ); + this.log.error({ + msg: 'Invalid attachment type for file in room', + attachmentType: (attachment as { type?: string }).type, + title: attachment.title, + rid: message.rid, + }); // ignore other types of attachments continue; } if (!isFileImageAttachment(attachment)) { - this.log.error(`Invalid attachment type ${attachment.type} for file ${attachment.title} in room ${message.rid}!`); + this.log.error({ + msg: 'Invalid attachment type for file in room', + attachmentType: attachment.type, + title: attachment.title, + rid: message.rid, + }); // ignore other types of attachments files.push({ name: attachment.title }); continue; } if (!this.worker.isMimeTypeValid(attachment.image_type)) { - this.log.error(`Invalid mime type ${attachment.image_type} for file ${attachment.title} in room ${message.rid}!`); + this.log.error({ + msg: 'Invalid mime type for file in room', + mimeType: attachment.image_type, + title: attachment.title, + rid: message.rid, + }); // ignore invalid mime types files.push({ name: attachment.title }); continue; } let file = message.files?.map((v) => ({ _id: v._id, name: v.name })).find((file) => file.name === attachment.title); if (!file) { - this.log.warn(`File ${attachment.title} not found in room ${message.rid}!`); + this.log.warn({ + msg: 'File not found in room', + title: attachment.title, + rid: message.rid, + }); // For some reason, when an image is uploaded from clipboard, it doesn't have a file :( // So, we'll try to get the FILE_ID from the `title_link` prop which has the format `/file-upload/FILE_ID/FILE_NAME` using a regex const fileId = attachment.title_link?.match(/\/file-upload\/(.*)\/.*/)?.[1]; if (!fileId) { - this.log.error(`File ${attachment.title} not found in room ${message.rid}!`); + this.log.error({ + msg: 'File not found in room', + title: attachment.title, + rid: message.rid, + }); // ignore attachments without file files.push({ name: attachment.title }); continue; @@ -197,7 +218,11 @@ export class OmnichannelTranscript extends ServiceClass implements IOmnichannelT } if (!file) { - this.log.warn(`File ${attachment.title} not found in room ${message.rid}!`); + this.log.warn({ + msg: 'File not found in room', + title: attachment.title, + rid: message.rid, + }); // ignore attachments without file files.push({ name: attachment.title }); continue; @@ -205,7 +230,11 @@ export class OmnichannelTranscript extends ServiceClass implements IOmnichannelT const uploadedFile = await Uploads.findOneById(file._id); if (!uploadedFile) { - this.log.error(`Uploaded file ${file._id} not found in room ${message.rid}!`); + this.log.error({ + msg: 'Uploaded file not found in room', + fileId: file._id, + rid: message.rid, + }); // ignore attachments without file files.push({ name: file.name }); continue; @@ -214,13 +243,13 @@ export class OmnichannelTranscript extends ServiceClass implements IOmnichannelT try { const fileBuffer = await uploadService.getFileBuffer({ file: uploadedFile }); files.push({ name: file.name, buffer: fileBuffer, extension: uploadedFile.extension }); - } catch (e: unknown) { - this.log.error(`Failed to get file ${file._id}`, e); + } catch (err: unknown) { + this.log.error({ msg: 'Failed to fetch file buffer', err }); // Push empty buffer so parser processes this as "unsupported file" files.push({ name: file.name }); // TODO: this is a NATS error message, even when we shouldn't tie it, since it's the only way we have right now we'll live with it for a while - if ((e as Error).message === 'MAX_PAYLOAD_EXCEEDED') { + if ((err as Error).message === 'MAX_PAYLOAD_EXCEEDED') { this.log.error( `File is too big to be processed by NATS. See NATS config for allowing bigger messages to be sent between services`, ); @@ -246,9 +275,17 @@ export class OmnichannelTranscript extends ServiceClass implements IOmnichannelT } async workOnPdf({ details }: { details: WorkDetailsWithSource }): Promise { - this.log.info(`Processing transcript for room ${details.rid} by user ${details.userId} - Received from queue`); + this.log.info({ + msg: 'Processing transcript received from queue', + rid: details.rid, + userId: details.userId, + }); if (this.maxNumberOfConcurrentJobs <= this.currentJobNumber) { - this.log.error(`Processing transcript for room ${details.rid} by user ${details.userId} - Too many concurrent jobs, queuing again`); + this.log.error({ + msg: 'Processing transcript exceeded concurrent jobs limit', + rid: details.rid, + userId: details.userId, + }); throw new Error('retry'); } this.currentJobNumber++; @@ -277,7 +314,11 @@ export class OmnichannelTranscript extends ServiceClass implements IOmnichannelT throw new Error('room-not-found'); } if (room.pdfTranscriptFileId) { - this.log.info(`Processing transcript for room ${details.rid} by user ${details.userId} - PDF already exists`); + this.log.info({ + msg: 'Processing transcript skipped because PDF already exists', + rid: details.rid, + userId: details.userId, + }); return; } const messages = await this.getMessagesFromRoom({ rid: room._id }); @@ -340,14 +381,23 @@ export class OmnichannelTranscript extends ServiceClass implements IOmnichannelT } private async pdfFailed({ details, e, i18n }: { details: WorkDetailsWithSource; e: Error; i18n: i18n }): Promise { - this.log.error(`Transcript for room ${details.rid} by user ${details.userId} - Failed: ${e.message}`); + this.log.error({ + msg: 'Transcript generation failed', + rid: details.rid, + userId: details.userId, + err: e, + }); const room = await LivechatRooms.findOneById>(details.rid, { projection: { _id: 1 } }); if (!room) { return; } const { rid } = await roomService.createDirectMessage({ to: details.userId, from: 'rocket.cat' }); - this.log.info(`Transcript for room ${details.rid} by user ${details.userId} - Sending error message to user`); + this.log.info({ + msg: 'Transcript error message being sent to user', + rid: details.rid, + userId: details.userId, + }); await messageService.sendMessage({ fromId: 'rocket.cat', rid, @@ -400,13 +450,21 @@ export class OmnichannelTranscript extends ServiceClass implements IOmnichannelT rocketCatFile: IUpload; i18n: i18n; }): Promise { - this.log.info(`Transcript for room ${details.rid} by user ${details.userId} - Complete`); + this.log.info({ + msg: 'Transcript completed successfully', + rid: details.rid, + userId: details.userId, + }); // Send the file to the livechat room where this was requested, to keep it in context try { await LivechatRooms.setPdfTranscriptFileIdById(details.rid, transcriptFile._id); - this.log.info(`Transcript for room ${details.rid} by user ${details.userId} - Sending success message to user`); + this.log.info({ + msg: 'Transcript success message being sent to user', + rid: details.rid, + userId: details.userId, + }); const result = await Promise.allSettled([ uploadService.sendFileMessage({ roomId: details.rid, @@ -433,7 +491,12 @@ export class OmnichannelTranscript extends ServiceClass implements IOmnichannelT throw e.reason; } } catch (err) { - this.log.error({ msg: `Transcript for room ${details.rid} by user ${details.userId} - Failed to send message`, err }); + this.log.error({ + msg: 'Transcript failed to send message', + rid: details.rid, + userId: details.userId, + err, + }); } } } diff --git a/packages/omni-core/src/visitor/create.ts b/packages/omni-core/src/visitor/create.ts index f3c8518c70872..8e91b7c49efa3 100644 --- a/packages/omni-core/src/visitor/create.ts +++ b/packages/omni-core/src/visitor/create.ts @@ -107,7 +107,7 @@ export const registerGuest = makeFunction( }); if (!upsertedLivechatVisitor) { - logger.debug(`No visitor found after upsert`); + logger.debug('No visitor found after upsert'); return null; } diff --git a/packages/ui-client/src/lib/callbacks/Callbacks.ts b/packages/ui-client/src/lib/callbacks/Callbacks.ts index 8f62fe3e5f791..3ad2e842fdf17 100644 --- a/packages/ui-client/src/lib/callbacks/Callbacks.ts +++ b/packages/ui-client/src/lib/callbacks/Callbacks.ts @@ -67,7 +67,7 @@ export class Callbacks< const wrapCallback = (callback: Callback) => async (item: unknown, constant?: unknown): Promise => { - this.logger?.debug(`Executing callback with id ${callback.id} for hook ${callback.hook}`); + this.logger?.debug({ msg: 'Executing callback for hook', callbackId: callback.id, hook: callback.hook }); return (await this.runOne(callback, item, constant)) ?? item; }; From e22b852f643b11f45a88600d079e7bb0617e2430 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Thu, 22 Jan 2026 15:32:54 -0600 Subject: [PATCH 010/131] chore: Strict log types (#38296) --- packages/logger/src/getPino.ts | 14 ------- packages/logger/src/index.ts | 72 +++++++++------------------------- 2 files changed, 18 insertions(+), 68 deletions(-) diff --git a/packages/logger/src/getPino.ts b/packages/logger/src/getPino.ts index 745188022d286..c9727b084673b 100644 --- a/packages/logger/src/getPino.ts +++ b/packages/logger/src/getPino.ts @@ -1,22 +1,8 @@ import { pino } from 'pino'; -import type { Logger } from 'pino'; - -// add support to multiple params on the log commands, i.e.: -// logger.info('user', await Meteor.userAsync()); // will print: {"level":30,"time":1629814080968,"msg":"user {\"username\": \"foo\"}"} -function logMethod(this: Logger, args: unknown[], method: any): void { - if (args.length === 2 && args[0] instanceof Error) { - return method.apply(this, args); - } - if (args.length > 1) { - args[0] = `${args[0]}${' %j'.repeat(args.length - 1)}`; - } - return method.apply(this, args); -} const infoLevel = process.env.LESS_INFO_LOGS ? 20 : 35; const mainPino = pino({ - hooks: { logMethod }, customLevels: { http: infoLevel, method: infoLevel, diff --git a/packages/logger/src/index.ts b/packages/logger/src/index.ts index f62e9d69a5da9..b731cb0982459 100644 --- a/packages/logger/src/index.ts +++ b/packages/logger/src/index.ts @@ -49,76 +49,40 @@ export class Logger { this.logger.level = newLevel; } - log(obj: T, ...args: any[]): void; - - log(obj: unknown, ...args: any[]): void; - - log(msg: string, ...args: any[]): void { - this.logger.info(msg, ...args); + log(msg: object | string): void { + this.logger.info(msg); } - debug(obj: T, ...args: any[]): void; - - debug(obj: unknown, ...args: any[]): void; - - debug(msg: string, ...args: any[]): void { - this.logger.debug(msg, ...args); + debug(msg: object | string): void { + this.logger.debug(msg); } - info(obj: T, ...args: any[]): void; - - info(obj: unknown, ...args: any[]): void; - - info(msg: string, ...args: any[]): void { - this.logger.info(msg, ...args); + info(msg: object | string): void { + this.logger.info(msg); } - startup(obj: T, ...args: any[]): void; - - startup(obj: unknown, ...args: any[]): void; - - startup(msg: string, ...args: any[]): void { - this.logger.startup(msg, ...args); + startup(msg: object | string): void { + this.logger.startup(msg); } - success(obj: T, ...args: any[]): void; - - success(obj: unknown, ...args: any[]): void; - - success(msg: string, ...args: any[]): void { - this.logger.info(msg, ...args); + success(msg: object | string): void { + this.logger.info(msg); } - warn(obj: T, ...args: any[]): void; - - warn(obj: unknown, ...args: any[]): void; - - warn(msg: string, ...args: any[]): void { - this.logger.warn(msg, ...args); + warn(msg: object | string): void { + this.logger.warn(msg); } - error(obj: T, ...args: any[]): void; - - error(obj: unknown, ...args: any[]): void; - - error(msg: string, ...args: any[]): void { - this.logger.error(msg, ...args); + error(msg: object | string): void { + this.logger.error(msg); } - method(obj: T, ...args: any[]): void; - - method(obj: unknown, ...args: any[]): void; - - method(msg: string, ...args: any[]): void { - this.logger.method(msg, ...args); + method(msg: object | string): void { + this.logger.method(msg); } - subscription(obj: T, ...args: any[]): void; - - subscription(obj: unknown, ...args: any[]): void; - - subscription(msg: string, ...args: any[]): void { - this.logger.subscription(msg, ...args); + subscription(msg: object | string): void { + this.logger.subscription(msg); } fatal(err: unknown, ...args: any[]): void { From c107092c5f70adcf287423f11e5781ebcf309ea2 Mon Sep 17 00:00:00 2001 From: Ricardo Garim Date: Fri, 23 Jan 2026 08:39:43 -0300 Subject: [PATCH 011/131] fix: restore banner dismissal fallback for cloud announcements (#38282) --- .changeset/proud-laws-melt.md | 5 +++++ .../core-apps/cloudAnnouncements.module.ts | 22 +++++++++++++------ .../model-typings/src/models/IBannersModel.ts | 2 ++ packages/models/src/models/Banners.ts | 4 ++++ 4 files changed, 26 insertions(+), 7 deletions(-) create mode 100644 .changeset/proud-laws-melt.md diff --git a/.changeset/proud-laws-melt.md b/.changeset/proud-laws-melt.md new file mode 100644 index 0000000000000..c15fdee5b3c4c --- /dev/null +++ b/.changeset/proud-laws-melt.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes dismissed banner popups reappearing after server restart. diff --git a/apps/meteor/server/modules/core-apps/cloudAnnouncements.module.ts b/apps/meteor/server/modules/core-apps/cloudAnnouncements.module.ts index e087d5452ba99..6e314b90459e2 100644 --- a/apps/meteor/server/modules/core-apps/cloudAnnouncements.module.ts +++ b/apps/meteor/server/modules/core-apps/cloudAnnouncements.module.ts @@ -1,8 +1,9 @@ import { Banner } from '@rocket.chat/core-services'; import type { IUiKitCoreApp, UiKitCoreAppPayload } from '@rocket.chat/core-services'; -import type { Cloud, IBanner, IUser } from '@rocket.chat/core-typings'; +import type { Cloud, IUser } from '@rocket.chat/core-typings'; import { Banners } from '@rocket.chat/models'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; +import { isTruthy } from '@rocket.chat/tools'; import type * as UiKit from '@rocket.chat/ui-kit'; import { getWorkspaceAccessToken } from '../../../app/cloud/server'; @@ -44,7 +45,7 @@ export class CloudAnnouncementsModule implements IUiKitCoreApp { async viewClosed(payload: UiKitCoreAppPayload): Promise { const { - payload: { view: { viewId } = {} }, + payload: { view: { viewId, id } = {} }, user: { _id: userId } = {}, } = payload; @@ -52,7 +53,7 @@ export class CloudAnnouncementsModule implements IUiKitCoreApp { throw new Error('invalid user'); } - if (!viewId) { + if (!id && !viewId) { throw new Error('invalid view'); } @@ -60,11 +61,18 @@ export class CloudAnnouncementsModule implements IUiKitCoreApp { throw new Error('invalid triggerId'); } - await Banner.dismiss(userId, viewId); + // For backwards compatibility: we prefer to use viewId, but some legacy banners + // may only have id. We fetch all matching banners and prioritize viewId match. + const bannerIds = [viewId, id].filter((bannerId) => isTruthy(bannerId)); + const banners = await Banners.findByIds(bannerIds).toArray(); + const announcement = banners.find((b) => b._id === viewId) || banners.find((b) => b._id === id); + if (!announcement) { + throw new Error('Banner not found'); + } - const announcement = await Banners.findOneById>(viewId, { projection: { surface: 1 } }); + await Banner.dismiss(userId, announcement._id); - const type = announcement?.surface === 'banner' ? 'banner.close' : 'modal.close'; + const type = announcement.surface === 'banner' ? 'banner.close' : 'modal.close'; // for viewClosed we just need to let Cloud know that the banner was closed, no need to wait for the response @@ -74,7 +82,7 @@ export class CloudAnnouncementsModule implements IUiKitCoreApp { type, triggerId: payload.triggerId, appId: payload.appId, - viewId, + viewId: announcement._id, }; } diff --git a/packages/model-typings/src/models/IBannersModel.ts b/packages/model-typings/src/models/IBannersModel.ts index e0706c2b184ab..fd54f4fd97621 100644 --- a/packages/model-typings/src/models/IBannersModel.ts +++ b/packages/model-typings/src/models/IBannersModel.ts @@ -11,4 +11,6 @@ export interface IBannersModel extends IBaseModel { disable(bannerId: string): Promise; createOrUpdate(banner: Optional): Promise; + + findByIds(bannerIds: string[]): FindCursor; } diff --git a/packages/models/src/models/Banners.ts b/packages/models/src/models/Banners.ts index 4dd35b27a1631..ffad398c162d0 100644 --- a/packages/models/src/models/Banners.ts +++ b/packages/models/src/models/Banners.ts @@ -71,4 +71,8 @@ export class BannersRaw extends BaseRaw implements IBannersModel { return this.updateOne({ _id: bannerId }, { $set: { active: true, ...doc } }, { upsert: true }); } + + findByIds(bannerIds: string[]): FindCursor { + return this.find({ _id: { $in: bannerIds } }); + } } From d9d212c55899517a580e2f12038b7a68787056c0 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com> Date: Fri, 23 Jan 2026 16:01:58 -0300 Subject: [PATCH 012/131] chore: rename slackbridge JS files to TS (#38311) --- .../server/{RocketAdapter.js => RocketAdapter.ts} | 5 +++++ .../app/slackbridge/server/{SlackAPI.js => SlackAPI.ts} | 4 ++++ .../server/{SlackAdapter.js => SlackAdapter.ts} | 6 ++++++ .../server/{slackbridge.js => slackbridge.ts} | 9 +++++++-- ...dge_import.server.js => slackbridge_import.server.ts} | 4 ++++ 5 files changed, 26 insertions(+), 2 deletions(-) rename apps/meteor/app/slackbridge/server/{RocketAdapter.js => RocketAdapter.ts} (98%) rename apps/meteor/app/slackbridge/server/{SlackAPI.js => SlackAPI.ts} (95%) rename apps/meteor/app/slackbridge/server/{SlackAdapter.js => SlackAdapter.ts} (99%) rename apps/meteor/app/slackbridge/server/{slackbridge.js => slackbridge.ts} (93%) rename apps/meteor/app/slackbridge/server/{slackbridge_import.server.js => slackbridge_import.server.ts} (88%) diff --git a/apps/meteor/app/slackbridge/server/RocketAdapter.js b/apps/meteor/app/slackbridge/server/RocketAdapter.ts similarity index 98% rename from apps/meteor/app/slackbridge/server/RocketAdapter.js rename to apps/meteor/app/slackbridge/server/RocketAdapter.ts index b6a0b1ccbcfec..87f549a07eb08 100644 --- a/apps/meteor/app/slackbridge/server/RocketAdapter.js +++ b/apps/meteor/app/slackbridge/server/RocketAdapter.ts @@ -1,3 +1,8 @@ +// This is a JS File that was renamed to TS so it won't lose its git history when converted to TS +// TODO: Remove the following lint/ts instructions when the file gets properly converted +/* eslint-disable @typescript-eslint/naming-convention */ +/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ +// @ts-nocheck import util from 'util'; import { Messages, Rooms, Users } from '@rocket.chat/models'; diff --git a/apps/meteor/app/slackbridge/server/SlackAPI.js b/apps/meteor/app/slackbridge/server/SlackAPI.ts similarity index 95% rename from apps/meteor/app/slackbridge/server/SlackAPI.js rename to apps/meteor/app/slackbridge/server/SlackAPI.ts index 540aa3b911605..e9338ce5792e2 100644 --- a/apps/meteor/app/slackbridge/server/SlackAPI.js +++ b/apps/meteor/app/slackbridge/server/SlackAPI.ts @@ -1,3 +1,7 @@ +// This is a JS File that was renamed to TS so it won't lose its git history when converted to TS +// TODO: Remove the following lint/ts instructions when the file gets properly converted +/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ +// @ts-nocheck import { serverFetch as fetch } from '@rocket.chat/server-fetch'; export class SlackAPI { diff --git a/apps/meteor/app/slackbridge/server/SlackAdapter.js b/apps/meteor/app/slackbridge/server/SlackAdapter.ts similarity index 99% rename from apps/meteor/app/slackbridge/server/SlackAdapter.js rename to apps/meteor/app/slackbridge/server/SlackAdapter.ts index 3783416c8d4d8..2bc5d56da88b3 100644 --- a/apps/meteor/app/slackbridge/server/SlackAdapter.js +++ b/apps/meteor/app/slackbridge/server/SlackAdapter.ts @@ -1,3 +1,9 @@ +// This is a JS File that was renamed to TS so it won't lose its git history when converted to TS +// TODO: Remove the following lint/ts instructions when the file gets properly converted +/* eslint-disable @typescript-eslint/no-empty-function */ +/* eslint-disable @typescript-eslint/naming-convention */ +/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ +// @ts-nocheck import http from 'http'; import https from 'https'; import url from 'url'; diff --git a/apps/meteor/app/slackbridge/server/slackbridge.js b/apps/meteor/app/slackbridge/server/slackbridge.ts similarity index 93% rename from apps/meteor/app/slackbridge/server/slackbridge.js rename to apps/meteor/app/slackbridge/server/slackbridge.ts index 75409f4314ceb..13455b8068508 100644 --- a/apps/meteor/app/slackbridge/server/slackbridge.js +++ b/apps/meteor/app/slackbridge/server/slackbridge.ts @@ -1,7 +1,12 @@ +// This is a JS File that was renamed to TS so it won't lose its git history when converted to TS +// TODO: Remove the following lint/ts instructions when the file gets properly converted +/* eslint-disable @typescript-eslint/no-floating-promises */ +/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ +// @ts-nocheck import { debounce } from 'lodash'; -import RocketAdapter from './RocketAdapter.js'; -import SlackAdapter from './SlackAdapter.js'; +import RocketAdapter from './RocketAdapter'; +import SlackAdapter from './SlackAdapter'; import { classLogger, connLogger } from './logger'; import { settings } from '../../settings/server'; diff --git a/apps/meteor/app/slackbridge/server/slackbridge_import.server.js b/apps/meteor/app/slackbridge/server/slackbridge_import.server.ts similarity index 88% rename from apps/meteor/app/slackbridge/server/slackbridge_import.server.js rename to apps/meteor/app/slackbridge/server/slackbridge_import.server.ts index 6e7117af976a2..7eda03f908c44 100644 --- a/apps/meteor/app/slackbridge/server/slackbridge_import.server.js +++ b/apps/meteor/app/slackbridge/server/slackbridge_import.server.ts @@ -1,3 +1,7 @@ +// This is a JS File that was renamed to TS so it won't lose its git history when converted to TS +// TODO: Remove the following lint/ts instructions when the file gets properly converted +/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ +// @ts-nocheck import { Rooms, Users } from '@rocket.chat/models'; import { Random } from '@rocket.chat/random'; import { Match } from 'meteor/check'; From 6d9bced28babf7f88ccb3ff5ecaca911d220c0e8 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Fri, 23 Jan 2026 17:01:57 -0300 Subject: [PATCH 013/131] test: Missing spec (#38293) --- apps/meteor/.mocharc.js | 2 +- apps/meteor/jest.config.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/meteor/.mocharc.js b/apps/meteor/.mocharc.js index a2230bb7dd5ff..0e9b30dffa699 100644 --- a/apps/meteor/.mocharc.js +++ b/apps/meteor/.mocharc.js @@ -10,7 +10,7 @@ module.exports = { ...base, // see https://github.com/mochajs/mocha/issues/3916 exit: true, spec: [ - 'lib/callbacks.spec.ts', + 'server/lib/callbacks.spec.ts', 'server/lib/ldap/*.spec.ts', 'server/lib/ldap/**/*.spec.ts', 'server/lib/dataExport/**/*.spec.ts', diff --git a/apps/meteor/jest.config.ts b/apps/meteor/jest.config.ts index 01d1fc9559519..6c13cd68ca135 100644 --- a/apps/meteor/jest.config.ts +++ b/apps/meteor/jest.config.ts @@ -19,6 +19,7 @@ export default { moduleNameMapper: { '^react($|/.+)': '/node_modules/react$1', + '^react-virtuoso($|/.+)': '/node_modules/react-virtuoso$1', '^react-dom($|/.+)': '/node_modules/react-dom$1', '^react-i18next($|/.+)': '/node_modules/react-i18next$1', '^@rocket.chat/(.+)': '/node_modules/@rocket.chat/$1', From 34580783730522be685fe1a5fdb87afd0f623e19 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Fri, 23 Jan 2026 18:19:41 -0300 Subject: [PATCH 014/131] refactor: Do not use JavaScript modules from `@rocket.chat/fuselage-tokens` (#38310) --- apps/meteor/app/livechat/server/lib/sendTranscript.ts | 2 +- .../meteor/client/components/Sidebar/SidebarGenericItem.tsx | 6 ------ .../admin/engagementDashboard/users/ContentForDays.tsx | 2 +- apps/meteor/client/views/room/Header/icons/Encrypted.tsx | 2 +- .../externals/rocket.chat/fuselage-tokens/colors.d.ts | 6 ------ 5 files changed, 3 insertions(+), 15 deletions(-) delete mode 100644 apps/meteor/definition/externals/rocket.chat/fuselage-tokens/colors.d.ts diff --git a/apps/meteor/app/livechat/server/lib/sendTranscript.ts b/apps/meteor/app/livechat/server/lib/sendTranscript.ts index 8a6f861907488..11ca540ab87bf 100644 --- a/apps/meteor/app/livechat/server/lib/sendTranscript.ts +++ b/apps/meteor/app/livechat/server/lib/sendTranscript.ts @@ -9,7 +9,7 @@ import { isFileImageAttachment, type AtLeast, } from '@rocket.chat/core-typings'; -import colors from '@rocket.chat/fuselage-tokens/colors'; +import colors from '@rocket.chat/fuselage-tokens/colors.json'; import { Logger } from '@rocket.chat/logger'; import { MessageTypes } from '@rocket.chat/message-types'; import { LivechatRooms, Messages, Uploads, Users } from '@rocket.chat/models'; diff --git a/apps/meteor/client/components/Sidebar/SidebarGenericItem.tsx b/apps/meteor/client/components/Sidebar/SidebarGenericItem.tsx index 4f62cda740cf1..1aee93a52c350 100644 --- a/apps/meteor/client/components/Sidebar/SidebarGenericItem.tsx +++ b/apps/meteor/client/components/Sidebar/SidebarGenericItem.tsx @@ -1,5 +1,4 @@ import { Box, SidebarItem } from '@rocket.chat/fuselage'; -import type colors from '@rocket.chat/fuselage-tokens/colors'; import type { ReactElement, ReactNode } from 'react'; import { memo } from 'react'; @@ -8,11 +7,6 @@ type SidebarGenericItemProps = { active?: boolean; featured?: boolean; children: ReactNode; - customColors?: { - default: (typeof colors)[string]; - hover: (typeof colors)[string]; - active: (typeof colors)[string]; - }; externalUrl?: boolean; }; diff --git a/apps/meteor/client/views/admin/engagementDashboard/users/ContentForDays.tsx b/apps/meteor/client/views/admin/engagementDashboard/users/ContentForDays.tsx index 3265533b5b196..01c622829e6be 100644 --- a/apps/meteor/client/views/admin/engagementDashboard/users/ContentForDays.tsx +++ b/apps/meteor/client/views/admin/engagementDashboard/users/ContentForDays.tsx @@ -1,6 +1,6 @@ import { ResponsiveBar } from '@nivo/bar'; import { Box, Flex, IconButton, Margins, Skeleton } from '@rocket.chat/fuselage'; -import colors from '@rocket.chat/fuselage-tokens/colors'; +import colors from '@rocket.chat/fuselage-tokens/colors.json'; import moment from 'moment'; import type { ReactElement } from 'react'; import { useMemo } from 'react'; diff --git a/apps/meteor/client/views/room/Header/icons/Encrypted.tsx b/apps/meteor/client/views/room/Header/icons/Encrypted.tsx index a4386d6b86336..65ffd3ecac502 100644 --- a/apps/meteor/client/views/room/Header/icons/Encrypted.tsx +++ b/apps/meteor/client/views/room/Header/icons/Encrypted.tsx @@ -1,5 +1,5 @@ import type { IRoom } from '@rocket.chat/core-typings'; -import colors from '@rocket.chat/fuselage-tokens/colors'; +import colors from '@rocket.chat/fuselage-tokens/colors.json'; import { HeaderState } from '@rocket.chat/ui-client'; import { useSetting } from '@rocket.chat/ui-contexts'; import { memo } from 'react'; diff --git a/apps/meteor/definition/externals/rocket.chat/fuselage-tokens/colors.d.ts b/apps/meteor/definition/externals/rocket.chat/fuselage-tokens/colors.d.ts deleted file mode 100644 index d9d4090972a92..0000000000000 --- a/apps/meteor/definition/externals/rocket.chat/fuselage-tokens/colors.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -declare module '@rocket.chat/fuselage-tokens/colors' { - const Colors: { - [key: string]: string; - }; - export = Colors; -} From 08b586df3c71f983f2ae54d2b2e48979253f4a03 Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Fri, 23 Jan 2026 19:38:21 -0300 Subject: [PATCH 015/131] test: Reorg omnichannel admin page objects (#37972) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Júlia Jaeger Foresti <60678893+juliajforesti@users.noreply.github.com> --- .../CustomUserStatusRow.tsx | 10 +- .../views/omnichannel/agents/AgentEdit.tsx | 10 +- .../omnichannel/agents/AgentInfoAction.tsx | 2 +- .../agents/AgentsTable/AgentsTable.tsx | 4 +- .../agents/AgentsTable/AgentsTableRow.tsx | 2 +- .../agents/hooks/useRemoveAgent.tsx | 10 +- .../businessHours/BusinessHoursRow.tsx | 2 +- .../businessHours/BusinessHoursTable.tsx | 2 +- .../modals/CannedResponsesTable.tsx | 2 +- .../customFields/CustomFieldsPage.tsx | 4 +- .../customFields/CustomFieldsTable.tsx | 4 +- .../DepartmentAgentsTable/AgentRow.tsx | 2 +- .../DepartmentAgentsTable.tsx | 5 +- .../departments/DepartmentTags.tsx | 9 +- .../DepartmentsTable/DepartmentItemMenu.tsx | 3 +- .../DepartmentsTable/DepartmentsTable.tsx | 2 +- .../RemoveDepartmentModal.tsx | 2 +- .../departments/EditDepartment.tsx | 36 +-- .../chats/ChatsTable/ChatsTableFilter.tsx | 10 +- .../chats/ChatsTable/ChatsTableRow.tsx | 2 +- .../chats/ChatsTable/RemoveChatButton.tsx | 10 +- .../directory/contacts/ContactTable.tsx | 2 +- .../directory/contacts/ContactTableRow.tsx | 11 +- .../omnichannel/managers/ManagersTable.tsx | 4 +- .../modals/EnterpriseDepartmentsModal.tsx | 7 +- .../omnichannel/modals/ForwardChatModal.tsx | 2 +- .../modals/ReturnChatQueueModal.tsx | 3 +- .../omnichannel/monitors/MonitorsTable.tsx | 14 +- .../priorities/PrioritiesTable.tsx | 2 +- .../priorities/PrioritiesTableRow.tsx | 2 +- .../priorities/PriorityEditForm.tsx | 6 +- .../sidebar/OmnichannelSidebar.tsx | 2 +- .../views/omnichannel/slaPolicies/SlaEdit.tsx | 4 +- .../omnichannel/slaPolicies/SlaTable.tsx | 4 +- .../client/views/omnichannel/tags/TagEdit.tsx | 12 +- .../views/omnichannel/tags/TagsTable.tsx | 4 +- .../omnichannel/triggers/TriggersRow.tsx | 2 +- .../omnichannel/triggers/TriggersTable.tsx | 2 +- .../views/omnichannel/units/UnitEdit.tsx | 16 +- .../views/omnichannel/units/UnitTableRow.tsx | 3 +- .../views/omnichannel/units/UnitsTable.tsx | 2 +- .../views/omnichannel/units/useRemoveUnit.tsx | 10 +- apps/meteor/tests/e2e/administration.spec.ts | 10 +- .../omnichannel/omnichannel-agents.spec.ts | 105 ++++---- .../omnichannel-appearance.spec.ts | 12 +- .../omnichannel-assign-room-tags.spec.ts | 30 +-- ...nichannel-auto-onhold-chat-closing.spec.ts | 3 +- ...nnel-auto-transfer-unanswered-chat.spec.ts | 3 +- .../omnichannel-business-hours.spec.ts | 52 ++-- ...nichannel-canned-responses-sidebar.spec.ts | 3 +- ...omnichannel-canned-responses-usage.spec.ts | 3 +- ...nel-changing-room-priority-and-sla.spec.ts | 3 +- .../omnichannel-chat-history.spec.ts | 11 +- .../omnichannel-close-chat.spec.ts | 3 +- .../omnichannel-close-inquiry.spec.ts | 3 +- ...annel-contact-center-chats-filters.spec.ts | 14 +- .../omnichannel-contact-center-chats.spec.ts | 38 +-- ...mnichannel-contact-center-contacts.spec.ts | 132 +++++----- ...omnichannel-contact-center-filters.spec.ts | 232 +++++++++--------- .../omnichannel-contact-info.spec.ts | 14 +- ...mnichannel-contact-unknown-callout.spec.ts | 3 +- .../omnichannel-custom-fields.spec.ts | 34 ++- .../omnichannel-departaments-ce.spec.ts | 26 +- .../omnichannel-departaments.spec.ts | 90 ++++--- .../omnichannel-livechat-api.spec.ts | 9 +- ...channel-livechat-avatar-visibility.spec.ts | 3 +- .../omnichannel-livechat-background.spec.ts | 2 +- .../omnichannel-livechat-department.spec.ts | 3 +- .../omnichannel-livechat-fileupload.spec.ts | 3 +- ...ichannel-livechat-hide-expand-chat.spec.ts | 5 +- .../omnichannel-livechat-logo.spec.ts | 2 +- ...nnel-livechat-message-bubble-color.spec.ts | 3 +- ...hat-queue-management-autoselection.spec.ts | 3 +- ...ichannel-livechat-queue-management.spec.ts | 3 +- ...channel-livechat-tab-communication.spec.ts | 3 +- .../omnichannel-livechat-watermark.spec.ts | 4 +- .../omnichannel-livechat-widget.spec.ts | 2 +- .../omnichannel/omnichannel-livechat.spec.ts | 3 +- .../omnichannel-manager-role.spec.ts | 81 +++--- .../omnichannel/omnichannel-manager.spec.ts | 17 +- .../omnichannel-manual-selection.spec.ts | 2 +- .../omnichannel-monitor-department.spec.ts | 30 +-- .../omnichannel-monitor-role.spec.ts | 32 +-- .../omnichannel/omnichannel-monitors.spec.ts | 59 ++--- .../omnichannel-priorities-sidebar.spec.ts | 35 ++- .../omnichannel-priorities.spec.ts | 60 +++-- .../omnichannel/omnichannel-reports.spec.ts | 2 +- .../omnichannel-send-pdf-transcript.spec.ts | 3 +- .../omnichannel-send-transcript.spec.ts | 3 +- .../omnichannel-sla-policies-sidebar.spec.ts | 39 ++- .../omnichannel-sla-policies.spec.ts | 24 +- .../e2e/omnichannel/omnichannel-tags.spec.ts | 83 +++---- .../omnichannel/omnichannel-takeChat.spec.ts | 3 +- ...hannel-triggers-after-registration.spec.ts | 3 +- ...nichannel-triggers-open-by-visitor.spec.ts | 3 +- ...omnichannel-triggers-setDepartment.spec.ts | 3 +- .../omnichannel-triggers-time-on-site.spec.ts | 3 +- .../omnichannel/omnichannel-triggers.spec.ts | 25 +- .../e2e/omnichannel/omnichannel-units.spec.ts | 147 +++++------ .../tests/e2e/page-objects/admin-rooms.ts | 2 +- .../fragments/admin-flextab-emoji.ts | 29 +-- .../fragments/edit-contact-flaxtab.ts | 33 +++ .../fragments/edit-room-flextab.ts | 46 +++- .../fragments/edit-user-flextab.ts | 4 - .../e2e/page-objects/fragments/flextab.ts | 31 ++- .../page-objects/fragments/home-content.ts | 4 - .../fragments/home-omnichannel-content.ts | 28 +-- .../tests/e2e/page-objects/fragments/index.ts | 1 - .../e2e/page-objects/fragments/listbox.ts | 18 +- .../tests/e2e/page-objects/fragments/menu.ts | 4 + .../fragments/modals/confirm-delete-modal.ts | 21 +- .../fragments/modals/create-new-modal.ts | 2 +- .../page-objects/fragments/modals/index.ts | 3 + .../omnichannel-contact-review-modal.ts | 2 +- .../omnichannel-delete-contact-modal.ts | 22 ++ .../omnichannel-reset-priorities-modal.ts | 18 ++ .../omnichannel-return-to-queue-modal.ts | 18 ++ .../modals/omnichannel-transfer-chat-modal.ts | 2 +- .../fragments/modals/upsell-modal.ts | 6 + .../fragments/omnichannel-sidenav.ts | 77 ------ .../fragments/room-info-flextab.ts | 31 +++ .../e2e/page-objects/fragments/sidebar.ts | 74 ++++++ .../tests/e2e/page-objects/fragments/table.ts | 17 ++ .../e2e/page-objects/home-omnichannel.ts | 49 ++-- apps/meteor/tests/e2e/page-objects/index.ts | 12 - .../omnichannel-administration.ts | 14 -- .../e2e/page-objects/omnichannel-agents.ts | 122 --------- .../omnichannel-business-hours.ts | 75 ------ ...mnichannel-contact-center-chats-filters.ts | 21 -- .../omnichannel-contact-center-chats.ts | 98 -------- .../page-objects/omnichannel-contacts-list.ts | 159 ------------ .../page-objects/omnichannel-custom-fields.ts | 50 ---- .../page-objects/omnichannel-departments.ts | 164 ------------- .../e2e/page-objects/omnichannel-info.ts | 46 ---- .../omnichannel-manage-contact.ts | 45 ---- .../e2e/page-objects/omnichannel-manager.ts | 53 ---- .../e2e/page-objects/omnichannel-monitors.ts | 38 --- .../page-objects/omnichannel-priorities.ts | 57 ----- .../e2e/page-objects/omnichannel-room-info.ts | 75 ------ .../e2e/page-objects/omnichannel-section.ts | 17 -- .../page-objects/omnichannel-sla-policies.ts | 73 ------ .../e2e/page-objects/omnichannel-tags.ts | 71 ------ .../e2e/page-objects/omnichannel-triggers.ts | 138 ----------- .../e2e/page-objects/omnichannel-units.ts | 99 -------- .../e2e/page-objects/omnichannel/index.ts | 19 ++ .../omnichannel/omnichannel-admin.ts | 46 ++++ .../omnichannel/omnichannel-agents.ts | 117 +++++++++ .../omnichannel/omnichannel-business-hours.ts | 66 +++++ .../omnichannel-canned-responses.ts | 4 +- .../omnichannel-contact-center/index.ts | 2 + .../omnichannel-contact-center-chats.ts | 179 ++++++++++++++ .../omnichannel-contact-center-contacts.ts | 45 ++++ .../omnichannel-contact-center.ts | 13 + .../omnichannel/omnichannel-custom-fields.ts | 50 ++++ .../omnichannel/omnichannel-departments.ts | 134 ++++++++++ .../omnichannel/omnichannel-info.ts | 46 ++++ .../omnichannel-livechat-appearance.ts | 8 +- .../omnichannel-livechat-embedded.ts | 0 .../{ => omnichannel}/omnichannel-livechat.ts | 2 +- .../omnichannel/omnichannel-manager.ts | 41 ++++ .../omnichannel/omnichannel-monitors.ts | 50 ++++ .../omnichannel/omnichannel-priorities.ts | 51 ++++ .../{ => omnichannel}/omnichannel-reports.ts | 0 .../{ => omnichannel}/omnichannel-settings.ts | 14 +- .../omnichannel/omnichannel-sla-policies.ts | 54 ++++ .../omnichannel/omnichannel-tags.ts | 52 ++++ .../omnichannel-transcript.ts | 15 +- .../omnichannel/omnichannel-triggers.ts | 127 ++++++++++ .../omnichannel/omnichannel-units.ts | 96 ++++++++ packages/i18n/src/locales/en.i18n.json | 1 + 170 files changed, 2362 insertions(+), 2615 deletions(-) create mode 100644 apps/meteor/tests/e2e/page-objects/fragments/edit-contact-flaxtab.ts create mode 100644 apps/meteor/tests/e2e/page-objects/fragments/modals/omnichannel-delete-contact-modal.ts create mode 100644 apps/meteor/tests/e2e/page-objects/fragments/modals/omnichannel-reset-priorities-modal.ts create mode 100644 apps/meteor/tests/e2e/page-objects/fragments/modals/omnichannel-return-to-queue-modal.ts delete mode 100644 apps/meteor/tests/e2e/page-objects/fragments/omnichannel-sidenav.ts create mode 100644 apps/meteor/tests/e2e/page-objects/fragments/room-info-flextab.ts create mode 100644 apps/meteor/tests/e2e/page-objects/fragments/table.ts delete mode 100644 apps/meteor/tests/e2e/page-objects/omnichannel-administration.ts delete mode 100644 apps/meteor/tests/e2e/page-objects/omnichannel-agents.ts delete mode 100644 apps/meteor/tests/e2e/page-objects/omnichannel-business-hours.ts delete mode 100644 apps/meteor/tests/e2e/page-objects/omnichannel-contact-center-chats-filters.ts delete mode 100644 apps/meteor/tests/e2e/page-objects/omnichannel-contact-center-chats.ts delete mode 100644 apps/meteor/tests/e2e/page-objects/omnichannel-contacts-list.ts delete mode 100644 apps/meteor/tests/e2e/page-objects/omnichannel-custom-fields.ts delete mode 100644 apps/meteor/tests/e2e/page-objects/omnichannel-departments.ts delete mode 100644 apps/meteor/tests/e2e/page-objects/omnichannel-info.ts delete mode 100644 apps/meteor/tests/e2e/page-objects/omnichannel-manage-contact.ts delete mode 100644 apps/meteor/tests/e2e/page-objects/omnichannel-manager.ts delete mode 100644 apps/meteor/tests/e2e/page-objects/omnichannel-monitors.ts delete mode 100644 apps/meteor/tests/e2e/page-objects/omnichannel-priorities.ts delete mode 100644 apps/meteor/tests/e2e/page-objects/omnichannel-room-info.ts delete mode 100644 apps/meteor/tests/e2e/page-objects/omnichannel-section.ts delete mode 100644 apps/meteor/tests/e2e/page-objects/omnichannel-sla-policies.ts delete mode 100644 apps/meteor/tests/e2e/page-objects/omnichannel-tags.ts delete mode 100644 apps/meteor/tests/e2e/page-objects/omnichannel-triggers.ts delete mode 100644 apps/meteor/tests/e2e/page-objects/omnichannel-units.ts create mode 100644 apps/meteor/tests/e2e/page-objects/omnichannel/index.ts create mode 100644 apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-admin.ts create mode 100644 apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-agents.ts create mode 100644 apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-business-hours.ts rename apps/meteor/tests/e2e/page-objects/{ => omnichannel}/omnichannel-canned-responses.ts (93%) create mode 100644 apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-contact-center/index.ts create mode 100644 apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-contact-center/omnichannel-contact-center-chats.ts create mode 100644 apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-contact-center/omnichannel-contact-center-contacts.ts create mode 100644 apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-contact-center/omnichannel-contact-center.ts create mode 100644 apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-custom-fields.ts create mode 100644 apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-departments.ts create mode 100644 apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-info.ts rename apps/meteor/tests/e2e/page-objects/{ => omnichannel}/omnichannel-livechat-appearance.ts (81%) rename apps/meteor/tests/e2e/page-objects/{ => omnichannel}/omnichannel-livechat-embedded.ts (100%) rename apps/meteor/tests/e2e/page-objects/{ => omnichannel}/omnichannel-livechat.ts (99%) create mode 100644 apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-manager.ts create mode 100644 apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-monitors.ts create mode 100644 apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-priorities.ts rename apps/meteor/tests/e2e/page-objects/{ => omnichannel}/omnichannel-reports.ts (100%) rename apps/meteor/tests/e2e/page-objects/{ => omnichannel}/omnichannel-settings.ts (77%) create mode 100644 apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-sla-policies.ts create mode 100644 apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-tags.ts rename apps/meteor/tests/e2e/page-objects/{ => omnichannel}/omnichannel-transcript.ts (62%) create mode 100644 apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-triggers.ts create mode 100644 apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-units.ts diff --git a/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusTable/CustomUserStatusRow.tsx b/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusTable/CustomUserStatusRow.tsx index de550a7c05424..5903e26bfad2c 100644 --- a/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusTable/CustomUserStatusRow.tsx +++ b/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusTable/CustomUserStatusRow.tsx @@ -17,15 +17,7 @@ const CustomUserStatusRow = ({ status, onClick }: CustomUserStatusRowProps): Rea const { t } = useTranslation(); return ( - onClick(_id)} - onClick={(): void => onClick(_id)} - tabIndex={0} - role='link' - action - qa-user-id={_id} - > + onClick(_id)} onClick={() => onClick(_id)} tabIndex={0} action> diff --git a/apps/meteor/client/views/omnichannel/agents/AgentEdit.tsx b/apps/meteor/client/views/omnichannel/agents/AgentEdit.tsx index 91532ebbfb91e..d7efcbd7b5ac7 100644 --- a/apps/meteor/client/views/omnichannel/agents/AgentEdit.tsx +++ b/apps/meteor/client/views/omnichannel/agents/AgentEdit.tsx @@ -164,13 +164,7 @@ const AgentEdit = ({ agentData, agentDepartments }: AgentEditProps) => { name='status' control={control} render={({ field }) => ( - )} /> @@ -185,7 +179,7 @@ const AgentEdit = ({ agentData, agentDepartments }: AgentEditProps) => { - diff --git a/apps/meteor/client/views/omnichannel/agents/AgentInfoAction.tsx b/apps/meteor/client/views/omnichannel/agents/AgentInfoAction.tsx index 71ee19bf36bba..1d922f530adb0 100644 --- a/apps/meteor/client/views/omnichannel/agents/AgentInfoAction.tsx +++ b/apps/meteor/client/views/omnichannel/agents/AgentInfoAction.tsx @@ -9,7 +9,7 @@ type AgentInfoActionProps = { } & Omit, 'is'>; const AgentInfoAction = ({ icon, label, ...props }: AgentInfoActionProps) => ( - ); diff --git a/apps/meteor/client/views/omnichannel/agents/AgentsTable/AgentsTable.tsx b/apps/meteor/client/views/omnichannel/agents/AgentsTable/AgentsTable.tsx index e8bd1faff7e08..7ea8bb3d9719c 100644 --- a/apps/meteor/client/views/omnichannel/agents/AgentsTable/AgentsTable.tsx +++ b/apps/meteor/client/views/omnichannel/agents/AgentsTable/AgentsTable.tsx @@ -96,9 +96,9 @@ const AgentsTable = () => { )} {isSuccess && data?.users.length > 0 && ( <> - + {headers} - + {data?.users.map((user) => )} diff --git a/apps/meteor/client/views/omnichannel/agents/AgentsTable/AgentsTableRow.tsx b/apps/meteor/client/views/omnichannel/agents/AgentsTable/AgentsTableRow.tsx index e4381f1aab523..4cff09defc94e 100644 --- a/apps/meteor/client/views/omnichannel/agents/AgentsTable/AgentsTableRow.tsx +++ b/apps/meteor/client/views/omnichannel/agents/AgentsTable/AgentsTableRow.tsx @@ -27,7 +27,7 @@ const AgentsTableRow = ({ const handleDelete = useRemoveAgent(_id); return ( - router.navigate(`/omnichannel/agents/info/${_id}`)}> + router.navigate(`/omnichannel/agents/info/${_id}`)}> {username && } diff --git a/apps/meteor/client/views/omnichannel/agents/hooks/useRemoveAgent.tsx b/apps/meteor/client/views/omnichannel/agents/hooks/useRemoveAgent.tsx index f32facb2287ac..670d2eb802312 100644 --- a/apps/meteor/client/views/omnichannel/agents/hooks/useRemoveAgent.tsx +++ b/apps/meteor/client/views/omnichannel/agents/hooks/useRemoveAgent.tsx @@ -30,15 +30,7 @@ export const useRemoveAgent = (uid: ILivechatAgent['_id']) => { } }; - setModal( - setModal()} - confirmText={t('Delete')} - />, - ); + setModal( setModal()} confirmText={t('Delete')} />); }); return handleDelete; diff --git a/apps/meteor/client/views/omnichannel/businessHours/BusinessHoursRow.tsx b/apps/meteor/client/views/omnichannel/businessHours/BusinessHoursRow.tsx index 836acdcaaa6ce..a9249f4eb5f33 100644 --- a/apps/meteor/client/views/omnichannel/businessHours/BusinessHoursRow.tsx +++ b/apps/meteor/client/views/omnichannel/businessHours/BusinessHoursRow.tsx @@ -27,7 +27,7 @@ const BusinessHoursRow = ({ _id, name, timezone, workHours, active, type }: Seri const openDays = useMemo(() => workHours.filter(({ open }) => !!open).map(({ day }) => day), [workHours]); return ( - + {name || t('Default')} {t(timezone.name as TranslationKey)} {openDays.join(', ')} diff --git a/apps/meteor/client/views/omnichannel/businessHours/BusinessHoursTable.tsx b/apps/meteor/client/views/omnichannel/businessHours/BusinessHoursTable.tsx index 8d25bbce31142..239995250419c 100644 --- a/apps/meteor/client/views/omnichannel/businessHours/BusinessHoursTable.tsx +++ b/apps/meteor/client/views/omnichannel/businessHours/BusinessHoursTable.tsx @@ -65,7 +65,7 @@ const BusinessHoursTable = () => { {isSuccess && data?.businessHours.length === 0 && } {isSuccess && data?.businessHours.length > 0 && ( <> - + {headers} {data?.businessHours.map((businessHour) => )} diff --git a/apps/meteor/client/views/omnichannel/cannedResponses/modals/CannedResponsesTable.tsx b/apps/meteor/client/views/omnichannel/cannedResponses/modals/CannedResponsesTable.tsx index 32dc9728572a5..40b982c809c9b 100644 --- a/apps/meteor/client/views/omnichannel/cannedResponses/modals/CannedResponsesTable.tsx +++ b/apps/meteor/client/views/omnichannel/cannedResponses/modals/CannedResponsesTable.tsx @@ -152,7 +152,7 @@ const CannedResponsesTable = () => { {headers} {data?.cannedResponses.map(({ _id, shortcut, scope, createdBy, _createdAt, tags = [] }) => ( - + {shortcut} {defaultOptions[scope as Scope]} diff --git a/apps/meteor/client/views/omnichannel/customFields/CustomFieldsPage.tsx b/apps/meteor/client/views/omnichannel/customFields/CustomFieldsPage.tsx index e5688517642e5..1fcf092ede7c3 100644 --- a/apps/meteor/client/views/omnichannel/customFields/CustomFieldsPage.tsx +++ b/apps/meteor/client/views/omnichannel/customFields/CustomFieldsPage.tsx @@ -21,9 +21,7 @@ const CustomFieldsPage = () => { - + diff --git a/apps/meteor/client/views/omnichannel/customFields/CustomFieldsTable.tsx b/apps/meteor/client/views/omnichannel/customFields/CustomFieldsTable.tsx index fcc62b50ca908..d249d816e4e70 100644 --- a/apps/meteor/client/views/omnichannel/customFields/CustomFieldsTable.tsx +++ b/apps/meteor/client/views/omnichannel/customFields/CustomFieldsTable.tsx @@ -104,11 +104,11 @@ const CustomFieldsTable = () => { {isSuccess && data.customFields.length > 0 && ( <> - + {headers} {data.customFields.map(({ label, _id, scope, visibility }) => ( - + {_id} {label} {scope === 'visitor' ? t('Visitor') : t('Room')} diff --git a/apps/meteor/client/views/omnichannel/departments/DepartmentAgentsTable/AgentRow.tsx b/apps/meteor/client/views/omnichannel/departments/DepartmentAgentsTable/AgentRow.tsx index bfc14fc58b889..e732a84b0e80d 100644 --- a/apps/meteor/client/views/omnichannel/departments/DepartmentAgentsTable/AgentRow.tsx +++ b/apps/meteor/client/views/omnichannel/departments/DepartmentAgentsTable/AgentRow.tsx @@ -19,7 +19,7 @@ const AgentRow = ({ index, agent, register, onRemove }: AgentRowProps) => { const { t } = useTranslation(); return ( - + diff --git a/apps/meteor/client/views/omnichannel/departments/DepartmentAgentsTable/DepartmentAgentsTable.tsx b/apps/meteor/client/views/omnichannel/departments/DepartmentAgentsTable/DepartmentAgentsTable.tsx index b6cd94e4cb2f6..0cfdf7e2bb3cf 100644 --- a/apps/meteor/client/views/omnichannel/departments/DepartmentAgentsTable/DepartmentAgentsTable.tsx +++ b/apps/meteor/client/views/omnichannel/departments/DepartmentAgentsTable/DepartmentAgentsTable.tsx @@ -26,22 +26,19 @@ function DepartmentAgentsTable({ control, register, 'aria-labelledby': ariaLabel return ( <> - - + {t('Name')} {t('Count')} {t('Order')} {t('Remove')} - {page.map((agent, index) => ( remove(index)} /> ))} - ) => setTagText(e.currentTarget.value)} {...props} /> - diff --git a/apps/meteor/client/views/omnichannel/departments/DepartmentsTable/DepartmentItemMenu.tsx b/apps/meteor/client/views/omnichannel/departments/DepartmentsTable/DepartmentItemMenu.tsx index e6a0b555b4e67..253ad9d10c4b6 100644 --- a/apps/meteor/client/views/omnichannel/departments/DepartmentsTable/DepartmentItemMenu.tsx +++ b/apps/meteor/client/views/omnichannel/departments/DepartmentsTable/DepartmentItemMenu.tsx @@ -18,6 +18,7 @@ type DepartmentItemMenuProps = { archived: boolean; }; +// TODO: Use MenuV2 instead of Menu const DepartmentItemMenu = ({ department, archived }: DepartmentItemMenuProps): ReactElement => { const t = useTranslation(); const queryClient = useQueryClient(); @@ -91,7 +92,7 @@ const DepartmentItemMenu = ({ department, archived }: DepartmentItemMenuProps): disabled: !departmentRemovalEnabled, }, }; - return ; + return ; }; export default DepartmentItemMenu; diff --git a/apps/meteor/client/views/omnichannel/departments/DepartmentsTable/DepartmentsTable.tsx b/apps/meteor/client/views/omnichannel/departments/DepartmentsTable/DepartmentsTable.tsx index a6cafe6dcfa4c..a0f5a72e50743 100644 --- a/apps/meteor/client/views/omnichannel/departments/DepartmentsTable/DepartmentsTable.tsx +++ b/apps/meteor/client/views/omnichannel/departments/DepartmentsTable/DepartmentsTable.tsx @@ -121,7 +121,7 @@ const DepartmentsTable = ({ archived }: { archived: boolean }) => { )} {isSuccess && data?.departments.length > 0 && ( <> - + {headers} {data.departments.map((department: Omit) => ( diff --git a/apps/meteor/client/views/omnichannel/departments/DepartmentsTable/RemoveDepartmentModal.tsx b/apps/meteor/client/views/omnichannel/departments/DepartmentsTable/RemoveDepartmentModal.tsx index 155b1c48d8521..38d704a7cf7bb 100644 --- a/apps/meteor/client/views/omnichannel/departments/DepartmentsTable/RemoveDepartmentModal.tsx +++ b/apps/meteor/client/views/omnichannel/departments/DepartmentsTable/RemoveDepartmentModal.tsx @@ -41,13 +41,13 @@ const RemoveDepartmentModal = ({ _id = '', name, reset, onClose }: RemoveDepartm title={t('Delete_Department?')} onClose={onClose} variant='danger' - data-qa-id='delete-department-modal' confirmDisabled={text !== name} > {t('Are_you_sure_delete_department')} ) => setText(event.currentTarget.value)} /> diff --git a/apps/meteor/client/views/omnichannel/departments/EditDepartment.tsx b/apps/meteor/client/views/omnichannel/departments/EditDepartment.tsx index 895706a3962cc..f29d60eace55f 100644 --- a/apps/meteor/client/views/omnichannel/departments/EditDepartment.tsx +++ b/apps/meteor/client/views/omnichannel/departments/EditDepartment.tsx @@ -161,7 +161,6 @@ function EditDepartment({ data, id, title, allowedToForwardData }: EditDepartmen - {t('Name')} @@ -169,7 +168,6 @@ function EditDepartment({ data, id, title, allowedToForwardData }: EditDepartmen {errors.name && ( - + {errors.name?.message} )} - {t('Description')} - + - - + {t('Show_on_registration_page')} - {t('Email')} @@ -209,7 +199,6 @@ function EditDepartment({ data, id, title, allowedToForwardData }: EditDepartmen } placeholder={t('Email')} @@ -221,19 +210,17 @@ function EditDepartment({ data, id, title, allowedToForwardData }: EditDepartmen /> {errors.email && ( - + {errors.email?.message} )} - {t('Show_on_offline_page')} - {t('Livechat_DepartmentOfflineMessageToChannel')} @@ -243,7 +230,6 @@ function EditDepartment({ data, id, title, allowedToForwardData }: EditDepartmen render={({ field: { value, onChange } }) => ( - {hasLicense && ( <> @@ -274,7 +259,6 @@ function EditDepartment({ data, id, title, allowedToForwardData }: EditDepartmen )} /> - - - - {t('List_of_departments_for_forward')} @@ -338,7 +319,6 @@ function EditDepartment({ data, id, title, allowedToForwardData }: EditDepartmen {t('List_of_departments_for_forward_description')} - {t('Fallback_forward_department')} - {t('Unit')} @@ -389,21 +368,19 @@ function EditDepartment({ data, id, title, allowedToForwardData }: EditDepartmen /> {errors.unit && ( - + {errors.unit?.message} )} )} - {t('Request_tag_before_closing_chat')} - {t('Conversation_closing_tags')} )} - {t('Accept_receive_inquiry_no_online_agents')} @@ -439,9 +415,7 @@ function EditDepartment({ data, id, title, allowedToForwardData }: EditDepartmen - - {t('Agents')} diff --git a/apps/meteor/client/views/omnichannel/directory/chats/ChatsTable/ChatsTableFilter.tsx b/apps/meteor/client/views/omnichannel/directory/chats/ChatsTable/ChatsTableFilter.tsx index 731b6d690756d..cb519d2c151f0 100644 --- a/apps/meteor/client/views/omnichannel/directory/chats/ChatsTable/ChatsTableFilter.tsx +++ b/apps/meteor/client/views/omnichannel/directory/chats/ChatsTable/ChatsTableFilter.tsx @@ -32,15 +32,7 @@ const ChatsTableFilter = () => { } }; - setModal( - setModal(null)} - confirmText={t('Delete')} - />, - ); + setModal( setModal(null)} confirmText={t('Delete')} />); }); const menuItems = [ diff --git a/apps/meteor/client/views/omnichannel/directory/chats/ChatsTable/ChatsTableRow.tsx b/apps/meteor/client/views/omnichannel/directory/chats/ChatsTable/ChatsTableRow.tsx index ebecb53fe8b91..e42127f5e75d0 100644 --- a/apps/meteor/client/views/omnichannel/directory/chats/ChatsTable/ChatsTableRow.tsx +++ b/apps/meteor/client/views/omnichannel/directory/chats/ChatsTable/ChatsTableRow.tsx @@ -46,7 +46,7 @@ const ChatsTableRow = (room: IOmnichannelRoomWithDepartment) => { ); return ( - onRowClick(_id)} action qa-user-id={_id}> + onRowClick(_id)} action> {fname} diff --git a/apps/meteor/client/views/omnichannel/directory/chats/ChatsTable/RemoveChatButton.tsx b/apps/meteor/client/views/omnichannel/directory/chats/ChatsTable/RemoveChatButton.tsx index 2b355f667f13b..ce568b3462c97 100644 --- a/apps/meteor/client/views/omnichannel/directory/chats/ChatsTable/RemoveChatButton.tsx +++ b/apps/meteor/client/views/omnichannel/directory/chats/ChatsTable/RemoveChatButton.tsx @@ -31,15 +31,7 @@ const RemoveChatButton = ({ _id }: RemoveChatButtonProps) => { setModal(null); }; - setModal( - setModal(null)} - confirmText={t('Delete')} - />, - ); + setModal( setModal(null)} confirmText={t('Delete')} />); }); return ; diff --git a/apps/meteor/client/views/omnichannel/directory/contacts/ContactTable.tsx b/apps/meteor/client/views/omnichannel/directory/contacts/ContactTable.tsx index f6187ef4294e9..d467cc5244195 100644 --- a/apps/meteor/client/views/omnichannel/directory/contacts/ContactTable.tsx +++ b/apps/meteor/client/views/omnichannel/directory/contacts/ContactTable.tsx @@ -121,7 +121,7 @@ function ContactTable() { )} {isSuccess && data?.contacts.length > 0 && ( <> - + {headers} {data?.contacts.map((contact) => )} diff --git a/apps/meteor/client/views/omnichannel/directory/contacts/ContactTableRow.tsx b/apps/meteor/client/views/omnichannel/directory/contacts/ContactTableRow.tsx index c6189f796cfec..602be761ef59f 100644 --- a/apps/meteor/client/views/omnichannel/directory/contacts/ContactTableRow.tsx +++ b/apps/meteor/client/views/omnichannel/directory/contacts/ContactTableRow.tsx @@ -31,16 +31,7 @@ const ContactTableRow = ({ _id, name, contactManager, lastChat, channels }: ILiv ); return ( - onRowClick(_id)} - > + onRowClick(_id)}> {name} {latestChannel?.details && ( diff --git a/apps/meteor/client/views/omnichannel/managers/ManagersTable.tsx b/apps/meteor/client/views/omnichannel/managers/ManagersTable.tsx index 3830008cfa4d3..f2201dd005a56 100644 --- a/apps/meteor/client/views/omnichannel/managers/ManagersTable.tsx +++ b/apps/meteor/client/views/omnichannel/managers/ManagersTable.tsx @@ -85,7 +85,7 @@ const ManagersTable = () => { setText(event.target.value)} /> )} {isLoading && ( - + {headers} @@ -107,7 +107,7 @@ const ManagersTable = () => { {headers} {data.users.map((user) => ( - + diff --git a/apps/meteor/client/views/omnichannel/modals/EnterpriseDepartmentsModal.tsx b/apps/meteor/client/views/omnichannel/modals/EnterpriseDepartmentsModal.tsx index f864f57f57667..1561619827da6 100644 --- a/apps/meteor/client/views/omnichannel/modals/EnterpriseDepartmentsModal.tsx +++ b/apps/meteor/client/views/omnichannel/modals/EnterpriseDepartmentsModal.tsx @@ -21,6 +21,8 @@ import { useTranslation } from 'react-i18next'; import { useExternalLink } from '../../../hooks/useExternalLink'; import { useCheckoutUrl } from '../../admin/subscription/hooks/useCheckoutUrl'; +// TODO: use `GenericModal` instead of creating a new modal from scratch +// This seems a upSell modal for enterprise feature const EnterpriseDepartmentsModal = ({ closeModal }: { closeModal: () => void }): ReactElement => { const { t } = useTranslation(); const router = useRouter(); @@ -42,13 +44,13 @@ const EnterpriseDepartmentsModal = ({ closeModal }: { closeModal: () => void }): useOutsideClick([ref], onClose); return ( - + {t('Premium_capability')} {t('Departments')} - + @@ -60,7 +62,6 @@ const EnterpriseDepartmentsModal = ({ closeModal }: { closeModal: () => void }): - diff --git a/apps/meteor/client/views/omnichannel/modals/ForwardChatModal.tsx b/apps/meteor/client/views/omnichannel/modals/ForwardChatModal.tsx index a0b59d9add935..3bc17602ce755 100644 --- a/apps/meteor/client/views/omnichannel/modals/ForwardChatModal.tsx +++ b/apps/meteor/client/views/omnichannel/modals/ForwardChatModal.tsx @@ -137,7 +137,7 @@ const ForwardChatModal = ({ onForward, onCancel, room, ...props }: ForwardChatMo - + diff --git a/apps/meteor/client/views/omnichannel/modals/ReturnChatQueueModal.tsx b/apps/meteor/client/views/omnichannel/modals/ReturnChatQueueModal.tsx index 9107d41a18c4b..09c912dec0d48 100644 --- a/apps/meteor/client/views/omnichannel/modals/ReturnChatQueueModal.tsx +++ b/apps/meteor/client/views/omnichannel/modals/ReturnChatQueueModal.tsx @@ -16,11 +16,12 @@ type ReturnChatQueueModalProps = { onCancel: () => void; }; +// TODO: use `GenericModal` instead of creating a new modal from scratch const ReturnChatQueueModal = ({ onCancel, onMoveChat, ...props }: ReturnChatQueueModalProps) => { const { t } = useTranslation(); return ( - + {t('Return_to_the_queue')} diff --git a/apps/meteor/client/views/omnichannel/monitors/MonitorsTable.tsx b/apps/meteor/client/views/omnichannel/monitors/MonitorsTable.tsx index 3aca318d9a480..d790c15b22c43 100644 --- a/apps/meteor/client/views/omnichannel/monitors/MonitorsTable.tsx +++ b/apps/meteor/client/views/omnichannel/monitors/MonitorsTable.tsx @@ -109,15 +109,7 @@ const MonitorsTable = () => { setModal(); }; - setModal( - setModal()} - confirmText={t('Delete')} - />, - ); + setModal( setModal()} confirmText={t('Delete')} />); }; const headers = useMemo( @@ -172,11 +164,11 @@ const MonitorsTable = () => { )} {isSuccess && data.monitors.length > 0 && ( <> - + {headers} {data.monitors?.map((monitor) => ( - + {monitor.name} {monitor.username} {monitor.email} diff --git a/apps/meteor/client/views/omnichannel/priorities/PrioritiesTable.tsx b/apps/meteor/client/views/omnichannel/priorities/PrioritiesTable.tsx index 010ea55e31496..5f8629d90ac16 100644 --- a/apps/meteor/client/views/omnichannel/priorities/PrioritiesTable.tsx +++ b/apps/meteor/client/views/omnichannel/priorities/PrioritiesTable.tsx @@ -42,7 +42,7 @@ export const PrioritiesTable = ({ priorities, onRowClick, isLoading }: Prioritie )} {priorities?.length === 0 && } {priorities && priorities?.length > 0 && ( - + {headers} {priorities?.map(({ _id, name, i18n, sortItem, dirty }) => ( diff --git a/apps/meteor/client/views/omnichannel/priorities/PrioritiesTableRow.tsx b/apps/meteor/client/views/omnichannel/priorities/PrioritiesTableRow.tsx index 19ad5262ac24f..1d298dbfde8dd 100644 --- a/apps/meteor/client/views/omnichannel/priorities/PrioritiesTableRow.tsx +++ b/apps/meteor/client/views/omnichannel/priorities/PrioritiesTableRow.tsx @@ -16,7 +16,7 @@ type PrioritiesTableRowProps = { const PrioritiesTableRow = ({ id, name, i18n, sortItem, dirty, onClick }: PrioritiesTableRowProps) => { const { t } = useTranslation(); return ( - + diff --git a/apps/meteor/client/views/omnichannel/priorities/PriorityEditForm.tsx b/apps/meteor/client/views/omnichannel/priorities/PriorityEditForm.tsx index 7b42fd3d92027..3a407477fe448 100644 --- a/apps/meteor/client/views/omnichannel/priorities/PriorityEditForm.tsx +++ b/apps/meteor/client/views/omnichannel/priorities/PriorityEditForm.tsx @@ -91,14 +91,12 @@ const PriorityEditForm = ({ data, onSave, onCancel }: PriorityEditFormProps): Re /> )} /> - {errors.name?.message} + {errors.name?.message} - - - diff --git a/apps/meteor/client/views/omnichannel/sidebar/OmnichannelSidebar.tsx b/apps/meteor/client/views/omnichannel/sidebar/OmnichannelSidebar.tsx index cae285427cac5..d11fdffed1fa3 100644 --- a/apps/meteor/client/views/omnichannel/sidebar/OmnichannelSidebar.tsx +++ b/apps/meteor/client/views/omnichannel/sidebar/OmnichannelSidebar.tsx @@ -16,7 +16,7 @@ const OmnichannelSidebar = () => { return ( - + diff --git a/apps/meteor/client/views/omnichannel/slaPolicies/SlaEdit.tsx b/apps/meteor/client/views/omnichannel/slaPolicies/SlaEdit.tsx index a7ffcc96d66ec..e8a5954499d73 100644 --- a/apps/meteor/client/views/omnichannel/slaPolicies/SlaEdit.tsx +++ b/apps/meteor/client/views/omnichannel/slaPolicies/SlaEdit.tsx @@ -80,7 +80,7 @@ function SlaEdit({ data, isNew, slaId, reload, ...props }: SlaEditProps): ReactE - {errors.name?.message} + {errors.name?.message} {t('Description')} @@ -98,7 +98,7 @@ function SlaEdit({ data, isNew, slaId, reload, ...props }: SlaEditProps): ReactE error={errors.dueTimeInMinutes?.message} /> - {errors.dueTimeInMinutes?.message} + {errors.dueTimeInMinutes?.message} diff --git a/apps/meteor/client/views/omnichannel/slaPolicies/SlaTable.tsx b/apps/meteor/client/views/omnichannel/slaPolicies/SlaTable.tsx index 217c927b11632..43fd80f41ec02 100644 --- a/apps/meteor/client/views/omnichannel/slaPolicies/SlaTable.tsx +++ b/apps/meteor/client/views/omnichannel/slaPolicies/SlaTable.tsx @@ -115,11 +115,11 @@ const SlaTable = ({ reload }: { reload: MutableRefObject<() => void> }) => { )} {isSuccess && data?.sla.length > 0 && ( <> - + {headers} {data?.sla.map(({ _id, name, description, dueTimeInMinutes }) => ( - + {name} {description} diff --git a/apps/meteor/client/views/omnichannel/tags/TagEdit.tsx b/apps/meteor/client/views/omnichannel/tags/TagEdit.tsx index 65294be980648..a8202f637b17b 100644 --- a/apps/meteor/client/views/omnichannel/tags/TagEdit.tsx +++ b/apps/meteor/client/views/omnichannel/tags/TagEdit.tsx @@ -95,11 +95,13 @@ const TagEdit = ({ tagData, currentDepartments, onClose }: TagEditProps) => { name='name' control={control} rules={{ required: t('Required_field', { field: t('Name') }) }} - render={({ field }) => } + render={({ field }) => ( + + )} /> {errors?.name && ( - + {errors?.name?.message} )} @@ -111,12 +113,14 @@ const TagEdit = ({ tagData, currentDepartments, onClose }: TagEditProps) => { - {t('Departments')} + {t('Departments')} } + render={({ field }) => ( + + )} /> diff --git a/apps/meteor/client/views/omnichannel/tags/TagsTable.tsx b/apps/meteor/client/views/omnichannel/tags/TagsTable.tsx index 00ea16484f7c6..2515e82a1fa29 100644 --- a/apps/meteor/client/views/omnichannel/tags/TagsTable.tsx +++ b/apps/meteor/client/views/omnichannel/tags/TagsTable.tsx @@ -101,11 +101,11 @@ const TagsTable = () => { )} {isSuccess && data?.tags.length > 0 && ( <> - + {headers} {data?.tags.map(({ _id, name, description }) => ( - onRowClick(_id)} action qa-user-id={_id}> + onRowClick(_id)} action> {name} {description} diff --git a/apps/meteor/client/views/omnichannel/triggers/TriggersRow.tsx b/apps/meteor/client/views/omnichannel/triggers/TriggersRow.tsx index ca381f25e02d2..fbf7431cfab0c 100644 --- a/apps/meteor/client/views/omnichannel/triggers/TriggersRow.tsx +++ b/apps/meteor/client/views/omnichannel/triggers/TriggersRow.tsx @@ -47,7 +47,7 @@ const TriggersRow = ({ _id, name, description, enabled, reload }: TriggersRowPro }); return ( - + {name} {description} {enabled ? t('Yes') : t('No')} diff --git a/apps/meteor/client/views/omnichannel/triggers/TriggersTable.tsx b/apps/meteor/client/views/omnichannel/triggers/TriggersTable.tsx index 081c27e472a73..1770d14094777 100644 --- a/apps/meteor/client/views/omnichannel/triggers/TriggersTable.tsx +++ b/apps/meteor/client/views/omnichannel/triggers/TriggersTable.tsx @@ -71,7 +71,7 @@ const TriggersTable = () => { )} {isSuccess && data.triggers.length > 0 && ( <> - + {headers} {data.triggers.map(({ _id, name, description, enabled }) => ( diff --git a/apps/meteor/client/views/omnichannel/units/UnitEdit.tsx b/apps/meteor/client/views/omnichannel/units/UnitEdit.tsx index 77cab5e92af63..a14aba067cb80 100644 --- a/apps/meteor/client/views/omnichannel/units/UnitEdit.tsx +++ b/apps/meteor/client/views/omnichannel/units/UnitEdit.tsx @@ -161,7 +161,7 @@ const UnitEdit = ({ unitData, unitMonitors, unitDepartments, onUpdate, onDelete, /> {errors?.name && ( - + {errors?.name.message} )} @@ -189,7 +189,11 @@ const UnitEdit = ({ unitData, unitMonitors, unitDepartments, onUpdate, onDelete, )} /> - {errors?.visibility && {errors?.visibility.message}} + {errors?.visibility && ( + + {errors?.visibility.message} + + )} @@ -218,13 +222,13 @@ const UnitEdit = ({ unitData, unitMonitors, unitDepartments, onUpdate, onDelete, /> {errors?.departments && ( - + {errors?.departments.message} )} - + {t('Monitors')} @@ -234,13 +238,13 @@ const UnitEdit = ({ unitData, unitMonitors, unitDepartments, onUpdate, onDelete, rules={{ required: t('Required_field', { field: t('Monitors') }) }} render={({ field: { name, value, onChange, onBlur } }) => ( @@ -248,7 +252,7 @@ const UnitEdit = ({ unitData, unitMonitors, unitDepartments, onUpdate, onDelete, /> {errors?.monitors && ( - + {errors?.monitors.message} )} diff --git a/apps/meteor/client/views/omnichannel/units/UnitTableRow.tsx b/apps/meteor/client/views/omnichannel/units/UnitTableRow.tsx index 2b546024b268a..a97bf405c6484 100644 --- a/apps/meteor/client/views/omnichannel/units/UnitTableRow.tsx +++ b/apps/meteor/client/views/omnichannel/units/UnitTableRow.tsx @@ -14,7 +14,7 @@ const UnitsTableRow = ({ _id, name, visibility }: { _id: string; name: string; v const handleDelete = useRemoveUnit(_id); return ( - + {name} {visibility} @@ -22,7 +22,6 @@ const UnitsTableRow = ({ _id, name, visibility }: { _id: string; name: string; v icon='trash' small title={t('Remove')} - data-qa-id={`remove-unit-${name}`} onClick={(e) => { e.stopPropagation(); handleDelete(); diff --git a/apps/meteor/client/views/omnichannel/units/UnitsTable.tsx b/apps/meteor/client/views/omnichannel/units/UnitsTable.tsx index ed4a0f88c8e36..0fc76f74e684f 100644 --- a/apps/meteor/client/views/omnichannel/units/UnitsTable.tsx +++ b/apps/meteor/client/views/omnichannel/units/UnitsTable.tsx @@ -96,7 +96,7 @@ const UnitsTable = () => { )} {isSuccess && data?.units.length > 0 && ( <> - + {headers} {data.units.map(({ _id, name, visibility }) => ( diff --git a/apps/meteor/client/views/omnichannel/units/useRemoveUnit.tsx b/apps/meteor/client/views/omnichannel/units/useRemoveUnit.tsx index ba5b9131f2c7f..4beea2ebb5326 100644 --- a/apps/meteor/client/views/omnichannel/units/useRemoveUnit.tsx +++ b/apps/meteor/client/views/omnichannel/units/useRemoveUnit.tsx @@ -28,15 +28,7 @@ export const useRemoveUnit = (id: string) => { } }; - setModal( - setModal()} - confirmText={t('Delete')} - />, - ); + setModal( setModal()} confirmText={t('Delete')} />); }); return handleDelete; diff --git a/apps/meteor/tests/e2e/administration.spec.ts b/apps/meteor/tests/e2e/administration.spec.ts index 30c26ead2815d..7b63a8cdf8f07 100644 --- a/apps/meteor/tests/e2e/administration.spec.ts +++ b/apps/meteor/tests/e2e/administration.spec.ts @@ -91,12 +91,12 @@ test.describe.parallel('administration', () => { const username = faker.internet.userName(); await poAdminUsers.btnNewUser.click(); - await poAdminUsers.editUser.inputName.type(faker.person.firstName()); - await poAdminUsers.editUser.inputUserName.type(username); - await poAdminUsers.editUser.inputEmail.type(faker.internet.email()); + await poAdminUsers.editUser.inputName.fill(faker.person.firstName()); + await poAdminUsers.editUser.inputUserName.fill(username); + await poAdminUsers.editUser.inputEmail.fill(faker.internet.email()); await poAdminUsers.editUser.inputSetManually.click(); - await poAdminUsers.editUser.inputPassword.type('P@ssw0rd1234.!'); - await poAdminUsers.editUser.inputConfirmPassword.type('P@ssw0rd1234.!'); + await poAdminUsers.editUser.inputPassword.fill('P@ssw0rd1234.!'); + await poAdminUsers.editUser.inputConfirmPassword.fill('P@ssw0rd1234.!'); await expect(poAdminUsers.editUser.userRole).toBeVisible(); await expect(poAdminUsers.editUser.joinDefaultChannels).toBeVisible(); await poAdminUsers.editUser.btnAddUser.click(); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-agents.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-agents.spec.ts index 2c7f203329d11..26ee32afd7215 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-agents.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-agents.spec.ts @@ -1,7 +1,7 @@ import { createFakeDepartment } from '../../mocks/data'; import { IS_EE } from '../config/constants'; import { Users } from '../fixtures/userStates'; -import { OmnichannelAgents } from '../page-objects'; +import { OmnichannelAgents } from '../page-objects/omnichannel'; import { createDepartment } from '../utils/omnichannel/departments'; import { test, expect } from '../utils/test'; @@ -21,7 +21,7 @@ test.describe.serial('OC - Manage Agents', () => { poOmnichannelAgents = new OmnichannelAgents(page); await page.goto('/omnichannel'); - await poOmnichannelAgents.sidenav.linkAgents.click(); + await poOmnichannelAgents.sidebar.linkAgents.click(); }); // Ensure that there is no leftover data even if test fails @@ -42,20 +42,18 @@ test.describe.serial('OC - Manage Agents', () => { await test.step('expect add "user1" as agent', async () => { await poOmnichannelAgents.selectUsername('user1'); - await poOmnichannelAgents.btnAdd.click(); + await poOmnichannelAgents.btnAddAgent.click(); - await poOmnichannelAgents.inputSearch.fill('user1'); - await expect(poOmnichannelAgents.firstRowInTable).toBeVisible(); - await expect(poOmnichannelAgents.firstRowInTable).toHaveText('user1'); + await poOmnichannelAgents.search('user1'); + await expect(poOmnichannelAgents.table.findRowByName('user1')).toBeVisible(); }); await test.step('expect remove "user1" as agent', async () => { - await poOmnichannelAgents.inputSearch.fill('user1'); - await poOmnichannelAgents.btnDeleteFirstRowInTable.click(); - await poOmnichannelAgents.btnModalRemove.click(); + await poOmnichannelAgents.search('user1'); + await poOmnichannelAgents.deleteAgent('user1'); - await poOmnichannelAgents.inputSearch.fill('user1'); - await expect(poOmnichannelAgents.findRowByUsername('user1')).not.toBeVisible(); + await poOmnichannelAgents.search('user1'); + await expect(poOmnichannelAgents.table.findRowByName('user1')).not.toBeVisible(); }); }); @@ -63,26 +61,26 @@ test.describe.serial('OC - Manage Agents', () => { test.skip(IS_EE, 'Community Edition Only'); await poOmnichannelAgents.selectUsername('user1'); - await poOmnichannelAgents.btnAdd.click(); + await poOmnichannelAgents.btnAddAgent.click(); - await poOmnichannelAgents.inputSearch.fill('user1'); - await poOmnichannelAgents.firstRowInTable.click(); - await poOmnichannelAgents.btnEdit.click(); + await poOmnichannelAgents.search('user1'); + await poOmnichannelAgents.table.findRowByName('user1').click(); + await poOmnichannelAgents.agentInfo.btnEdit.click(); await test.step('expect max chats fields to be hidden', async () => { - await expect(poOmnichannelAgents.inputMaxChats).toBeHidden(); + await expect(poOmnichannelAgents.editAgent.inputMaxChats).toBeHidden(); }); await test.step('expect update "user1" information', async () => { - await poOmnichannelAgents.selectStatus('Not available'); - await poOmnichannelAgents.selectDepartment(department.data.name); - await poOmnichannelAgents.btnSave.click(); + await poOmnichannelAgents.editAgent.selectStatus('Not Available'); + await poOmnichannelAgents.editAgent.selectDepartment(department.data.name); + await poOmnichannelAgents.editAgent.btnSave.click(); }); await test.step('expect removing "user1" via sidebar', async () => { - await poOmnichannelAgents.inputSearch.fill('user1'); - await poOmnichannelAgents.firstRowInTable.click(); - await poOmnichannelAgents.btnRemove.click(); + await poOmnichannelAgents.search('user1'); + await poOmnichannelAgents.table.findRowByName('user1').click(); + await poOmnichannelAgents.agentInfo.btnRemove.click(); }); }); @@ -90,33 +88,33 @@ test.describe.serial('OC - Manage Agents', () => { test.skip(!IS_EE, 'Enterprise Only'); await poOmnichannelAgents.selectUsername('user1'); - await poOmnichannelAgents.btnAdd.click(); + await poOmnichannelAgents.btnAddAgent.click(); - await poOmnichannelAgents.inputSearch.fill('user1'); - await poOmnichannelAgents.findRowByUsername('user1').click(); - await poOmnichannelAgents.btnEdit.click(); + await poOmnichannelAgents.search('user1'); + await poOmnichannelAgents.table.findRowByName('user1').click(); + await poOmnichannelAgents.agentInfo.btnEdit.click(); await test.step('expect max chats field to be visible', async () => { - await expect(poOmnichannelAgents.inputMaxChats).toBeVisible(); + await expect(poOmnichannelAgents.editAgent.inputMaxChats).toBeVisible(); }); await test.step('expect update "user1" information', async () => { - await poOmnichannelAgents.inputMaxChats.click(); - await poOmnichannelAgents.inputMaxChats.fill('2'); - await poOmnichannelAgents.btnSave.click(); + await poOmnichannelAgents.editAgent.inputMaxChats.click(); + await poOmnichannelAgents.editAgent.inputMaxChats.fill('2'); + await poOmnichannelAgents.editAgent.btnSave.click(); }); }); test('OC - Edit agent - Manage departments', async ({ page }) => { await poOmnichannelAgents.selectUsername('user1'); - await poOmnichannelAgents.btnAdd.click(); - await poOmnichannelAgents.inputSearch.fill('user1'); - await poOmnichannelAgents.findRowByUsername('user1').click(); + await poOmnichannelAgents.btnAddAgent.click(); + await poOmnichannelAgents.search('user1'); + await poOmnichannelAgents.table.findRowByName('user1').click(); - await poOmnichannelAgents.btnEdit.click(); - await poOmnichannelAgents.selectDepartment(department.data.name); + await poOmnichannelAgents.agentInfo.btnEdit.click(); + await poOmnichannelAgents.editAgent.selectDepartment(department.data.name); const response = page.waitForResponse('**/api/v1/livechat/agents.saveInfo'); - await poOmnichannelAgents.btnSave.click(); + await poOmnichannelAgents.editAgent.btnSave.click(); /** * between saving and opening the agent info again it is necessary to @@ -127,18 +125,18 @@ test.describe.serial('OC - Manage Agents', () => { await response; - await expect(poOmnichannelAgents.editCtxBar).not.toBeVisible(); + await expect(poOmnichannelAgents.editAgent.root).not.toBeVisible(); await test.step('expect the selected department is visible', async () => { - await poOmnichannelAgents.findRowByUsername('user1').click(); + await poOmnichannelAgents.table.findRowByName('user1').click(); // mock the endpoint to use the one without pagination await page.route('/api/v1/livechat/department?showArchived=true', async (route) => { await route.fulfill({ json: { departments: [] } }); }); - await poOmnichannelAgents.btnEdit.click(); - await expect(poOmnichannelAgents.findSelectedDepartment(department.data.name)).toBeVisible(); + await poOmnichannelAgents.agentInfo.btnEdit.click(); + await expect(poOmnichannelAgents.editAgent.findSelectedDepartment(department.data.name)).toBeVisible(); }); }); @@ -159,32 +157,31 @@ test.describe.serial('OC - Manage Agents', () => { await test.step('expect to add agent', async () => { await poOmnichannelAgents.selectUsername('user1'); - await poOmnichannelAgents.btnAdd.click(); + await poOmnichannelAgents.btnAddAgent.click(); }); await test.step('expect to edit agent', async () => { - await poOmnichannelAgents.inputSearch.fill('user1'); - await poOmnichannelAgents.findRowByUsername('user1').click(); - await poOmnichannelAgents.btnEdit.click(); + await poOmnichannelAgents.search('user1'); + await poOmnichannelAgents.table.findRowByName('user1').click(); + await poOmnichannelAgents.agentInfo.btnEdit.click(); }); await test.step('expect department field to be paginated', async () => { - await poOmnichannelAgents.inputDepartment.click(); - await expect(poOmnichannelAgents.findOption('Department 0')).toBeVisible(); + await poOmnichannelAgents.editAgent.inputDepartment.click(); + await expect(poOmnichannelAgents.editAgent.getDepartmentOption('Department 0')).toBeVisible(); await poOmnichannelAgents.scrollToListBottom(); - await expect(poOmnichannelAgents.findOption('Department 49')).toBeVisible(); + await expect(poOmnichannelAgents.editAgent.getDepartmentOption('Department 49')).toBeVisible(); await poOmnichannelAgents.scrollToListBottom(); - await expect(poOmnichannelAgents.findOption('Department 99')).toBeVisible(); - await poOmnichannelAgents.btnClose.click(); + await expect(poOmnichannelAgents.editAgent.getDepartmentOption('Department 99')).toBeVisible(); + await poOmnichannelAgents.editAgent.close(); }); await test.step('expect remove "user1" as agent', async () => { - await poOmnichannelAgents.inputSearch.fill('user1'); - await poOmnichannelAgents.btnDeleteFirstRowInTable.click(); - await poOmnichannelAgents.btnModalRemove.click(); + await poOmnichannelAgents.search('user1'); + await poOmnichannelAgents.deleteAgent('user1'); - await poOmnichannelAgents.inputSearch.fill('user1'); - await expect(poOmnichannelAgents.findRowByUsername('user1')).not.toBeVisible(); + await poOmnichannelAgents.search('user1'); + await expect(poOmnichannelAgents.table.findRowByName('user1')).not.toBeVisible(); }); }); }); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-appearance.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-appearance.spec.ts index e0d9f92df2ea6..f9a8747d3c3a1 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-appearance.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-appearance.spec.ts @@ -1,6 +1,6 @@ import { IS_EE } from '../config/constants'; import { Users } from '../fixtures/userStates'; -import { OmnichannelLivechatAppearance } from '../page-objects/omnichannel-livechat-appearance'; +import { OmnichannelLivechatAppearance } from '../page-objects/omnichannel'; import { test, expect } from '../utils/test'; test.use({ storageState: Users.admin.state }); @@ -13,7 +13,7 @@ test.describe.serial('OC - Livechat Appearance - EE', () => { poLivechatAppearance = new OmnichannelLivechatAppearance(page); await page.goto('/omnichannel'); - await poLivechatAppearance.sidenav.linkLivechatAppearance.click(); + await poLivechatAppearance.sidebar.linkLivechatAppearance.click(); }); test.afterAll(async ({ api }) => { @@ -42,7 +42,7 @@ test.describe.serial('OC - Livechat Appearance - EE', () => { await poLivechatAppearance.inputHideSystemMessages.locator('.rcx-icon--name-chevron-down').click(); await poLivechatAppearance.findHideSystemMessageOption('livechat_transfer_history').click(); await poLivechatAppearance.findHideSystemMessageOption('livechat-close').click(); - await poLivechatAppearance.btnSave.click(); + await poLivechatAppearance.btnSaveChanges.click(); }); await test.step('expect to have saved changes', async () => { @@ -63,7 +63,7 @@ test.describe.serial('OC - Livechat Appearance - EE', () => { await test.step('expect to change value', async () => { await poLivechatAppearance.inputLivechatBackground.fill('rgb(186, 1, 85)'); - await poLivechatAppearance.btnSave.click(); + await poLivechatAppearance.btnSaveChanges.click(); }); await test.step('expect to have saved changes', async () => { @@ -80,7 +80,7 @@ test.describe('OC - Livechat Appearance - CE', () => { poLivechatAppearance = new OmnichannelLivechatAppearance(page); await page.goto('/omnichannel'); - await poLivechatAppearance.sidenav.linkLivechatAppearance.click(); + await poLivechatAppearance.sidebar.linkLivechatAppearance.click(); }); test.afterAll(async ({ api }) => { @@ -98,7 +98,7 @@ test.describe('OC - Livechat Appearance - CE', () => { await test.step('expect to change value', async () => { await poLivechatAppearance.inputLivechatTitle.fill('Test Title'); - await poLivechatAppearance.btnSave.click(); + await poLivechatAppearance.btnSaveChanges.click(); }); await test.step('expect to have saved changes', async () => { diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-assign-room-tags.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-assign-room-tags.spec.ts index 187d3428a39ee..c3ceeae55d853 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-assign-room-tags.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-assign-room-tags.spec.ts @@ -81,31 +81,31 @@ test.describe('OC - Tags Visibility', () => { }); await test.step('check available tags', async () => { - await poOmnichannel.roomInfo.btnEditRoomInfo.click(); - await expect(poOmnichannel.roomInfo.dialogEditRoom).toBeVisible(); - await poOmnichannel.roomInfo.inputTags.click(); + await poOmnichannel.roomInfo.btnEdit.click(); + await expect(poOmnichannel.editRoomInfo.root).toBeVisible(); + await poOmnichannel.editRoomInfo.inputTags.click(); }); await test.step('Should see TagA (department A specific)', async () => { - await expect(poOmnichannel.roomInfo.optionTags(tagA.data.name)).toBeVisible(); + await expect(poOmnichannel.editRoomInfo.optionTag(tagA.data.name)).toBeVisible(); }); await test.step('Should see SharedTag (both departments)', async () => { - await expect(poOmnichannel.roomInfo.optionTags(sharedTag.data.name)).toBeVisible(); + await expect(poOmnichannel.editRoomInfo.optionTag(sharedTag.data.name)).toBeVisible(); }); await test.step('Should see Public Tags for all chats (no department restriction)', async () => { - await expect(poOmnichannel.roomInfo.optionTags(globalTag.data.name)).toBeVisible(); + await expect(poOmnichannel.editRoomInfo.optionTag(globalTag.data.name)).toBeVisible(); }); await test.step('Should not see TagB (department B specific)', async () => { - await expect(poOmnichannel.roomInfo.optionTags(tagB.data.name)).not.toBeVisible(); + await expect(poOmnichannel.editRoomInfo.optionTag(tagB.data.name)).not.toBeVisible(); }); await test.step('add tags and save', async () => { - await poOmnichannel.roomInfo.selectTag(tagA.data.name); - await poOmnichannel.roomInfo.selectTag(globalTag.data.name); - await poOmnichannel.roomInfo.btnSaveEditRoom.click(); + await poOmnichannel.editRoomInfo.selectTag(tagA.data.name); + await poOmnichannel.editRoomInfo.selectTag(globalTag.data.name); + await poOmnichannel.editRoomInfo.btnSave.click(); }); await test.step('verify selected tags are displayed under room information', async () => { @@ -118,17 +118,17 @@ test.describe('OC - Tags Visibility', () => { test('Verify tags visibility for agent associated with multiple departments', async () => { await test.step('Open room info', async () => { await poOmnichannel.sidebar.getSidebarItemByName(visitorB.name).click(); - await poOmnichannel.roomInfo.btnEditRoomInfo.click(); - await expect(poOmnichannel.roomInfo.dialogEditRoom).toBeVisible(); - await poOmnichannel.roomInfo.inputTags.click(); + await poOmnichannel.roomInfo.btnEdit.click(); + await expect(poOmnichannel.editRoomInfo.root).toBeVisible(); + await poOmnichannel.editRoomInfo.inputTags.click(); }); await test.step('Agent associated with DepartmentB should be able to see tags for Department B', async () => { - await expect(poOmnichannel.roomInfo.optionTags(tagB.data.name)).toBeVisible(); + await expect(poOmnichannel.editRoomInfo.optionTag(tagB.data.name)).toBeVisible(); }); await test.step('Agent associated with DepartmentB should not be able to see tags for DepartmentA', async () => { - await expect(poOmnichannel.roomInfo.optionTags(tagA.data.name)).not.toBeVisible(); + await expect(poOmnichannel.editRoomInfo.optionTag(tagA.data.name)).not.toBeVisible(); }); }); }); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-auto-onhold-chat-closing.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-auto-onhold-chat-closing.spec.ts index 56875ec87f0dc..9789d0b858850 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-auto-onhold-chat-closing.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-auto-onhold-chat-closing.spec.ts @@ -4,7 +4,8 @@ import { createFakeVisitor } from '../../mocks/data'; import { IS_EE } from '../config/constants'; import { createAuxContext } from '../fixtures/createAuxContext'; import { Users } from '../fixtures/userStates'; -import { OmnichannelLiveChat, HomeOmnichannel } from '../page-objects'; +import { HomeOmnichannel } from '../page-objects'; +import { OmnichannelLiveChat } from '../page-objects/omnichannel'; import { test, expect } from '../utils/test'; test.describe('omnichannel-auto-onhold-chat-closing', () => { diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-auto-transfer-unanswered-chat.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-auto-transfer-unanswered-chat.spec.ts index 4fcae544ea84f..f8dc8c1833292 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-auto-transfer-unanswered-chat.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-auto-transfer-unanswered-chat.spec.ts @@ -4,7 +4,8 @@ import { createFakeVisitor } from '../../mocks/data'; import { IS_EE } from '../config/constants'; import { createAuxContext } from '../fixtures/createAuxContext'; import { Users } from '../fixtures/userStates'; -import { OmnichannelLiveChat, HomeChannel } from '../page-objects'; +import { HomeChannel } from '../page-objects'; +import { OmnichannelLiveChat } from '../page-objects/omnichannel'; import { test, expect } from '../utils/test'; test.describe('omnichannel-auto-transfer-unanswered-chat', () => { diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-business-hours.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-business-hours.spec.ts index f1904e053cd1c..40a064256c401 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-business-hours.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-business-hours.spec.ts @@ -2,7 +2,7 @@ import type { Page } from '@playwright/test'; import { IS_EE } from '../config/constants'; import { Users } from '../fixtures/userStates'; -import { OmnichannelBusinessHours } from '../page-objects'; +import { OmnichannelBusinessHours } from '../page-objects/omnichannel'; import { createAgent } from '../utils/omnichannel/agents'; import { createBusinessHour } from '../utils/omnichannel/businessHours'; import { createDepartment } from '../utils/omnichannel/departments'; @@ -10,6 +10,7 @@ import { test, expect } from '../utils/test'; test.use({ storageState: Users.admin.state }); +// TODO: These tests needs to be refactored to be independent from each other test.describe('OC - Business Hours', () => { test.skip(!IS_EE, 'OC - Manage Business Hours > Enterprise Edition Only'); @@ -42,7 +43,7 @@ test.describe('OC - Business Hours', () => { test('OC - Manage Business Hours - Create Business Hours', async ({ page }) => { await page.goto('/omnichannel'); - await poOmnichannelBusinessHours.sidenav.linkBusinessHours.click(); + await poOmnichannelBusinessHours.sidebar.linkBusinessHours.click(); await test.step('expect correct form default state', async () => { await poOmnichannelBusinessHours.btnCreateBusinessHour.click(); @@ -58,29 +59,17 @@ test.describe('OC - Business Hours', () => { await test.step('expect business hours to have been created', async () => { await poOmnichannelBusinessHours.search(BHName); - await expect(poOmnichannelBusinessHours.findRowByName(BHName)).toBeVisible(); + await expect(poOmnichannelBusinessHours.table.findRowByName(BHName)).toBeVisible(); }); }); await test.step('expect to be able to delete business hours', async () => { - await test.step('expect to be able to cancel delete', async () => { - await poOmnichannelBusinessHours.btnDeleteByName(BHName).click(); - await expect(poOmnichannelBusinessHours.confirmDeleteModal).toBeVisible(); - await poOmnichannelBusinessHours.btnCancelDeleteModal.click(); - await expect(poOmnichannelBusinessHours.confirmDeleteModal).not.toBeVisible(); - }); - - await test.step('expect to confirm delete', async () => { - await poOmnichannelBusinessHours.btnDeleteByName(BHName).click(); - await expect(poOmnichannelBusinessHours.confirmDeleteModal).toBeVisible(); - await poOmnichannelBusinessHours.btnConfirmDeleteModal.click(); - await expect(poOmnichannelBusinessHours.confirmDeleteModal).not.toBeVisible(); - }); + await poOmnichannelBusinessHours.deleteBusinessHour(BHName); }); await test.step('expect business hours to have been deleted', async () => { await poOmnichannelBusinessHours.search(BHName); - await expect(poOmnichannelBusinessHours.findRowByName(BHName)).not.toBeVisible(); + await expect(poOmnichannelBusinessHours.table.findRowByName(BHName)).not.toBeVisible(); }); }); @@ -95,41 +84,38 @@ test.describe('OC - Business Hours', () => { }); await page.goto('/omnichannel'); - await poOmnichannelBusinessHours.sidenav.linkBusinessHours.click(); + await poOmnichannelBusinessHours.sidebar.linkBusinessHours.click(); await test.step('expect to add business hours departments', async () => { await poOmnichannelBusinessHours.search(BHName); - await poOmnichannelBusinessHours.findRowByName(BHName).click(); + await poOmnichannelBusinessHours.table.findRowByName(BHName).click(); await poOmnichannelBusinessHours.selectDepartment(department2.data.name); await poOmnichannelBusinessHours.btnSave.click(); }); await test.step('expect department to be in the chosen departments list', async () => { await poOmnichannelBusinessHours.search(BHName); - await poOmnichannelBusinessHours.findRowByName(BHName).click(); + await poOmnichannelBusinessHours.table.findRowByName(BHName).click(); await expect(poOmnichannelBusinessHours.findDepartmentsChipOption(department2.data.name)).toBeVisible(); await poOmnichannelBusinessHours.btnBack.click(); }); await test.step('expect to remove business hours departments', async () => { await poOmnichannelBusinessHours.search(BHName); - await poOmnichannelBusinessHours.findRowByName(BHName).click(); + await poOmnichannelBusinessHours.table.findRowByName(BHName).click(); await poOmnichannelBusinessHours.selectDepartment(department2.data.name); await poOmnichannelBusinessHours.btnSave.click(); }); await test.step('expect department to not be in the chosen departments list', async () => { await poOmnichannelBusinessHours.search(BHName); - await poOmnichannelBusinessHours.findRowByName(BHName).click(); + await poOmnichannelBusinessHours.table.findRowByName(BHName).click(); await expect(poOmnichannelBusinessHours.findDepartmentsChipOption(department2.data.name)).toBeHidden(); await poOmnichannelBusinessHours.btnBack.click(); }); await test.step('expect delete business hours', async () => { - await poOmnichannelBusinessHours.btnDeleteByName(BHName).click(); - await expect(poOmnichannelBusinessHours.confirmDeleteModal).toBeVisible(); - await poOmnichannelBusinessHours.btnConfirmDeleteModal.click(); - await expect(poOmnichannelBusinessHours.confirmDeleteModal).not.toBeVisible(); + await poOmnichannelBusinessHours.deleteBusinessHour(BHName); }); }); @@ -144,13 +130,13 @@ test.describe('OC - Business Hours', () => { }); await page.goto('/omnichannel'); - await poOmnichannelBusinessHours.sidenav.linkBusinessHours.click(); + await poOmnichannelBusinessHours.sidebar.linkBusinessHours.click(); await test.step('expect to disable business hours', async () => { - await poOmnichannelBusinessHours.sidenav.linkBusinessHours.click(); + await poOmnichannelBusinessHours.sidebar.linkBusinessHours.click(); await poOmnichannelBusinessHours.search(BHName); - await poOmnichannelBusinessHours.findRowByName(BHName).click(); + await poOmnichannelBusinessHours.table.findRowByName(BHName).click(); await poOmnichannelBusinessHours.getCheckboxByLabel('Enabled').click(); await expect(poOmnichannelBusinessHours.getCheckboxByLabel('Enabled')).not.toBeChecked(); @@ -159,10 +145,10 @@ test.describe('OC - Business Hours', () => { }); await test.step('expect to enable business hours', async () => { - await poOmnichannelBusinessHours.sidenav.linkBusinessHours.click(); + await poOmnichannelBusinessHours.sidebar.linkBusinessHours.click(); await poOmnichannelBusinessHours.search(BHName); - await poOmnichannelBusinessHours.findRowByName(BHName).click(); + await poOmnichannelBusinessHours.table.findRowByName(BHName).click(); await poOmnichannelBusinessHours.getCheckboxByLabel('Enabled').click(); await expect(poOmnichannelBusinessHours.getCheckboxByLabel('Enabled')).toBeChecked(); @@ -171,9 +157,7 @@ test.describe('OC - Business Hours', () => { }); await test.step('expect delete business hours', async () => { - await poOmnichannelBusinessHours.btnDeleteByName(BHName).click(); - await expect(poOmnichannelBusinessHours.confirmDeleteModal).toBeVisible(); - await poOmnichannelBusinessHours.btnConfirmDeleteModal.click(); + await poOmnichannelBusinessHours.deleteBusinessHour(BHName); }); }); }); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-canned-responses-sidebar.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-canned-responses-sidebar.spec.ts index 52434f08e4f72..c81117b4dcb90 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-canned-responses-sidebar.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-canned-responses-sidebar.spec.ts @@ -5,7 +5,8 @@ import { createFakeVisitor } from '../../mocks/data'; import { IS_EE } from '../config/constants'; import { createAuxContext } from '../fixtures/createAuxContext'; import { Users } from '../fixtures/userStates'; -import { OmnichannelLiveChat, HomeOmnichannel } from '../page-objects'; +import { HomeOmnichannel } from '../page-objects'; +import { OmnichannelLiveChat } from '../page-objects/omnichannel'; import { test } from '../utils/test'; test.describe.serial('OC - Canned Responses Sidebar', () => { diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-canned-responses-usage.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-canned-responses-usage.spec.ts index 15daec85c8070..a6c8be054e005 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-canned-responses-usage.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-canned-responses-usage.spec.ts @@ -5,7 +5,8 @@ import { createFakeVisitor } from '../../mocks/data'; import { IS_EE } from '../config/constants'; import { createAuxContext } from '../fixtures/createAuxContext'; import { Users } from '../fixtures/userStates'; -import { OmnichannelLiveChat, HomeOmnichannel } from '../page-objects'; +import { HomeOmnichannel } from '../page-objects'; +import { OmnichannelLiveChat } from '../page-objects/omnichannel'; import { test, expect } from '../utils/test'; test.describe('OC - Canned Responses Usage', () => { diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-changing-room-priority-and-sla.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-changing-room-priority-and-sla.spec.ts index 4dcecb76d5d06..47ee200924cfb 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-changing-room-priority-and-sla.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-changing-room-priority-and-sla.spec.ts @@ -4,7 +4,8 @@ import { createFakeVisitor } from '../../mocks/data'; import { ADMIN_CREDENTIALS, IS_EE } from '../config/constants'; import { createAuxContext } from '../fixtures/createAuxContext'; import { Users } from '../fixtures/userStates'; -import { OmnichannelLiveChat, HomeChannel } from '../page-objects'; +import { HomeChannel } from '../page-objects'; +import { OmnichannelLiveChat } from '../page-objects/omnichannel'; import { getPriorityByi18nLabel } from '../utils/omnichannel/priority'; import { createSLA } from '../utils/omnichannel/sla'; import { test, expect } from '../utils/test'; diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-chat-history.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-chat-history.spec.ts index 52823f0aefd2f..a9b4137365532 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-chat-history.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-chat-history.spec.ts @@ -3,7 +3,8 @@ import type { Page } from '@playwright/test'; import { createFakeVisitor } from '../../mocks/data'; import { createAuxContext } from '../fixtures/createAuxContext'; import { Users } from '../fixtures/userStates'; -import { OmnichannelLiveChat, HomeOmnichannel } from '../page-objects'; +import { HomeOmnichannel } from '../page-objects'; +import { OmnichannelLiveChat } from '../page-objects/omnichannel'; import { test, expect } from '../utils/test'; test.describe('Omnichannel chat history', () => { @@ -49,11 +50,11 @@ test.describe('Omnichannel chat history', () => { }); await test.step('expect to be able to edit room info', async () => { - await agent.poHomeOmnichannel.roomInfo.btnEditRoomInfo.click(); - await agent.poHomeOmnichannel.roomInfo.inputTopic.fill('any_topic'); - await agent.poHomeOmnichannel.roomInfo.btnSaveEditRoom.click(); + await agent.poHomeOmnichannel.roomInfo.btnEdit.click(); + await agent.poHomeOmnichannel.editRoomInfo.inputTopic.fill('any_topic'); + await agent.poHomeOmnichannel.editRoomInfo.btnSave.click(); - await expect(agent.poHomeOmnichannel.roomInfo.dialogRoomInfo).toContainText('any_topic'); + await expect(agent.poHomeOmnichannel.roomInfo.root).toContainText('any_topic'); }); await test.step('Expect to be able to close an omnichannel to conversation', async () => { diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-close-chat.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-close-chat.spec.ts index e4a7dabc5fd06..936d4c93ea2fd 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-close-chat.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-close-chat.spec.ts @@ -3,7 +3,8 @@ import type { Page } from '@playwright/test'; import { createFakeVisitor } from '../../mocks/data'; import { createAuxContext } from '../fixtures/createAuxContext'; import { Users } from '../fixtures/userStates'; -import { OmnichannelLiveChat, HomeOmnichannel } from '../page-objects'; +import { HomeOmnichannel } from '../page-objects'; +import { OmnichannelLiveChat } from '../page-objects/omnichannel'; import { test } from '../utils/test'; test.describe('Omnichannel close chat', () => { diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-close-inquiry.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-close-inquiry.spec.ts index da744ce849e94..28fa3f2d1f758 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-close-inquiry.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-close-inquiry.spec.ts @@ -3,7 +3,8 @@ import type { Page } from '@playwright/test'; import { createFakeVisitor } from '../../mocks/data'; import { createAuxContext } from '../fixtures/createAuxContext'; import { Users } from '../fixtures/userStates'; -import { OmnichannelLiveChat, HomeOmnichannel } from '../page-objects'; +import { HomeOmnichannel } from '../page-objects'; +import { OmnichannelLiveChat } from '../page-objects/omnichannel'; import { test, expect } from '../utils/test'; test.describe('Omnichannel close inquiry', () => { diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-center-chats-filters.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-center-chats-filters.spec.ts index 9a62b6b376fac..9cebdc580443c 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-center-chats-filters.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-center-chats-filters.spec.ts @@ -1,7 +1,7 @@ import { faker } from '@faker-js/faker/locale/af_ZA'; import { Users } from '../fixtures/userStates'; -import { OmnichannelChats } from '../page-objects/omnichannel-contact-center-chats'; +import { OmnichannelContactCenterChats } from '../page-objects/omnichannel'; import { setSettingValueById } from '../utils'; import { createAgent, makeAgentAvailable } from '../utils/omnichannel/agents'; import { createConversation } from '../utils/omnichannel/rooms'; @@ -13,7 +13,7 @@ test.describe('OC - Contact Center - Chats', () => { let conversations: Awaited>[]; let agent: Awaited>; - let poOmniChats: OmnichannelChats; + let poOmniChats: OmnichannelContactCenterChats; const uuid = faker.string.uuid(); const visitorA = `visitorA_${uuid}`; @@ -37,7 +37,7 @@ test.describe('OC - Contact Center - Chats', () => { }); test.beforeEach(async ({ page }) => { - poOmniChats = new OmnichannelChats(page); + poOmniChats = new OmnichannelContactCenterChats(page); await page.goto('/omnichannel-directory/chats'); }); @@ -54,8 +54,8 @@ test.describe('OC - Contact Center - Chats', () => { test(`OC - Contact Center - Chats - Filter from and to same date`, async ({ page }) => { await test.step('expect conversations to be visible', async () => { await poOmniChats.inputSearch.fill(uuid); - await expect(poOmniChats.findRowByName(visitorA)).toBeVisible(); - await expect(poOmniChats.findRowByName(visitorB)).toBeVisible(); + await expect(poOmniChats.table.findRowByName(visitorA)).toBeVisible(); + await expect(poOmniChats.table.findRowByName(visitorB)).toBeVisible(); }); await test.step('expect to filter [from] and [to] today', async () => { @@ -70,8 +70,8 @@ test.describe('OC - Contact Center - Chats', () => { }); await test.step('expect conversations to be visible', async () => { - await expect(poOmniChats.findRowByName(visitorA)).toBeVisible(); - await expect(poOmniChats.findRowByName(visitorB)).toBeVisible(); + await expect(poOmniChats.table.findRowByName(visitorA)).toBeVisible(); + await expect(poOmniChats.table.findRowByName(visitorB)).toBeVisible(); }); }); }); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-center-chats.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-center-chats.spec.ts index 3e46bfc04960b..873fc625f8686 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-center-chats.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-center-chats.spec.ts @@ -3,23 +3,24 @@ import type { Page } from '@playwright/test'; import { IS_EE } from '../config/constants'; import { Users } from '../fixtures/userStates'; -import { HomeOmnichannel, OmnichannelChats } from '../page-objects'; +import { HomeOmnichannel } from '../page-objects'; +import { OmnichannelContactCenterChats } from '../page-objects/omnichannel'; import { createAgent, makeAgentAvailable } from '../utils/omnichannel/agents'; import { addAgentToDepartment, createDepartment } from '../utils/omnichannel/departments'; import { createConversation, updateRoom } from '../utils/omnichannel/rooms'; import { createTag } from '../utils/omnichannel/tags'; import { test, expect } from '../utils/test'; -const visitorA = faker.person.firstName(); -const visitorB = faker.person.firstName(); -const visitorC = faker.person.firstName(); +const visitorA = `${faker.person.firstName()}-${faker.database.mongodbObjectId()}`; +const visitorB = `${faker.person.firstName()}-${faker.database.mongodbObjectId()}`; +const visitorC = `${faker.person.firstName()}-${faker.database.mongodbObjectId()}`; test.skip(!IS_EE, 'OC - Contact Center Chats > Enterprise Only'); test.use({ storageState: Users.admin.state }); test.describe('OC - Contact Center Chats [Auto Selection]', async () => { - let poOmnichats: OmnichannelChats; + let poOmnichats: OmnichannelContactCenterChats; let poHomeOmnichannel: HomeOmnichannel; let departments: Awaited>[]; let conversations: Awaited>[]; @@ -110,10 +111,11 @@ test.describe('OC - Contact Center Chats [Auto Selection]', async () => { }); test.beforeEach(async ({ page }: { page: Page }) => { - poOmnichats = new OmnichannelChats(page); + poOmnichats = new OmnichannelContactCenterChats(page); + poHomeOmnichannel = new HomeOmnichannel(page); await page.goto('/omnichannel'); - await poOmnichats.sidenav.linkCurrentChats.click(); + await poOmnichats.sidebar.linkCurrentChats.click(); }); test.afterAll(async ({ api }) => { @@ -147,14 +149,12 @@ test.describe('OC - Contact Center Chats [Auto Selection]', async () => { test('OC - Contact Center Chats - Basic navigation', async ({ page }) => { await test.step('expect to be return using return button', async () => { await poOmnichats.openChat(visitorA); - await poOmnichats.content.btnReturn.click(); + await poHomeOmnichannel.content.btnReturn.click(); expect(page.url()).toContain(`/omnichannel/current`); }); }); - test('OC - Contact Center Chats - Access in progress conversation from another agent', async ({ page }) => { - poHomeOmnichannel = new HomeOmnichannel(page); - + test('OC - Contact Center Chats - Access in progress conversation from another agent', async () => { await test.step('expect to be able to join', async () => { const { visitor: visitorB } = conversations[1].data; await poOmnichats.openChat(visitorB.name); @@ -167,7 +167,7 @@ test.describe('OC - Contact Center Chats [Auto Selection]', async () => { test('OC - Contact Center Chats - Remove conversations', async () => { await test.step('expect to be able to remove conversation from table', async () => { await poOmnichats.removeChatByName(visitorC); - await expect(poOmnichats.findRowByName(visitorC)).not.toBeVisible(); + await expect(poOmnichats.table.findRowByName(visitorC)).not.toBeVisible(); }); // TODO: await test.step('expect to be able to close all closes conversations', async () => {}); @@ -176,7 +176,8 @@ test.describe('OC - Contact Center Chats [Auto Selection]', async () => { test.describe('OC - Contact Center [Manual Selection]', () => { let queuedConversation: Awaited>; - let poCurrentChats: OmnichannelChats; + let poHomeOmnichannel: HomeOmnichannel; + let poCurrentChats: OmnichannelContactCenterChats; let agent: Awaited>; test.beforeAll(async ({ api }) => { @@ -193,10 +194,11 @@ test.describe('OC - Contact Center [Manual Selection]', () => { }); test.beforeEach(async ({ page }: { page: Page }) => { - poCurrentChats = new OmnichannelChats(page); + poCurrentChats = new OmnichannelContactCenterChats(page); + poHomeOmnichannel = new HomeOmnichannel(page); await page.goto('/omnichannel'); - await poCurrentChats.sidenav.linkCurrentChats.click(); + await poCurrentChats.sidebar.linkCurrentChats.click(); }); test('OC - Contact Center Chats - Access queued conversation', async ({ api }) => { @@ -206,9 +208,9 @@ test.describe('OC - Contact Center [Manual Selection]', () => { const { visitor } = queuedConversation.data; await poCurrentChats.inputSearch.fill(visitor.name); await poCurrentChats.openChat(visitor.name); - await expect(poCurrentChats.content.btnTakeChat).toBeVisible(); - await poCurrentChats.content.btnTakeChat.click(); - await expect(poCurrentChats.content.btnTakeChat).not.toBeVisible(); + await expect(poHomeOmnichannel.content.btnTakeChat).toBeVisible(); + await poHomeOmnichannel.content.btnTakeChat.click(); + await expect(poHomeOmnichannel.content.btnTakeChat).not.toBeVisible(); }); }); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-center-contacts.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-center-contacts.spec.ts index 15a38eba7f8de..14c54560a4af0 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-center-contacts.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-center-contacts.spec.ts @@ -3,8 +3,8 @@ import { randomBytes } from 'crypto'; import { faker } from '@faker-js/faker'; import { Users } from '../fixtures/userStates'; -import { OmnichannelContacts } from '../page-objects/omnichannel-contacts-list'; -import { OmnichannelSection } from '../page-objects/omnichannel-section'; +import { Navbar } from '../page-objects/fragments'; +import { OmnichannelContactCenterContacts } from '../page-objects/omnichannel'; import { test, expect } from '../utils/test'; const createToken = (): string => { @@ -66,8 +66,8 @@ const ERROR = { test.use({ storageState: Users.admin.state }); test.describe('OC - Contact Center - Contacts', () => { - let poContacts: OmnichannelContacts; - let poOmniSection: OmnichannelSection; + let poContacts: OmnichannelContactCenterContacts; + let poNavbar: Navbar; test.beforeAll(async ({ api }) => { // Add contacts @@ -80,8 +80,8 @@ test.describe('OC - Contact Center - Contacts', () => { }); test.beforeEach(async ({ page }) => { - poContacts = new OmnichannelContacts(page); - poOmniSection = new OmnichannelSection(page); + poContacts = new OmnichannelContactCenterContacts(page); + poNavbar = new Navbar(page); }); test.afterEach(async ({ api }) => { @@ -104,8 +104,8 @@ test.describe('OC - Contact Center - Contacts', () => { test.beforeEach(async ({ page }) => { await page.goto('/'); - await poOmniSection.btnContactCenter.click(); - await poOmniSection.tabContacts.click(); + await poNavbar.btnContactCenter.click(); + await poContacts.tabContacts.click(); await page.waitForURL(URL.contactCenter); }); @@ -113,75 +113,75 @@ test.describe('OC - Contact Center - Contacts', () => { await test.step('cancel button', async () => { await poContacts.btnNewContact.click(); await page.waitForURL(URL.newContact); - await expect(poContacts.newContact.inputName).toBeVisible(); - await poContacts.newContact.btnCancel.click(); + await expect(poContacts.editContact.inputName).toBeVisible(); + await poContacts.editContact.btnCancel.click(); await page.waitForURL(URL.contactCenter); - await expect(poContacts.newContact.inputName).not.toBeVisible(); + await expect(poContacts.editContact.inputName).not.toBeVisible(); }); await test.step('open contextual bar', async () => { await poContacts.btnNewContact.click(); await page.waitForURL(URL.newContact); - await expect(poContacts.newContact.inputName).toBeVisible(); + await expect(poContacts.editContact.inputName).toBeVisible(); }); await test.step('input name', async () => { - await poContacts.newContact.inputName.fill(NEW_CONTACT.name); + await poContacts.editContact.inputName.fill(NEW_CONTACT.name); }); await test.step('validate email format', async () => { - await poContacts.newContact.btnAddEmail.click(); - await poContacts.newContact.inputEmail.fill('invalidemail'); + await poContacts.editContact.btnAddEmail.click(); + await poContacts.editContact.inputEmail.fill('invalidemail'); await page.keyboard.press('Tab'); - await expect(poContacts.newContact.getErrorMessage(ERROR.invalidEmail)).toBeVisible(); + await expect(poContacts.editContact.getErrorMessage(ERROR.invalidEmail)).toBeVisible(); }); await test.step('validate email is duplicated', async () => { - await poContacts.newContact.inputEmail.fill(EXISTING_CONTACT.emails[0]); + await poContacts.editContact.inputEmail.fill(EXISTING_CONTACT.emails[0]); await page.keyboard.press('Tab'); - await expect(poContacts.newContact.getErrorMessage(ERROR.emailAlreadyExists)).toBeVisible(); + await expect(poContacts.editContact.getErrorMessage(ERROR.emailAlreadyExists)).toBeVisible(); }); await test.step('validate email is required', async () => { - await poContacts.newContact.inputEmail.clear(); + await poContacts.editContact.inputEmail.clear(); await page.keyboard.press('Tab'); - await expect(poContacts.newContact.getErrorMessage(ERROR.emailRequired)).toBeVisible(); + await expect(poContacts.editContact.getErrorMessage(ERROR.emailRequired)).toBeVisible(); }); await test.step('input email', async () => { - await poContacts.newContact.inputEmail.fill(NEW_CONTACT.email); + await poContacts.editContact.inputEmail.fill(NEW_CONTACT.email); await page.keyboard.press('Tab'); - await expect(poContacts.newContact.getErrorMessage(ERROR.invalidEmail)).not.toBeVisible(); - await expect(poContacts.newContact.getErrorMessage(ERROR.emailRequired)).not.toBeVisible(); - await expect(poContacts.newContact.getErrorMessage(ERROR.emailAlreadyExists)).not.toBeVisible(); + await expect(poContacts.editContact.getErrorMessage(ERROR.invalidEmail)).not.toBeVisible(); + await expect(poContacts.editContact.getErrorMessage(ERROR.emailRequired)).not.toBeVisible(); + await expect(poContacts.editContact.getErrorMessage(ERROR.emailAlreadyExists)).not.toBeVisible(); }); await test.step('validate phone is duplicated', async () => { - await poContacts.newContact.btnAddPhone.click(); - await poContacts.newContact.inputPhone.fill(EXISTING_CONTACT.phones[0]); + await poContacts.editContact.btnAddPhone.click(); + await poContacts.editContact.inputPhone.fill(EXISTING_CONTACT.phones[0]); await page.keyboard.press('Tab'); - await expect(poContacts.newContact.getErrorMessage(ERROR.phoneAlreadyExists)).toBeVisible(); + await expect(poContacts.editContact.getErrorMessage(ERROR.phoneAlreadyExists)).toBeVisible(); }); await test.step('input phone', async () => { - await poContacts.newContact.inputPhone.fill(NEW_CONTACT.phone); + await poContacts.editContact.inputPhone.fill(NEW_CONTACT.phone); await page.keyboard.press('Tab'); - await expect(poContacts.newContact.getErrorMessage(ERROR.phoneRequired)).not.toBeVisible(); + await expect(poContacts.editContact.getErrorMessage(ERROR.phoneRequired)).not.toBeVisible(); }); await test.step('save new contact', async () => { - await expect(poContacts.newContact.btnSave).toBeEnabled(); - await poContacts.newContact.btnSave.click(); + await expect(poContacts.editContact.btnSave).toBeEnabled(); + await poContacts.editContact.btnSave.click(); await poContacts.inputSearch.fill(NEW_CONTACT.name); - await expect(poContacts.findRowByName(NEW_CONTACT.name)).toBeVisible(); + await expect(poContacts.table.findRowByName(NEW_CONTACT.name)).toBeVisible(); }); }); test('Edit new contact', async ({ page }) => { await test.step('search contact and open contextual bar', async () => { await poContacts.inputSearch.fill(NEW_CONTACT.name); - const row = poContacts.findRowByName(NEW_CONTACT.name); + const row = poContacts.table.findRowByName(NEW_CONTACT.name); await expect(row).toBeVisible(); await row.click(); await page.waitForURL(URL.contactInfo); @@ -190,7 +190,7 @@ test.describe('OC - Contact Center - Contacts', () => { await test.step('cancel button', async () => { await poContacts.contactInfo.btnEdit.click(); await page.waitForURL(URL.editContact); - await poContacts.contactInfo.btnCancel.click(); + await poContacts.editContact.btnCancel.click(); await page.waitForURL(URL.contactInfo); }); @@ -200,93 +200,87 @@ test.describe('OC - Contact Center - Contacts', () => { }); await test.step('initial values', async () => { - await expect(poContacts.contactInfo.inputName).toHaveValue(NEW_CONTACT.name); - await expect(poContacts.contactInfo.inputEmail).toHaveValue(NEW_CONTACT.email); - await expect(poContacts.contactInfo.inputPhone).toHaveValue(NEW_CONTACT.phone); + await expect(poContacts.editContact.inputName).toHaveValue(NEW_CONTACT.name); + await expect(poContacts.editContact.inputEmail).toHaveValue(NEW_CONTACT.email); + await expect(poContacts.editContact.inputPhone).toHaveValue(NEW_CONTACT.phone); }); await test.step('validate email format', async () => { - await poContacts.newContact.inputEmail.fill('invalidemail'); + await poContacts.editContact.inputEmail.fill('invalidemail'); await page.keyboard.press('Tab'); - await expect(poContacts.newContact.getErrorMessage(ERROR.invalidEmail)).toBeVisible(); + await expect(poContacts.editContact.getErrorMessage(ERROR.invalidEmail)).toBeVisible(); }); await test.step('validate email is duplicated', async () => { - await poContacts.newContact.inputEmail.fill(EXISTING_CONTACT.emails[0]); + await poContacts.editContact.inputEmail.fill(EXISTING_CONTACT.emails[0]); await page.keyboard.press('Tab'); - await expect(poContacts.newContact.getErrorMessage(ERROR.emailAlreadyExists)).toBeVisible(); + await expect(poContacts.editContact.getErrorMessage(ERROR.emailAlreadyExists)).toBeVisible(); }); await test.step('validate email is required', async () => { - await poContacts.newContact.inputEmail.clear(); + await poContacts.editContact.inputEmail.clear(); await page.keyboard.press('Tab'); - await expect(poContacts.newContact.getErrorMessage(ERROR.emailRequired)).toBeVisible(); + await expect(poContacts.editContact.getErrorMessage(ERROR.emailRequired)).toBeVisible(); }); await test.step('validate name is required', async () => { - await poContacts.contactInfo.inputName.clear(); + await poContacts.editContact.inputName.clear(); await page.keyboard.press('Tab'); - await expect(poContacts.contactInfo.getErrorMessage(ERROR.nameRequired)).toBeVisible(); + await expect(poContacts.editContact.getErrorMessage(ERROR.nameRequired)).toBeVisible(); }); await test.step('edit name', async () => { - await poContacts.contactInfo.inputName.fill(EDIT_CONTACT.name); + await poContacts.editContact.inputName.fill(EDIT_CONTACT.name); }); await test.step('validate phone is duplicated', async () => { - await poContacts.newContact.inputPhone.fill(EXISTING_CONTACT.phones[0]); + await poContacts.editContact.inputPhone.fill(EXISTING_CONTACT.phones[0]); await page.keyboard.press('Tab'); - await expect(poContacts.newContact.getErrorMessage(ERROR.phoneAlreadyExists)).toBeVisible(); + await expect(poContacts.editContact.getErrorMessage(ERROR.phoneAlreadyExists)).toBeVisible(); }); await test.step('input phone ', async () => { - await poContacts.newContact.inputPhone.fill(EDIT_CONTACT.phone); + await poContacts.editContact.inputPhone.fill(EDIT_CONTACT.phone); await page.keyboard.press('Tab'); - await expect(poContacts.newContact.getErrorMessage(ERROR.phoneRequired)).not.toBeVisible(); + await expect(poContacts.editContact.getErrorMessage(ERROR.phoneRequired)).not.toBeVisible(); }); await test.step('input email', async () => { - await poContacts.newContact.inputEmail.fill(EDIT_CONTACT.email); + await poContacts.editContact.inputEmail.fill(EDIT_CONTACT.email); await page.keyboard.press('Tab'); - await expect(poContacts.newContact.getErrorMessage(ERROR.invalidEmail)).not.toBeVisible(); - await expect(poContacts.newContact.getErrorMessage(ERROR.emailRequired)).not.toBeVisible(); - await expect(poContacts.newContact.getErrorMessage(ERROR.emailAlreadyExists)).not.toBeVisible(); + await expect(poContacts.editContact.getErrorMessage(ERROR.invalidEmail)).not.toBeVisible(); + await expect(poContacts.editContact.getErrorMessage(ERROR.emailRequired)).not.toBeVisible(); + await expect(poContacts.editContact.getErrorMessage(ERROR.emailAlreadyExists)).not.toBeVisible(); }); await test.step('save new contact ', async () => { - await poContacts.contactInfo.btnSave.click(); + await poContacts.editContact.btnSave.click(); await poContacts.inputSearch.fill(EDIT_CONTACT.name); - await expect(poContacts.findRowByName(EDIT_CONTACT.name)).toBeVisible(); + await expect(poContacts.table.findRowByName(EDIT_CONTACT.name)).toBeVisible(); }); }); test('Delete a contact', async () => { await test.step('Find contact and open modal', async () => { await poContacts.inputSearch.fill(DELETE_CONTACT.name); - await poContacts.findRowMenu(DELETE_CONTACT.name).click(); - await poContacts.findMenuItem('Delete').click(); + await poContacts.deleteContact(DELETE_CONTACT.name); }); await test.step('Fill confirmation and delete contact', async () => { - await expect(poContacts.deleteContactModal).toBeVisible(); - await expect(poContacts.btnDeleteContact).toBeDisabled(); - // Fills the input with the wrong confirmation - await poContacts.inputDeleteContactConfirmation.fill('wrong'); - await expect(poContacts.btnDeleteContact).toBeDisabled(); + await poContacts.deleteContactModal.inputConfirmation.fill('wrong'); + await expect(poContacts.deleteContactModal.btnDelete).toBeDisabled(); // Fills the input correctly - await poContacts.inputDeleteContactConfirmation.fill('delete'); - await expect(poContacts.btnDeleteContact).toBeEnabled(); - await poContacts.btnDeleteContact.click(); - - await expect(poContacts.deleteContactModal).not.toBeVisible(); + await poContacts.deleteContactModal.inputConfirmation.fill('delete'); + await expect(poContacts.deleteContactModal.btnDelete).toBeEnabled(); + await poContacts.deleteContactModal.delete(); }); await test.step('Confirm contact removal', async () => { await poContacts.inputSearch.fill(DELETE_CONTACT.name); - await expect(poContacts.findRowByName(DELETE_CONTACT.name)).not.toBeVisible(); + await expect(poContacts.table.findRowByName(DELETE_CONTACT.name)).not.toBeVisible(); }); }); }); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-center-filters.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-center-filters.spec.ts index f84707292cb84..bbe5a52572ab6 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-center-filters.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-center-filters.spec.ts @@ -1,8 +1,8 @@ import { createFakeVisitor } from '../../mocks/data'; import { IS_EE } from '../config/constants'; import { Users } from '../fixtures/userStates'; -import { OmnichannelContacts } from '../page-objects/omnichannel-contacts-list'; -import { OmnichannelSection } from '../page-objects/omnichannel-section'; +import { Navbar } from '../page-objects/fragments'; +import { OmnichannelContactCenterChats } from '../page-objects/omnichannel'; import { createAgent, makeAgentAvailable } from '../utils/omnichannel/agents'; import { addAgentToDepartment, createDepartment } from '../utils/omnichannel/departments'; import { createConversation, updateRoom } from '../utils/omnichannel/rooms'; @@ -29,8 +29,8 @@ test.describe('OC - Contact Center', async () => { let tagA: Awaited>; let tagB: Awaited>; let units: Awaited>[]; - let poContacts: OmnichannelContacts; - let poOmniSection: OmnichannelSection; + let poNavbar: Navbar; + let poOmniChats: OmnichannelContactCenterChats; // Allow manual on hold test.beforeAll(async ({ api }) => { @@ -164,11 +164,12 @@ test.describe('OC - Contact Center', async () => { }); test.beforeEach(async ({ page }) => { - poContacts = new OmnichannelContacts(page); - poOmniSection = new OmnichannelSection(page); + poOmniChats = new OmnichannelContactCenterChats(page); + poNavbar = new Navbar(page); + await page.goto('/'); - await poOmniSection.btnContactCenter.click(); - await poContacts.tabChats.click(); + await poNavbar.btnContactCenter.click(); + await poOmniChats.tabChats.click(); await page.waitForURL(URL.contactCenterChats); }); @@ -177,131 +178,131 @@ test.describe('OC - Contact Center', async () => { const [unitA, unitB] = units.map((unit) => unit.data); await test.step('expect to filter by guest', async () => { - await poContacts.inputSearch.fill(visitorA); - await expect(poContacts.findRowByName(visitorA)).toBeVisible(); - await expect(poContacts.findRowByName(visitorB)).not.toBeVisible(); - await expect(poContacts.btnSearchChip(visitorA)).toBeVisible(); - - await poContacts.inputSearch.fill(''); - await expect(poContacts.findRowByName(visitorA)).toBeVisible(); - await expect(poContacts.findRowByName(visitorB)).toBeVisible(); + await poOmniChats.inputSearch.fill(visitorA); + await expect(poOmniChats.table.findRowByName(visitorA)).toBeVisible(); + await expect(poOmniChats.table.findRowByName(visitorB)).not.toBeVisible(); + await expect(poOmniChats.btnSearchChip(visitorA)).toBeVisible(); + + await poOmniChats.inputSearch.fill(''); + await expect(poOmniChats.table.findRowByName(visitorA)).toBeVisible(); + await expect(poOmniChats.table.findRowByName(visitorB)).toBeVisible(); }); await test.step('expect to filter by Served By', async () => { - await expect(poContacts.findRowByName(visitorA)).toBeVisible(); - await expect(poContacts.findRowByName(visitorB)).toBeVisible(); + await expect(poOmniChats.table.findRowByName(visitorA)).toBeVisible(); + await expect(poOmniChats.table.findRowByName(visitorB)).toBeVisible(); - await poContacts.btnFilters.click(); + await poOmniChats.btnFilters.click(); // Select user1 - await poContacts.selectServedBy('user1'); - await expect(poContacts.findRowByName(visitorA)).toBeVisible(); - await expect(poContacts.btnServedByChip('user1')).toBeVisible(); - await expect(poContacts.findRowByName(visitorB)).not.toBeVisible(); + await poOmniChats.filters.selectServedBy('user1'); + await expect(poOmniChats.table.findRowByName(visitorA)).toBeVisible(); + await expect(poOmniChats.btnServedByChip('user1')).toBeVisible(); + await expect(poOmniChats.table.findRowByName(visitorB)).not.toBeVisible(); // Select user2 - await poContacts.btnServedByChip('user1').locator('i').click(); - await poContacts.selectServedBy('user2'); - await expect(poContacts.findRowByName(visitorB)).toBeVisible(); - await expect(poContacts.findRowByName(visitorA)).not.toBeVisible(); - await expect(poContacts.btnServedByChip('user2')).toBeVisible(); + await poOmniChats.btnServedByChip('user1').locator('i').click(); + await poOmniChats.filters.selectServedBy('user2'); + await expect(poOmniChats.table.findRowByName(visitorB)).toBeVisible(); + await expect(poOmniChats.table.findRowByName(visitorA)).not.toBeVisible(); + await expect(poOmniChats.btnServedByChip('user2')).toBeVisible(); // Select all users - await poContacts.btnServedByChip('user2').locator('i').click(); - await poContacts.selectServedBy('user1'); - await poContacts.selectServedBy('user2'); - await expect(poContacts.findRowByName(visitorA)).toBeVisible(); - await expect(poContacts.findRowByName(visitorB)).toBeVisible(); - await poContacts.btnServedByChip('user1').locator('i').click(); + await poOmniChats.btnServedByChip('user2').locator('i').click(); + await poOmniChats.filters.selectServedBy('user1'); + await poOmniChats.filters.selectServedBy('user2'); + await expect(poOmniChats.table.findRowByName(visitorA)).toBeVisible(); + await expect(poOmniChats.table.findRowByName(visitorB)).toBeVisible(); + await poOmniChats.btnServedByChip('user1').locator('i').click(); }); await test.step('expect to filter by status', async () => { - await poContacts.selectStatus('closed'); - await expect(poContacts.findRowByName(visitorA)).not.toBeVisible(); - await expect(poContacts.findRowByName(visitorB)).not.toBeVisible(); - await expect(poContacts.findRowByName(visitorC)).toBeVisible(); - await expect(poContacts.btnStatusChip('Closed')).toBeVisible(); - - await poContacts.selectStatus('opened'); - await expect(poContacts.findRowByName(visitorA)).not.toBeVisible(); - await expect(poContacts.findRowByName(visitorB)).toBeVisible(); - await expect(poContacts.findRowByName(visitorC)).not.toBeVisible(); - await expect(poContacts.btnStatusChip('Open')).toBeVisible(); - - await poContacts.selectStatus('all'); - await expect(poContacts.findRowByName(visitorA)).toBeVisible(); - await expect(poContacts.findRowByName(visitorB)).toBeVisible(); - await expect(poContacts.findRowByName(visitorC)).toBeVisible(); - - await poContacts.selectStatus('onhold'); - await expect(poContacts.findRowByName(visitorA)).toBeVisible(); - await expect(poContacts.findRowByName(visitorB)).not.toBeVisible(); - await expect(poContacts.findRowByName(visitorC)).not.toBeVisible(); - await expect(poContacts.btnStatusChip('On hold')).toBeVisible(); - await poContacts.btnStatusChip('On hold').locator('i').click(); + await poOmniChats.filters.selectStatus('Closed'); + await expect(poOmniChats.table.findRowByName(visitorA)).not.toBeVisible(); + await expect(poOmniChats.table.findRowByName(visitorB)).not.toBeVisible(); + await expect(poOmniChats.table.findRowByName(visitorC)).toBeVisible(); + await expect(poOmniChats.btnStatusChip('Closed')).toBeVisible(); + + await poOmniChats.filters.selectStatus('Open'); + await expect(poOmniChats.table.findRowByName(visitorA)).not.toBeVisible(); + await expect(poOmniChats.table.findRowByName(visitorB)).toBeVisible(); + await expect(poOmniChats.table.findRowByName(visitorC)).not.toBeVisible(); + await expect(poOmniChats.btnStatusChip('Open')).toBeVisible(); + + await poOmniChats.filters.selectStatus('all'); + await expect(poOmniChats.table.findRowByName(visitorA)).toBeVisible(); + await expect(poOmniChats.table.findRowByName(visitorB)).toBeVisible(); + await expect(poOmniChats.table.findRowByName(visitorC)).toBeVisible(); + + await poOmniChats.filters.selectStatus('On hold'); + await expect(poOmniChats.table.findRowByName(visitorA)).toBeVisible(); + await expect(poOmniChats.table.findRowByName(visitorB)).not.toBeVisible(); + await expect(poOmniChats.table.findRowByName(visitorC)).not.toBeVisible(); + await expect(poOmniChats.btnStatusChip('On hold')).toBeVisible(); + await poOmniChats.btnStatusChip('On hold').locator('i').click(); }); await test.step('expect to filter by department', async () => { // select department A - await poContacts.selectDepartment(departmentA.name); - await expect(poContacts.findRowByName(visitorA)).toBeVisible(); - await expect(poContacts.findRowByName(visitorB)).not.toBeVisible(); - await expect(poContacts.findRowByName(visitorC)).not.toBeVisible(); - await expect(poContacts.btnDepartmentChip(departmentA.name)).toBeVisible(); - await poContacts.btnDepartmentChip(departmentA.name).locator('i').click(); + await poOmniChats.filters.selectDepartment(departmentA.name); + await expect(poOmniChats.table.findRowByName(visitorA)).toBeVisible(); + await expect(poOmniChats.table.findRowByName(visitorB)).not.toBeVisible(); + await expect(poOmniChats.table.findRowByName(visitorC)).not.toBeVisible(); + await expect(poOmniChats.btnDepartmentChip(departmentA.name)).toBeVisible(); + await poOmniChats.btnDepartmentChip(departmentA.name).locator('i').click(); // select department B - await poContacts.selectDepartment(departmentB.name); - await expect(poContacts.findRowByName(visitorA)).not.toBeVisible(); - await expect(poContacts.findRowByName(visitorB)).toBeVisible(); - await expect(poContacts.findRowByName(visitorC)).not.toBeVisible(); - await expect(poContacts.btnDepartmentChip(departmentB.name)).toBeVisible(); - await poContacts.btnDepartmentChip(departmentB.name).locator('i').click(); + await poOmniChats.filters.selectDepartment(departmentB.name); + await expect(poOmniChats.table.findRowByName(visitorA)).not.toBeVisible(); + await expect(poOmniChats.table.findRowByName(visitorB)).toBeVisible(); + await expect(poOmniChats.table.findRowByName(visitorC)).not.toBeVisible(); + await expect(poOmniChats.btnDepartmentChip(departmentB.name)).toBeVisible(); + await poOmniChats.btnDepartmentChip(departmentB.name).locator('i').click(); // select all departments - await poContacts.selectDepartment(departmentA.name); - await poContacts.selectDepartment(departmentB.name); - await expect(poContacts.findRowByName(visitorA)).toBeVisible(); - await expect(poContacts.findRowByName(visitorB)).toBeVisible(); + await poOmniChats.filters.selectDepartment(departmentA.name); + await poOmniChats.filters.selectDepartment(departmentB.name); + await expect(poOmniChats.table.findRowByName(visitorA)).toBeVisible(); + await expect(poOmniChats.table.findRowByName(visitorB)).toBeVisible(); }); await test.step('expect to filter by tags', async () => { - await poContacts.selectTag(tagA.data.name); - await expect(poContacts.findRowByName(visitorA)).toBeVisible(); - await expect(poContacts.findRowByName(visitorB)).not.toBeVisible(); + await poOmniChats.filters.selectTag(tagA.data.name); + await expect(poOmniChats.table.findRowByName(visitorA)).toBeVisible(); + await expect(poOmniChats.table.findRowByName(visitorB)).not.toBeVisible(); - await poContacts.selectTag(tagB.data.name); - await expect(poContacts.findRowByName(visitorA)).toBeVisible(); - await expect(poContacts.findRowByName(visitorB)).toBeVisible(); + await poOmniChats.filters.selectTag(tagB.data.name); + await expect(poOmniChats.table.findRowByName(visitorA)).toBeVisible(); + await expect(poOmniChats.table.findRowByName(visitorB)).toBeVisible(); - await poContacts.removeTag(tagA.data.name); - await expect(poContacts.findRowByName(visitorB)).toBeVisible(); - await expect(poContacts.findRowByName(visitorA)).not.toBeVisible(); + await poOmniChats.filters.removeTag(tagA.data.name); + await expect(poOmniChats.table.findRowByName(visitorB)).toBeVisible(); + await expect(poOmniChats.table.findRowByName(visitorA)).not.toBeVisible(); - await poContacts.removeTag(tagB.data.name); - await expect(poContacts.findRowByName(visitorB)).toBeVisible(); - await expect(poContacts.findRowByName(visitorA)).toBeVisible(); + await poOmniChats.filters.removeTag(tagB.data.name); + await expect(poOmniChats.table.findRowByName(visitorB)).toBeVisible(); + await expect(poOmniChats.table.findRowByName(visitorA)).toBeVisible(); }); await test.step('expect to filter by units', async () => { // select unitA - await poContacts.selectUnit(unitA.name); - await expect(poContacts.findRowByName(visitorA)).toBeVisible(); - await expect(poContacts.findRowByName(visitorB)).not.toBeVisible(); - await expect(poContacts.findRowByName(visitorC)).not.toBeVisible(); - await poContacts.btnUnitsChip(unitA.name).locator('i').click(); + await poOmniChats.filters.selectUnit(unitA.name); + await expect(poOmniChats.table.findRowByName(visitorA)).toBeVisible(); + await expect(poOmniChats.table.findRowByName(visitorB)).not.toBeVisible(); + await expect(poOmniChats.table.findRowByName(visitorC)).not.toBeVisible(); + await poOmniChats.btnUnitsChip(unitA.name).locator('i').click(); // select unitB - await poContacts.selectUnit(unitB.name); - await expect(poContacts.findRowByName(visitorA)).not.toBeVisible(); - await expect(poContacts.findRowByName(visitorB)).toBeVisible(); - await expect(poContacts.findRowByName(visitorC)).not.toBeVisible(); - await poContacts.btnUnitsChip(unitB.name).locator('i').click(); + await poOmniChats.filters.selectUnit(unitB.name); + await expect(poOmniChats.table.findRowByName(visitorA)).not.toBeVisible(); + await expect(poOmniChats.table.findRowByName(visitorB)).toBeVisible(); + await expect(poOmniChats.table.findRowByName(visitorC)).not.toBeVisible(); + await poOmniChats.btnUnitsChip(unitB.name).locator('i').click(); // no unit selected - await expect(poContacts.findRowByName(visitorA)).toBeVisible(); - await expect(poContacts.findRowByName(visitorB)).toBeVisible(); - await expect(poContacts.findRowByName(visitorC)).not.toBeVisible(); + await expect(poOmniChats.table.findRowByName(visitorA)).toBeVisible(); + await expect(poOmniChats.table.findRowByName(visitorB)).toBeVisible(); + await expect(poOmniChats.table.findRowByName(visitorC)).not.toBeVisible(); }); }); @@ -310,32 +311,31 @@ test.describe('OC - Contact Center', async () => { const [unitA] = units.map((unit) => unit.data); await test.step('expect to display result as per applied filters ', async () => { - await poContacts.btnFilters.click(); - await poContacts.selectServedBy('user1'); - await poContacts.selectStatus('onhold'); - await poContacts.selectDepartment(departmentA.name); - await poContacts.selectTag(tagA.data.name); - await poContacts.selectUnit(unitA.name); - - await expect(poContacts.findRowByName(visitorA)).toBeVisible(); - await expect(poContacts.findRowByName(visitorB)).not.toBeVisible(); - await expect(poContacts.findRowByName(visitorC)).not.toBeVisible(); + await poOmniChats.btnFilters.click(); + await poOmniChats.filters.selectServedBy('user1'); + await poOmniChats.filters.selectStatus('On hold'); + await poOmniChats.filters.selectDepartment(departmentA.name); + await poOmniChats.filters.selectTag(tagA.data.name); + await poOmniChats.filters.selectUnit(unitA.name); + await expect(poOmniChats.table.findRowByName(visitorA)).toBeVisible(); + await expect(poOmniChats.table.findRowByName(visitorB)).not.toBeVisible(); + await expect(poOmniChats.table.findRowByName(visitorC)).not.toBeVisible(); }); await test.step('expect to clear all filters ', async () => { - await poContacts.btnClearFilters.click(); - await expect(poContacts.findRowByName(visitorA)).toBeVisible(); - await expect(poContacts.findRowByName(visitorB)).toBeVisible(); - await expect(poContacts.findRowByName(visitorC)).toBeVisible(); + await poOmniChats.filters.btnClearFilters.click(); + await expect(poOmniChats.table.findRowByName(visitorA)).toBeVisible(); + await expect(poOmniChats.table.findRowByName(visitorB)).toBeVisible(); + await expect(poOmniChats.table.findRowByName(visitorC)).toBeVisible(); }); }); test('OC - Contact Center - Close contextual bar with filter screen', async () => { await test.step('expect to close filters contextual bar ', async () => { - await poContacts.btnFilters.click(); - await poContacts.btnClose.click(); - await expect(poContacts.btnFilters).toBeVisible(); - await expect(poContacts.btnClearFilters).not.toBeVisible(); + await poOmniChats.btnFilters.click(); + await poOmniChats.filters.close(); + await expect(poOmniChats.btnFilters).toBeVisible(); + await expect(poOmniChats.filters.btnClearFilters).not.toBeVisible(); }); }); }); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-info.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-info.spec.ts index c0e590cbd5545..b4e18cd88b442 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-info.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-info.spec.ts @@ -3,15 +3,15 @@ import type { Page } from '@playwright/test'; import { createFakeVisitor } from '../../mocks/data'; import { createAuxContext } from '../fixtures/createAuxContext'; import { Users } from '../fixtures/userStates'; -import { HomeOmnichannel, OmnichannelLiveChat } from '../page-objects'; -import { OmnichannelContacts } from '../page-objects/omnichannel-contacts-list'; +import { HomeOmnichannel } from '../page-objects'; +import { OmnichannelLiveChat } from '../page-objects/omnichannel'; import { expect, test } from '../utils/test'; test.describe('Omnichannel contact info', () => { let poLiveChat: OmnichannelLiveChat; let newVisitor: { email: string; name: string }; - let agent: { page: Page; poHomeChannel: HomeOmnichannel; poContacts: OmnichannelContacts }; + let agent: { page: Page; poHomeChannel: HomeOmnichannel }; test.beforeAll(async ({ api, browser }) => { newVisitor = createFakeVisitor(); @@ -21,7 +21,7 @@ test.describe('Omnichannel contact info', () => { await api.post('/livechat/users/manager', { username: 'user1' }); const { page } = await createAuxContext(browser, Users.user1); - agent = { page, poHomeChannel: new HomeOmnichannel(page), poContacts: new OmnichannelContacts(page) }; + agent = { page, poHomeChannel: new HomeOmnichannel(page) }; }); test.beforeEach(async ({ page, api }) => { poLiveChat = new OmnichannelLiveChat(page, api); @@ -48,12 +48,12 @@ test.describe('Omnichannel contact info', () => { await test.step('Expect to be able to see contact information and edit', async () => { await agent.poHomeChannel.roomToolbar.openContactInfo(); - await agent.poHomeChannel.content.btnContactEdit.click(); + await agent.poHomeChannel.contacts.contactInfo.btnEdit.click(); }); await test.step('Expect to update room name and subscription when updating contact name', async () => { - await agent.poContacts.newContact.inputName.fill('Edited Contact Name'); - await agent.poContacts.newContact.btnSave.click(); + await agent.poHomeChannel.contacts.editContact.inputName.fill('Edited Contact Name'); + await agent.poHomeChannel.contacts.editContact.btnSave.click(); await expect(agent.poHomeChannel.sidebar.channelsList.getByText('Edited Contact Name')).toBeVisible(); await expect(agent.poHomeChannel.content.channelHeader.getByText('Edited Contact Name')).toBeVisible(); }); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-unknown-callout.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-unknown-callout.spec.ts index 7dfc43ee40cc1..684e3f7c3f4af 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-unknown-callout.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-unknown-callout.spec.ts @@ -4,7 +4,8 @@ import { createFakeVisitor } from '../../mocks/data'; import { IS_EE } from '../config/constants'; import { createAuxContext } from '../fixtures/createAuxContext'; import { Users } from '../fixtures/userStates'; -import { OmnichannelLiveChat, HomeChannel } from '../page-objects'; +import { HomeChannel } from '../page-objects'; +import { OmnichannelLiveChat } from '../page-objects/omnichannel'; import { expect, test } from '../utils/test'; test.describe('OC - Contact Unknown Callout', () => { diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-custom-fields.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-custom-fields.spec.ts index 6749f6905466c..7613201c7afde 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-custom-fields.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-custom-fields.spec.ts @@ -1,5 +1,5 @@ import { Users } from '../fixtures/userStates'; -import { OmnichannelCustomFields } from '../page-objects'; +import { OmnichannelCustomFields } from '../page-objects/omnichannel'; import { test, expect } from '../utils/test'; test.use({ storageState: Users.admin.state }); @@ -11,40 +11,38 @@ test.describe('omnichannel-customFields', () => { poOmnichannelCustomFields = new OmnichannelCustomFields(page); await page.goto('/omnichannel'); - await poOmnichannelCustomFields.sidenav.linkCustomFields.click(); + await poOmnichannelCustomFields.sidebar.linkCustomFields.click(); }); test('expect add new "custom field"', async ({ page }) => { - await poOmnichannelCustomFields.btnAdd.click(); + await poOmnichannelCustomFields.createNew(); await page.waitForURL('/omnichannel/customfields/new'); - await poOmnichannelCustomFields.inputField.type(newField); - await poOmnichannelCustomFields.inputLabel.type('any_label'); + await poOmnichannelCustomFields.manageCustomFields.inputField.fill(newField); + await poOmnichannelCustomFields.manageCustomFields.inputLabel.fill('any_label'); + await poOmnichannelCustomFields.manageCustomFields.save(); - await poOmnichannelCustomFields.btnSave.click(); - - await expect(poOmnichannelCustomFields.firstRowInTable(newField)).toBeVisible(); + await expect(poOmnichannelCustomFields.table.findRowByName(newField)).toBeVisible(); }); - test('expect update "newField"', async ({ page }) => { + test('expect update "newField"', async () => { const newLabel = 'new_any_label'; await poOmnichannelCustomFields.inputSearch.fill(newField); - await poOmnichannelCustomFields.firstRowInTable(newField).click(); + await poOmnichannelCustomFields.table.findRowByName(newField).click(); - await poOmnichannelCustomFields.inputLabel.fill('new_any_label'); - await poOmnichannelCustomFields.visibleLabel.click(); - await poOmnichannelCustomFields.btnSave.click(); + await poOmnichannelCustomFields.manageCustomFields.inputLabel.fill('new_any_label'); + await poOmnichannelCustomFields.manageCustomFields.labelVisible.click(); + await poOmnichannelCustomFields.manageCustomFields.save(); - await expect(page.locator(`[qa-user-id="${newField}"] td:nth-child(2)`)).toHaveText(newLabel); + await expect(poOmnichannelCustomFields.table.findRowByName(newField)).toContainText(newLabel); }); test('expect remove "new_field"', async () => { await poOmnichannelCustomFields.inputSearch.fill(newField); - await poOmnichannelCustomFields.firstRowInTable(newField).click(); - await poOmnichannelCustomFields.btnDeleteCustomField.click(); - await poOmnichannelCustomFields.btnModalRemove.click(); + await poOmnichannelCustomFields.table.findRowByName(newField).click(); + await poOmnichannelCustomFields.deleteCustomField(newField); await poOmnichannelCustomFields.inputSearch.fill(newField); - await expect(poOmnichannelCustomFields.firstRowInTable(newField)).toBeHidden(); + await expect(poOmnichannelCustomFields.table.findRowByName(newField)).toBeHidden(); }); }); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-departaments-ce.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-departaments-ce.spec.ts index aab1b27ff1d0d..922de338cdc70 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-departaments-ce.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-departaments-ce.spec.ts @@ -1,9 +1,8 @@ import { faker } from '@faker-js/faker'; -import type { Page } from '@playwright/test'; import { IS_EE } from '../config/constants'; import { Users } from '../fixtures/userStates'; -import { OmnichannelDepartments } from '../page-objects'; +import { OmnichannelDepartments } from '../page-objects/omnichannel'; import { test, expect } from '../utils/test'; test.use({ storageState: Users.admin.state }); @@ -11,39 +10,32 @@ test.use({ storageState: Users.admin.state }); test.describe.serial('OC - Manage Departments (CE)', () => { test.skip(IS_EE, 'Community Edition Only'); let poOmnichannelDepartments: OmnichannelDepartments; - let departmentName: string; test.beforeAll(async () => { departmentName = faker.string.uuid(); }); - test.beforeEach(async ({ page }: { page: Page }) => { + test.beforeEach(async ({ page }) => { poOmnichannelDepartments = new OmnichannelDepartments(page); await page.goto('/omnichannel'); - await poOmnichannelDepartments.sidenav.linkDepartments.click(); + await poOmnichannelDepartments.sidebar.linkDepartments.click(); }); test('OC - Manage Departments (CE) - Create department', async () => { await test.step('expect create new department', async () => { - await poOmnichannelDepartments.headingButtonNew('Create department').click(); - await poOmnichannelDepartments.btnEnabled.click(); - await poOmnichannelDepartments.inputName.fill(departmentName); - await poOmnichannelDepartments.inputEmail.fill(faker.internet.email()); - await poOmnichannelDepartments.btnSave.click(); - await poOmnichannelDepartments.toastMessage.dismissToast(); + await poOmnichannelDepartments.createNew(); + await poOmnichannelDepartments.createDepartment(departmentName, faker.internet.email()); await poOmnichannelDepartments.inputSearch.fill(departmentName); - await expect(poOmnichannelDepartments.firstRowInTable).toBeVisible(); + await expect(poOmnichannelDepartments.departmentsTable.findRowByName(departmentName)).toBeVisible(); }); await test.step('expect to not be possible adding a second department ', async () => { - await poOmnichannelDepartments.headingButtonNew('Create department').click(); - - await expect(poOmnichannelDepartments.upgradeDepartmentsModal).toBeVisible(); - - await poOmnichannelDepartments.btnUpgradeDepartmentsModalClose.click(); + await poOmnichannelDepartments.createNew(); + await poOmnichannelDepartments.upsellDepartmentsModal.waitForDisplay(); + await poOmnichannelDepartments.upsellDepartmentsModal.close(); }); }); }); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-departaments.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-departaments.spec.ts index e555c4da7f6a9..9070744d23f4c 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-departaments.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-departaments.spec.ts @@ -3,7 +3,7 @@ import type { Page } from '@playwright/test'; import { IS_EE } from '../config/constants'; import { Users } from '../fixtures/userStates'; -import { OmnichannelDepartments } from '../page-objects'; +import { OmnichannelDepartments } from '../page-objects/omnichannel'; import { createAgent } from '../utils/omnichannel/agents'; import { createDepartment, deleteDepartment } from '../utils/omnichannel/departments'; import { test, expect } from '../utils/test'; @@ -39,38 +39,34 @@ test.describe('OC - Manage Departments', () => { poOmnichannelDepartments = new OmnichannelDepartments(page); await page.goto('/omnichannel'); - await poOmnichannelDepartments.sidenav.linkDepartments.click(); + await poOmnichannelDepartments.sidebar.linkDepartments.click(); }); test('Create department', async ({ page }) => { const departmentName = faker.string.uuid(); - await poOmnichannelDepartments.headingButtonNew('Create department').click(); + await poOmnichannelDepartments.createNew(); await test.step('expect name and email to be required', async () => { - await expect(poOmnichannelDepartments.invalidInputEmail).not.toBeVisible(); + await expect(poOmnichannelDepartments.errorMessage(ERROR.requiredName)).not.toBeVisible(); await poOmnichannelDepartments.inputName.fill('any_text'); await poOmnichannelDepartments.inputName.fill(''); - await expect(poOmnichannelDepartments.invalidInputName).toBeVisible(); await expect(poOmnichannelDepartments.errorMessage(ERROR.requiredName)).toBeVisible(); await poOmnichannelDepartments.inputName.fill('any_text'); - await expect(poOmnichannelDepartments.invalidInputName).not.toBeVisible(); + await expect(poOmnichannelDepartments.errorMessage(ERROR.requiredName)).not.toBeVisible(); await poOmnichannelDepartments.inputEmail.fill('any_text'); - await expect(poOmnichannelDepartments.invalidInputEmail).toBeVisible(); await expect(poOmnichannelDepartments.errorMessage(ERROR.invalidEmail)).toBeVisible(); await poOmnichannelDepartments.inputEmail.fill(''); - await expect(poOmnichannelDepartments.invalidInputEmail).toBeVisible(); await expect(poOmnichannelDepartments.errorMessage(ERROR.requiredEmail)).toBeVisible(); await poOmnichannelDepartments.inputEmail.fill(faker.internet.email()); - await expect(poOmnichannelDepartments.invalidInputEmail).not.toBeVisible(); await expect(poOmnichannelDepartments.errorMessage(ERROR.requiredEmail)).not.toBeVisible(); }); await test.step('expect to fill required fields', async () => { - await poOmnichannelDepartments.btnEnabled.click(); + await poOmnichannelDepartments.labelEnabled.click(); await poOmnichannelDepartments.inputName.fill(departmentName); await poOmnichannelDepartments.inputEmail.fill(faker.internet.email()); }); @@ -98,38 +94,38 @@ test.describe('OC - Manage Departments', () => { await poOmnichannelDepartments.inputAgents.fill('user1'); await poOmnichannelDepartments.findOption('user1 (@user1)').click(); await poOmnichannelDepartments.btnAddAgent.click(); - await expect(poOmnichannelDepartments.findAgentRow('user1')).toBeVisible(); + await expect(poOmnichannelDepartments.agentsTable.findRowByName('user1')).toBeVisible(); }); await test.step('expect create new department', async () => { await poOmnichannelDepartments.btnSave.click(); await poOmnichannelDepartments.search(departmentName); - await expect(poOmnichannelDepartments.firstRowInTable).toBeVisible(); + await expect(poOmnichannelDepartments.departmentsTable.findRowByName(departmentName)).toBeVisible(); }); await test.step('expect to delete department', async () => { await poOmnichannelDepartments.search(departmentName); - await poOmnichannelDepartments.selectedDepartmentMenu(departmentName).click(); + await poOmnichannelDepartments.getDepartmentMenuByName(departmentName).click(); await poOmnichannelDepartments.menuDeleteOption.click(); await test.step('expect confirm delete department', async () => { await test.step('expect delete to be disabled when name is incorrect', async () => { - await expect(poOmnichannelDepartments.btnModalConfirmDelete).toBeDisabled(); - await poOmnichannelDepartments.inputModalConfirmDelete.fill('someramdomname'); - await expect(poOmnichannelDepartments.btnModalConfirmDelete).toBeDisabled(); + await expect(poOmnichannelDepartments.deleteModal.btnDelete).toBeDisabled(); + await poOmnichannelDepartments.deleteModal.inputConfirmDepartmentName.fill('someramdomname'); + await expect(poOmnichannelDepartments.deleteModal.btnDelete).toBeDisabled(); }); await test.step('expect to successfuly delete if department name is correct', async () => { - await expect(poOmnichannelDepartments.btnModalConfirmDelete).toBeDisabled(); - await poOmnichannelDepartments.inputModalConfirmDelete.fill(departmentName); - await expect(poOmnichannelDepartments.btnModalConfirmDelete).toBeEnabled(); - await poOmnichannelDepartments.btnModalConfirmDelete.click(); + await expect(poOmnichannelDepartments.deleteModal.btnDelete).toBeDisabled(); + await poOmnichannelDepartments.deleteModal.inputConfirmDepartmentName.fill(departmentName); + await expect(poOmnichannelDepartments.deleteModal.btnDelete).toBeEnabled(); + await poOmnichannelDepartments.deleteModal.deleteDepartment(departmentName); }); }); await test.step('expect department to have been deleted', async () => { await poOmnichannelDepartments.search(departmentName); - await expect(poOmnichannelDepartments.firstRowInTable).toHaveCount(0); + await expect(poOmnichannelDepartments.departmentsTable.findRowByName(departmentName)).toHaveCount(0); }); }); }); @@ -152,63 +148,59 @@ test.describe('OC - Manage Departments', () => { test('Edit department', async () => { await test.step('expect create new department', async () => { await poOmnichannelDepartments.search(department.name); - await expect(poOmnichannelDepartments.firstRowInTable).toBeVisible(); + await expect(poOmnichannelDepartments.departmentsTable.findRowByName(department.name)).toBeVisible(); return department; }); await test.step('expect update department name', async () => { await poOmnichannelDepartments.search(department.name); - - await poOmnichannelDepartments.firstRowInTableMenu.click(); + await poOmnichannelDepartments.getDepartmentMenuByName(department.name).click(); await poOmnichannelDepartments.menuEditOption.click(); await poOmnichannelDepartments.inputName.fill(`edited-${department.name}`); await poOmnichannelDepartments.btnSave.click(); await poOmnichannelDepartments.search(`edited-${department.name}`); - await expect(poOmnichannelDepartments.firstRowInTable).toBeVisible(); + await expect(poOmnichannelDepartments.departmentsTable.findRowByName(`edited-${department.name}`)).toBeVisible(); }); }); test('Archive department', async () => { await test.step('expect create new department', async () => { await poOmnichannelDepartments.search(department.name); - await expect(poOmnichannelDepartments.firstRowInTable).toBeVisible(); + await expect(poOmnichannelDepartments.departmentsTable.findRowByName(department.name)).toBeVisible(); }); await test.step('expect archive department', async () => { await poOmnichannelDepartments.search(department.name); + await expect(poOmnichannelDepartments.departmentsTable.findRowByName(department.name)).toBeVisible(); - await expect(poOmnichannelDepartments.firstRowInTable).toBeVisible(); - await poOmnichannelDepartments.firstRowInTableMenu.click(); - await poOmnichannelDepartments.menuArchiveOption.click(); - await poOmnichannelDepartments.toastMessage.waitForDisplay(); - - await poOmnichannelDepartments.archivedDepartmentsTab.click(); + await poOmnichannelDepartments.archiveDepartmentByName(department.name); + await poOmnichannelDepartments.tabArchivedDepartments.click(); await poOmnichannelDepartments.search(department.name); - await expect(poOmnichannelDepartments.firstRowInTable).toBeVisible(); + await expect(poOmnichannelDepartments.departmentsTable.findRowByName(department.name)).toBeVisible(); }); await test.step('expect archived department to not be editable', async () => { - await poOmnichannelDepartments.firstRowInTableMenu.click(); + await poOmnichannelDepartments.getDepartmentMenuByName(department.name).click(); await expect(poOmnichannelDepartments.menuEditOption).not.toBeVisible(); }); await test.step('expect unarchive department', async () => { await poOmnichannelDepartments.menuUnarchiveOption.click(); - await expect(poOmnichannelDepartments.firstRowInTable).toHaveCount(0); + await expect(poOmnichannelDepartments.departmentsTable.findRowByName(department.name)).toHaveCount(0); }); }); test('Request tag(s) before closing conversation', async () => { await test.step('should create new department', async () => { await poOmnichannelDepartments.search(department.name); - await expect(poOmnichannelDepartments.firstRowInTable).toBeVisible(); + await expect(poOmnichannelDepartments.departmentsTable.findRowByName(department.name)).toBeVisible(); }); const tagName = faker.string.sample(5); - await poOmnichannelDepartments.firstRowInTableMenu.click(); + await poOmnichannelDepartments.getDepartmentMenuByName(department.name).click(); await poOmnichannelDepartments.menuEditOption.click(); await test.step('should form save button be disabled', async () => { @@ -216,8 +208,8 @@ test.describe('OC - Manage Departments', () => { }); await test.step('should be able to add a tag properly', async () => { - await poOmnichannelDepartments.inputTags.fill(tagName); - await poOmnichannelDepartments.btnTagsAdd.click(); + await poOmnichannelDepartments.inputConversationClosingTags.fill(tagName); + await poOmnichannelDepartments.btnAddTags.click(); await expect(poOmnichannelDepartments.btnTag(tagName)).toBeVisible(); await expect(poOmnichannelDepartments.btnSave).toBeEnabled(); @@ -225,32 +217,32 @@ test.describe('OC - Manage Departments', () => { await test.step('should be able to remove a tag properly', async () => { await poOmnichannelDepartments.btnTag(tagName).click(); - await expect(poOmnichannelDepartments.btnTagsAdd).toBeDisabled(); + await expect(poOmnichannelDepartments.btnAddTags).toBeDisabled(); }); await test.step('should not be possible to add empty tags', async () => { - await poOmnichannelDepartments.inputTags.fill(''); - await expect(poOmnichannelDepartments.btnTagsAdd).toBeDisabled(); + await poOmnichannelDepartments.inputConversationClosingTags.fill(''); + await expect(poOmnichannelDepartments.btnAddTags).toBeDisabled(); }); await test.step('should not be possible to add same tag twice', async () => { const tagName = faker.string.sample(5); - await poOmnichannelDepartments.inputTags.fill(tagName); - await poOmnichannelDepartments.btnTagsAdd.click(); - await poOmnichannelDepartments.inputTags.fill(tagName); - await expect(poOmnichannelDepartments.btnTagsAdd).toBeDisabled(); + await poOmnichannelDepartments.inputConversationClosingTags.fill(tagName); + await poOmnichannelDepartments.btnAddTags.click(); + await poOmnichannelDepartments.inputConversationClosingTags.fill(tagName); + await expect(poOmnichannelDepartments.btnAddTags).toBeDisabled(); }); }); test('Toggle department removal', async ({ api }) => { await test.step('expect create new department', async () => { await poOmnichannelDepartments.search(department.name); - await expect(poOmnichannelDepartments.firstRowInTable).toBeVisible(); + await expect(poOmnichannelDepartments.departmentsTable.findRowByName(department.name)).toBeVisible(); }); await test.step('expect to be able to delete department', async () => { await poOmnichannelDepartments.search(department.name); - await poOmnichannelDepartments.selectedDepartmentMenu(department.name).click(); + await poOmnichannelDepartments.getDepartmentMenuByName(department.name).click(); await expect(poOmnichannelDepartments.menuDeleteOption).toBeEnabled(); }); @@ -261,7 +253,7 @@ test.describe('OC - Manage Departments', () => { await test.step('expect not to be able to delete department', async () => { await poOmnichannelDepartments.search(department.name); - await poOmnichannelDepartments.selectedDepartmentMenu(department.name).click(); + await poOmnichannelDepartments.getDepartmentMenuByName(department.name).click(); await expect(poOmnichannelDepartments.menuDeleteOption).toBeDisabled(); }); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-api.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-api.spec.ts index 852020dae50d5..581394c5910d2 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-api.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-api.spec.ts @@ -5,7 +5,8 @@ import { createFakeVisitor } from '../../mocks/data'; import { IS_EE } from '../config/constants'; import { createAuxContext } from '../fixtures/createAuxContext'; import { Users } from '../fixtures/userStates'; -import { HomeOmnichannel, OmnichannelLiveChatEmbedded } from '../page-objects'; +import { HomeOmnichannel } from '../page-objects'; +import { OmnichannelLiveChatEmbedded } from '../page-objects/omnichannel'; import { createAgent } from '../utils/omnichannel/agents'; import { addAgentToDepartment, createDepartment } from '../utils/omnichannel/departments'; import { test, expect } from '../utils/test'; @@ -489,7 +490,9 @@ test.describe('OC - Livechat API', () => { await poAuxContext.poHomeOmnichannel.roomToolbar.openContactInfo(); // For some reason the guest info email information is being set to lowercase - await expect(poAuxContext.poHomeOmnichannel.content.infoContactEmail).toHaveText(registerGuestVisitor.email.toLowerCase()); + await expect(poAuxContext.poHomeOmnichannel.contacts.contactInfo.infoContactEmail).toHaveText( + registerGuestVisitor.email.toLowerCase(), + ); }); await test.step('Expect registerGuest to log in an existing guest and load chat history', async () => { @@ -623,7 +626,7 @@ test.describe('OC - Livechat API', () => { await poAuxContext.poHomeOmnichannel.roomToolbar.openContactInfo(); // For some reason the guest info email information is being set to lowercase - await expect(poAuxContext.poHomeOmnichannel.content.infoContactEmail).toHaveText( + await expect(poAuxContext.poHomeOmnichannel.contacts.contactInfo.infoContactEmail).toHaveText( `changed${registerGuestVisitor.email}`.toLowerCase(), ); }); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-avatar-visibility.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-avatar-visibility.spec.ts index bf384c864d791..0eb318b871119 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-avatar-visibility.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-avatar-visibility.spec.ts @@ -3,7 +3,8 @@ import type { Page } from '@playwright/test'; import { createFakeVisitor } from '../../mocks/data'; import { createAuxContext } from '../fixtures/createAuxContext'; import { Users } from '../fixtures/userStates'; -import { HomeOmnichannel, OmnichannelLiveChatEmbedded } from '../page-objects'; +import { HomeOmnichannel } from '../page-objects'; +import { OmnichannelLiveChatEmbedded } from '../page-objects/omnichannel'; import { createAgent, makeAgentAvailable } from '../utils/omnichannel/agents'; import { test, expect } from '../utils/test'; diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-background.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-background.spec.ts index b7a058e719ee1..dcd2dbc0a68c1 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-background.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-background.spec.ts @@ -1,7 +1,7 @@ import { createFakeVisitor } from '../../mocks/data'; import { IS_EE } from '../config/constants'; import { Users } from '../fixtures/userStates'; -import { OmnichannelLiveChatEmbedded } from '../page-objects'; +import { OmnichannelLiveChatEmbedded } from '../page-objects/omnichannel'; import { createAgent, makeAgentAvailable } from '../utils/omnichannel/agents'; import { test, expect } from '../utils/test'; diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-department.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-department.spec.ts index e10c134de579d..37fb79abc25cd 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-department.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-department.spec.ts @@ -2,7 +2,8 @@ import { createFakeVisitor } from '../../mocks/data'; import { IS_EE } from '../config/constants'; import { createAuxContext } from '../fixtures/createAuxContext'; import { Users } from '../fixtures/userStates'; -import { HomeOmnichannel, OmnichannelLiveChat } from '../page-objects'; +import { HomeOmnichannel } from '../page-objects'; +import { OmnichannelLiveChat } from '../page-objects/omnichannel'; import { createAgent } from '../utils/omnichannel/agents'; import { addAgentToDepartment, createDepartment } from '../utils/omnichannel/departments'; import { test, expect } from '../utils/test'; diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-fileupload.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-fileupload.spec.ts index d96aa4006bc4b..4bbc1292dd423 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-fileupload.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-fileupload.spec.ts @@ -1,7 +1,8 @@ import { createFakeVisitor } from '../../mocks/data'; import { createAuxContext } from '../fixtures/createAuxContext'; import { Users } from '../fixtures/userStates'; -import { HomeOmnichannel, OmnichannelLiveChat } from '../page-objects'; +import { HomeOmnichannel } from '../page-objects'; +import { OmnichannelLiveChat } from '../page-objects/omnichannel'; import { createAgent } from '../utils/omnichannel/agents'; import { test, expect } from '../utils/test'; diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-hide-expand-chat.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-hide-expand-chat.spec.ts index 2e6640c1b435c..a256dd09d8b45 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-hide-expand-chat.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-hide-expand-chat.spec.ts @@ -1,8 +1,7 @@ import { IS_EE } from '../config/constants'; import { createAuxContext } from '../fixtures/createAuxContext'; import { Users } from '../fixtures/userStates'; -import { OmnichannelLiveChat } from '../page-objects'; -import { OmnichannelLivechatAppearance } from '../page-objects/omnichannel-livechat-appearance'; +import { OmnichannelLiveChat, OmnichannelLivechatAppearance } from '../page-objects/omnichannel'; import { createAgent, makeAgentAvailable } from '../utils/omnichannel/agents'; import { test, expect } from '../utils/test'; @@ -55,7 +54,7 @@ test.describe('OC - Livechat - Hide "Expand chat"', async () => { await test.step('expect to change setting', async () => { await poLivechatAppearance.labelHideExpandChat.click(); - await poLivechatAppearance.btnSave.click(); + await poLivechatAppearance.btnSaveChanges.click(); }); await test.step('expect "Expand chat" button to be hidden', async () => { diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-logo.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-logo.spec.ts index 51f59b5af2879..fe6581cf0f932 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-logo.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-logo.spec.ts @@ -1,7 +1,7 @@ import { IS_EE } from '../config/constants'; import { createAuxContext } from '../fixtures/createAuxContext'; import { Users } from '../fixtures/userStates'; -import { OmnichannelLiveChat, OmnichannelSettings } from '../page-objects'; +import { OmnichannelLiveChat, OmnichannelSettings } from '../page-objects/omnichannel'; import { test, expect } from '../utils/test'; test.skip(!IS_EE, 'Enterprise Only'); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-message-bubble-color.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-message-bubble-color.spec.ts index 76b255a11157a..370fdf191463f 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-message-bubble-color.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-message-bubble-color.spec.ts @@ -3,7 +3,8 @@ import type { Page } from '@playwright/test'; import { createFakeVisitor } from '../../mocks/data'; import { createAuxContext } from '../fixtures/createAuxContext'; import { Users } from '../fixtures/userStates'; -import { HomeOmnichannel, OmnichannelLiveChatEmbedded } from '../page-objects'; +import { HomeOmnichannel } from '../page-objects'; +import { OmnichannelLiveChatEmbedded } from '../page-objects/omnichannel'; import { createAgent, makeAgentAvailable } from '../utils/omnichannel/agents'; import { test, expect } from '../utils/test'; diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-queue-management-autoselection.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-queue-management-autoselection.spec.ts index a11ae455e9ad6..c666d4a603a30 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-queue-management-autoselection.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-queue-management-autoselection.spec.ts @@ -2,7 +2,8 @@ import { createFakeVisitor } from '../../mocks/data'; import { IS_EE } from '../config/constants'; import { createAuxContext } from '../fixtures/createAuxContext'; import { Users } from '../fixtures/userStates'; -import { HomeOmnichannel, OmnichannelLiveChat } from '../page-objects'; +import { HomeOmnichannel } from '../page-objects'; +import { OmnichannelLiveChat } from '../page-objects/omnichannel'; import { test, expect } from '../utils/test'; const firstVisitor = createFakeVisitor(); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-queue-management.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-queue-management.spec.ts index a55ef80ecb1b4..f807014636041 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-queue-management.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-queue-management.spec.ts @@ -2,7 +2,8 @@ import { createFakeVisitor } from '../../mocks/data'; import { IS_EE } from '../config/constants'; import { createAuxContext } from '../fixtures/createAuxContext'; import { Users } from '../fixtures/userStates'; -import { HomeOmnichannel, OmnichannelLiveChat } from '../page-objects'; +import { HomeOmnichannel } from '../page-objects'; +import { OmnichannelLiveChat } from '../page-objects/omnichannel'; import { test, expect } from '../utils/test'; const firstVisitor = createFakeVisitor(); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-tab-communication.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-tab-communication.spec.ts index 5f3a35004a8da..3e76820283543 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-tab-communication.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-tab-communication.spec.ts @@ -1,7 +1,8 @@ import { createFakeVisitor } from '../../mocks/data'; import { createAuxContext } from '../fixtures/createAuxContext'; import { Users } from '../fixtures/userStates'; -import { HomeOmnichannel, OmnichannelLiveChat } from '../page-objects'; +import { HomeOmnichannel } from '../page-objects'; +import { OmnichannelLiveChat } from '../page-objects/omnichannel'; import { createAgent } from '../utils/omnichannel/agents'; import { test, expect } from '../utils/test'; diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-watermark.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-watermark.spec.ts index 09883da3c16b6..6a064c5c4b76e 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-watermark.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-watermark.spec.ts @@ -2,7 +2,7 @@ import { createFakeVisitor } from '../../mocks/data'; import { IS_EE } from '../config/constants'; import { createAuxContext } from '../fixtures/createAuxContext'; import { Users } from '../fixtures/userStates'; -import { OmnichannelLiveChat, OmnichannelSettings } from '../page-objects'; +import { OmnichannelLiveChat, OmnichannelSettings } from '../page-objects/omnichannel'; import { createAgent, makeAgentAvailable } from '../utils/omnichannel/agents'; import { test, expect } from '../utils/test'; @@ -60,7 +60,7 @@ test.describe('OC - Livechat - Hide watermark', async () => { await test.step('expect to change setting', async () => { await poOmnichannelSettings.group('Livechat').click(); await poOmnichannelSettings.labelHideWatermark.click(); - await poOmnichannelSettings.btnSave.click(); + await poOmnichannelSettings.btnSaveChanges.click(); }); await test.step('expect watermark to be hidden', async () => { diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-widget.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-widget.spec.ts index 5b57920bb7a43..1ea37ee7c3c2c 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-widget.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-widget.spec.ts @@ -1,6 +1,6 @@ import type { Page } from '@playwright/test'; -import { OmnichannelLiveChatEmbedded } from '../page-objects'; +import { OmnichannelLiveChatEmbedded } from '../page-objects/omnichannel'; import { test, expect } from '../utils/test'; test.describe('Omnichannel - Livechat Widget Embedded', () => { diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat.spec.ts index 81283f264d07a..2822a9c691321 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat.spec.ts @@ -3,7 +3,8 @@ import type { Page } from 'playwright-core'; import { createFakeVisitor } from '../../mocks/data'; import { createAuxContext } from '../fixtures/createAuxContext'; import { Users } from '../fixtures/userStates'; -import { HomeOmnichannel, OmnichannelLiveChat } from '../page-objects'; +import { HomeOmnichannel } from '../page-objects'; +import { OmnichannelLiveChat } from '../page-objects/omnichannel'; import { setSettingValueById } from '../utils'; import { createAgent } from '../utils/omnichannel/agents'; import { test, expect } from '../utils/test'; diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-manager-role.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-manager-role.spec.ts index 5df8a4218ab18..27eea4e9f2ce1 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-manager-role.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-manager-role.spec.ts @@ -4,6 +4,7 @@ import type { Page } from '@playwright/test'; import { IS_EE } from '../config/constants'; import { Users } from '../fixtures/userStates'; import { HomeOmnichannel } from '../page-objects'; +import { OmnichannelAgents, OmnichannelManager, OmnichannelMonitors } from '../page-objects/omnichannel'; import { createAgent, makeAgentAvailable } from '../utils/omnichannel/agents'; import { createDepartment } from '../utils/omnichannel/departments'; import { createManager } from '../utils/omnichannel/managers'; @@ -114,9 +115,9 @@ test.describe('OC - Manager Role', () => { test('OC - Manager Role - Contact Center', async ({ page }) => { await test.step('expect to be able to view all chats', async () => { - await expect(poOmnichannel.chats.findRowByName(ROOM_A)).toBeVisible(); - await expect(poOmnichannel.chats.findRowByName(ROOM_B)).toBeVisible(); - await expect(poOmnichannel.chats.findRowByName(ROOM_C)).toBeVisible(); + await expect(poOmnichannel.chats.table.findRowByName(ROOM_A)).toBeVisible(); + await expect(poOmnichannel.chats.table.findRowByName(ROOM_B)).toBeVisible(); + await expect(poOmnichannel.chats.table.findRowByName(ROOM_C)).toBeVisible(); }); await test.step('expect to be able to join chats', async () => { @@ -153,76 +154,74 @@ test.describe('OC - Manager Role', () => { await test.step('expect to be able to remove closed rooms', async () => { await poOmnichannel.chats.removeChatByName(ROOM_A); - await expect(poOmnichannel.chats.findRowByName(ROOM_A)).not.toBeVisible(); + await expect(poOmnichannel.chats.table.findRowByName(ROOM_A)).not.toBeVisible(); }); }); - test('OC - Manager Role - Add/remove agents', async () => { - await poOmnichannel.agents.sidenav.linkAgents.click(); + test('OC - Manager Role - Add/remove agents', async ({ page }) => { + const poOmnichannelAgents = new OmnichannelAgents(page); + await poOmnichannelAgents.sidebar.linkAgents.click(); await test.step('expect add "user1" as agent', async () => { - await poOmnichannel.agents.selectUsername('user1'); - await poOmnichannel.agents.btnAdd.click(); + await poOmnichannelAgents.selectUsername('user1'); + await poOmnichannelAgents.btnAddAgent.click(); - await poOmnichannel.agents.inputSearch.fill('user1'); - await expect(poOmnichannel.agents.findRowByName('user1')).toBeVisible(); + await poOmnichannelAgents.inputSearch.fill('user1'); + await expect(poOmnichannelAgents.table.findRowByName('user1')).toBeVisible(); }); await test.step('expect remove "user1" as agent', async () => { - await poOmnichannel.agents.inputSearch.fill('user1'); - await poOmnichannel.agents.btnDeleteFirstRowInTable.click(); - await poOmnichannel.agents.btnModalRemove.click(); + await poOmnichannelAgents.inputSearch.fill('user1'); + await poOmnichannelAgents.deleteAgent('user1'); - await poOmnichannel.agents.inputSearch.fill(''); - await poOmnichannel.agents.inputSearch.fill('user1'); - await expect(poOmnichannel.agents.findRowByName('user1')).toBeHidden(); + await poOmnichannelAgents.inputSearch.fill(''); + await poOmnichannelAgents.inputSearch.fill('user1'); + await expect(poOmnichannelAgents.table.findRowByName('user1')).toBeHidden(); }); }); - test('OC - Manager Role - Add/remove managers', async () => { - await poOmnichannel.omnisidenav.linkManagers.click(); + test('OC - Manager Role - Add/remove managers', async ({ page }) => { + const poOmnichannelManagers = new OmnichannelManager(page); + await poOmnichannelManagers.sidebar.linkManagers.click(); await test.step('expect add "user1" as manager', async () => { - await poOmnichannel.managers.selectUsername('user1'); - await poOmnichannel.managers.btnAdd.click(); + await poOmnichannelManagers.selectUsername('user1'); + await poOmnichannelManagers.btnAddManager.click(); - await expect(poOmnichannel.managers.findRowByName('user1')).toBeVisible(); + await expect(poOmnichannelManagers.table.findRowByName('user1')).toBeVisible(); }); await test.step('expect search for manager', async () => { - await poOmnichannel.managers.search('user1'); - await expect(poOmnichannel.managers.findRowByName('user1')).toBeVisible(); + await poOmnichannelManagers.search('user1'); + await expect(poOmnichannelManagers.table.findRowByName('user1')).toBeVisible(); - await poOmnichannel.managers.search('NonExistingUser'); - await expect(poOmnichannel.managers.findRowByName('user1')).toBeHidden(); - - await poOmnichannel.managers.clearSearch(); + await poOmnichannelManagers.search('NonExistingUser'); + await expect(poOmnichannelManagers.table.findRowByName('user1')).toBeHidden(); + await poOmnichannelManagers.clearSearch(); }); await test.step('expect remove "user1" as manager', async () => { - await poOmnichannel.managers.search('user1'); - await poOmnichannel.managers.btnDeleteSelectedAgent('user1').click(); - await poOmnichannel.managers.btnModalRemove.click(); + await poOmnichannelManagers.search('user1'); + await poOmnichannelManagers.removeManager('user1'); - await expect(poOmnichannel.managers.findRowByName('user1')).toBeHidden(); + await expect(poOmnichannelManagers.table.findRowByName('user1')).toBeHidden(); }); }); - test('OC - Manager Role - Add/remove monitors', async () => { - await poOmnichannel.omnisidenav.linkMonitors.click(); + test('OC - Manager Role - Add/remove monitors', async ({ page }) => { + const poOmnichannelMonitors = new OmnichannelMonitors(page); + await poOmnichannelMonitors.sidebar.linkMonitors.click(); await test.step('expect to add agent as monitor', async () => { - await expect(poOmnichannel.monitors.findRowByName('user1')).not.toBeVisible(); - await poOmnichannel.monitors.selectMonitor('user1'); - await poOmnichannel.monitors.btnAddMonitor.click(); - await expect(poOmnichannel.monitors.findRowByName('user1')).toBeVisible(); + await expect(poOmnichannelMonitors.table.findRowByName('user1')).not.toBeVisible(); + await poOmnichannelMonitors.addMonitor('user1'); + await expect(poOmnichannelMonitors.table.findRowByName('user1')).toBeVisible(); }); await test.step('expect to remove agent from monitor', async () => { - await poOmnichannel.monitors.btnRemoveByName('user1').click(); - await expect(poOmnichannel.monitors.modalConfirmRemove).toBeVisible(); - await poOmnichannel.monitors.btnConfirmRemove.click(); - await expect(poOmnichannel.monitors.findRowByName('user1')).not.toBeVisible(); + await poOmnichannelMonitors.removeMonitor('user1'); + + await expect(poOmnichannelMonitors.table.findRowByName('user1')).not.toBeVisible(); }); }); }); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-manager.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-manager.spec.ts index c11978abcb692..1574928db18ce 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-manager.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-manager.spec.ts @@ -1,5 +1,5 @@ import { Users } from '../fixtures/userStates'; -import { OmnichannelManager } from '../page-objects'; +import { OmnichannelManager } from '../page-objects/omnichannel'; import { test, expect } from '../utils/test'; test.use({ storageState: Users.admin.state }); @@ -11,7 +11,7 @@ test.describe.serial('omnichannel-manager', () => { poOmnichannelManagers = new OmnichannelManager(page); await page.goto('/omnichannel'); - await poOmnichannelManagers.sidenav.linkManagers.click(); + await poOmnichannelManagers.sidebar.linkManagers.click(); }); test('OC - Manage Managers - Add, Search and Remove', async ({ page }) => { @@ -22,27 +22,26 @@ test.describe.serial('omnichannel-manager', () => { await test.step('expect add "user1" as manager', async () => { await poOmnichannelManagers.selectUsername('user1'); - await poOmnichannelManagers.btnAdd.click(); + await poOmnichannelManagers.btnAddManager.click(); - await expect(poOmnichannelManagers.findRowByName('user1')).toBeVisible(); + await expect(poOmnichannelManagers.table.findRowByName('user1')).toBeVisible(); }); await test.step('expect search for manager', async () => { await poOmnichannelManagers.search('user1'); - await expect(poOmnichannelManagers.findRowByName('user1')).toBeVisible(); + await expect(poOmnichannelManagers.table.findRowByName('user1')).toBeVisible(); await poOmnichannelManagers.search('NonExistingUser'); - await expect(poOmnichannelManagers.findRowByName('user1')).toBeHidden(); + await expect(poOmnichannelManagers.table.findRowByName('user1')).toBeHidden(); await poOmnichannelManagers.clearSearch(); }); await test.step('expect remove "user1" as manager', async () => { await poOmnichannelManagers.search('user1'); - await poOmnichannelManagers.btnDeleteSelectedAgent('user1').click(); - await poOmnichannelManagers.btnModalRemove.click(); + await poOmnichannelManagers.removeManager('user1'); - await expect(poOmnichannelManagers.findRowByName('user1')).toBeHidden(); + await expect(poOmnichannelManagers.table.findRowByName('user1')).toBeHidden(); }); }); }); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-manual-selection.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-manual-selection.spec.ts index 1a96b2f8eaab7..41f647111b0c0 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-manual-selection.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-manual-selection.spec.ts @@ -87,7 +87,7 @@ test.describe('OC - Manual Selection', () => { await test.step('expect to be able return to queue', async () => { await poOmnichannel.content.btnReturnToQueue.click(); - await poOmnichannel.content.btnReturnToQueueConfirm.click(); + await poOmnichannel.content.returnToQueueModal.confirm(); await expect(page).toHaveURL('/home'); }); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-monitor-department.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-monitor-department.spec.ts index 92b40fb477bfd..109198cfe5bfa 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-monitor-department.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-monitor-department.spec.ts @@ -3,7 +3,7 @@ import type { Page } from '@playwright/test'; import { IS_EE } from '../config/constants'; import { Users } from '../fixtures/userStates'; -import { OmnichannelDepartments } from '../page-objects'; +import { OmnichannelDepartments } from '../page-objects/omnichannel'; import { createAgent } from '../utils/omnichannel/agents'; import { createDepartment } from '../utils/omnichannel/departments'; import { createMonitor } from '../utils/omnichannel/monitors'; @@ -86,11 +86,11 @@ test.describe.serial('OC - Monitor Role', () => { const [unitA, unitB, unitC] = units.map((unit) => unit.data); await test.step('expect to see only departmentA in the list', async () => { - await expect(poOmnichannelDepartments.findDepartment(departmentA.name)).toBeVisible(); + await expect(poOmnichannelDepartments.departmentsTable.findRowByName(departmentA.name)).toBeVisible(); }); await test.step('expect to fill departments mandatory field', async () => { - await poOmnichannelDepartments.headingButtonNew('Create department').click(); + await poOmnichannelDepartments.createNew(); await poOmnichannelDepartments.inputName.fill(newDepartmentName); await poOmnichannelDepartments.inputEmail.fill(faker.internet.email()); }); @@ -121,20 +121,20 @@ test.describe.serial('OC - Monitor Role', () => { }); await test.step('expect to save department', async () => { - await poOmnichannelDepartments.btnEnabled.click(); + await poOmnichannelDepartments.labelEnabled.click(); await poOmnichannelDepartments.btnSave.click(); }); await test.step('expect to have departmentA and departmentB visible', async () => { - await expect(poOmnichannelDepartments.findDepartment(departmentA.name)).toBeVisible(); - await expect(poOmnichannelDepartments.findDepartment(newDepartmentName)).toBeVisible(); + await expect(poOmnichannelDepartments.departmentsTable.findRowByName(departmentA.name)).toBeVisible(); + await expect(poOmnichannelDepartments.departmentsTable.findRowByName(newDepartmentName)).toBeVisible(); }); }); test('OC - Monitor Role - Not allow editing department business unit', async () => { await test.step('expect not to be able to edit unit', async () => { await poOmnichannelDepartments.search(newDepartmentName); - await poOmnichannelDepartments.selectedDepartmentMenu(newDepartmentName).click(); + await poOmnichannelDepartments.getDepartmentMenuByName(newDepartmentName).click(); await poOmnichannelDepartments.menuEditOption.click(); await expect(poOmnichannelDepartments.inputUnit).toBeDisabled(); }); @@ -147,16 +147,16 @@ test.describe.serial('OC - Monitor Role', () => { await test.step('expect to edit unit', async () => { await poOmnichannelDepartments.search(newDepartmentName); - await poOmnichannelDepartments.selectedDepartmentMenu(newDepartmentName).click(); + await poOmnichannelDepartments.getDepartmentMenuByName(newDepartmentName).click(); await poOmnichannelDepartments.menuEditOption.click(); await poOmnichannelDepartments.selectUnit(unitC.name); - await poOmnichannelDepartments.btnEnabled.click(); + await poOmnichannelDepartments.labelEnabled.click(); await poOmnichannelDepartments.btnSave.click(); }); await test.step('expect departmentB to still be visible', async () => { - await expect(poOmnichannelDepartments.findDepartment(departmentA.name)).toBeVisible(); - await expect(poOmnichannelDepartments.findDepartment(newDepartmentName)).toBeVisible(); + await expect(poOmnichannelDepartments.departmentsTable.findRowByName(departmentA.name)).toBeVisible(); + await expect(poOmnichannelDepartments.departmentsTable.findRowByName(newDepartmentName)).toBeVisible(); }); }); @@ -165,16 +165,16 @@ test.describe.serial('OC - Monitor Role', () => { await test.step('expect to edit unit', async () => { await poOmnichannelDepartments.search(newDepartmentName); - await poOmnichannelDepartments.selectedDepartmentMenu(newDepartmentName).click(); + await poOmnichannelDepartments.getDepartmentMenuByName(newDepartmentName).click(); await poOmnichannelDepartments.menuEditOption.click(); await poOmnichannelDepartments.selectUnit('None'); - await poOmnichannelDepartments.btnEnabled.click(); + await poOmnichannelDepartments.labelEnabled.click(); await poOmnichannelDepartments.btnSave.click(); }); await test.step('expect departmentB to not be visible', async () => { - await expect(poOmnichannelDepartments.findDepartment(departmentA.name)).toBeVisible(); - await expect(poOmnichannelDepartments.findDepartment(newDepartmentName)).not.toBeVisible(); + await expect(poOmnichannelDepartments.departmentsTable.findRowByName(departmentA.name)).toBeVisible(); + await expect(poOmnichannelDepartments.departmentsTable.findRowByName(newDepartmentName)).not.toBeVisible(); }); }); }); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-monitor-role.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-monitor-role.spec.ts index 618b63c13b1b2..4cde8f2e0a35d 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-monitor-role.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-monitor-role.spec.ts @@ -165,10 +165,10 @@ test.describe('OC - Monitor Role', () => { test('OC - Monitor Role - Contact Center', async ({ page }) => { await test.step('expect to be able to view only chats from same unit', async () => { - await expect(poOmnichannel.chats.findRowByName(ROOM_A)).toBeVisible(); - await expect(poOmnichannel.chats.findRowByName(ROOM_B)).toBeVisible(); - await expect(poOmnichannel.chats.findRowByName(ROOM_C)).toBeVisible(); - await expect(poOmnichannel.chats.findRowByName(ROOM_D)).not.toBeVisible(); + await expect(poOmnichannel.chats.table.findRowByName(ROOM_A)).toBeVisible(); + await expect(poOmnichannel.chats.table.findRowByName(ROOM_B)).toBeVisible(); + await expect(poOmnichannel.chats.table.findRowByName(ROOM_C)).toBeVisible(); + await expect(poOmnichannel.chats.table.findRowByName(ROOM_D)).not.toBeVisible(); }); await test.step('expect to be able to join chats from same unit', async () => { @@ -204,8 +204,8 @@ test.describe('OC - Monitor Role', () => { }); await test.step('expect not to be able to remove closed room', async () => { - await expect(poOmnichannel.chats.findRowByName(ROOM_A)).toBeVisible(); - await expect(poOmnichannel.chats.btnRemoveByName(ROOM_A)).not.toBeVisible(); + await expect(poOmnichannel.chats.table.findRowByName(ROOM_A)).toBeVisible(); + await expect(poOmnichannel.chats.table.btnRemoveByName(ROOM_A)).not.toBeVisible(); }); }); @@ -217,9 +217,9 @@ test.describe('OC - Monitor Role', () => { await test.step('expect not to be able to see chats from removed department', async () => { await test.step('expect rooms from both departments to be visible', async () => { - await expect(poOmnichannel.chats.findRowByName(ROOM_B)).toBeVisible(); - await expect(poOmnichannel.chats.findRowByName(ROOM_C)).toBeVisible(); - await expect(poOmnichannel.chats.findRowByName(ROOM_D)).not.toBeVisible(); + await expect(poOmnichannel.chats.table.findRowByName(ROOM_B)).toBeVisible(); + await expect(poOmnichannel.chats.table.findRowByName(ROOM_C)).toBeVisible(); + await expect(poOmnichannel.chats.table.findRowByName(ROOM_D)).not.toBeVisible(); }); await test.step('expect to remove departmentB from unit', async () => { @@ -235,9 +235,9 @@ test.describe('OC - Monitor Role', () => { }); await test.step('expect to have only room B visible', async () => { - await expect(poOmnichannel.chats.findRowByName(ROOM_B)).toBeVisible(); - await expect(poOmnichannel.chats.findRowByName(ROOM_C)).not.toBeVisible(); - await expect(poOmnichannel.chats.findRowByName(ROOM_D)).not.toBeVisible(); + await expect(poOmnichannel.chats.table.findRowByName(ROOM_B)).toBeVisible(); + await expect(poOmnichannel.chats.table.findRowByName(ROOM_C)).not.toBeVisible(); + await expect(poOmnichannel.chats.table.findRowByName(ROOM_D)).not.toBeVisible(); }); }); @@ -245,16 +245,16 @@ test.describe('OC - Monitor Role', () => { const res = await unitA.delete(); expect(res.status()).toBe(200); await page.reload(); - await expect(poOmnichannel.chats.findRowByName(ROOM_B)).not.toBeVisible(); - await expect(poOmnichannel.chats.findRowByName(ROOM_C)).not.toBeVisible(); - await expect(poOmnichannel.chats.findRowByName(ROOM_D)).not.toBeVisible(); + await expect(poOmnichannel.chats.table.findRowByName(ROOM_B)).not.toBeVisible(); + await expect(poOmnichannel.chats.table.findRowByName(ROOM_C)).not.toBeVisible(); + await expect(poOmnichannel.chats.table.findRowByName(ROOM_D)).not.toBeVisible(); }); await test.step('expect to be able to see all conversations once all units are removed', async () => { const res = await unitB.delete(); expect(res.status()).toBe(200); await page.reload(); - await expect(poOmnichannel.chats.findRowByName(ROOM_D)).toBeVisible(); + await expect(poOmnichannel.chats.table.findRowByName(ROOM_D)).toBeVisible(); }); await test.step('expect not to be able to see chats once role is removed', async () => { diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-monitors.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-monitors.spec.ts index 5a118afcc508e..be7b8b996a44a 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-monitors.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-monitors.spec.ts @@ -1,6 +1,6 @@ import { IS_EE } from '../config/constants'; import { Users } from '../fixtures/userStates'; -import { OmnichannelMonitors } from '../page-objects'; +import { OmnichannelMonitors } from '../page-objects/omnichannel'; import { test, expect } from '../utils/test'; test.use({ storageState: Users.user1.state }); @@ -32,67 +32,54 @@ test.describe.serial('OC - Manage Monitors', () => { await page.goto('/omnichannel'); await page.locator('#main-content').waitFor(); - await poMonitors.sidenav.linkMonitors.click(); + await poMonitors.sidebar.linkMonitors.click(); }); test('OC - Manager Monitors - Add monitor', async () => { await test.step('expect to add agent as monitor', async () => { - await expect(poMonitors.findRowByName('user1')).not.toBeVisible(); - await poMonitors.selectMonitor('user1'); - await poMonitors.btnAddMonitor.click(); - await expect(poMonitors.findRowByName('user1')).toBeVisible(); + await expect(poMonitors.table.findRowByName('user1')).not.toBeVisible(); + await poMonitors.addMonitor('user1'); + await expect(poMonitors.table.findRowByName('user1')).toBeVisible(); }); await test.step('expect to remove agent from monitor', async () => { - await poMonitors.btnRemoveByName('user1').click(); - await expect(poMonitors.modalConfirmRemove).toBeVisible(); - await poMonitors.btnConfirmRemove.click(); - await expect(poMonitors.findRowByName('user1')).not.toBeVisible(); + await poMonitors.removeMonitor('user1'); + await expect(poMonitors.table.findRowByName('user1')).not.toBeVisible(); }); }); test('OC - Manager Monitors - Search', async () => { await test.step('expect to add 2 monitors', async () => { - await poMonitors.selectMonitor('user1'); - await poMonitors.btnAddMonitor.click(); + await poMonitors.addMonitor('user1'); + await expect(poMonitors.table.findRowByName('user1')).toBeVisible(); - await expect(poMonitors.findRowByName('user1')).toBeVisible(); - - await poMonitors.selectMonitor('user2'); - await poMonitors.btnAddMonitor.click(); - - await expect(poMonitors.findRowByName('user2')).toBeVisible(); + await poMonitors.addMonitor('user2'); + await expect(poMonitors.table.findRowByName('user2')).toBeVisible(); }); await test.step('expect to search monitor', async () => { - await expect(poMonitors.findRowByName('user1')).toBeVisible(); - await expect(poMonitors.findRowByName('user2')).toBeVisible(); + await expect(poMonitors.table.findRowByName('user1')).toBeVisible(); + await expect(poMonitors.table.findRowByName('user2')).toBeVisible(); await poMonitors.inputSearch.fill('user1'); - await expect(poMonitors.findRowByName('user1')).toBeVisible(); - await expect(poMonitors.findRowByName('user2')).not.toBeVisible(); + await expect(poMonitors.table.findRowByName('user1')).toBeVisible(); + await expect(poMonitors.table.findRowByName('user2')).not.toBeVisible(); await poMonitors.inputSearch.fill('user2'); - await expect(poMonitors.findRowByName('user1')).not.toBeVisible(); - await expect(poMonitors.findRowByName('user2')).toBeVisible(); + await expect(poMonitors.table.findRowByName('user1')).not.toBeVisible(); + await expect(poMonitors.table.findRowByName('user2')).toBeVisible(); await poMonitors.inputSearch.fill(''); - await expect(poMonitors.findRowByName('user1')).toBeVisible(); - await expect(poMonitors.findRowByName('user2')).toBeVisible(); + await expect(poMonitors.table.findRowByName('user1')).toBeVisible(); + await expect(poMonitors.table.findRowByName('user2')).toBeVisible(); }); await test.step('expect to remove monitors', async () => { - await poMonitors.btnRemoveByName('user1').click(); - await expect(poMonitors.modalConfirmRemove).toBeVisible(); - await poMonitors.btnConfirmRemove.click(); - - await expect(poMonitors.findRowByName('user1')).not.toBeVisible(); - - await poMonitors.btnRemoveByName('user2').click(); - await expect(poMonitors.modalConfirmRemove).toBeVisible(); - await poMonitors.btnConfirmRemove.click(); + await poMonitors.removeMonitor('user1'); + await expect(poMonitors.table.findRowByName('user1')).not.toBeVisible(); - await expect(poMonitors.findRowByName('user2')).not.toBeVisible(); + await poMonitors.removeMonitor('user2'); + await expect(poMonitors.table.findRowByName('user2')).not.toBeVisible(); }); }); }); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-priorities-sidebar.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-priorities-sidebar.spec.ts index 5b0e3dd46dc87..53224ea2b4232 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-priorities-sidebar.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-priorities-sidebar.spec.ts @@ -2,7 +2,6 @@ import { createFakeVisitor } from '../../mocks/data'; import { IS_EE } from '../config/constants'; import { Users } from '../fixtures/userStates'; import { HomeOmnichannel } from '../page-objects'; -import { OmnichannelRoomInfo } from '../page-objects/omnichannel-room-info'; import { createConversation } from '../utils/omnichannel/rooms'; import { test, expect } from '../utils/test'; @@ -17,7 +16,6 @@ test.use({ storageState: Users.user1.state }); test.describe.serial('OC - Priorities [Sidebar]', () => { let poHomeChannel: HomeOmnichannel; - let poRoomInfo: OmnichannelRoomInfo; test.beforeAll(async ({ api }) => { ( @@ -31,7 +29,6 @@ test.describe.serial('OC - Priorities [Sidebar]', () => { test.beforeEach(async ({ page }) => { poHomeChannel = new HomeOmnichannel(page); - poRoomInfo = new OmnichannelRoomInfo(page); }); test.beforeEach(async ({ page }) => { @@ -61,23 +58,23 @@ test.describe.serial('OC - Priorities [Sidebar]', () => { await poHomeChannel.sidebar.getSidebarItemByName(visitor.name).click(); await expect(poHomeChannel.content.btnTakeChat).toBeVisible(); - await expect(poRoomInfo.getLabel('Priority')).not.toBeVisible(); + await expect(poHomeChannel.roomInfo.getLabel('Priority')).not.toBeVisible(); await poHomeChannel.sidebar.selectPriority(visitor.name, 'Lowest'); await systemMessage.locator(`text="${getPrioritySystemMessage('user1', 'Lowest')}"`).waitFor(); - await expect(poRoomInfo.getLabel('Priority')).toBeVisible(); - await expect(poRoomInfo.getBadgeIndicator(visitor.name, 'Lowest')).toBeVisible(); - await expect(poRoomInfo.getInfo('Lowest')).toBeVisible(); + await expect(poHomeChannel.roomInfo.getLabel('Priority')).toBeVisible(); + await expect(poHomeChannel.sidebar.getBadgeIndicator(visitor.name, 'Lowest')).toBeVisible(); + await expect(poHomeChannel.roomInfo.getInfo('Lowest')).toBeVisible(); await poHomeChannel.sidebar.selectPriority(visitor.name, 'Highest'); await systemMessage.locator(`text="${getPrioritySystemMessage('user1', 'Highest')}"`).waitFor(); - await expect(poRoomInfo.getInfo('Highest')).toBeVisible(); - await expect(poRoomInfo.getBadgeIndicator(visitor.name, 'Highest')).toBeVisible(); + await expect(poHomeChannel.roomInfo.getInfo('Highest')).toBeVisible(); + await expect(poHomeChannel.sidebar.getBadgeIndicator(visitor.name, 'Highest')).toBeVisible(); await poHomeChannel.sidebar.selectPriority(visitor.name, 'Unprioritized'); await systemMessage.locator(`text="${getPrioritySystemMessage('user1', 'Unprioritized')}"`).waitFor(); - await expect(poRoomInfo.getLabel('Priority')).not.toBeVisible(); - await expect(poRoomInfo.getInfo('Unprioritized')).not.toBeVisible(); + await expect(poHomeChannel.roomInfo.getLabel('Priority')).not.toBeVisible(); + await expect(poHomeChannel.sidebar.getBadgeIndicator(visitor.name, 'Unprioritized')).not.toBeVisible(); }); await test.step('expect to change subscription priority using sidebar menu', async () => { @@ -85,23 +82,23 @@ test.describe.serial('OC - Priorities [Sidebar]', () => { await systemMessage.locator('text="joined the channel"').waitFor(); await page.waitForTimeout(500); - await expect(poRoomInfo.getLabel('Priority')).not.toBeVisible(); + await expect(poHomeChannel.roomInfo.getLabel('Priority')).not.toBeVisible(); await poHomeChannel.sidebar.selectPriority(visitor.name, 'Lowest'); await systemMessage.locator(`text="${getPrioritySystemMessage('user1', 'Lowest')}"`).waitFor(); - await expect(poRoomInfo.getLabel('Priority')).toBeVisible(); - await expect(poRoomInfo.getInfo('Lowest')).toBeVisible(); - await expect(poRoomInfo.getBadgeIndicator(visitor.name, 'Lowest')).toBeVisible(); + await expect(poHomeChannel.roomInfo.getLabel('Priority')).toBeVisible(); + await expect(poHomeChannel.roomInfo.getInfo('Lowest')).toBeVisible(); + await expect(poHomeChannel.sidebar.getBadgeIndicator(visitor.name, 'Lowest')).toBeVisible(); await poHomeChannel.sidebar.selectPriority(visitor.name, 'Highest'); await systemMessage.locator(`text="${getPrioritySystemMessage('user1', 'Highest')}"`).waitFor(); - await expect(poRoomInfo.getInfo('Highest')).toBeVisible(); - await expect(poRoomInfo.getBadgeIndicator(visitor.name, 'Highest')).toBeVisible(); + await expect(poHomeChannel.roomInfo.getInfo('Highest')).toBeVisible(); + await expect(poHomeChannel.sidebar.getBadgeIndicator(visitor.name, 'Highest')).toBeVisible(); await poHomeChannel.sidebar.selectPriority(visitor.name, 'Unprioritized'); await systemMessage.locator(`text="${getPrioritySystemMessage('user1', 'Unprioritized')}"`).waitFor(); - await expect(poRoomInfo.getLabel('Priority')).not.toBeVisible(); - await expect(poRoomInfo.getInfo('Unprioritized')).not.toBeVisible(); + await expect(poHomeChannel.roomInfo.getLabel('Priority')).not.toBeVisible(); + await expect(poHomeChannel.roomInfo.getInfo('Unprioritized')).not.toBeVisible(); }); }); }); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-priorities.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-priorities.spec.ts index 5aa5a76596d3a..625af75f5afca 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-priorities.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-priorities.spec.ts @@ -2,7 +2,7 @@ import { faker } from '@faker-js/faker'; import { IS_EE } from '../config/constants'; import { Users } from '../fixtures/userStates'; -import { OmnichannelPriorities } from '../page-objects/omnichannel-priorities'; +import { OmnichannelPriorities } from '../page-objects/omnichannel'; import { test, expect } from '../utils/test'; const PRIORITY_NAME = faker.person.firstName(); @@ -30,7 +30,7 @@ test.describe.serial('Omnichannel Priorities', () => { await page.goto('/omnichannel'); await page.locator('#main-content').waitFor(); - await poOmnichannelPriorities.sidenav.linkPriorities.click(); + await poOmnichannelPriorities.sidebar.linkPriorities.click(); }); test.afterAll(async ({ api }) => { @@ -54,31 +54,30 @@ test.describe.serial('Omnichannel Priorities', () => { await test.step('default state', async () => { await Promise.all([ - expect(poOmnichannelPriorities.managePriority.btnSave).toBeDisabled(), - expect(poOmnichannelPriorities.managePriority.btnReset).not.toBeVisible(), - expect(poOmnichannelPriorities.managePriority.inputName).toHaveValue('Highest'), + expect(poOmnichannelPriorities.editPriority.btnSave).toBeDisabled(), + expect(poOmnichannelPriorities.editPriority.btnReset).not.toBeVisible(), + expect(poOmnichannelPriorities.editPriority.inputName).toHaveValue('Highest'), ]); }); await test.step('field name is required', async () => { - await poOmnichannelPriorities.managePriority.inputName.fill('any_text'); - await expect(poOmnichannelPriorities.managePriority.btnSave).toBeEnabled(); - await poOmnichannelPriorities.managePriority.inputName.fill(''); - await expect(poOmnichannelPriorities.managePriority.errorMessage(ERROR.fieldNameRequired)).toBeVisible(); - await expect(poOmnichannelPriorities.managePriority.btnSave).toBeDisabled(); + await poOmnichannelPriorities.editPriority.inputName.fill('any_text'); + await expect(poOmnichannelPriorities.editPriority.btnSave).toBeEnabled(); + await poOmnichannelPriorities.editPriority.inputName.fill(''); + await expect(poOmnichannelPriorities.editPriority.errorMessage(ERROR.fieldNameRequired)).toBeVisible(); + await expect(poOmnichannelPriorities.editPriority.btnSave).toBeDisabled(); }); await test.step('edit and save priority', async () => { - await poOmnichannelPriorities.managePriority.inputName.fill(PRIORITY_NAME); + await poOmnichannelPriorities.editPriority.inputName.fill(PRIORITY_NAME); await Promise.all([ - expect(poOmnichannelPriorities.managePriority.btnReset).toBeVisible(), - expect(poOmnichannelPriorities.managePriority.btnSave).toBeEnabled(), + expect(poOmnichannelPriorities.editPriority.btnReset).toBeVisible(), + expect(poOmnichannelPriorities.editPriority.btnSave).toBeEnabled(), ]); - await poOmnichannelPriorities.managePriority.btnSave.click(); + await poOmnichannelPriorities.editPriority.save(); await Promise.all([ - poOmnichannelPriorities.toastMessage.dismissToast(), - expect(poOmnichannelPriorities.managePriority.inputName).not.toBeVisible(), + expect(poOmnichannelPriorities.editPriority.inputName).not.toBeVisible(), expect(poOmnichannelPriorities.findPriority(PRIORITY_NAME)).toBeVisible(), expect(poOmnichannelPriorities.findPriority('Highest')).not.toBeVisible(), ]); @@ -88,16 +87,15 @@ test.describe.serial('Omnichannel Priorities', () => { await test.step('Reset priority', async () => { await test.step('reset individual', async () => { await poOmnichannelPriorities.findPriority(PRIORITY_NAME).click(); - await expect(poOmnichannelPriorities.managePriority.btnReset).toBeVisible(); - await poOmnichannelPriorities.managePriority.btnReset.click(); + await expect(poOmnichannelPriorities.editPriority.btnReset).toBeVisible(); + await poOmnichannelPriorities.editPriority.btnReset.click(); await Promise.all([ - expect(poOmnichannelPriorities.managePriority.inputName).toHaveValue('Highest'), - expect(poOmnichannelPriorities.managePriority.btnReset).not.toBeVisible(), + expect(poOmnichannelPriorities.editPriority.inputName).toHaveValue('Highest'), + expect(poOmnichannelPriorities.editPriority.btnReset).not.toBeVisible(), ]); - await expect(poOmnichannelPriorities.managePriority.btnSave).toBeEnabled(); - await poOmnichannelPriorities.managePriority.btnSave.click(); - await poOmnichannelPriorities.toastMessage.dismissToast(); + await expect(poOmnichannelPriorities.editPriority.btnSave).toBeEnabled(); + await poOmnichannelPriorities.editPriority.save(); await expect(poOmnichannelPriorities.findPriority('Highest')).toBeVisible(); await expect(poOmnichannelPriorities.btnReset).not.toBeEnabled(); }); @@ -105,25 +103,23 @@ test.describe.serial('Omnichannel Priorities', () => { await test.step('reset all', async () => { await poOmnichannelPriorities.findPriority('Highest').click(); - await poOmnichannelPriorities.managePriority.inputName.fill(PRIORITY_NAME); + await poOmnichannelPriorities.editPriority.inputName.fill(PRIORITY_NAME); await Promise.all([ - expect(poOmnichannelPriorities.managePriority.btnReset).toBeVisible(), - expect(poOmnichannelPriorities.managePriority.btnSave).toBeEnabled(), + expect(poOmnichannelPriorities.editPriority.btnReset).toBeVisible(), + expect(poOmnichannelPriorities.editPriority.btnSave).toBeEnabled(), ]); - await poOmnichannelPriorities.managePriority.btnSave.click(); - await poOmnichannelPriorities.toastMessage.dismissToast(); + await poOmnichannelPriorities.editPriority.save(); + await Promise.all([ - expect(poOmnichannelPriorities.managePriority.inputName).not.toBeVisible(), + expect(poOmnichannelPriorities.editPriority.inputName).not.toBeVisible(), expect(poOmnichannelPriorities.findPriority(PRIORITY_NAME)).toBeVisible(), expect(poOmnichannelPriorities.findPriority('Highest')).not.toBeVisible(), ]); await expect(poOmnichannelPriorities.btnReset).toBeEnabled(); - await poOmnichannelPriorities.btnReset.click(); - await poOmnichannelPriorities.btnResetConfirm.click(); + await poOmnichannelPriorities.resetPriorities(); await Promise.all([ - await poOmnichannelPriorities.toastMessage.waitForDisplay(), expect(poOmnichannelPriorities.btnReset).not.toBeEnabled(), expect(poOmnichannelPriorities.findPriority(PRIORITY_NAME)).not.toBeVisible(), expect(poOmnichannelPriorities.findPriority('Highest')).toBeVisible(), diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-reports.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-reports.spec.ts index ba598900bac81..125c4568f8e45 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-reports.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-reports.spec.ts @@ -2,7 +2,7 @@ import type { Route } from '@playwright/test'; import { IS_EE } from '../config/constants'; import { Users } from '../fixtures/userStates'; -import { OmnichannelReports } from '../page-objects/omnichannel-reports'; +import { OmnichannelReports } from '../page-objects/omnichannel'; import { test, expect } from '../utils/test'; const ENDPOINTS = { diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-send-pdf-transcript.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-send-pdf-transcript.spec.ts index 4421636f33627..5171e1c4739f6 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-send-pdf-transcript.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-send-pdf-transcript.spec.ts @@ -4,7 +4,8 @@ import { createFakeVisitor } from '../../mocks/data'; import { IS_EE } from '../config/constants'; import { createAuxContext } from '../fixtures/createAuxContext'; import { Users } from '../fixtures/userStates'; -import { OmnichannelLiveChat, HomeOmnichannel } from '../page-objects'; +import { HomeOmnichannel } from '../page-objects'; +import { OmnichannelLiveChat } from '../page-objects/omnichannel'; import { test, expect } from '../utils/test'; test.skip(!IS_EE, 'Export transcript as PDF > Enterprie Only'); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-send-transcript.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-send-transcript.spec.ts index ea73de532709a..a21dd3853ff0b 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-send-transcript.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-send-transcript.spec.ts @@ -4,7 +4,8 @@ import { createFakeVisitor } from '../../mocks/data'; import { IS_EE } from '../config/constants'; import { createAuxContext } from '../fixtures/createAuxContext'; import { Users } from '../fixtures/userStates'; -import { OmnichannelLiveChat, HomeChannel } from '../page-objects'; +import { HomeChannel } from '../page-objects'; +import { OmnichannelLiveChat } from '../page-objects/omnichannel'; import { test, expect } from '../utils/test'; test.describe('omnichannel-transcript', () => { diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-sla-policies-sidebar.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-sla-policies-sidebar.spec.ts index 57ca3e424243b..0cd3811069678 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-sla-policies-sidebar.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-sla-policies-sidebar.spec.ts @@ -8,7 +8,6 @@ import { createFakeVisitor } from '../../mocks/data'; import { IS_EE } from '../config/constants'; import { Users } from '../fixtures/userStates'; import { HomeOmnichannel } from '../page-objects'; -import { OmnichannelRoomInfo } from '../page-objects/omnichannel-room-info'; import { createConversation } from '../utils/omnichannel/rooms'; import { createSLA } from '../utils/omnichannel/sla'; import { test, expect } from '../utils/test'; @@ -23,7 +22,6 @@ test.use({ storageState: Users.user1.state }); test.describe('OC - SLA Policies [Sidebar]', () => { let poHomeChannel: HomeOmnichannel; - let poRoomInfo: OmnichannelRoomInfo; let conversations: Awaited>[] = []; let slas: Serialized>[] = []; @@ -50,7 +48,6 @@ test.describe('OC - SLA Policies [Sidebar]', () => { test.beforeEach(async ({ page }) => { poHomeChannel = new HomeOmnichannel(page); - poRoomInfo = new OmnichannelRoomInfo(page); }); test.beforeEach(async ({ page }) => { @@ -91,18 +88,18 @@ test.describe('OC - SLA Policies [Sidebar]', () => { await test.step('expect to change room SLA policy to "Not urgent"', async () => { await test.step('expect to open room and room info to be visible', async () => { await poHomeChannel.sidebar.getSidebarItemByName(visitorA.name).click(); - await expect(poRoomInfo.dialogRoomInfo).toBeVisible(); + await expect(poHomeChannel.roomInfo.root).toBeVisible(); }); await test.step('expect to update room SLA policy', async () => { - await expect(poRoomInfo.getInfoByLabel('SLA Policy')).not.toBeVisible(); - await poRoomInfo.btnEditRoomInfo.click(); - await poRoomInfo.selectSLA('Not Urgent'); - await poRoomInfo.btnSaveEditRoom.click(); + await expect(poHomeChannel.roomInfo.getInfoByLabel('SLA Policy')).not.toBeVisible(); + await poHomeChannel.roomInfo.btnEdit.click(); + await poHomeChannel.editRoomInfo.selectSLA('Not Urgent'); + await poHomeChannel.editRoomInfo.btnSave.click(); }); await test.step('expect SLA to have been updated in the room info and queue order to be correct', async () => { - await expect(poRoomInfo.getInfoByLabel('SLA Policy')).toHaveText('Not Urgent'); + await expect(poHomeChannel.roomInfo.getInfoByLabel('SLA Policy')).toHaveText('Not Urgent'); await expect(poHomeChannel.sidebar.getSidebarListItem(visitorA.name)).toBeVisible(); await expect(poHomeChannel.sidebar.getSidebarListItem(visitorA.name)).toHaveAttribute('data-index', '1'); }); @@ -111,18 +108,18 @@ test.describe('OC - SLA Policies [Sidebar]', () => { await test.step('expect to change room SLA policy to "Urgent"', async () => { await test.step('expect to open room and room info to be visible', async () => { await poHomeChannel.sidebar.getSidebarItemByName(visitorB.name).click(); - await expect(poRoomInfo.dialogRoomInfo).toBeVisible(); + await expect(poHomeChannel.roomInfo.root).toBeVisible(); }); await test.step('expect to update room SLA policy', async () => { - await expect(poRoomInfo.getInfoByLabel('SLA Policy')).not.toBeVisible(); - await poRoomInfo.btnEditRoomInfo.click(); - await poRoomInfo.selectSLA('Urgent'); - await poRoomInfo.btnSaveEditRoom.click(); + await expect(poHomeChannel.roomInfo.getInfoByLabel('SLA Policy')).not.toBeVisible(); + await poHomeChannel.roomInfo.btnEdit.click(); + await poHomeChannel.editRoomInfo.selectSLA('Urgent'); + await poHomeChannel.editRoomInfo.btnSave.click(); }); await test.step('expect SLA to have been updated in the room info and queue order to be correct', async () => { - await expect(poRoomInfo.getInfoByLabel('SLA Policy')).toHaveText('Urgent'); + await expect(poHomeChannel.roomInfo.getInfoByLabel('SLA Policy')).toHaveText('Urgent'); await expect(poHomeChannel.sidebar.getSidebarListItem(visitorB.name)).toHaveAttribute('data-index', '1'); await expect(poHomeChannel.sidebar.getSidebarListItem(visitorA.name)).toHaveAttribute('data-index', '2'); }); @@ -131,18 +128,18 @@ test.describe('OC - SLA Policies [Sidebar]', () => { await test.step('expect to change room SLA policy to "Very Urgent"', async () => { await test.step('expect to open room and room info to be visible', async () => { await poHomeChannel.sidebar.getSidebarItemByName(visitorC.name).click(); - await expect(poRoomInfo.dialogRoomInfo).toBeVisible(); + await expect(poHomeChannel.roomInfo.root).toBeVisible(); }); await test.step('expect to update room SLA policy', async () => { - await expect(poRoomInfo.getInfoByLabel('SLA Policy')).not.toBeVisible(); - await poRoomInfo.btnEditRoomInfo.click(); - await poRoomInfo.selectSLA('Very Urgent'); - await poRoomInfo.btnSaveEditRoom.click(); + await expect(poHomeChannel.roomInfo.getInfoByLabel('SLA Policy')).not.toBeVisible(); + await poHomeChannel.roomInfo.btnEdit.click(); + await poHomeChannel.editRoomInfo.selectSLA('Very Urgent'); + await poHomeChannel.editRoomInfo.btnSave.click(); }); await test.step('expect SLA to have been updated in the room info and queue order to be correct', async () => { - await expect(poRoomInfo.getInfoByLabel('SLA Policy')).toHaveText('Very Urgent'); + await expect(poHomeChannel.roomInfo.getInfoByLabel('SLA Policy')).toHaveText('Very Urgent'); await expect(poHomeChannel.sidebar.getSidebarListItem(visitorC.name)).toHaveAttribute('data-index', '1'); await expect(poHomeChannel.sidebar.getSidebarListItem(visitorB.name)).toHaveAttribute('data-index', '2'); await expect(poHomeChannel.sidebar.getSidebarListItem(visitorA.name)).toHaveAttribute('data-index', '3'); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-sla-policies.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-sla-policies.spec.ts index d5b5544e9ace3..e467798cbac00 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-sla-policies.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-sla-policies.spec.ts @@ -2,7 +2,7 @@ import { faker } from '@faker-js/faker'; import { IS_EE } from '../config/constants'; import { Users } from '../fixtures/userStates'; -import { OmnichannelSlaPolicies } from '../page-objects/omnichannel-sla-policies'; +import { OmnichannelSlaPolicies } from '../page-objects/omnichannel'; import { test, expect } from '../utils/test'; const ERROR = { @@ -38,7 +38,7 @@ test.describe('Omnichannel SLA Policies', () => { poOmnichannelSlaPolicies = new OmnichannelSlaPolicies(page); await page.goto('/omnichannel'); - await poOmnichannelSlaPolicies.sidenav.linkSlaPolicies.click(); + await poOmnichannelSlaPolicies.sidebar.linkSlaPolicies.click(); }); test.afterAll(async ({ api }) => { @@ -48,7 +48,7 @@ test.describe('Omnichannel SLA Policies', () => { test('Manage SLAs', async () => { await test.step('Add new SLA', async () => { - await poOmnichannelSlaPolicies.headingButtonNew('Create SLA policy').click(); + await poOmnichannelSlaPolicies.createNew(); await test.step('field name is required', async () => { await poOmnichannelSlaPolicies.manageSlaPolicy.inputName.fill('any_text'); @@ -89,22 +89,22 @@ test.describe('Omnichannel SLA Policies', () => { await poOmnichannelSlaPolicies.manageSlaPolicy.btnSave.click(); await expect(poOmnichannelSlaPolicies.manageSlaPolicy.inputName).not.toBeVisible(); - await expect(poOmnichannelSlaPolicies.findRowByName(INITIAL_SLA.name)).toBeVisible(); + await expect(poOmnichannelSlaPolicies.table.findRowByName(INITIAL_SLA.name)).toBeVisible(); }); }); await test.step('Search SLA', async () => { - await poOmnichannelSlaPolicies.inputSearch.type('random_text_that_should_have_no_match'); - await expect(poOmnichannelSlaPolicies.findRowByName(INITIAL_SLA.name)).not.toBeVisible(); + await poOmnichannelSlaPolicies.inputSearch.fill('random_text_that_should_have_no_match'); + await expect(poOmnichannelSlaPolicies.table.findRowByName(INITIAL_SLA.name)).not.toBeVisible(); await expect(poOmnichannelSlaPolicies.txtEmptyState).toBeVisible(); await poOmnichannelSlaPolicies.inputSearch.fill(INITIAL_SLA.name); - await expect(poOmnichannelSlaPolicies.findRowByName(INITIAL_SLA.name)).toBeVisible(); + await expect(poOmnichannelSlaPolicies.table.findRowByName(INITIAL_SLA.name)).toBeVisible(); await expect(poOmnichannelSlaPolicies.txtEmptyState).not.toBeVisible(); await poOmnichannelSlaPolicies.inputSearch.fill(''); }); await test.step('Edit SLA', async () => { - await poOmnichannelSlaPolicies.findRowByName(INITIAL_SLA.name).click(); + await poOmnichannelSlaPolicies.table.findRowByName(INITIAL_SLA.name).click(); await expect(poOmnichannelSlaPolicies.manageSlaPolicy.inputName).toHaveValue(INITIAL_SLA.name); await expect(poOmnichannelSlaPolicies.manageSlaPolicy.inputDescription).toHaveValue(INITIAL_SLA.description); @@ -129,15 +129,13 @@ test.describe('Omnichannel SLA Policies', () => { await poOmnichannelSlaPolicies.manageSlaPolicy.btnSave.click(); await expect(poOmnichannelSlaPolicies.manageSlaPolicy.inputName).not.toBeVisible(); - await expect(poOmnichannelSlaPolicies.findRowByName(EDITED_SLA.name)).toBeVisible(); + await expect(poOmnichannelSlaPolicies.table.findRowByName(EDITED_SLA.name)).toBeVisible(); }); }); await test.step('Remove SLA', async () => { - await poOmnichannelSlaPolicies.btnRemove(EDITED_SLA.name).click(); - await expect(poOmnichannelSlaPolicies.txtDeleteModalTitle).toBeVisible(); - await poOmnichannelSlaPolicies.btnDelete.click(); - await expect(poOmnichannelSlaPolicies.findRowByName(EDITED_SLA.name)).not.toBeVisible(); + await poOmnichannelSlaPolicies.removeSLA(EDITED_SLA.name); + await expect(poOmnichannelSlaPolicies.table.findRowByName(EDITED_SLA.name)).not.toBeVisible(); }); }); }); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-tags.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-tags.spec.ts index d29bac292f1fa..d17aab773cef1 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-tags.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-tags.spec.ts @@ -3,7 +3,7 @@ import type { Page } from '@playwright/test'; import { IS_EE } from '../config/constants'; import { Users } from '../fixtures/userStates'; -import { OmnichannelTags } from '../page-objects'; +import { OmnichannelTags } from '../page-objects/omnichannel'; import { createAgent } from '../utils/omnichannel/agents'; import { createDepartment } from '../utils/omnichannel/departments'; import { createTag } from '../utils/omnichannel/tags'; @@ -43,45 +43,33 @@ test.describe('OC - Manage Tags', () => { const tagName = faker.string.uuid(); await page.goto('/omnichannel'); - await poOmnichannelTags.sidenav.linkTags.click(); + await poOmnichannelTags.sidebar.linkTags.click(); await test.step('expect correct form default state', async () => { - await poOmnichannelTags.btnCreateTag.click(); - await expect(poOmnichannelTags.contextualBar).toBeVisible(); - await expect(poOmnichannelTags.btnSave).toBeDisabled(); - await expect(poOmnichannelTags.btnCancel).toBeEnabled(); - await poOmnichannelTags.btnCancel.click(); - await expect(poOmnichannelTags.contextualBar).not.toBeVisible(); + await poOmnichannelTags.createNew(); + await expect(poOmnichannelTags.editTag.root).toBeVisible(); + await expect(poOmnichannelTags.editTag.btnSave).toBeDisabled(); + await expect(poOmnichannelTags.editTag.btnCancel).toBeEnabled(); + await poOmnichannelTags.editTag.btnCancel.click(); + await expect(poOmnichannelTags.editTag.root).not.toBeVisible(); }); await test.step('expect to create new tag', async () => { - await poOmnichannelTags.btnCreateTag.click(); - await poOmnichannelTags.inputName.fill(tagName); - await poOmnichannelTags.selectDepartment(department.data.name); - await poOmnichannelTags.btnSave.click(); - await expect(poOmnichannelTags.contextualBar).not.toBeVisible(); + await poOmnichannelTags.createNew(); + await poOmnichannelTags.editTag.inputName.fill(tagName); + await poOmnichannelTags.editTag.selectDepartment(department.data.name); + await poOmnichannelTags.editTag.btnSave.click(); + await expect(poOmnichannelTags.editTag.root).not.toBeVisible(); await test.step('expect tag to have been created', async () => { await poOmnichannelTags.search(tagName); - await expect(poOmnichannelTags.findRowByName(tagName)).toBeVisible(); + await expect(poOmnichannelTags.table.findRowByName(tagName)).toBeVisible(); }); }); - await test.step('expect to delete tag', async () => { - await test.step('expect to be able to cancel delete', async () => { - await poOmnichannelTags.btnDeleteByName(tagName).click(); - await expect(poOmnichannelTags.confirmDeleteModal).toBeVisible(); - await poOmnichannelTags.btnCancelDeleteModal.click(); - await expect(poOmnichannelTags.confirmDeleteModal).not.toBeVisible(); - }); - - await test.step('expect to confirm delete', async () => { - await poOmnichannelTags.btnDeleteByName(tagName).click(); - await expect(poOmnichannelTags.confirmDeleteModal).toBeVisible(); - await poOmnichannelTags.btnConfirmDeleteModal.click(); - await expect(poOmnichannelTags.confirmDeleteModal).not.toBeVisible(); - await expect(page.locator('h3 >> text="No results found"')).toBeVisible(); - }); + await test.step('should be able to delete tag', async () => { + await poOmnichannelTags.deleteTag(tagName); + await poOmnichannelTags.waitForEmptyState(); }); }); @@ -96,45 +84,42 @@ test.describe('OC - Manage Tags', () => { }); await page.goto('/omnichannel'); - await poOmnichannelTags.sidenav.linkTags.click(); + await poOmnichannelTags.sidebar.linkTags.click(); await test.step('expect to add tag departments', async () => { await poOmnichannelTags.search(tag.name); - await poOmnichannelTags.findRowByName(tag.name).click(); - await expect(poOmnichannelTags.contextualBar).toBeVisible(); - await poOmnichannelTags.selectDepartment(department2.data.name); - await poOmnichannelTags.btnSave.click(); + await poOmnichannelTags.table.findRowByName(tag.name).click(); + await expect(poOmnichannelTags.editTag.root).toBeVisible(); + await poOmnichannelTags.editTag.selectDepartment(department2.data.name); + await poOmnichannelTags.editTag.btnSave.click(); }); await test.step('expect department to be in the chosen departments list', async () => { await poOmnichannelTags.search(tag.name); - await poOmnichannelTags.findRowByName(tag.name).click(); - await expect(poOmnichannelTags.contextualBar).toBeVisible(); - await expect(page.getByRole('option', { name: department2.data.name })).toBeVisible(); - await poOmnichannelTags.btnContextualbarClose.click(); + await poOmnichannelTags.table.findRowByName(tag.name).click(); + await expect(poOmnichannelTags.editTag.root).toBeVisible(); + await expect(poOmnichannelTags.editTag.inputDepartments).toBeVisible(); + await poOmnichannelTags.editTag.close(); }); await test.step('expect to remove tag departments', async () => { await poOmnichannelTags.search(tag.name); - await poOmnichannelTags.findRowByName(tag.name).click(); - await expect(poOmnichannelTags.contextualBar).toBeVisible(); - await poOmnichannelTags.selectDepartment(department2.data.name); - await poOmnichannelTags.btnSave.click(); + await poOmnichannelTags.table.findRowByName(tag.name).click(); + await expect(poOmnichannelTags.editTag.root).toBeVisible(); + await poOmnichannelTags.editTag.selectDepartment(department2.data.name); + await poOmnichannelTags.editTag.btnSave.click(); }); await test.step('expect department to not be in the chosen departments list', async () => { await poOmnichannelTags.search(tag.name); - await poOmnichannelTags.findRowByName(tag.name).click(); - await expect(poOmnichannelTags.contextualBar).toBeVisible(); + await poOmnichannelTags.table.findRowByName(tag.name).click(); + await expect(poOmnichannelTags.editTag.root).toBeVisible(); await expect(page.getByRole('option', { name: department2.data.name })).toBeHidden(); }); await test.step('expect to delete tag', async () => { - await poOmnichannelTags.btnDeleteByName(tag.name).click(); - await expect(poOmnichannelTags.confirmDeleteModal).toBeVisible(); - await poOmnichannelTags.btnConfirmDeleteModal.click(); - await expect(poOmnichannelTags.confirmDeleteModal).not.toBeVisible(); - await expect(page.locator('h3 >> text="No results found"')).toBeVisible(); + await poOmnichannelTags.deleteTag(tag.name); + await poOmnichannelTags.waitForEmptyState(); }); }); }); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-takeChat.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-takeChat.spec.ts index 2b11fdad694a0..745d82d87526d 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-takeChat.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-takeChat.spec.ts @@ -3,7 +3,8 @@ import type { Page } from '@playwright/test'; import { createFakeVisitor } from '../../mocks/data'; import { createAuxContext } from '../fixtures/createAuxContext'; import { Users } from '../fixtures/userStates'; -import { OmnichannelLiveChat, HomeOmnichannel } from '../page-objects'; +import { HomeOmnichannel } from '../page-objects'; +import { OmnichannelLiveChat } from '../page-objects/omnichannel'; import { test, expect } from '../utils/test'; test.describe('omnichannel-takeChat', () => { diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-triggers-after-registration.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-triggers-after-registration.spec.ts index 75d104cf02dcc..9f89a3d267d90 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-triggers-after-registration.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-triggers-after-registration.spec.ts @@ -4,7 +4,8 @@ import type { Page } from '@playwright/test'; import { createFakeVisitor } from '../../mocks/data'; import { createAuxContext } from '../fixtures/createAuxContext'; import { Users } from '../fixtures/userStates'; -import { OmnichannelLiveChat, HomeOmnichannel } from '../page-objects'; +import { HomeOmnichannel } from '../page-objects'; +import { OmnichannelLiveChat } from '../page-objects/omnichannel'; import { test, expect } from '../utils/test'; test.describe('OC - Livechat New Chat Triggers - After Registration', () => { diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-triggers-open-by-visitor.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-triggers-open-by-visitor.spec.ts index 1045036a926c5..610d7d5bd1358 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-triggers-open-by-visitor.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-triggers-open-by-visitor.spec.ts @@ -3,7 +3,8 @@ import type { Page } from '@playwright/test'; import { createFakeVisitor } from '../../mocks/data'; import { createAuxContext } from '../fixtures/createAuxContext'; import { Users } from '../fixtures/userStates'; -import { OmnichannelLiveChat, HomeOmnichannel } from '../page-objects'; +import { HomeOmnichannel } from '../page-objects'; +import { OmnichannelLiveChat } from '../page-objects/omnichannel'; import { test, expect } from '../utils/test'; test.use({ storageState: Users.admin.state }); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-triggers-setDepartment.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-triggers-setDepartment.spec.ts index 0c6415dd058ed..6b07f2156ae50 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-triggers-setDepartment.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-triggers-setDepartment.spec.ts @@ -1,7 +1,8 @@ import { IS_EE } from '../config/constants'; import { createAuxContext } from '../fixtures/createAuxContext'; import { Users } from '../fixtures/userStates'; -import { HomeOmnichannel, OmnichannelLiveChatEmbedded } from '../page-objects'; +import { HomeOmnichannel } from '../page-objects'; +import { OmnichannelLiveChatEmbedded } from '../page-objects/omnichannel'; import { createAgent } from '../utils/omnichannel/agents'; import { addAgentToDepartment, createDepartment } from '../utils/omnichannel/departments'; import { test, expect } from '../utils/test'; diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-triggers-time-on-site.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-triggers-time-on-site.spec.ts index 66bf1058ebfa6..7f1aaa5499ed3 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-triggers-time-on-site.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-triggers-time-on-site.spec.ts @@ -3,7 +3,8 @@ import type { Page } from '@playwright/test'; import { createFakeVisitor } from '../../mocks/data'; import { createAuxContext } from '../fixtures/createAuxContext'; import { Users } from '../fixtures/userStates'; -import { OmnichannelLiveChat, HomeOmnichannel } from '../page-objects'; +import { HomeOmnichannel } from '../page-objects'; +import { OmnichannelLiveChat } from '../page-objects/omnichannel'; import { test, expect } from '../utils/test'; test.use({ storageState: Users.admin.state }); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-triggers.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-triggers.spec.ts index 67266c142a16c..f5a5a3c921cda 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-triggers.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-triggers.spec.ts @@ -4,7 +4,7 @@ import type { Page } from '@playwright/test'; import { createFakeVisitor } from '../../mocks/data'; import { createAuxContext } from '../fixtures/createAuxContext'; import { Users } from '../fixtures/userStates'; -import { OmnichannelLiveChat, HomeOmnichannel } from '../page-objects'; +import { OmnichannelLiveChat, OmnichannelTriggers } from '../page-objects/omnichannel'; import { test, expect } from '../utils/test'; test.describe.serial('OC - Livechat Triggers', () => { @@ -12,7 +12,7 @@ test.describe.serial('OC - Livechat Triggers', () => { let triggerMessage: string; let poLiveChat: OmnichannelLiveChat; let newVisitor: { email: string; name: string }; - let agent: { page: Page; poHomeOmnichannel: HomeOmnichannel }; + let agent: { page: Page; poHomeOmnichannelTriggers: OmnichannelTriggers }; test.beforeAll(async ({ api, browser }) => { newVisitor = createFakeVisitor(); @@ -26,7 +26,7 @@ test.describe.serial('OC - Livechat Triggers', () => { requests.every((e) => expect(e.status()).toBe(200)); const { page } = await createAuxContext(browser, Users.user1, '/omnichannel/triggers'); - agent = { page, poHomeOmnichannel: new HomeOmnichannel(page) }; + agent = { page, poHomeOmnichannelTriggers: new OmnichannelTriggers(page) }; await page.emulateMedia({ reducedMotion: 'reduce' }); }); @@ -73,15 +73,13 @@ test.describe.serial('OC - Livechat Triggers', () => { test('OC - Livechat Triggers - Create and edit trigger', async () => { triggerMessage = 'This is a trigger message time on site'; await test.step('expect create new trigger', async () => { - await agent.poHomeOmnichannel.triggers.createTrigger(triggersName, triggerMessage, 'time-on-site', 5); - await agent.poHomeOmnichannel.triggers.toastMessage.dismissToast(); + await agent.poHomeOmnichannelTriggers.createTrigger(triggersName, triggerMessage, 'Visitor time on site', 5); }); triggerMessage = 'This is a trigger message chat opened by visitor'; await test.step('expect update trigger', async () => { - await agent.poHomeOmnichannel.triggers.firstRowInTriggerTable(triggersName).click(); - await agent.poHomeOmnichannel.triggers.updateTrigger(triggersName, triggerMessage); - await agent.poHomeOmnichannel.triggers.toastMessage.dismissToast(); + await agent.poHomeOmnichannelTriggers.table.findRowByName(triggersName).click(); + await agent.poHomeOmnichannelTriggers.updateTrigger(`edited-${triggersName}`, triggerMessage); }); }); @@ -119,11 +117,8 @@ test.describe.serial('OC - Livechat Triggers', () => { test('OC - Livechat Triggers - Condition: after guest registration', async ({ page }) => { triggerMessage = 'This is a trigger message after guest registration'; await test.step('expect update trigger to after guest registration', async () => { - await agent.poHomeOmnichannel.triggers.firstRowInTriggerTable(`edited-${triggersName}`).click(); - await agent.poHomeOmnichannel.triggers.fillTriggerForm({ condition: 'after-guest-registration', triggerMessage }); - await agent.poHomeOmnichannel.triggers.btnSave.click(); - await agent.poHomeOmnichannel.triggers.toastMessage.dismissToast(); - await agent.page.waitForTimeout(500); + await agent.poHomeOmnichannelTriggers.table.findRowByName(`edited-${triggersName}`).click(); + await agent.poHomeOmnichannelTriggers.updateTrigger(`re-edited-${triggersName}`, triggerMessage, 'After guest registration'); }); await test.step('expect to start conversation', async () => { @@ -158,8 +153,6 @@ test.describe.serial('OC - Livechat Triggers', () => { }); test('OC - Livechat Triggers - Delete trigger', async () => { - await agent.poHomeOmnichannel.triggers.btnDeletefirstRowInTable.click(); - await agent.poHomeOmnichannel.triggers.btnModalRemove.click(); - await expect(agent.poHomeOmnichannel.triggers.removeToastMessage).toBeVisible(); + await agent.poHomeOmnichannelTriggers.removeTrigger(`re-edited-${triggersName}`); }); }); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-units.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-units.spec.ts index 9e0f8eac1bebc..0af2c31bc108e 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-units.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-units.spec.ts @@ -3,7 +3,7 @@ import type { Page } from '@playwright/test'; import { IS_EE } from '../config/constants'; import { Users } from '../fixtures/userStates'; -import { OmnichannelUnits } from '../page-objects'; +import { OmnichannelUnits } from '../page-objects/omnichannel'; import { createAgent } from '../utils/omnichannel/agents'; import { createDepartment } from '../utils/omnichannel/departments'; import { createMonitor } from '../utils/omnichannel/monitors'; @@ -51,39 +51,36 @@ test.describe('OC - Manage Units', () => { test.beforeEach(async ({ page }: { page: Page }) => { poOmnichannelUnits = new OmnichannelUnits(page); await page.goto('/omnichannel'); - await poOmnichannelUnits.sidenav.linkUnits.click(); + await poOmnichannelUnits.sidebar.linkUnits.click(); }); test('OC - Manage Units - Create Unit', async ({ page }) => { const unitName = faker.string.uuid(); await test.step('expect correct form default state', async () => { - await poOmnichannelUnits.btnCreateUnit.click(); - await expect(poOmnichannelUnits.contextualBar).toBeVisible(); - await expect(poOmnichannelUnits.btnSave).toBeDisabled(); - await expect(poOmnichannelUnits.btnCancel).toBeEnabled(); - await poOmnichannelUnits.btnCancel.click(); - await expect(poOmnichannelUnits.contextualBar).not.toBeVisible(); + await poOmnichannelUnits.createNew(); + await expect(poOmnichannelUnits.manageUnit.root).toBeVisible(); + await expect(poOmnichannelUnits.manageUnit.btnSave).toBeDisabled(); + await expect(poOmnichannelUnits.manageUnit.btnCancel).toBeEnabled(); + await poOmnichannelUnits.manageUnit.btnCancel.click(); + await expect(poOmnichannelUnits.manageUnit.root).not.toBeVisible(); }); await test.step('expect to create new unit', async () => { - await poOmnichannelUnits.btnCreateUnit.click(); - await poOmnichannelUnits.inputName.fill(unitName); - await poOmnichannelUnits.selectVisibility('public'); - await poOmnichannelUnits.selectDepartment(department.data.name); - await poOmnichannelUnits.selectMonitor('user2'); - await poOmnichannelUnits.btnSave.click(); - await expect(poOmnichannelUnits.contextualBar).not.toBeVisible(); + await poOmnichannelUnits.createNew(); + await poOmnichannelUnits.manageUnit.inputName.fill(unitName); + await poOmnichannelUnits.manageUnit.selectVisibility('public'); + await poOmnichannelUnits.manageUnit.selectDepartment(department.data.name); + await poOmnichannelUnits.manageUnit.selectMonitor('user2'); + await poOmnichannelUnits.manageUnit.btnSave.click(); + await expect(poOmnichannelUnits.manageUnit.root).not.toBeVisible(); await poOmnichannelUnits.search(unitName); - await expect(poOmnichannelUnits.findRowByName(unitName)).toBeVisible(); + await expect(poOmnichannelUnits.table.findRowByName(unitName)).toBeVisible(); }); await test.step('expect to delete unit', async () => { - await poOmnichannelUnits.btnDeleteByName(unitName).click(); - await expect(poOmnichannelUnits.confirmDeleteModal).toBeVisible(); - await poOmnichannelUnits.btnConfirmDeleteModal.click(); - await expect(poOmnichannelUnits.confirmDeleteModal).not.toBeVisible(); + await poOmnichannelUnits.deleteUnit(unitName); await expect(page.locator('h3 >> text="No units yet"')).toBeVisible(); }); }); @@ -104,59 +101,52 @@ test.describe('OC - Manage Units', () => { }); await page.goto('/omnichannel'); - await poOmnichannelUnits.sidenav.linkUnits.click(); + await poOmnichannelUnits.sidebar.linkUnits.click(); await test.step('expect to edit unit', async () => { await poOmnichannelUnits.search(unit.name); - await poOmnichannelUnits.findRowByName(unit.name).click(); - await expect(poOmnichannelUnits.contextualBar).toBeVisible(); - await poOmnichannelUnits.inputName.fill(editedUnitName); - await poOmnichannelUnits.btnSave.click(); - await expect(poOmnichannelUnits.contextualBar).not.toBeVisible(); + await poOmnichannelUnits.table.findRowByName(unit.name).click(); + await expect(poOmnichannelUnits.manageUnit.root).toBeVisible(); + await poOmnichannelUnits.manageUnit.inputName.fill(editedUnitName); + await poOmnichannelUnits.manageUnit.btnSave.click(); + await expect(poOmnichannelUnits.manageUnit.root).not.toBeVisible(); await expect(poOmnichannelUnits.inputSearch).toBeVisible(); await poOmnichannelUnits.search(editedUnitName); - await expect(poOmnichannelUnits.findRowByName(editedUnitName)).toBeVisible(); + await expect(poOmnichannelUnits.table.findRowByName(editedUnitName)).toBeVisible(); }); await test.step('expect to add another monitor to list', async () => { - await poOmnichannelUnits.findRowByName(editedUnitName).click(); - await poOmnichannelUnits.selectMonitor('user3'); - await poOmnichannelUnits.btnSave.click(); - await expect(poOmnichannelUnits.contextualBar).not.toBeVisible(); + await poOmnichannelUnits.table.findRowByName(editedUnitName).click(); + await poOmnichannelUnits.manageUnit.selectMonitor('user3'); + await poOmnichannelUnits.manageUnit.btnSave.click(); + await expect(poOmnichannelUnits.manageUnit.root).not.toBeVisible(); await poOmnichannelUnits.search(editedUnitName); - await poOmnichannelUnits.findRowByName(editedUnitName).click(); + await poOmnichannelUnits.table.findRowByName(editedUnitName).click(); - await expect(poOmnichannelUnits.inputMonitors).toHaveText(/user2/); - await expect(poOmnichannelUnits.inputMonitors).toHaveText(/user3/); + await expect(poOmnichannelUnits.manageUnit.findMonitorChipOption('user2')).toBeVisible(); + await expect(poOmnichannelUnits.manageUnit.findMonitorChipOption('user3')).toBeVisible(); }); await test.step('expect unit to remove one of the two monitors', async () => { await poOmnichannelUnits.search(editedUnitName); - await poOmnichannelUnits.findRowByName(editedUnitName).click(); - await poOmnichannelUnits.selectMonitor('user2'); - await poOmnichannelUnits.btnSave.click(); - await expect(poOmnichannelUnits.contextualBar).not.toBeVisible(); + await poOmnichannelUnits.table.findRowByName(editedUnitName).click(); + await poOmnichannelUnits.manageUnit.removeMonitor('user2'); + await poOmnichannelUnits.manageUnit.btnSave.click(); + await expect(poOmnichannelUnits.manageUnit.root).not.toBeVisible(); await poOmnichannelUnits.search(editedUnitName); - await poOmnichannelUnits.findRowByName(editedUnitName).click(); - await expect(poOmnichannelUnits.inputMonitors).toHaveText(/user3/); - await expect(poOmnichannelUnits.inputMonitors).not.toHaveText(/user2/); + await poOmnichannelUnits.table.findRowByName(editedUnitName).click(); + await expect(poOmnichannelUnits.manageUnit.findMonitorChipOption('user3')).toBeVisible(); + await expect(poOmnichannelUnits.manageUnit.findMonitorChipOption('user2')).toBeHidden(); }); await test.step('expect to delete unit', async () => { - await poOmnichannelUnits.findRowByName(editedUnitName).click(); - await expect(poOmnichannelUnits.contextualBar).toBeVisible(); - - await test.step('expect to confirm delete', async () => { - await poOmnichannelUnits.btnDelete.click(); - await expect(poOmnichannelUnits.confirmDeleteModal).toBeVisible(); - await poOmnichannelUnits.btnConfirmDeleteModal.click(); - await expect(poOmnichannelUnits.confirmDeleteModal).not.toBeVisible(); - }); + await poOmnichannelUnits.table.findRowByName(editedUnitName).click(); + await expect(poOmnichannelUnits.manageUnit.root).toBeVisible(); - await expect(poOmnichannelUnits.contextualBar).not.toBeVisible(); - await expect(poOmnichannelUnits.findRowByName(editedUnitName)).not.toBeVisible(); + await poOmnichannelUnits.deleteUnit(editedUnitName); + await expect(poOmnichannelUnits.table.findRowByName(editedUnitName)).not.toBeVisible(); }); }); @@ -176,49 +166,44 @@ test.describe('OC - Manage Units', () => { await test.step('expect to add unit departments', async () => { await poOmnichannelUnits.search(unit.name); - await poOmnichannelUnits.findRowByName(unit.name).click(); - await expect(poOmnichannelUnits.contextualBar).toBeVisible(); - await poOmnichannelUnits.selectDepartment(department2.data.name); - await poOmnichannelUnits.btnSave.click(); - await expect(poOmnichannelUnits.contextualBar).not.toBeVisible(); + await poOmnichannelUnits.table.findRowByName(unit.name).click(); + await expect(poOmnichannelUnits.manageUnit.root).toBeVisible(); + await poOmnichannelUnits.manageUnit.selectDepartment(department2.data.name); + await poOmnichannelUnits.manageUnit.btnSave.click(); + await expect(poOmnichannelUnits.manageUnit.root).not.toBeVisible(); await poOmnichannelUnits.search(unit.name); - await poOmnichannelUnits.findRowByName(unit.name).click(); - await expect(poOmnichannelUnits.contextualBar).toBeVisible(); - await expect(poOmnichannelUnits.findDepartmentsChipOption(department2.data.name)).toBeVisible(); - await poOmnichannelUnits.findDepartmentsChipOption(department2.data.name).hover(); + await poOmnichannelUnits.table.findRowByName(unit.name).click(); + await expect(poOmnichannelUnits.manageUnit.root).toBeVisible(); + await expect(poOmnichannelUnits.manageUnit.findDepartmentsChipOption(department2.data.name)).toBeVisible(); + await poOmnichannelUnits.manageUnit.findDepartmentsChipOption(department2.data.name).hover(); await expect(page.getByRole('tooltip', { name: department2.data.name })).toBeVisible(); - await poOmnichannelUnits.btnContextualbarClose.click(); + await poOmnichannelUnits.manageUnit.close(); }); await test.step('expect to remove unit departments', async () => { await poOmnichannelUnits.search(unit.name); - await poOmnichannelUnits.findRowByName(unit.name).click(); - await expect(poOmnichannelUnits.contextualBar).toBeVisible(); - await poOmnichannelUnits.selectDepartment(department2.data.name); - await poOmnichannelUnits.btnSave.click(); - await expect(poOmnichannelUnits.contextualBar).not.toBeVisible(); + await poOmnichannelUnits.table.findRowByName(unit.name).click(); + await expect(poOmnichannelUnits.manageUnit.root).toBeVisible(); + await poOmnichannelUnits.manageUnit.selectDepartment(department2.data.name); + await poOmnichannelUnits.manageUnit.btnSave.click(); + await expect(poOmnichannelUnits.manageUnit.root).not.toBeVisible(); await poOmnichannelUnits.search(unit.name); - await poOmnichannelUnits.findRowByName(unit.name).click(); - await expect(poOmnichannelUnits.contextualBar).toBeVisible(); - await expect(page.getByRole('option', { name: department2.data.name })).toBeHidden(); - await poOmnichannelUnits.btnContextualbarClose.click(); + await poOmnichannelUnits.table.findRowByName(unit.name).click(); + await expect(poOmnichannelUnits.manageUnit.root).toBeVisible(); + await expect(poOmnichannelUnits.manageUnit.findDepartmentsChipOption(department2.data.name)).toBeHidden(); + await poOmnichannelUnits.manageUnit.close(); }); await test.step('expect to delete unit', async () => { - await poOmnichannelUnits.findRowByName(unit.name).click(); - await expect(poOmnichannelUnits.contextualBar).toBeVisible(); - await test.step('expect to confirm delete', async () => { - await poOmnichannelUnits.btnDelete.click(); - await expect(poOmnichannelUnits.confirmDeleteModal).toBeVisible(); - await poOmnichannelUnits.btnConfirmDeleteModal.click(); - await expect(poOmnichannelUnits.confirmDeleteModal).not.toBeVisible(); - }); + await poOmnichannelUnits.search(unit.name); + await poOmnichannelUnits.table.findRowByName(unit.name).click(); + await expect(poOmnichannelUnits.manageUnit.root).toBeVisible(); - await expect(poOmnichannelUnits.contextualBar).not.toBeVisible(); - await expect(poOmnichannelUnits.findRowByName(unit.name)).not.toBeVisible(); + await poOmnichannelUnits.deleteUnit(unit.name); + await expect(poOmnichannelUnits.table.findRowByName(unit.name)).not.toBeVisible(); }); }); }); diff --git a/apps/meteor/tests/e2e/page-objects/admin-rooms.ts b/apps/meteor/tests/e2e/page-objects/admin-rooms.ts index 79194654bc42c..eda0a45707606 100644 --- a/apps/meteor/tests/e2e/page-objects/admin-rooms.ts +++ b/apps/meteor/tests/e2e/page-objects/admin-rooms.ts @@ -8,7 +8,7 @@ export class AdminRooms extends Admin { constructor(page: Page) { super(page); - this.editRoom = new EditRoomFlexTab(page); + this.editRoom = new EditRoomFlexTab(page.getByRole('dialog', { name: 'Room Information' })); } get adminPageContent(): Locator { diff --git a/apps/meteor/tests/e2e/page-objects/fragments/admin-flextab-emoji.ts b/apps/meteor/tests/e2e/page-objects/fragments/admin-flextab-emoji.ts index e587457ea1db4..5821a77ae6b55 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/admin-flextab-emoji.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/admin-flextab-emoji.ts @@ -1,41 +1,18 @@ -import type { Locator, Page } from '@playwright/test'; +import type { Page } from '@playwright/test'; import { FlexTab } from './flextab'; -abstract class EmojiFlexTab extends FlexTab { - constructor(root: Locator) { - super(root); - } - - get inputName(): Locator { - return this.root.getByRole('textbox', { name: 'Name' }); - } - - private get btnSave() { - return this.root.getByRole('button', { name: 'Save' }); - } - - async save() { - await this.btnSave.click(); - await this.waitForDismissal(); - } -} - -export class AddEmojiFlexTab extends EmojiFlexTab { +export class AddEmojiFlexTab extends FlexTab { constructor(page: Page) { super(page.getByRole('dialog', { name: 'Add New Emoji' })); } } -export class EditEmojiFlexTab extends EmojiFlexTab { +export class EditEmojiFlexTab extends FlexTab { constructor(page: Page) { super(page.getByRole('dialog', { name: 'Custom Emoji Info' })); } - private get btnDelete() { - return this.root.getByRole('button', { name: 'Delete' }); - } - async delete() { await this.btnDelete.click(); } diff --git a/apps/meteor/tests/e2e/page-objects/fragments/edit-contact-flaxtab.ts b/apps/meteor/tests/e2e/page-objects/fragments/edit-contact-flaxtab.ts new file mode 100644 index 0000000000000..05afd758691fe --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/fragments/edit-contact-flaxtab.ts @@ -0,0 +1,33 @@ +import type { Locator, Page } from '@playwright/test'; + +import { FlexTab } from './flextab'; + +export class OmnichannelEditContactFlexTab extends FlexTab { + constructor(page: Page) { + super(page.getByRole('dialog', { name: 'contact' })); + } + + get inputEmail(): Locator { + return this.root.locator('input[name="emails.0.address"]'); + } + + get inputPhone(): Locator { + return this.root.locator('input[name="phones.0.phoneNumber"]'); + } + + get inputContactManager(): Locator { + return this.root.locator('input[name=contactManager]'); + } + + get btnAddEmail(): Locator { + return this.root.locator('role=button[name="Add email"]'); + } + + get btnAddPhone(): Locator { + return this.root.locator('role=button[name="Add phone"]'); + } + + getErrorMessage(message: string): Locator { + return this.root.locator(`role=alert >> text="${message}"`); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/fragments/edit-room-flextab.ts b/apps/meteor/tests/e2e/page-objects/fragments/edit-room-flextab.ts index 1b5e48fc168b8..bfc88661991bb 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/edit-room-flextab.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/edit-room-flextab.ts @@ -1,14 +1,11 @@ import type { Locator, Page } from '@playwright/test'; import { FlexTab } from './flextab'; +import { Listbox } from './listbox'; export class EditRoomFlexTab extends FlexTab { - constructor(page: Page) { - super(page.getByRole('dialog', { name: 'Room Information' })); - } - - get btnSave(): Locator { - return this.root.locator('button >> text="Save"'); + constructor(locator: Locator) { + super(locator); } get roomNameInput(): Locator { @@ -51,3 +48,40 @@ export class EditRoomFlexTab extends FlexTab { return this.root.locator('input[name="isDefault"]'); } } + +export class OmnichannelEditRoomFlexTab extends EditRoomFlexTab { + private readonly tagsListbox: Listbox; + + private readonly slaListbox: Listbox; + + constructor(page: Page) { + super(page.getByRole('dialog', { name: 'Edit Room' })); + this.tagsListbox = new Listbox(page); + this.slaListbox = new Listbox(page, 'SLA Policy'); + } + + get inputTopic(): Locator { + return this.root.getByRole('textbox', { name: 'Topic', exact: true }); + } + + get inputSLAPolicy(): Locator { + return this.root.getByRole('button', { name: 'SLA Policy', exact: true }); + } + + optionTag(name: string): Locator { + return this.tagsListbox.getOption(name); + } + + async selectTag(name: string) { + await this.tagsListbox.selectOption(name); + } + + async selectSLA(name: string) { + await this.inputSLAPolicy.click(); + await this.slaListbox.selectOption(name, true); + } + + get inputTags(): Locator { + return this.root.getByRole('textbox', { name: 'Select an option' }); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/fragments/edit-user-flextab.ts b/apps/meteor/tests/e2e/page-objects/fragments/edit-user-flextab.ts index 796660efc25d1..0133c63d7694e 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/edit-user-flextab.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/edit-user-flextab.ts @@ -15,10 +15,6 @@ export class EditUserFlexTab extends FlexTab { return this.root.locator('role=button[name="Save user"]'); } - get inputName(): Locator { - return this.root.getByRole('textbox', { name: 'Name', exact: true }); - } - get inputUserName(): Locator { return this.root.getByRole('textbox', { name: 'Username', exact: true }); } diff --git a/apps/meteor/tests/e2e/page-objects/fragments/flextab.ts b/apps/meteor/tests/e2e/page-objects/fragments/flextab.ts index 770cf5997b440..e406a0decb43a 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/flextab.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/flextab.ts @@ -14,11 +14,40 @@ export abstract class FlexTab { } private get btnClose() { - return this.root.getByRole('button', { name: 'Close' }); + return this.root.getByRole('button', { name: 'Close', exact: true }); + } + + get inputName() { + return this.root.getByRole('textbox', { name: 'Name', exact: true }); + } + + get btnSave() { + return this.root.getByRole('button', { name: 'Save', exact: true }); + } + + get btnCancel() { + return this.root.getByRole('button', { name: 'Cancel', exact: true }); + } + + get btnDelete() { + return this.root.getByRole('button', { name: 'Delete', exact: true }); + } + + get btnReset() { + return this.root.getByRole('button', { name: 'Reset', exact: true }); + } + + errorMessage(message: string): Locator { + return this.root.locator('[role="alert"]', { hasText: message }); } async close() { await this.btnClose.click(); await this.waitForDismissal(); } + + async save() { + await this.btnSave.click(); + await this.waitForDismissal(); + } } diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts index 3f36cc6d13ca7..eba1a2f986a26 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts @@ -307,10 +307,6 @@ export class HomeContent { return this.page.getByRole('group').getByRole('button', { name: 'Voice call' }); } - get btnContactEdit(): Locator { - return this.page.getByRole('dialog').getByRole('button', { name: 'Edit', exact: true }); - } - get btnSendTranscript(): Locator { return this.page.locator('role=button[name="Send transcript"]'); } diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-omnichannel-content.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-omnichannel-content.ts index 47630b06220ad..71534bdb38498 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-omnichannel-content.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-omnichannel-content.ts @@ -1,44 +1,27 @@ import type { Locator, Page } from '@playwright/test'; import { HomeContent } from './home-content'; -import { OmnichannelTransferChatModal } from './modals'; +import { OmnichannelTransferChatModal, OmnichannelReturnToQueueModal } from './modals'; export class HomeOmnichannelContent extends HomeContent { readonly forwardChatModal: OmnichannelTransferChatModal; + readonly returnToQueueModal: OmnichannelReturnToQueueModal; + constructor(page: Page) { super(page); this.forwardChatModal = new OmnichannelTransferChatModal(page); + this.returnToQueueModal = new OmnichannelReturnToQueueModal(page); } get btnReturnToQueue(): Locator { return this.page.locator('role=button[name="Move to the queue"]'); } - get modalReturnToQueue(): Locator { - return this.page.locator('[data-qa-id="return-to-queue-modal"]'); - } - - get btnReturnToQueueConfirm(): Locator { - return this.modalReturnToQueue.locator('role=button[name="Confirm"]'); - } - - get btnReturnToQueueCancel(): Locator { - return this.modalReturnToQueue.locator('role=button[name="Cancel"]'); - } - get btnTakeChat(): Locator { return this.page.locator('role=button[name="Take it!"]'); } - get contactContextualBar() { - return this.page.getByRole('dialog', { name: 'Contact' }); - } - - get infoContactEmail(): Locator { - return this.contactContextualBar.getByRole('list', { name: 'Email' }).getByRole('listitem').first().locator('p'); - } - get header(): Locator { return this.page.locator('header'); } @@ -55,6 +38,9 @@ export class HomeOmnichannelContent extends HomeContent { return this.page.locator('.rcx-room-header').getByRole('heading'); } + /** + * FIXME: useX naming convention should be exclusively for react hooks + **/ async useCannedResponse(cannedResponseName: string): Promise { await this.composer.inputMessage.pressSequentially('!'); await this.page.locator('[role="menu"][name="ComposerBoxPopup"]').waitFor({ state: 'visible' }); diff --git a/apps/meteor/tests/e2e/page-objects/fragments/index.ts b/apps/meteor/tests/e2e/page-objects/fragments/index.ts index f1f349b6a82ab..f6a076472ab99 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/index.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/index.ts @@ -3,7 +3,6 @@ export * from './user-info-flextab'; export * from './home-content'; export * from './home-omnichannel-content'; export * from './home-flextab'; -export * from './omnichannel-sidenav'; export * from './navbar'; export * from './sidebar'; export * from './sidepanel'; diff --git a/apps/meteor/tests/e2e/page-objects/fragments/listbox.ts b/apps/meteor/tests/e2e/page-objects/fragments/listbox.ts index 055c7ccb56b84..8b22ca2f27484 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/listbox.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/listbox.ts @@ -1,13 +1,21 @@ -import type { Locator } from 'playwright-core'; +import type { Locator, Page } from 'playwright-core'; export class Listbox { - constructor(private root: Locator) {} + readonly root: Locator; - async selectOption(name: string) { - return this.root.getByRole('option', { name }).click(); + constructor(page: Page, name?: string) { + /** + * Currently, our selects and multiSelects has multiple listboxes in the DOM. + * So, to avoid selecting the wrong one, we will always select the last until we fix it. + */ + this.root = page.getByRole('listbox', { name }).last(); + } + + async selectOption(name: string, exact?: boolean) { + return this.root.getByRole('option', { name, exact }).click(); } public getOption(name: string): Locator { - return this.root.getByRole('option', { name }); + return this.root.getByRole('option', { name, exact: true }); } } diff --git a/apps/meteor/tests/e2e/page-objects/fragments/menu.ts b/apps/meteor/tests/e2e/page-objects/fragments/menu.ts index 1bafdbcaa0cd4..4353ec8d12e03 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/menu.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/menu.ts @@ -12,6 +12,10 @@ export abstract class Menu { waitForDismissal() { return expect(this.root).not.toBeVisible(); } + + selectMenuItem(itemName: string) { + return this.root.getByRole('menuitem', { name: itemName, exact: true }).click(); + } } export class MenuMore extends Menu { diff --git a/apps/meteor/tests/e2e/page-objects/fragments/modals/confirm-delete-modal.ts b/apps/meteor/tests/e2e/page-objects/fragments/modals/confirm-delete-modal.ts index 66826b0489d40..64967bbdf19ca 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/modals/confirm-delete-modal.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/modals/confirm-delete-modal.ts @@ -1,4 +1,4 @@ -import type { Locator } from 'playwright-core'; +import type { Locator, Page } from 'playwright-core'; import { Modal } from './modal'; @@ -7,12 +7,27 @@ export class ConfirmDeleteModal extends Modal { super(root); } - private btnDelete() { + get btnDelete() { return this.root.getByRole('button', { name: 'Delete' }); } async confirmDelete() { - await this.btnDelete().click(); + await this.btnDelete.click(); await this.waitForDismissal(); } } + +export class ConfirmDeleteDepartmentModal extends ConfirmDeleteModal { + constructor(page: Page) { + super(page.getByRole('dialog', { name: 'Delete Department?' })); + } + + get inputConfirmDepartmentName() { + return this.root.getByRole('textbox', { name: 'Department name' }); + } + + async deleteDepartment(departmentName: string) { + await this.inputConfirmDepartmentName.fill(departmentName); + await this.confirmDelete(); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/fragments/modals/create-new-modal.ts b/apps/meteor/tests/e2e/page-objects/fragments/modals/create-new-modal.ts index 2ad3e2fe12666..33d6cc4f8b934 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/modals/create-new-modal.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/modals/create-new-modal.ts @@ -8,7 +8,7 @@ export abstract class CreateNewModal extends Modal { constructor(root: Locator, page: Page) { super(root, page); - this.listbox = new Listbox(page.getByRole('listbox')); + this.listbox = new Listbox(page); } get inputName(): Locator { diff --git a/apps/meteor/tests/e2e/page-objects/fragments/modals/index.ts b/apps/meteor/tests/e2e/page-objects/fragments/modals/index.ts index 462b2836f5ec2..77c38b3dd78db 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/modals/index.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/modals/index.ts @@ -12,6 +12,9 @@ export * from './omnichannel-on-hold-modal'; export * from './omnichannel-transfer-chat-modal'; export * from './omnichannel-confirm-remove-chat'; export * from './omnichannel-contact-review-modal'; +export * from './omnichannel-delete-contact-modal'; +export * from './omnichannel-reset-priorities-modal'; +export * from './omnichannel-return-to-queue-modal'; export * from './report-message-modal'; export * from './reset-e2ee-password-modal'; export * from './save-e2ee-password-modal'; diff --git a/apps/meteor/tests/e2e/page-objects/fragments/modals/omnichannel-contact-review-modal.ts b/apps/meteor/tests/e2e/page-objects/fragments/modals/omnichannel-contact-review-modal.ts index b4314202283a9..9ed8beb2b22b2 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/modals/omnichannel-contact-review-modal.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/modals/omnichannel-contact-review-modal.ts @@ -9,7 +9,7 @@ export class OmnichannelContactReviewModal extends Modal { constructor(page: Page) { super(page.getByRole('dialog', { name: 'Review contact' }), page); - this.listbox = new Listbox(page.getByRole('listbox')); + this.listbox = new Listbox(page); } private getFieldByName(name: string): Locator { diff --git a/apps/meteor/tests/e2e/page-objects/fragments/modals/omnichannel-delete-contact-modal.ts b/apps/meteor/tests/e2e/page-objects/fragments/modals/omnichannel-delete-contact-modal.ts new file mode 100644 index 0000000000000..a7494ce08da64 --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/fragments/modals/omnichannel-delete-contact-modal.ts @@ -0,0 +1,22 @@ +import type { Locator, Page } from '@playwright/test'; + +import { Modal } from './modal'; + +export class OmnichannelDeleteContactModal extends Modal { + constructor(page: Page) { + super(page.getByRole('dialog', { name: 'Delete Contact' })); + } + + get inputConfirmation(): Locator { + return this.root.getByRole('textbox', { name: 'Confirm Contact Removal' }); + } + + get btnDelete(): Locator { + return this.root.getByRole('button', { name: 'Delete' }); + } + + async delete() { + await this.btnDelete.click(); + await this.waitForDismissal(); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/fragments/modals/omnichannel-reset-priorities-modal.ts b/apps/meteor/tests/e2e/page-objects/fragments/modals/omnichannel-reset-priorities-modal.ts new file mode 100644 index 0000000000000..d5be391821edb --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/fragments/modals/omnichannel-reset-priorities-modal.ts @@ -0,0 +1,18 @@ +import type { Page } from '@playwright/test'; + +import { Modal } from './modal'; + +export class OmnichannelResetPrioritiesModal extends Modal { + constructor(page: Page) { + super(page.getByRole('dialog', { name: 'Reset priorities' })); + } + + private get btnResetConfirm() { + return this.root.getByRole('button', { name: 'Reset' }); + } + + async reset() { + await this.btnResetConfirm.click(); + await this.waitForDismissal(); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/fragments/modals/omnichannel-return-to-queue-modal.ts b/apps/meteor/tests/e2e/page-objects/fragments/modals/omnichannel-return-to-queue-modal.ts new file mode 100644 index 0000000000000..6a6eb706e94ff --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/fragments/modals/omnichannel-return-to-queue-modal.ts @@ -0,0 +1,18 @@ +import type { Page } from '@playwright/test'; + +import { Modal } from './modal'; + +export class OmnichannelReturnToQueueModal extends Modal { + constructor(page: Page) { + super(page.getByRole('dialog', { name: 'Return back to the Queue' })); + } + + private get btnReturnToQueueConfirm() { + return this.root.getByRole('button', { name: 'Confirm' }); + } + + async confirm() { + await this.btnReturnToQueueConfirm.click(); + await this.waitForDismissal(); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/fragments/modals/omnichannel-transfer-chat-modal.ts b/apps/meteor/tests/e2e/page-objects/fragments/modals/omnichannel-transfer-chat-modal.ts index 9398213613477..9aa499d0bc851 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/modals/omnichannel-transfer-chat-modal.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/modals/omnichannel-transfer-chat-modal.ts @@ -8,7 +8,7 @@ export class OmnichannelTransferChatModal extends Modal { constructor(page: Page) { super(page.getByRole('dialog', { name: 'Forward chat' })); - this.listbox = new Listbox(page.getByRole('listbox')); + this.listbox = new Listbox(page); } get inputComment(): Locator { diff --git a/apps/meteor/tests/e2e/page-objects/fragments/modals/upsell-modal.ts b/apps/meteor/tests/e2e/page-objects/fragments/modals/upsell-modal.ts index d56da056247b0..0881a016e6449 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/modals/upsell-modal.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/modals/upsell-modal.ts @@ -7,3 +7,9 @@ export class VoiceCallsUpsellModal extends Modal { super(page.getByRole('dialog', { name: 'Team voice calls' })); } } + +export class OmnichannelUpsellDepartmentsModal extends Modal { + constructor(page: Page) { + super(page.getByRole('dialog', { name: 'Departments' })); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/fragments/omnichannel-sidenav.ts b/apps/meteor/tests/e2e/page-objects/fragments/omnichannel-sidenav.ts deleted file mode 100644 index 63fc3c3919358..0000000000000 --- a/apps/meteor/tests/e2e/page-objects/fragments/omnichannel-sidenav.ts +++ /dev/null @@ -1,77 +0,0 @@ -import type { Locator, Page } from '@playwright/test'; - -export class OmnichannelSidenav { - private readonly page: Page; - - constructor(page: Page) { - this.page = page; - } - - get linkDepartments(): Locator { - return this.page.locator('a[href="/omnichannel/departments"]'); - } - - get linkAgents(): Locator { - return this.page.locator('a[href="/omnichannel/agents"]'); - } - - get linkManagers(): Locator { - return this.page.locator('a[href="/omnichannel/managers"]'); - } - - get linkCustomFields(): Locator { - return this.page.locator('a[href="/omnichannel/customfields"]'); - } - - get linkCurrentChats(): Locator { - return this.page.locator('a[href="/omnichannel/current"]'); - } - - get linkTriggers(): Locator { - return this.page.locator('a[href="/omnichannel/triggers"]'); - } - - get linkSlaPolicies(): Locator { - return this.page.locator('a[href="/omnichannel/sla-policies"]'); - } - - get linkPriorities(): Locator { - return this.page.locator('a[href="/omnichannel/priorities"]'); - } - - get linkMonitors(): Locator { - return this.page.locator('a[href="/omnichannel/monitors"]'); - } - - get linkBusinessHours(): Locator { - return this.page.locator('a[href="/omnichannel/businessHours"]'); - } - - get linkAnalytics(): Locator { - return this.page.locator('a[href="/omnichannel/analytics"]'); - } - - get linkRealTimeMonitoring(): Locator { - return this.page.locator('a[href="/omnichannel/realtime-monitoring"]'); - } - - get linkReports(): Locator { - return this.page.locator('a[href="/omnichannel/reports"]'); - } - - get linkCannedResponses(): Locator { - return this.page.locator('a[href="/omnichannel/canned-responses"]'); - } - - get linkUnits(): Locator { - return this.page.locator('a[href="/omnichannel/units"]'); - } - - get linkLivechatAppearance(): Locator { - return this.page.locator('a[href="/omnichannel/appearance"]'); - } - - get linkTags(): Locator { - return this.page.locator('a[href="/omnichannel/tags"]'); - } -} diff --git a/apps/meteor/tests/e2e/page-objects/fragments/room-info-flextab.ts b/apps/meteor/tests/e2e/page-objects/fragments/room-info-flextab.ts new file mode 100644 index 0000000000000..1983f8c6c57b7 --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/fragments/room-info-flextab.ts @@ -0,0 +1,31 @@ +import type { Locator, Page } from '@playwright/test'; + +import { FlexTab } from './flextab'; + +export class RoomInfoFlexTab extends FlexTab { + constructor(page: Page) { + super(page.getByRole('dialog', { name: 'Room Information' })); + } + + get btnEdit(): Locator { + return this.root.getByRole('button', { name: 'Edit' }); + } +} + +export class OmnichannelRoomInfoFlexTab extends RoomInfoFlexTab { + getInfo(value: string): Locator { + return this.root.locator(`span >> text="${value}"`); + } + + getLabel(label: string): Locator { + return this.root.locator(`div >> text="${label}"`); + } + + getInfoByLabel(label: string): Locator { + return this.root.getByLabel(label); + } + + getTagInfoByLabel(label: string): Locator { + return this.root.getByRole('list', { name: 'Tags' }).getByText(label, { exact: true }); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/fragments/sidebar.ts b/apps/meteor/tests/e2e/page-objects/fragments/sidebar.ts index 26600055cea29..1d041a3db1735 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/sidebar.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/sidebar.ts @@ -101,6 +101,10 @@ export class RoomSidebar extends Sidebar { return item.getByRole('status', { name: 'unread' }); } + getBadgeIndicator(name: string, title: string): Locator { + return this.getSidebarItemByName(name).getByTitle(title); + } + async selectPriority(name: string, priority: string) { const sidebarItem = this.getSidebarItemByName(name); await sidebarItem.hover(); @@ -143,3 +147,73 @@ export class AccountSidebar extends Sidebar { await this.waitForDismissal(); } } + +export class OmnichannelSidebar extends Sidebar { + constructor(page: Page) { + super(page.getByRole('navigation', { name: 'Omnichannel' })); + } + + get linkDepartments(): Locator { + return this.root.locator('a[href="/omnichannel/departments"]'); + } + + get linkAgents(): Locator { + return this.root.locator('a[href="/omnichannel/agents"]'); + } + + get linkManagers(): Locator { + return this.root.locator('a[href="/omnichannel/managers"]'); + } + + get linkCustomFields(): Locator { + return this.root.locator('a[href="/omnichannel/customfields"]'); + } + + get linkCurrentChats(): Locator { + return this.root.locator('a[href="/omnichannel/current"]'); + } + + get linkSlaPolicies(): Locator { + return this.root.locator('a[href="/omnichannel/sla-policies"]'); + } + + get linkPriorities(): Locator { + return this.root.locator('a[href="/omnichannel/priorities"]'); + } + + get linkMonitors(): Locator { + return this.root.locator('a[href="/omnichannel/monitors"]'); + } + + get linkBusinessHours(): Locator { + return this.root.locator('a[href="/omnichannel/businessHours"]'); + } + + get linkAnalytics(): Locator { + return this.root.locator('a[href="/omnichannel/analytics"]'); + } + + get linkRealTimeMonitoring(): Locator { + return this.root.locator('a[href="/omnichannel/realtime-monitoring"]'); + } + + get linkReports(): Locator { + return this.root.locator('a[href="/omnichannel/reports"]'); + } + + get linkCannedResponses(): Locator { + return this.root.locator('a[href="/omnichannel/canned-responses"]'); + } + + get linkUnits(): Locator { + return this.root.locator('a[href="/omnichannel/units"]'); + } + + get linkLivechatAppearance(): Locator { + return this.root.locator('a[href="/omnichannel/appearance"]'); + } + + get linkTags(): Locator { + return this.root.locator('a[href="/omnichannel/tags"]'); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/fragments/table.ts b/apps/meteor/tests/e2e/page-objects/fragments/table.ts new file mode 100644 index 0000000000000..f91557ab4ef00 --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/fragments/table.ts @@ -0,0 +1,17 @@ +import type { Locator } from '@playwright/test'; + +import { expect } from '../../utils/test'; + +export abstract class Table { + constructor(protected root: Locator) {} + + waitForDisplay() { + return expect(this.root).toBeVisible(); + } + + findRowByName(name: string): Locator { + return this.root.getByRole('row').filter({ + has: this.root.page().getByText(name, { exact: true }), + }); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/home-omnichannel.ts b/apps/meteor/tests/e2e/page-objects/home-omnichannel.ts index 9b23eadef8d12..ea7695d398bf5 100644 --- a/apps/meteor/tests/e2e/page-objects/home-omnichannel.ts +++ b/apps/meteor/tests/e2e/page-objects/home-omnichannel.ts @@ -1,38 +1,30 @@ import type { Locator, Page } from '@playwright/test'; -import { HomeOmnichannelContent, OmnichannelSidenav } from './fragments'; -import { OmnichannelQuickActionsRoomToolbar, OmnichannelRoomToolbar } from './fragments/toolbar'; +import { HomeOmnichannelContent, OmnichannelQuickActionsRoomToolbar, OmnichannelRoomToolbar, OmnichannelSidebar } from './fragments'; +import { OmnichannelEditRoomFlexTab } from './fragments/edit-room-flextab'; +import { OmnichannelRoomInfoFlexTab } from './fragments/room-info-flextab'; import { HomeChannel } from './home-channel'; -import { OmnichannelAgents } from './omnichannel-agents'; -import { OmnichannelCannedResponses } from './omnichannel-canned-responses'; -import { OmnichannelChats } from './omnichannel-contact-center-chats'; -import { OmnichannelContacts } from './omnichannel-contacts-list'; -import { OmnichannelManager } from './omnichannel-manager'; -import { OmnichannelMonitors } from './omnichannel-monitors'; -import { OmnichannelRoomInfo } from './omnichannel-room-info'; -import { OmnichannelTranscript } from './omnichannel-transcript'; -import { OmnichannelTriggers } from './omnichannel-triggers'; +import { + OmnichannelCannedResponses, + OmnichannelTranscript, + OmnichannelContactCenterContacts, + OmnichannelContactCenterChats, +} from './omnichannel'; export class HomeOmnichannel extends HomeChannel { - readonly triggers: OmnichannelTriggers; - - readonly omnisidenav: OmnichannelSidenav; + readonly omnisidenav: OmnichannelSidebar; readonly transcript: OmnichannelTranscript; readonly cannedResponses: OmnichannelCannedResponses; - readonly agents: OmnichannelAgents; - - readonly managers: OmnichannelManager; - - readonly monitors: OmnichannelMonitors; + readonly contacts: OmnichannelContactCenterContacts; - readonly contacts: OmnichannelContacts; + readonly chats: OmnichannelContactCenterChats; - readonly chats: OmnichannelChats; + readonly roomInfo: OmnichannelRoomInfoFlexTab; - readonly roomInfo: OmnichannelRoomInfo; + readonly editRoomInfo: OmnichannelEditRoomFlexTab; readonly quickActionsRoomToolbar: OmnichannelQuickActionsRoomToolbar; @@ -42,19 +34,16 @@ export class HomeOmnichannel extends HomeChannel { constructor(page: Page) { super(page); - this.triggers = new OmnichannelTriggers(page); - this.omnisidenav = new OmnichannelSidenav(page); + this.omnisidenav = new OmnichannelSidebar(page); this.transcript = new OmnichannelTranscript(page); this.cannedResponses = new OmnichannelCannedResponses(page); - this.agents = new OmnichannelAgents(page); - this.managers = new OmnichannelManager(page); - this.monitors = new OmnichannelMonitors(page); - this.contacts = new OmnichannelContacts(page); - this.chats = new OmnichannelChats(page); - this.roomInfo = new OmnichannelRoomInfo(page); + this.contacts = new OmnichannelContactCenterContacts(page); + this.chats = new OmnichannelContactCenterChats(page); + this.roomInfo = new OmnichannelRoomInfoFlexTab(page); this.quickActionsRoomToolbar = new OmnichannelQuickActionsRoomToolbar(page); this.content = new HomeOmnichannelContent(page); this.roomToolbar = new OmnichannelRoomToolbar(page); + this.editRoomInfo = new OmnichannelEditRoomFlexTab(page); } get btnContactInfo(): Locator { diff --git a/apps/meteor/tests/e2e/page-objects/index.ts b/apps/meteor/tests/e2e/page-objects/index.ts index e6b3c41e01120..f69b583b5c756 100644 --- a/apps/meteor/tests/e2e/page-objects/index.ts +++ b/apps/meteor/tests/e2e/page-objects/index.ts @@ -15,17 +15,5 @@ export * from './auth'; export * from './home-channel'; export * from './home-discussion'; export * from './home-team'; -export * from './omnichannel-agents'; -export * from './omnichannel-departments'; -export * from './omnichannel-contact-center-chats'; -export * from './omnichannel-livechat'; -export * from './omnichannel-livechat-embedded'; -export * from './omnichannel-manager'; -export * from './omnichannel-custom-fields'; -export * from './omnichannel-units'; export * from './home-omnichannel'; -export * from './omnichannel-monitors'; -export * from './omnichannel-settings'; -export * from './omnichannel-business-hours'; -export * from './omnichannel-tags'; export * from './marketplace'; diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-administration.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-administration.ts deleted file mode 100644 index 01b8a6ed6ed45..0000000000000 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-administration.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { Page } from '@playwright/test'; - -import { OmnichannelSidenav } from './fragments'; - -export class OmnichannelAdministration { - protected readonly page: Page; - - readonly sidenav: OmnichannelSidenav; - - constructor(page: Page) { - this.page = page; - this.sidenav = new OmnichannelSidenav(page); - } -} diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-agents.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-agents.ts deleted file mode 100644 index 3eaf9fa304d09..0000000000000 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-agents.ts +++ /dev/null @@ -1,122 +0,0 @@ -import type { Locator, Page } from '@playwright/test'; - -import { OmnichannelSidenav } from './fragments'; - -export class OmnichannelAgents { - private readonly page: Page; - - readonly sidenav: OmnichannelSidenav; - - readonly editCtxBar: Locator; - - readonly infoCtxBar: Locator; - - constructor(page: Page) { - this.page = page; - this.sidenav = new OmnichannelSidenav(page); - this.editCtxBar = page.getByRole('dialog', { name: 'Edit User' }); - this.infoCtxBar = page.getByRole('dialog', { name: 'User Info' }); - } - - get inputUsername(): Locator { - return this.page.getByRole('textbox', { name: 'Username' }); - } - - get inputSearch(): Locator { - return this.page.getByRole('main').getByRole('textbox', { name: 'Search' }); - } - - get btnAdd(): Locator { - return this.page.locator('role=button[name="Add agent"]'); - } - - get firstRowInTable() { - return this.page.locator('[data-qa-id="agents-table"] tr:first-child td:first-child'); - } - - get btnDeleteFirstRowInTable() { - return this.page.locator('[data-qa-id="agents-table"] tr:first-child').locator('role=button[name="Remove"]'); - } - - get modalRemoveAgent(): Locator { - return this.page.locator('[data-qa-id="remove-agent-modal"]'); - } - - get btnModalRemove(): Locator { - return this.modalRemoveAgent.locator('role=button[name="Delete"]'); - } - - get btnEdit(): Locator { - return this.infoCtxBar.locator('[data-qa="agent-info-action-edit"]'); - } - - get btnRemove(): Locator { - return this.infoCtxBar.locator('role=button[name="Remove"]'); - } - - get btnClose(): Locator { - return this.editCtxBar.getByRole('button', { name: 'Close', exact: true }); - } - - get btnSave(): Locator { - return this.editCtxBar.locator('[data-qa-id="agent-edit-save"]'); - } - - get inputMaxChats(): Locator { - return this.editCtxBar.locator('input[name="maxNumberSimultaneousChat"]'); - } - - get inputStatus(): Locator { - return this.page.locator('[data-qa-id="agent-edit-status"]'); - } - - get inputDepartment(): Locator { - return this.editCtxBar.getByLabel('Departments').getByRole('textbox'); - } - - get scrollContainer(): Locator { - return this.page.locator('#position-container').getByTestId('virtuoso-scroller'); - } - - findOption(name: string) { - return this.page.locator('#position-container').getByRole('option', { name, exact: true }); - } - - scrollToListBottom() { - return this.scrollContainer.evaluate((el) => { - el.scrollTop = el.scrollHeight; - el.dispatchEvent(new Event('scroll')); - }); - } - - async selectDepartment(name: string) { - await this.inputDepartment.click(); - await this.inputDepartment.fill(name); - await this.findOption(name).click(); - // TODO: This is necessary due to the PaginatedMultiSelectFiltered not closing the list when an option is selected nor when close is clicked. - // The line below can be removed once the component is adjusted in Fuselage - await this.editCtxBar.click(); - } - - async selectStatus(status: string) { - await this.inputStatus.click(); - await this.page.locator(`.rcx-option__content:has-text("${status}")`).click(); - } - - async selectUsername(username: string) { - await this.inputUsername.fill(username); - await this.page.locator(`role=option[name="${username}"]`).click(); - } - - findRowByUsername(username: string) { - return this.page.locator(`[data-qa-id="${username}"]`); - } - - findRowByName(name: string) { - return this.page.locator('tr', { has: this.page.locator(`td >> text="${name}"`) }); - } - - findSelectedDepartment(name: string) { - return this.editCtxBar.getByLabel('Departments', { exact: true }).getByRole('option', { name }); - } -} diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-business-hours.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-business-hours.ts deleted file mode 100644 index 4417d6a274143..0000000000000 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-business-hours.ts +++ /dev/null @@ -1,75 +0,0 @@ -import type { Locator } from '@playwright/test'; - -import { OmnichannelAdministration } from './omnichannel-administration'; - -export class OmnichannelBusinessHours extends OmnichannelAdministration { - get btnCreateBusinessHour(): Locator { - return this.page.locator('header').locator('role=button[name="New"]'); - } - - get btnSave(): Locator { - return this.page.locator('role=button[name="Save"]'); - } - - get btnBack(): Locator { - return this.page.locator('role=button[name="Back"]'); - } - - get inputSearch(): Locator { - return this.page.getByRole('main').getByRole('textbox', { name: 'Search' }); - } - - get inputName(): Locator { - return this.page.locator('[name="name"]'); - } - - get fieldDepartment(): Locator { - return this.page.getByLabel('Departments', { exact: true }); - } - - get inputDepartments(): Locator { - return this.fieldDepartment.getByRole('textbox'); - } - - findRowByName(name: string): Locator { - return this.page.locator(`tr:has-text("${name}")`); - } - - btnDeleteByName(name: string): Locator { - return this.page.locator(`tr:has-text("${name}") button[title="Remove"]`); - } - - get confirmDeleteModal(): Locator { - return this.page.locator('dialog:has(h2:has-text("Are you sure?"))'); - } - - get btnCancelDeleteModal(): Locator { - return this.confirmDeleteModal.locator('role=button[name="Cancel"]'); - } - - get btnConfirmDeleteModal(): Locator { - return this.confirmDeleteModal.locator('role=button[name="Delete"]'); - } - - getCheckboxByLabel(name: string): Locator { - return this.page.locator('label', { has: this.page.getByRole('checkbox', { name }) }); - } - - findOption(name: string): Locator { - return this.page.locator('#position-container').getByRole('option', { name, exact: true }); - } - - findDepartmentsChipOption(name: string) { - return this.fieldDepartment.getByRole('option', { name, exact: true }); - } - - async selectDepartment(name: string) { - await this.inputDepartments.click(); - await this.inputDepartments.fill(name); - await this.findOption(name).click(); - } - - async search(text: string) { - await this.inputSearch.fill(text); - } -} diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-contact-center-chats-filters.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-contact-center-chats-filters.ts deleted file mode 100644 index 2bc57c870b2bb..0000000000000 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-contact-center-chats-filters.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { Locator, Page } from '@playwright/test'; - -import { FlexTab } from './fragments/flextab'; - -export class OmnichannelChatsFilters extends FlexTab { - constructor(page: Page) { - super(page.getByRole('dialog', { name: 'Filters' })); - } - - get inputFrom(): Locator { - return this.root.locator('input[name="from"]'); - } - - get inputTo(): Locator { - return this.root.locator('input[name="to"]'); - } - - get btnApply(): Locator { - return this.root.locator('role=button[name="Apply"]'); - } -} diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-contact-center-chats.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-contact-center-chats.ts deleted file mode 100644 index 3f5918ba07787..0000000000000 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-contact-center-chats.ts +++ /dev/null @@ -1,98 +0,0 @@ -import type { Locator, Page } from '@playwright/test'; - -import { HomeOmnichannelContent } from './fragments'; -import { FlexTab } from './fragments/flextab'; -import { OmnichannelConfirmRemoveChat } from './fragments/modals'; -import { OmnichannelAdministration } from './omnichannel-administration'; -import { OmnichannelChatsFilters } from './omnichannel-contact-center-chats-filters'; - -class ConversationFlexTab extends FlexTab { - constructor(page: Page) { - super(page.getByRole('dialog', { name: 'Conversation' })); - } - - private get btnOpenChat(): Locator { - return this.root.getByRole('button', { name: 'Open chat' }); - } - - async openChat() { - await this.btnOpenChat.click(); - await this.waitForDismissal(); - } -} - -export class OmnichannelChats extends OmnichannelAdministration { - private readonly confirmRemoveChatModal: OmnichannelConfirmRemoveChat; - - private readonly conversation: ConversationFlexTab; - - readonly filters: OmnichannelChatsFilters; - - readonly content: HomeOmnichannelContent; - - constructor(page: Page) { - super(page); - this.filters = new OmnichannelChatsFilters(page); - this.confirmRemoveChatModal = new OmnichannelConfirmRemoveChat(page); - this.content = new HomeOmnichannelContent(page); - this.conversation = new ConversationFlexTab(page); - } - - get btnFilters(): Locator { - return this.page.locator('role=button[name="Filters"]'); - } - - get inputSearch(): Locator { - return this.page.locator('role=textbox[name="Search"]'); - } - - private get contactCenterChatsTable(): Locator { - return this.page.getByRole('table', { name: 'Omnichannel Contact Center Chats' }); - } - - findRowByName(contactName: string) { - return this.contactCenterChatsTable.getByRole('link', { name: contactName }); - } - - get inputStatus(): Locator { - return this.page.locator('[data-qa="current-chats-status"]'); - } - - get inputTags(): Locator { - return this.page.locator('[data-qa="current-chats-tags"] [role="listbox"]'); - } - - get modalConfirmRemoveAllClosed(): Locator { - return this.page.locator('[data-qa-id="current-chats-modal-remove-all-closed"]'); - } - - async addTag(option: string) { - await this.inputTags.click(); - await this.page.locator(`[role='option'][value='${option}']`).click(); - await this.inputTags.click(); - } - - async removeTag(option: string) { - await this.page.locator(`role=option[name='${option}']`).click(); - } - - async selectStatus(option: string) { - await this.inputStatus.click(); - await this.page.locator(`[role='option'][data-key='${option}']`).click(); - } - - btnRemoveByName(name: string): Locator { - return this.findRowByName(name).getByRole('button', { name: 'Remove' }); - } - - async removeChatByName(name: string) { - await this.btnRemoveByName(name).click(); - await this.confirmRemoveChatModal.confirm(); - } - - async openChat(name: string) { - await this.findRowByName(name).click(); - await this.conversation.openChat(); - await this.page.locator('#main-content').waitFor(); - } -} diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-contacts-list.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-contacts-list.ts deleted file mode 100644 index 5a288ccf3e639..0000000000000 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-contacts-list.ts +++ /dev/null @@ -1,159 +0,0 @@ -import type { Locator, Page } from '@playwright/test'; - -import { OmnichannelContactInfo } from './omnichannel-info'; -import { OmnichannelManageContact } from './omnichannel-manage-contact'; - -export class OmnichannelContacts { - private readonly page: Page; - - readonly newContact: OmnichannelManageContact; - - readonly contactInfo: OmnichannelContactInfo; - - constructor(page: Page) { - this.page = page; - this.newContact = new OmnichannelManageContact(page); - this.contactInfo = new OmnichannelContactInfo(page); - } - - get btnNewContact(): Locator { - return this.page.locator('button >> text="New contact"'); - } - - get inputSearch(): Locator { - return this.page.locator('input[placeholder="Search"]'); - } - - findRowByName(contactName: string) { - return this.page.locator('tr', { has: this.page.locator(`td >> text="${contactName}"`) }); - } - - findRowMenu(contactName: string): Locator { - return this.findRowByName(contactName).getByRole('button', { name: 'More Actions' }); - } - - findMenuItem(name: string): Locator { - return this.page.getByRole('menuitem', { name }); - } - - get deleteContactModal(): Locator { - return this.page.getByRole('dialog', { name: 'Delete Contact' }); - } - - get inputDeleteContactConfirmation(): Locator { - return this.deleteContactModal.getByRole('textbox', { name: 'Confirm contact removal' }); - } - - get btnDeleteContact(): Locator { - return this.deleteContactModal.getByRole('button', { name: 'Delete' }); - } - - get btnFilters(): Locator { - return this.page.getByRole('button', { name: 'Filters' }); - } - - get inputServedBy(): Locator { - return this.filtersContextualBar.getByLabel('Served By').locator('input'); - } - - get inputDepartment(): Locator { - return this.filtersContextualBar.getByLabel('Department').locator('input'); - } - - get btnApply(): Locator { - return this.page.getByRole('button', { name: 'Apply' }); - } - - get tabChats(): Locator { - return this.page.getByRole('tab', { name: 'Chats' }); - } - - get selectStatusContainer(): Locator { - return this.filtersContextualBar.getByRole('button', { name: 'Status' }); - } - - get inputTags(): Locator { - return this.filtersContextualBar.getByLabel('Tags').locator('input'); - } - - get inputUnits(): Locator { - return this.filtersContextualBar.getByLabel('Units').locator('input'); - } - - get btnClearFilters(): Locator { - return this.page.getByRole('button', { name: 'Clear filters' }); - } - - get filtersContextualBar(): Locator { - return this.page.getByRole('dialog', { name: 'Filters' }); - } - - get btnClose(): Locator { - return this.filtersContextualBar.getByRole('button', { name: 'Close' }); - } - - btnStatusChip(name: string): Locator { - return this.page.getByRole('button', { name: `Status: ${name}` }); - } - - btnServedByChip(name: string): Locator { - return this.page.getByRole('button', { name: `Served by: ${name}` }); - } - - btnDepartmentChip(name: string): Locator { - return this.page.getByRole('button', { name: `Department: ${name}` }); - } - - btnSearchChip(name: string): Locator { - return this.page.getByRole('button', { name: `Text: ${name}` }); - } - - btnUnitsChip(name: string): Locator { - return this.page.getByRole('button', { name: `Units: ${name}` }); - } - - async selectServedBy(option: string) { - await this.inputServedBy.click(); - await this.inputServedBy.fill(option); - await this.page.locator(`[role='option'][value='${option}']`).click(); - await this.btnApply.click(); - } - - async selectStatus(option: string) { - await this.selectStatusContainer.click(); - await this.page.locator(`[role='option'][data-key='${option}']`).click(); - await this.btnApply.click(); - } - - async selectDepartment(option: string) { - await this.inputDepartment.click(); - await this.inputDepartment.fill(option); - await this.page.locator(`role=option[name='${option}']`).click(); - await this.inputDepartment.click(); - await this.btnApply.click(); - } - - async selectTag(option: string) { - await this.inputTags.click(); - await this.inputTags.fill(option); - await this.page.locator(`[role='option'][value='${option}']`).click(); - await this.inputTags.click(); - await this.btnApply.click(); - } - - async removeTag(option: string) { - await this.page.locator(`role=option[name='${option}']`).click(); - await this.btnApply.click(); - } - - findOption(optionText: string) { - return this.page.locator(`role=option[name="${optionText}"]`); - } - - async selectUnit(unitName: string) { - await this.inputUnits.click(); - await this.inputUnits.fill(unitName); - await this.findOption(unitName).click(); - await this.btnApply.click(); - } -} diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-custom-fields.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-custom-fields.ts deleted file mode 100644 index e3fd6e382b9fa..0000000000000 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-custom-fields.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type { Locator, Page } from '@playwright/test'; - -import { OmnichannelSidenav } from './fragments'; - -export class OmnichannelCustomFields { - private readonly page: Page; - - readonly sidenav: OmnichannelSidenav; - - constructor(page: Page) { - this.page = page; - this.sidenav = new OmnichannelSidenav(page); - } - - get btnAdd(): Locator { - return this.page.locator('[data-qa-id="CustomFieldPageBtnNew"]'); - } - - get inputField(): Locator { - return this.page.locator('input[name="field"]'); - } - - get inputLabel(): Locator { - return this.page.locator('input[name="label"]'); - } - - get visibleLabel(): Locator { - return this.page.locator('label >> text="Visible"'); - } - - get btnSave(): Locator { - return this.page.locator('button >> text=Save'); - } - - get inputSearch(): Locator { - return this.page.getByRole('main').getByRole('textbox', { name: 'Search' }); - } - - firstRowInTable(filedName: string) { - return this.page.locator(`[qa-user-id="${filedName}"]`); - } - - get btnDeleteCustomField() { - return this.page.locator('button >> text=Delete'); - } - - get btnModalRemove(): Locator { - return this.page.locator('#modal-root dialog .rcx-modal__inner .rcx-modal__footer .rcx-button--danger'); - } -} diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-departments.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-departments.ts deleted file mode 100644 index e5965b67a3a5a..0000000000000 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-departments.ts +++ /dev/null @@ -1,164 +0,0 @@ -import type { Page, Locator } from '@playwright/test'; - -import { OmnichannelSidenav, ToastMessages } from './fragments'; - -export class OmnichannelDepartments { - private readonly page: Page; - - readonly sidenav: OmnichannelSidenav; - - // TODO: This will be inherited from a BasePage Object - readonly toastMessage: ToastMessages; - - constructor(page: Page) { - this.page = page; - this.sidenav = new OmnichannelSidenav(page); - this.toastMessage = new ToastMessages(page); - } - - get inputSearch() { - return this.page.getByRole('main').getByRole('textbox', { name: 'Search' }); - } - - async search(text: string) { - await this.inputSearch.fill(text); - await this.page.waitForTimeout(500); - } - - headingButtonNew(name: string) { - return this.page.locator(`role=main >> role=button[name="${name}"]`).first(); - } - - get btnEnabled() { - return this.page.locator('label >> text="Enabled"'); - } - - get inputName() { - return this.page.locator('[data-qa="DepartmentEditTextInput-Name"]'); - } - - get inputEmail() { - return this.page.locator('[data-qa="DepartmentEditTextInput-Email"]'); - } - - get toggleRequestTags() { - return this.page.locator('label >> text="Request tag(s) before closing conversation"'); - } - - get inputTags() { - return this.page.locator('[data-qa="DepartmentEditTextInput-ConversationClosingTags"]'); - } - - get invalidInputName() { - return this.page.locator('[data-qa="DepartmentEditTextInput-Name"]:invalid'); - } - - get invalidInputEmail() { - return this.page.locator('[data-qa="DepartmentEditTextInput-Email"]:invalid'); - } - - get btnTagsAdd() { - return this.page.locator('[data-qa="DepartmentEditAddButton-ConversationClosingTags"]'); - } - - get btnSave() { - return this.page.locator('role=button[name="Save"]'); - } - - get btnBack() { - return this.page.locator('role=button[name="Back"]'); - } - - get archivedDepartmentsTab() { - return this.page.locator('[role="tab"]:nth-child(2)'); - } - - get firstRowInTable() { - return this.page.locator('table tr:first-child td:first-child'); - } - - get firstRowInTableMenu() { - return this.page.locator('table tr:first-child [data-testid="menu"]'); - } - - findDepartment(name: string) { - return this.page.locator('tr', { has: this.page.locator(`td >> text="${name}"`) }); - } - - selectedDepartmentMenu(name: string) { - return this.page.locator('tr', { has: this.page.locator(`td >> text="${name}"`) }).locator('[data-testid="menu"]'); - } - - get menuEditOption() { - return this.page.locator('[role=option][value="edit"]'); - } - - get menuDeleteOption() { - return this.page.locator('[role=option][value="delete"]'); - } - - get menuArchiveOption() { - return this.page.locator('[role=option][value="archive"]'); - } - - get menuUnarchiveOption() { - return this.page.locator('[role=option][value="unarchive"]'); - } - - get inputModalConfirmDelete() { - return this.modalConfirmDelete.locator('input[name="confirmDepartmentName"]'); - } - - get modalConfirmDelete() { - return this.page.locator('[data-qa-id="delete-department-modal"]'); - } - - get btnModalConfirmDelete() { - return this.modalConfirmDelete.locator('role=button[name="Delete"]'); - } - - get upgradeDepartmentsModal() { - return this.page.locator('[data-qa-id="enterprise-departments-modal"]'); - } - - get btnUpgradeDepartmentsModalClose() { - return this.page.locator('[data-qa="modal-close"]'); - } - - get inputUnit(): Locator { - return this.page.getByLabel('Unit').getByRole('textbox', { name: 'Select an option' }); - } - - btnTag(tagName: string) { - return this.page.locator('button', { hasText: tagName }); - } - - errorMessage(message: string): Locator { - return this.page.locator(`.rcx-field__error >> text="${message}"`); - } - - findOption(optionText: string) { - return this.page.locator(`role=option[name="${optionText}"]`); - } - - async selectUnit(unitName: string) { - await this.inputUnit.click(); - await this.findOption(unitName).click(); - } - - get fieldGroupAgents() { - return this.page.getByLabel('Agents', { exact: true }); - } - - get inputAgents() { - return this.fieldGroupAgents.getByRole('textbox'); - } - - get btnAddAgent() { - return this.fieldGroupAgents.getByRole('button', { name: 'Add', exact: true }); - } - - findAgentRow(name: string) { - return this.page.locator('tr', { has: this.page.getByText(name, { exact: true }) }); - } -} diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-info.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-info.ts deleted file mode 100644 index c6b5dc3e25e81..0000000000000 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-info.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { Locator, Page } from '@playwright/test'; - -import { OmnichannelContactReviewModal } from './fragments'; -import { OmnichannelManageContact } from './omnichannel-manage-contact'; - -export class OmnichannelContactInfo extends OmnichannelManageContact { - readonly contactReviewModal: OmnichannelContactReviewModal; - - constructor(page: Page) { - super(page); - this.contactReviewModal = new OmnichannelContactReviewModal(page); - } - - get dialogContactInfo(): Locator { - return this.page.getByRole('dialog', { name: 'Contact' }); - } - - get btnEdit(): Locator { - return this.page.locator('role=button[name="Edit"]'); - } - - get tabHistory(): Locator { - return this.dialogContactInfo.getByRole('tab', { name: 'History' }); - } - - get historyItem(): Locator { - return this.dialogContactInfo.getByRole('listitem').first(); - } - - get historyMessage(): Locator { - return this.dialogContactInfo.getByRole('listitem').first(); - } - - get btnOpenChat(): Locator { - return this.dialogContactInfo.getByRole('button', { name: 'Open chat' }); - } - - get btnSeeConflicts(): Locator { - return this.dialogContactInfo.getByRole('button', { name: 'See conflicts' }); - } - - async solveConflict(field: string, value: string) { - await this.btnSeeConflicts.click(); - await this.contactReviewModal.solveConfirmation(field, value); - } -} diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-manage-contact.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-manage-contact.ts deleted file mode 100644 index 6d87af0d52b82..0000000000000 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-manage-contact.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { Locator, Page } from '@playwright/test'; - -export class OmnichannelManageContact { - protected readonly page: Page; - - constructor(page: Page) { - this.page = page; - } - - get inputName(): Locator { - return this.page.locator('input[name=name]'); - } - - get inputEmail(): Locator { - return this.page.locator('input[name="emails.0.address"]'); - } - - get inputPhone(): Locator { - return this.page.locator('input[name="phones.0.phoneNumber"]'); - } - - get inputContactManager(): Locator { - return this.page.locator('input[name=contactManager]'); - } - - get btnSave(): Locator { - return this.page.locator('button >> text="Save"'); - } - - get btnCancel(): Locator { - return this.page.locator('button >> text="Cancel"'); - } - - get btnAddEmail(): Locator { - return this.page.locator('role=button[name="Add email"]'); - } - - get btnAddPhone(): Locator { - return this.page.locator('role=button[name="Add phone"]'); - } - - getErrorMessage(message: string): Locator { - return this.page.locator(`role=alert >> text="${message}"`); - } -} diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-manager.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-manager.ts deleted file mode 100644 index f52aa66fda629..0000000000000 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-manager.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { Locator, Page } from '@playwright/test'; - -import { OmnichannelSidenav } from './fragments'; - -export class OmnichannelManager { - private readonly page: Page; - - readonly sidenav: OmnichannelSidenav; - - constructor(page: Page) { - this.page = page; - this.sidenav = new OmnichannelSidenav(page); - } - - private get inputSearch() { - return this.page.getByRole('main').getByRole('textbox', { name: 'Search' }); - } - - async search(text: string) { - await this.inputSearch.fill(text); - await this.page.waitForTimeout(500); - } - - async clearSearch() { - await this.inputSearch.fill(''); - await this.page.waitForTimeout(500); - } - - get inputUsername(): Locator { - return this.page.getByRole('main').getByLabel('Username'); - } - - async selectUsername(username: string) { - await this.inputUsername.fill(username); - await this.page.locator(`role=option[name="${username}"]`).click(); - } - - get btnAdd(): Locator { - return this.page.locator('button.rcx-button--primary.rcx-button >> text="Add manager"'); - } - - findRowByName(name: string) { - return this.page.locator('role=table[name="Managers"] >> role=row', { has: this.page.locator(`role=cell[name="${name}"]`) }); - } - - btnDeleteSelectedAgent(text: string) { - return this.page.locator('tr', { has: this.page.locator(`td >> text="${text}"`) }).locator('button[title="Remove"]'); - } - - get btnModalRemove(): Locator { - return this.page.locator('#modal-root dialog .rcx-modal__inner .rcx-modal__footer .rcx-button--danger'); - } -} diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-monitors.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-monitors.ts deleted file mode 100644 index e31878842ddb4..0000000000000 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-monitors.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { Locator } from '@playwright/test'; - -import { OmnichannelAdministration } from './omnichannel-administration'; - -export class OmnichannelMonitors extends OmnichannelAdministration { - get modalConfirmRemove(): Locator { - return this.page.locator('[data-qa-id="manage-monitors-confirm-remove"]'); - } - - get btnConfirmRemove(): Locator { - return this.modalConfirmRemove.locator('role=button[name="Delete"]'); - } - - get btnAddMonitor(): Locator { - return this.page.locator('role=button[name="Add monitor"]'); - } - - get inputMonitor(): Locator { - return this.page.locator('input[name="monitor"]'); - } - - get inputSearch(): Locator { - return this.page.getByRole('main').getByRole('textbox', { name: 'Search' }); - } - - findRowByName(name: string): Locator { - return this.page.locator(`tr[data-qa-id="${name}"]`); - } - - btnRemoveByName(name: string): Locator { - return this.findRowByName(name).locator('role=button[name="Remove"]'); - } - - async selectMonitor(name: string) { - await this.inputMonitor.fill(name); - await this.page.locator(`li[role="option"]`, { has: this.page.locator(`[data-username='${name}']`) }).click(); - } -} diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-priorities.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-priorities.ts deleted file mode 100644 index ec75c336adff7..0000000000000 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-priorities.ts +++ /dev/null @@ -1,57 +0,0 @@ -import type { Locator, Page } from '@playwright/test'; - -import { OmnichannelSidenav, ToastMessages } from './fragments'; - -class OmnichannelManagePriority { - private readonly page: Page; - - constructor(page: Page) { - this.page = page; - } - - get inputName(): Locator { - return this.page.locator('[name="name"]'); - } - - get btnSave() { - return this.page.locator('button.rcx-button >> text="Save"'); - } - - get btnReset() { - return this.page.locator('.rcx-vertical-bar').locator('role=button[name="Reset"]'); - } - - errorMessage(message: string): Locator { - return this.page.locator(`.rcx-field__error >> text="${message}"`); - } -} - -export class OmnichannelPriorities { - private readonly page: Page; - - readonly managePriority: OmnichannelManagePriority; - - readonly sidenav: OmnichannelSidenav; - - // TODO: This will be inherited from a BasePage Object - readonly toastMessage: ToastMessages; - - constructor(page: Page) { - this.page = page; - this.managePriority = new OmnichannelManagePriority(page); - this.sidenav = new OmnichannelSidenav(page); - this.toastMessage = new ToastMessages(page); - } - - get btnReset() { - return this.page.locator('role=button[name="Reset"]'); - } - - get btnResetConfirm() { - return this.page.locator('.rcx-modal').locator('role=button[name="Reset"]'); - } - - findPriority(name: string) { - return this.page.locator('tr', { has: this.page.locator(`td >> text="${name}"`) }); - } -} diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-room-info.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-room-info.ts deleted file mode 100644 index e7f6430fc38e0..0000000000000 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-room-info.ts +++ /dev/null @@ -1,75 +0,0 @@ -import type { Locator, Page } from '@playwright/test'; - -import { RoomSidebar } from './fragments'; - -export class OmnichannelRoomInfo { - private readonly page: Page; - - private readonly sidebar: RoomSidebar; - - constructor(page: Page) { - this.page = page; - this.sidebar = new RoomSidebar(page); - } - - get dialogRoomInfo(): Locator { - return this.page.getByRole('dialog', { name: 'Room Information' }); - } - - get btnEditRoomInfo(): Locator { - return this.dialogRoomInfo.getByRole('button', { name: 'Edit' }); - } - - get dialogEditRoom(): Locator { - return this.page.getByRole('dialog', { name: 'Edit Room' }); - } - - get inputTopic(): Locator { - return this.dialogEditRoom.getByRole('textbox', { name: 'Topic' }); - } - - get btnSaveEditRoom(): Locator { - return this.dialogEditRoom.getByRole('button', { name: 'Save' }); - } - - getInfo(value: string): Locator { - return this.page.locator(`span >> text="${value}"`); - } - - getLabel(label: string): Locator { - return this.page.locator(`div >> text="${label}"`); - } - - getInfoByLabel(label: string): Locator { - return this.dialogRoomInfo.getByLabel(label); - } - - get inputSLAPolicy(): Locator { - return this.dialogEditRoom.getByRole('button', { name: 'SLA Policy' }); - } - - async selectSLA(name: string): Promise { - await this.inputSLAPolicy.click(); - return this.page.getByRole('option', { name, exact: true }).click(); - } - - get inputTags(): Locator { - return this.page.getByRole('textbox', { name: 'Select an option' }); - } - - optionTags(name: string): Locator { - return this.page.getByRole('option', { name, exact: true }); - } - - async selectTag(name: string): Promise { - await this.optionTags(name).click(); - } - - getTagInfoByLabel(label: string): Locator { - return this.dialogRoomInfo.getByRole('list', { name: 'Tags' }).getByText(label, { exact: true }); - } - - getBadgeIndicator(name: string, title: string): Locator { - return this.sidebar.getSidebarItemByName(name).getByTitle(title); - } -} diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-section.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-section.ts deleted file mode 100644 index 10887fef5672b..0000000000000 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-section.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { Locator, Page } from '@playwright/test'; - -export class OmnichannelSection { - private readonly page: Page; - - constructor(page: Page) { - this.page = page; - } - - get btnContactCenter(): Locator { - return this.page.locator('role=button[name="Contact Center"]'); - } - - get tabContacts(): Locator { - return this.page.locator('role=tab[name="Contacts"]'); - } -} diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-sla-policies.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-sla-policies.ts deleted file mode 100644 index f884a876fb841..0000000000000 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-sla-policies.ts +++ /dev/null @@ -1,73 +0,0 @@ -import type { Locator, Page } from '@playwright/test'; - -import { OmnichannelSidenav } from './fragments'; - -class OmnichannelManageSlaPolicy { - private readonly page: Page; - - constructor(page: Page) { - this.page = page; - } - - get inputName(): Locator { - return this.page.locator('[name="name"]'); - } - - get inputDescription(): Locator { - return this.page.locator('[name="description"]'); - } - - get inputEstimatedWaitTime(): Locator { - return this.page.locator('[name="dueTimeInMinutes"]'); - } - - get btnSave() { - return this.page.locator('button.rcx-button >> text="Save"'); - } - - errorMessage(message: string): Locator { - return this.page.locator(`.rcx-field__error >> text="${message}"`); - } -} - -export class OmnichannelSlaPolicies { - private readonly page: Page; - - readonly manageSlaPolicy: OmnichannelManageSlaPolicy; - - readonly sidenav: OmnichannelSidenav; - - constructor(page: Page) { - this.page = page; - this.manageSlaPolicy = new OmnichannelManageSlaPolicy(page); - this.sidenav = new OmnichannelSidenav(page); - } - - findRowByName(name: string) { - return this.page.locator('tr', { has: this.page.locator(`td >> text="${name}"`) }); - } - - btnRemove(name: string) { - return this.findRowByName(name).locator('button[title="Remove"]'); - } - - get inputSearch() { - return this.page.getByRole('main').getByRole('textbox', { name: 'Search' }); - } - - headingButtonNew(name: string) { - return this.page.locator(`role=main >> role=button[name="${name}"]`).first(); - } - - get btnDelete() { - return this.page.locator('button.rcx-button >> text="Delete"'); - } - - get txtDeleteModalTitle() { - return this.page.locator('role=dialog >> text="Are you sure?"'); - } - - get txtEmptyState() { - return this.page.locator('div >> text="No results found"'); - } -} diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-tags.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-tags.ts deleted file mode 100644 index 015db21fdd4fc..0000000000000 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-tags.ts +++ /dev/null @@ -1,71 +0,0 @@ -import type { Locator } from '@playwright/test'; - -import { OmnichannelAdministration } from './omnichannel-administration'; - -export class OmnichannelTags extends OmnichannelAdministration { - get btnCreateTag(): Locator { - return this.page.locator('header').locator('role=button[name="Create tag"]'); - } - - get contextualBar(): Locator { - return this.page.locator('div[role="dialog"].rcx-vertical-bar'); - } - - get btnSave(): Locator { - return this.contextualBar.locator('role=button[name="Save"]'); - } - - get btnCancel(): Locator { - return this.contextualBar.locator('role=button[name="Cancel"]'); - } - - get inputName(): Locator { - return this.page.locator('[name="name"]'); - } - - get inputSearch(): Locator { - return this.page.getByRole('main').getByRole('textbox', { name: 'Search' }); - } - - get confirmDeleteModal(): Locator { - return this.page.locator('dialog:has(h2:has-text("Are you sure?"))'); - } - - get btnCancelDeleteModal(): Locator { - return this.confirmDeleteModal.locator('role=button[name="Cancel"]'); - } - - get btnConfirmDeleteModal(): Locator { - return this.confirmDeleteModal.locator('role=button[name="Delete"]'); - } - - get btnContextualbarClose(): Locator { - return this.contextualBar.locator('button[aria-label="Close"]'); - } - - btnDeleteByName(name: string): Locator { - return this.page.getByRole('link', { name }).getByRole('button'); - } - - findRowByName(name: string): Locator { - return this.page.getByRole('link', { name }); - } - - get inputDepartments(): Locator { - return this.page.locator('input[placeholder="Select an option"]'); - } - - private selectOption(name: string): Locator { - return this.page.locator('#position-container').getByRole('option', { name }); - } - - async search(text: string) { - await this.inputSearch.fill(text); - } - - async selectDepartment(name: string) { - await this.inputDepartments.click(); - await this.inputDepartments.fill(name); - await this.selectOption(name).click(); - } -} diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-triggers.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-triggers.ts deleted file mode 100644 index 273343f731d3b..0000000000000 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-triggers.ts +++ /dev/null @@ -1,138 +0,0 @@ -import type { Locator, Page } from '@playwright/test'; - -import { OmnichannelSidenav, ToastMessages } from './fragments'; - -export class OmnichannelTriggers { - private readonly page: Page; - - readonly sidenav: OmnichannelSidenav; - - // TODO: This will be inherited from a BasePage Object - readonly toastMessage: ToastMessages; - - constructor(page: Page) { - this.page = page; - this.sidenav = new OmnichannelSidenav(page); - this.toastMessage = new ToastMessages(page); - } - - headingButtonNew(name: string) { - return this.page.locator(`role=main >> role=button[name="${name}"]`).first(); - } - - get inputName(): Locator { - return this.page.locator('input[name="name"]'); - } - - get inputDescription(): Locator { - return this.page.locator('input[name="description"]'); - } - - get btnSave(): Locator { - return this.page.locator('button >> text="Save"'); - } - - firstRowInTriggerTable(triggersName1: string) { - return this.page.locator(`text="${triggersName1}"`); - } - - get btnDeletefirstRowInTable() { - return this.page.locator('table tr:first-child td:last-child button'); - } - - get btnModalRemove(): Locator { - return this.page.locator('#modal-root dialog .rcx-modal__inner .rcx-modal__footer .rcx-button--danger'); - } - - get removeToastMessage(): Locator { - return this.page.locator('text=Trigger removed'); - } - - get conditionLabel(): Locator { - return this.page.locator('label >> text="Condition"'); - } - - get inputConditionValue(): Locator { - return this.page.locator('input[name="conditions.0.value"]'); - } - - get senderLabel(): Locator { - return this.page.locator('label >> text="Sender"'); - } - - get inputAgentName(): Locator { - return this.page.locator('input[name="actions.0.params.name"]'); - } - - get inputTriggerMessage(): Locator { - return this.page.locator('textarea[name="actions.0.params.msg"]'); - } - - async selectCondition(condition: string) { - await this.conditionLabel.click(); - await this.page.locator(`li.rcx-option[data-key="${condition}"]`).click(); - } - - async selectSender(sender: 'queue' | 'custom') { - await this.senderLabel.click(); - await this.page.locator(`li.rcx-option[data-key="${sender}"]`).click(); - } - - public async createTrigger( - triggersName: string, - triggerMessage: string, - condition: 'time-on-site' | 'chat-opened-by-visitor' | 'after-guest-registration', - conditionValue?: number | string, - ) { - await this.headingButtonNew('Create trigger').click(); - await this.fillTriggerForm({ - name: triggersName, - description: 'Creating a fresh trigger', - condition, - conditionValue, - triggerMessage, - }); - await this.btnSave.click(); - } - - public async updateTrigger(newName: string, triggerMessage: string) { - await this.fillTriggerForm({ - name: `edited-${newName}`, - description: 'Updating the existing trigger', - condition: 'chat-opened-by-visitor', - sender: 'custom', - agentName: 'Rocket.cat', - triggerMessage, - }); - await this.btnSave.click(); - } - - public async fillTriggerForm( - data: Partial<{ - name: string; - description: string; - condition: 'time-on-site' | 'chat-opened-by-visitor' | 'after-guest-registration'; - conditionValue?: string | number; - sender: 'queue' | 'custom'; - agentName?: string; - triggerMessage: string; - }>, - ) { - data.name && (await this.inputName.fill(data.name)); - data.description && (await this.inputDescription.fill(data.description)); - data.condition && (await this.selectCondition(data.condition)); - - if (data.conditionValue) { - await this.inputConditionValue.fill(data.conditionValue.toString()); - } - - data.sender && (await this.selectSender(data.sender)); - if (data.sender === 'custom' && !data.agentName) { - throw new Error('A custom agent is required for this action'); - } else { - data.agentName && (await this.inputAgentName.fill(data.agentName)); - } - - data.triggerMessage && (await this.inputTriggerMessage.fill(data.triggerMessage)); - } -} diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-units.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-units.ts deleted file mode 100644 index ba0a2b863d61d..0000000000000 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-units.ts +++ /dev/null @@ -1,99 +0,0 @@ -import type { Locator } from '@playwright/test'; - -import { OmnichannelAdministration } from './omnichannel-administration'; - -export class OmnichannelUnits extends OmnichannelAdministration { - get inputSearch() { - return this.page.getByRole('main').getByRole('textbox', { name: 'Search' }); - } - - async search(text: string) { - await this.inputSearch.fill(text); - } - - findRowByName(name: string) { - return this.page.locator(`tr[data-qa-id="${name}"]`); - } - - get inputName() { - return this.page.locator('[name="name"]'); - } - - get fieldDepartments() { - return this.page.getByLabel('Departments'); - } - - get inputDepartments() { - return this.fieldDepartments.getByRole('textbox'); - } - - get inputMonitors() { - return this.page.locator('[name="monitors"]'); - } - - get inputVisibility(): Locator { - return this.page.locator('button', { has: this.page.locator('select[name="visibility"]') }); - } - - get btnContextualbarClose(): Locator { - return this.page.locator('[data-qa="ContextualbarActionClose"]'); - } - - private findOption(name: string) { - return this.page.locator('#position-container').getByRole('option', { name, exact: true }); - } - - public findDepartmentsChipOption(name: string) { - return this.fieldDepartments.getByRole('option', { name, exact: true }); - } - - async selectDepartment(name: string) { - await this.inputDepartments.click(); - await this.inputDepartments.fill(name); - await this.findOption(name).click(); - await this.inputDepartments.click(); - } - - async selectMonitor(option: string) { - await this.inputMonitors.click(); - await this.findOption(option).click(); - await this.inputMonitors.click(); - } - - async selectVisibility(option: string) { - await this.inputVisibility.click(); - await this.page.locator(`li.rcx-option[data-key="${option}"]`).click(); - } - - get btnCreateUnit() { - return this.page.locator('header').locator('role=button[name="Create unit"]'); - } - - get contextualBar() { - return this.page.locator('div[role="dialog"][aria-labelledby="contextualbarTitle"]'); - } - - get btnSave() { - return this.contextualBar.locator('role=button[name="Save"]'); - } - - get btnCancel() { - return this.contextualBar.locator('role=button[name="Cancel"]'); - } - - get btnDelete() { - return this.contextualBar.locator('role=button[name="Delete"]'); - } - - btnDeleteByName(name: string) { - return this.page.locator(`button[data-qa-id="remove-unit-${name}"]`); - } - - get confirmDeleteModal() { - return this.page.locator('dialog[data-qa-id="units-confirm-delete-modal"]'); - } - - get btnConfirmDeleteModal() { - return this.confirmDeleteModal.locator('role=button[name="Delete"]'); - } -} diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel/index.ts b/apps/meteor/tests/e2e/page-objects/omnichannel/index.ts new file mode 100644 index 0000000000000..3816f1d1dec34 --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/omnichannel/index.ts @@ -0,0 +1,19 @@ +export * from './omnichannel-agents'; +export * from './omnichannel-departments'; +export * from './omnichannel-canned-responses'; +export * from './omnichannel-contact-center'; +export * from './omnichannel-livechat'; +export * from './omnichannel-livechat-appearance'; +export * from './omnichannel-livechat-embedded'; +export * from './omnichannel-manager'; +export * from './omnichannel-custom-fields'; +export * from './omnichannel-units'; +export * from './omnichannel-monitors'; +export * from './omnichannel-settings'; +export * from './omnichannel-business-hours'; +export * from './omnichannel-tags'; +export * from './omnichannel-transcript'; +export * from './omnichannel-triggers'; +export * from './omnichannel-priorities'; +export * from './omnichannel-reports'; +export * from './omnichannel-sla-policies'; diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-admin.ts b/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-admin.ts new file mode 100644 index 0000000000000..125b71d744736 --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-admin.ts @@ -0,0 +1,46 @@ +import type { Locator, Page } from '@playwright/test'; + +import { expect } from '../../utils/test'; +import { OmnichannelSidebar, ToastMessages } from '../fragments'; +import { ConfirmDeleteModal } from '../fragments/modals'; + +export abstract class OmnichannelAdmin { + protected readonly page: Page; + + protected readonly toastMessage: ToastMessages; + + readonly sidebar: OmnichannelSidebar; + + readonly deleteModal: ConfirmDeleteModal; + + constructor(page: Page) { + this.page = page; + this.sidebar = new OmnichannelSidebar(page); + this.toastMessage = new ToastMessages(page); + this.deleteModal = new ConfirmDeleteModal(page.getByRole('dialog', { name: 'Are you sure?' })); + } + + get inputSearch() { + return this.page.getByRole('main').getByRole('textbox', { name: 'Search' }); + } + + get btnSaveChanges(): Locator { + return this.page.getByRole('button', { name: 'Save changes' }); + } + + getButtonByType(type: 'unit' | 'SLA policy' | 'tag' | 'trigger' | 'department' | 'custom field'): Locator { + return this.page.locator('header').getByRole('button', { name: `Create ${type}` }); + } + + async search(text: string) { + await this.inputSearch.fill(text); + } + + async clearSearch() { + await this.inputSearch.fill(''); + } + + waitForEmptyState() { + return expect(this.page.locator('h3 >> text="No results found"')).toBeVisible(); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-agents.ts b/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-agents.ts new file mode 100644 index 0000000000000..b40141802759c --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-agents.ts @@ -0,0 +1,117 @@ +import type { Locator, Page } from '@playwright/test'; + +import { OmnichannelAdmin } from './omnichannel-admin'; +import { FlexTab } from '../fragments/flextab'; +import { Listbox } from '../fragments/listbox'; +import { Table } from '../fragments/table'; + +class OmnichannelEditAgentFlexTab extends FlexTab { + readonly listbox: Listbox; + + constructor(page: Page) { + super(page.getByRole('dialog', { name: 'Edit User' })); + this.listbox = new Listbox(page); + } + + get inputMaxChats(): Locator { + return this.root.locator('input[name="maxNumberSimultaneousChat"]'); + } + + get inputDepartment(): Locator { + return this.root.getByLabel('Departments').getByRole('textbox'); + } + + getDepartmentOption(name: string) { + return this.listbox.getOption(name); + } + + async selectDepartment(name: string) { + await this.inputDepartment.click(); + await this.inputDepartment.fill(name); + await this.listbox.selectOption(name); + // TODO: This is necessary due to the PaginatedMultiSelectFiltered not closing the list when an option is selected nor when close is clicked. + // The line below can be removed once the component is adjusted in Fuselage + await this.root.click(); + } + + findSelectedDepartment(name: string) { + return this.root.getByLabel('Departments', { exact: true }).getByRole('option', { name }); + } + + private get inputStatus(): Locator { + return this.root.getByText('Status', { exact: true }); + } + + async selectStatus(status: string) { + await this.inputStatus.click(); + await this.listbox.selectOption(status); + } +} + +class OmnichannelAgentInfoFlexTab extends FlexTab { + constructor(page: Page) { + super(page.getByRole('dialog', { name: 'User Info' })); + } + + get btnEdit(): Locator { + return this.root.getByRole('button', { name: 'Edit', exact: true }); + } + + get btnRemove(): Locator { + return this.root.getByRole('button', { name: 'Remove', exact: true }); + } +} + +class OmnichannelAgentsTable extends Table { + constructor(page: Page) { + super(page.getByRole('table', { name: 'Agents' })); + } +} + +export class OmnichannelAgents extends OmnichannelAdmin { + readonly editAgent: OmnichannelEditAgentFlexTab; + + readonly agentInfo: OmnichannelAgentInfoFlexTab; + + readonly table: OmnichannelAgentsTable; + + readonly listbox: Listbox; + + constructor(page: Page) { + super(page); + this.editAgent = new OmnichannelEditAgentFlexTab(page); + this.agentInfo = new OmnichannelAgentInfoFlexTab(page); + this.table = new OmnichannelAgentsTable(page); + this.listbox = new Listbox(page); + } + + get inputUsername(): Locator { + return this.page.getByRole('textbox', { name: 'Username' }); + } + + get btnAddAgent(): Locator { + return this.page.getByRole('button', { name: 'Add agent', exact: true }); + } + + async deleteAgent(name: string) { + await this.search(name); + await this.table.findRowByName(name).getByRole('button', { name: 'Remove' }).click(); + await this.deleteModal.confirmDelete(); + } + + get scrollContainer(): Locator { + return this.page.locator('#position-container').getByTestId('virtuoso-scroller'); + } + + scrollToListBottom() { + return this.scrollContainer.evaluate((el) => { + el.scrollTop = el.scrollHeight; + el.dispatchEvent(new Event('scroll')); + }); + } + + async selectUsername(username: string) { + await this.inputUsername.fill(username); + await this.listbox.selectOption(username); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-business-hours.ts b/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-business-hours.ts new file mode 100644 index 0000000000000..4cb327e3f661f --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-business-hours.ts @@ -0,0 +1,66 @@ +import type { Locator, Page } from '@playwright/test'; + +import { OmnichannelAdmin } from './omnichannel-admin'; +import { Listbox } from '../fragments/listbox'; +import { Table } from '../fragments/table'; + +class OmnichannelBusinessHoursTable extends Table { + constructor(page: Page) { + super(page.getByRole('table', { name: 'Business Hours' })); + } +} + +export class OmnichannelBusinessHours extends OmnichannelAdmin { + readonly table: OmnichannelBusinessHoursTable; + + readonly listbox: Listbox; + + constructor(page: Page) { + super(page); + this.table = new OmnichannelBusinessHoursTable(page); + this.listbox = new Listbox(page); + } + + get btnCreateBusinessHour(): Locator { + return this.page.locator('header').getByRole('button', { name: 'New', exact: true }); + } + + get btnSave(): Locator { + return this.page.getByRole('button', { name: 'Save', exact: true }); + } + + get btnBack(): Locator { + return this.page.locator('header').getByRole('button', { name: 'Back', exact: true }); + } + + get inputName(): Locator { + return this.page.getByRole('textbox', { name: 'Name', exact: true }); + } + + get fieldDepartment(): Locator { + return this.page.getByLabel('Departments', { exact: true }); + } + + get inputDepartments(): Locator { + return this.fieldDepartment.getByRole('textbox'); + } + + async deleteBusinessHour(name: string) { + await this.table.findRowByName(name).getByRole('button', { name: 'Remove' }).click(); + await this.deleteModal.confirmDelete(); + } + + getCheckboxByLabel(name: string): Locator { + return this.page.locator('label', { has: this.page.getByRole('checkbox', { name }) }); + } + + findDepartmentsChipOption(name: string) { + return this.fieldDepartment.getByRole('option', { name, exact: true }); + } + + async selectDepartment(name: string) { + await this.inputDepartments.click(); + await this.inputDepartments.fill(name); + await this.listbox.selectOption(name); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-canned-responses.ts b/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-canned-responses.ts similarity index 93% rename from apps/meteor/tests/e2e/page-objects/omnichannel-canned-responses.ts rename to apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-canned-responses.ts index 4de383668ea7a..84e73566e2ef8 100644 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-canned-responses.ts +++ b/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-canned-responses.ts @@ -1,8 +1,8 @@ import type { Locator } from '@playwright/test'; -import { OmnichannelAdministration } from './omnichannel-administration'; +import { OmnichannelAdmin } from './omnichannel-admin'; -export class OmnichannelCannedResponses extends OmnichannelAdministration { +export class OmnichannelCannedResponses extends OmnichannelAdmin { get inputShortcut() { return this.page.getByRole('textbox', { name: 'Shortcut', exact: true }); } diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-contact-center/index.ts b/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-contact-center/index.ts new file mode 100644 index 0000000000000..03b370997f6aa --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-contact-center/index.ts @@ -0,0 +1,2 @@ +export * from './omnichannel-contact-center-chats'; +export * from './omnichannel-contact-center-contacts'; diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-contact-center/omnichannel-contact-center-chats.ts b/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-contact-center/omnichannel-contact-center-chats.ts new file mode 100644 index 0000000000000..6c2a7ca0793eb --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-contact-center/omnichannel-contact-center-chats.ts @@ -0,0 +1,179 @@ +import type { Locator, Page } from '@playwright/test'; + +import { OmnichannelContactCenter } from './omnichannel-contact-center'; +import { FlexTab } from '../../fragments/flextab'; +import { Listbox } from '../../fragments/listbox'; +import { OmnichannelConfirmRemoveChat } from '../../fragments/modals'; +import { Table } from '../../fragments/table'; + +class OmnichannelConversationFlexTab extends FlexTab { + constructor(page: Page) { + super(page.getByRole('dialog', { name: 'Conversation' })); + } + + private get btnOpenChat(): Locator { + return this.root.getByRole('button', { name: 'Open chat' }); + } + + async openChat() { + await this.btnOpenChat.click(); + await this.waitForDismissal(); + } +} + +export class OmnichannelChatsFilters extends FlexTab { + readonly listbox: Listbox; + + constructor(page: Page) { + super(page.getByRole('dialog', { name: 'Filters' })); + this.listbox = new Listbox(page); + } + + get inputFrom(): Locator { + return this.root.locator('input[name="from"]'); + } + + get inputTo(): Locator { + return this.root.locator('input[name="to"]'); + } + + get btnApply(): Locator { + return this.root.getByRole('button', { name: 'Apply' }); + } + + get inputServedBy(): Locator { + return this.root.getByLabel('Served By').locator('input'); + } + + get inputDepartment(): Locator { + return this.root.getByLabel('Department').locator('input'); + } + + get selectStatusContainer(): Locator { + return this.root.getByRole('button', { name: 'Status' }); + } + + get inputTags(): Locator { + return this.root.getByLabel('Tags').locator('input'); + } + + get inputUnits(): Locator { + return this.root.getByLabel('Units').locator('input'); + } + + get btnClearFilters(): Locator { + return this.root.getByRole('button', { name: 'Clear filters' }); + } + + async selectServedBy(option: string) { + await this.inputServedBy.click(); + await this.inputServedBy.fill(option); + await this.listbox.selectOption(option); + await this.btnApply.click(); + } + + async selectStatus(option: string) { + await this.selectStatusContainer.click(); + await this.listbox.selectOption(option); + await this.btnApply.click(); + } + + async selectDepartment(option: string) { + await this.inputDepartment.click(); + await this.inputDepartment.fill(option); + await this.listbox.selectOption(option); + await this.inputDepartment.click(); + await this.btnApply.click(); + } + + async selectTag(option: string) { + await this.inputTags.click(); + await this.inputTags.fill(option); + await this.listbox.selectOption(option); + await this.inputTags.click(); + await this.btnApply.click(); + } + + async removeTag(option: string) { + await this.inputTags.click(); + await this.listbox.selectOption(option); + await this.inputTags.click(); + await this.btnApply.click(); + } + + async selectUnit(unitName: string) { + await this.inputUnits.click(); + await this.inputUnits.fill(unitName); + await this.listbox.selectOption(unitName); + await this.btnApply.click(); + } + + async addTag(option: string) { + await this.inputTags.click(); + await this.listbox.selectOption(option); + await this.inputTags.click(); + } +} + +class OmnichannelContactCenterChatsTable extends Table { + constructor(page: Page) { + super(page.getByRole('table', { name: 'Omnichannel Contact Center Chats' })); + } + + btnRemoveByName(name: string): Locator { + return this.findRowByName(name).getByRole('button', { name: 'Remove' }); + } +} + +export class OmnichannelContactCenterChats extends OmnichannelContactCenter { + readonly filters: OmnichannelChatsFilters; + + readonly confirmRemoveChatModal: OmnichannelConfirmRemoveChat; + + readonly conversation: OmnichannelConversationFlexTab; + + readonly table: OmnichannelContactCenterChatsTable; + + constructor(page: Page) { + super(page); + this.filters = new OmnichannelChatsFilters(page); + this.confirmRemoveChatModal = new OmnichannelConfirmRemoveChat(page); + this.conversation = new OmnichannelConversationFlexTab(page); + this.table = new OmnichannelContactCenterChatsTable(page); + } + + async removeChatByName(name: string) { + await this.table.btnRemoveByName(name).click(); + await this.confirmRemoveChatModal.confirm(); + } + + async openChat(name: string) { + await this.table.findRowByName(name).click(); + await this.conversation.openChat(); + await this.page.locator('#main-content').waitFor(); + } + + get btnFilters(): Locator { + return this.page.getByRole('button', { name: 'Filters' }); + } + + btnStatusChip(name: string): Locator { + return this.page.getByRole('button', { name: `Status: ${name}` }); + } + + btnServedByChip(name: string): Locator { + return this.page.getByRole('button', { name: `Served by: ${name}` }); + } + + btnDepartmentChip(name: string): Locator { + return this.page.getByRole('button', { name: `Department: ${name}` }); + } + + btnSearchChip(name: string): Locator { + return this.page.getByRole('button', { name: `Text: ${name}` }); + } + + btnUnitsChip(name: string): Locator { + return this.page.getByRole('button', { name: `Units: ${name}` }); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-contact-center/omnichannel-contact-center-contacts.ts b/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-contact-center/omnichannel-contact-center-contacts.ts new file mode 100644 index 0000000000000..00989c8510325 --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-contact-center/omnichannel-contact-center-contacts.ts @@ -0,0 +1,45 @@ +import type { Locator, Page } from '@playwright/test'; + +import { OmnichannelContactInfo } from '../omnichannel-info'; +import { OmnichannelContactCenter } from './omnichannel-contact-center'; +import { MenuMoreActions } from '../../fragments'; +import { OmnichannelEditContactFlexTab } from '../../fragments/edit-contact-flaxtab'; +import { OmnichannelDeleteContactModal } from '../../fragments/modals'; +import { Table } from '../../fragments/table'; + +class OmnichannelContactCenterContactsTable extends Table { + constructor(page: Page) { + super(page.getByRole('table', { name: 'Omnichannel Contact Center Contacts' })); + } +} + +export class OmnichannelContactCenterContacts extends OmnichannelContactCenter { + readonly contactInfo: OmnichannelContactInfo; + + readonly editContact: OmnichannelEditContactFlexTab; + + readonly table: OmnichannelContactCenterContactsTable; + + readonly deleteContactModal: OmnichannelDeleteContactModal; + + readonly menu: MenuMoreActions; + + constructor(page: Page) { + super(page); + this.contactInfo = new OmnichannelContactInfo(page); + this.editContact = new OmnichannelEditContactFlexTab(page); + this.table = new OmnichannelContactCenterContactsTable(page); + this.deleteContactModal = new OmnichannelDeleteContactModal(page); + this.menu = new MenuMoreActions(page); + } + + get btnNewContact(): Locator { + return this.page.getByRole('button', { name: 'New contact' }); + } + + async deleteContact(contactName: string) { + await this.table.findRowByName(contactName).getByRole('button', { name: 'More Actions' }).click(); + await this.menu.selectMenuItem('Delete'); + await this.deleteContactModal.waitForDisplay(); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-contact-center/omnichannel-contact-center.ts b/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-contact-center/omnichannel-contact-center.ts new file mode 100644 index 0000000000000..22eec38981e82 --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-contact-center/omnichannel-contact-center.ts @@ -0,0 +1,13 @@ +import type { Locator } from '@playwright/test'; + +import { OmnichannelAdmin } from '../omnichannel-admin'; + +export abstract class OmnichannelContactCenter extends OmnichannelAdmin { + get tabContacts(): Locator { + return this.page.getByRole('tab', { name: 'Contacts' }); + } + + get tabChats(): Locator { + return this.page.getByRole('tab', { name: 'Chats' }); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-custom-fields.ts b/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-custom-fields.ts new file mode 100644 index 0000000000000..ca9fcf7ad26d0 --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-custom-fields.ts @@ -0,0 +1,50 @@ +import type { Locator, Page } from '@playwright/test'; + +import { OmnichannelAdmin } from './omnichannel-admin'; +import { FlexTab } from '../fragments/flextab'; +import { Table } from '../fragments/table'; + +class OmnichannelManageCustomFieldsFlexTab extends FlexTab { + constructor(page: Page) { + super(page.getByRole('dialog', { name: 'Custom Field' })); + } + + get inputField(): Locator { + return this.root.getByRole('textbox', { name: 'Field', exact: true }); + } + + get inputLabel(): Locator { + return this.root.getByRole('textbox', { name: 'Label', exact: true }); + } + + get labelVisible(): Locator { + return this.root.getByText('Visible'); + } +} + +class OmnichannelCustomFieldsTable extends Table { + constructor(page: Page) { + super(page.getByRole('table', { name: 'Custom Fields' })); + } +} + +export class OmnichannelCustomFields extends OmnichannelAdmin { + readonly manageCustomFields: OmnichannelManageCustomFieldsFlexTab; + + readonly table: OmnichannelCustomFieldsTable; + + constructor(page: Page) { + super(page); + this.manageCustomFields = new OmnichannelManageCustomFieldsFlexTab(page); + this.table = new OmnichannelCustomFieldsTable(page); + } + + async createNew() { + await this.getButtonByType('custom field').click(); + } + + async deleteCustomField(fieldName: string) { + await this.table.findRowByName(fieldName).getByRole('button', { name: 'Remove' }).click(); + await this.deleteModal.confirmDelete(); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-departments.ts b/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-departments.ts new file mode 100644 index 0000000000000..fea573cd2cc6d --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-departments.ts @@ -0,0 +1,134 @@ +import type { Locator, Page } from '@playwright/test'; + +import { OmnichannelAdmin } from './omnichannel-admin'; +import { Listbox } from '../fragments/listbox'; +import { OmnichannelUpsellDepartmentsModal, ConfirmDeleteDepartmentModal } from '../fragments/modals'; +import { Table } from '../fragments/table'; + +class OmnichannelDepartmentsTable extends Table { + constructor(page: Page) { + super(page.getByRole('table', { name: 'Departments' })); + } +} + +class OmnichannelDepartmentAgentsTable extends Table { + constructor(page: Page) { + super(page.getByRole('table', { name: 'Agents' })); + } +} + +export class OmnichannelDepartments extends OmnichannelAdmin { + readonly departmentsTable: OmnichannelDepartmentsTable; + + readonly agentsTable: OmnichannelDepartmentAgentsTable; + + readonly upsellDepartmentsModal: OmnichannelUpsellDepartmentsModal; + + readonly listbox: Listbox; + + override readonly deleteModal: ConfirmDeleteDepartmentModal; + + constructor(page: Page) { + super(page); + this.departmentsTable = new OmnichannelDepartmentsTable(page); + this.agentsTable = new OmnichannelDepartmentAgentsTable(page); + this.upsellDepartmentsModal = new OmnichannelUpsellDepartmentsModal(page); + this.listbox = new Listbox(page); + this.deleteModal = new ConfirmDeleteDepartmentModal(page); + } + + async createNew() { + await this.getButtonByType('department').click(); + } + + get labelEnabled() { + return this.page.locator('label', { hasText: 'Enabled' }); + } + + get inputName() { + return this.page.getByRole('textbox', { name: 'Name', exact: true }); + } + + get inputEmail() { + return this.page.getByRole('textbox', { name: 'Email', exact: true }); + } + + get inputConversationClosingTags() { + return this.page.getByRole('textbox', { name: 'Conversation closing tags', exact: true }); + } + + get btnAddTags() { + return this.page.getByText('Conversation closing tags', { exact: true }).locator('..').getByRole('button', { name: 'Add' }); + } + + get btnSave() { + return this.page.getByRole('button', { name: 'Save' }); + } + + get tabArchivedDepartments() { + return this.page.getByRole('tab', { name: 'Archived' }); + } + + getDepartmentMenuByName(name: string) { + return this.departmentsTable.findRowByName(name).getByRole('button', { name: 'Options' }); + } + + get menuEditOption() { + return this.listbox.getOption('Edit'); + } + + get menuDeleteOption() { + return this.listbox.getOption('Delete'); + } + + get menuArchiveOption() { + return this.listbox.getOption('Archive'); + } + + get menuUnarchiveOption() { + return this.listbox.getOption('Unarchive'); + } + + async archiveDepartmentByName(name: string) { + await this.getDepartmentMenuByName(name).click(); + await this.menuArchiveOption.click(); + await this.toastMessage.waitForDisplay(); + } + + get inputUnit(): Locator { + return this.page.getByLabel('Unit').getByRole('textbox', { name: 'Select an option' }); + } + + btnTag(tagName: string) { + return this.page.locator('button', { hasText: tagName }); + } + + errorMessage(message: string): Locator { + return this.page.locator(`[role="alert"] >> text="${message}"`); + } + + findOption(optionText: string) { + return this.listbox.getOption(optionText); + } + + async selectUnit(unitName: string) { + await this.inputUnit.click(); + await this.listbox.selectOption(unitName); + } + + get inputAgents() { + return this.page.getByRole('group', { name: 'Agents' }).getByRole('textbox'); + } + + get btnAddAgent() { + return this.page.getByRole('group', { name: 'Agents' }).getByRole('button', { name: 'Add', exact: true }); + } + + async createDepartment(departmentName: string, email: string) { + await this.labelEnabled.click(); + await this.inputName.fill(departmentName); + await this.inputEmail.fill(email); + await this.btnSave.click(); + await this.toastMessage.dismissToast(); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-info.ts b/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-info.ts new file mode 100644 index 0000000000000..d911a81305d00 --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-info.ts @@ -0,0 +1,46 @@ +import type { Locator, Page } from '@playwright/test'; + +import { FlexTab } from '../fragments/flextab'; +import { OmnichannelContactReviewModal } from '../fragments/modals'; + +export class OmnichannelContactInfo extends FlexTab { + readonly contactReviewModal: OmnichannelContactReviewModal; + + constructor(page: Page) { + super(page.getByRole('dialog', { name: 'Contact' })); + this.contactReviewModal = new OmnichannelContactReviewModal(page); + } + + get infoContactEmail(): Locator { + return this.root.getByRole('list', { name: 'Email' }).getByRole('listitem').first().locator('p'); + } + + get btnEdit(): Locator { + return this.root.locator('role=button[name="Edit"]'); + } + + get tabHistory(): Locator { + return this.root.getByRole('tab', { name: 'History' }); + } + + get historyItem(): Locator { + return this.root.getByRole('listitem').first(); + } + + get historyMessage(): Locator { + return this.root.getByRole('listitem').first(); + } + + get btnOpenChat(): Locator { + return this.root.getByRole('button', { name: 'Open chat' }); + } + + get btnSeeConflicts(): Locator { + return this.root.getByRole('button', { name: 'See conflicts' }); + } + + async solveConflict(field: string, value: string) { + await this.btnSeeConflicts.click(); + await this.contactReviewModal.solveConfirmation(field, value); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-livechat-appearance.ts b/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-livechat-appearance.ts similarity index 81% rename from apps/meteor/tests/e2e/page-objects/omnichannel-livechat-appearance.ts rename to apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-livechat-appearance.ts index c42c8363981f0..66ae39c3b72c7 100644 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-livechat-appearance.ts +++ b/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-livechat-appearance.ts @@ -1,8 +1,8 @@ import type { Locator } from '@playwright/test'; -import { OmnichannelAdministration } from './omnichannel-administration'; +import { OmnichannelAdmin } from './omnichannel-admin'; -export class OmnichannelLivechatAppearance extends OmnichannelAdministration { +export class OmnichannelLivechatAppearance extends OmnichannelAdmin { get inputHideSystemMessages(): Locator { return this.page.locator('[name="Livechat_hide_system_messages"]'); } @@ -26,8 +26,4 @@ export class OmnichannelLivechatAppearance extends OmnichannelAdministration { findHideSystemMessageOption(option: string): Locator { return this.page.locator(`[role="option"][value="${option}"]`); } - - get btnSave(): Locator { - return this.page.locator('role=button[name="Save changes"]'); - } } diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-livechat-embedded.ts b/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-livechat-embedded.ts similarity index 100% rename from apps/meteor/tests/e2e/page-objects/omnichannel-livechat-embedded.ts rename to apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-livechat-embedded.ts diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-livechat.ts b/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-livechat.ts similarity index 99% rename from apps/meteor/tests/e2e/page-objects/omnichannel-livechat.ts rename to apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-livechat.ts index 5fcf966bd0f91..12df33a2762b2 100644 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-livechat.ts +++ b/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-livechat.ts @@ -2,7 +2,7 @@ import fs from 'fs/promises'; import type { Page, Locator, APIResponse } from '@playwright/test'; -import { expect } from '../utils/test'; +import { expect } from '../../utils/test'; export class OmnichannelLiveChat { readonly page: Page; diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-manager.ts b/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-manager.ts new file mode 100644 index 0000000000000..7a68257bdb6bf --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-manager.ts @@ -0,0 +1,41 @@ +import type { Locator, Page } from '@playwright/test'; + +import { OmnichannelAdmin } from './omnichannel-admin'; +import { Listbox } from '../fragments/listbox'; +import { Table } from '../fragments/table'; + +class OmnichannelManagersTable extends Table { + constructor(page: Page) { + super(page.getByRole('table', { name: 'Managers' })); + } +} + +export class OmnichannelManager extends OmnichannelAdmin { + readonly table: OmnichannelManagersTable; + + readonly listbox: Listbox; + + constructor(page: Page) { + super(page); + this.table = new OmnichannelManagersTable(page); + this.listbox = new Listbox(page); + } + + get inputUsername(): Locator { + return this.page.getByRole('main').getByLabel('Username'); + } + + async selectUsername(username: string) { + await this.inputUsername.fill(username); + await this.listbox.selectOption(username); + } + + get btnAddManager(): Locator { + return this.page.getByRole('button', { name: 'Add manager' }); + } + + async removeManager(name: string) { + await this.table.findRowByName(name).getByRole('button', { name: 'Remove' }).click(); + await this.deleteModal.confirmDelete(); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-monitors.ts b/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-monitors.ts new file mode 100644 index 0000000000000..4ebbaee051835 --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-monitors.ts @@ -0,0 +1,50 @@ +import type { Locator, Page } from '@playwright/test'; + +import { OmnichannelAdmin } from './omnichannel-admin'; +import { Listbox } from '../fragments/listbox'; +import { Table } from '../fragments/table'; + +class OmnichannelMonitorsTable extends Table { + constructor(page: Page) { + super(page.getByRole('table', { name: 'Monitors' })); + } +} + +export class OmnichannelMonitors extends OmnichannelAdmin { + readonly table: OmnichannelMonitorsTable; + + readonly listbox: Listbox; + + constructor(page: Page) { + super(page); + this.table = new OmnichannelMonitorsTable(page); + this.listbox = new Listbox(page); + } + + private get btnAddMonitor(): Locator { + return this.page.getByRole('button', { name: 'Add monitor' }); + } + + get inputMonitor(): Locator { + return this.page.locator('input[name="monitor"]'); + } + + private btnRemoveByName(name: string): Locator { + return this.table.findRowByName(name).getByRole('button', { name: 'Remove' }); + } + + private async selectMonitor(name: string) { + await this.inputMonitor.fill(name); + await this.listbox.selectOption(name); + } + + async removeMonitor(name: string) { + await this.btnRemoveByName(name).click(); + await this.deleteModal.confirmDelete(); + } + + async addMonitor(name: string) { + await this.selectMonitor(name); + await this.btnAddMonitor.click(); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-priorities.ts b/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-priorities.ts new file mode 100644 index 0000000000000..d458c9f4f3970 --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-priorities.ts @@ -0,0 +1,51 @@ +import type { Page } from '@playwright/test'; + +import { OmnichannelAdmin } from './omnichannel-admin'; +import { ToastMessages } from '../fragments'; +import { FlexTab } from '../fragments/flextab'; +import { OmnichannelResetPrioritiesModal } from '../fragments/modals'; +import { Table } from '../fragments/table'; + +class OmnichannelEditPriorityFlexTab extends FlexTab { + readonly toastMessage: ToastMessages; + + constructor(page: Page) { + super(page.getByRole('dialog', { name: 'Priority' })); + this.toastMessage = new ToastMessages(page); + } +} + +class OmnichannelPrioritiesTable extends Table { + constructor(page: Page) { + super(page.getByRole('table', { name: 'Priorities' })); + } +} + +export class OmnichannelPriorities extends OmnichannelAdmin { + readonly editPriority: OmnichannelEditPriorityFlexTab; + + readonly resetPrioritiesModal: OmnichannelResetPrioritiesModal; + + readonly table: OmnichannelPrioritiesTable; + + constructor(page: Page) { + super(page); + this.resetPrioritiesModal = new OmnichannelResetPrioritiesModal(page); + this.editPriority = new OmnichannelEditPriorityFlexTab(page); + this.table = new OmnichannelPrioritiesTable(page); + } + + get btnReset() { + return this.page.getByRole('button', { name: 'Reset' }); + } + + async resetPriorities() { + await this.btnReset.click(); + await this.resetPrioritiesModal.reset(); + await this.toastMessage.dismissToast(); + } + + findPriority(name: string) { + return this.table.findRowByName(name); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-reports.ts b/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-reports.ts similarity index 100% rename from apps/meteor/tests/e2e/page-objects/omnichannel-reports.ts rename to apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-reports.ts diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-settings.ts b/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-settings.ts similarity index 77% rename from apps/meteor/tests/e2e/page-objects/omnichannel-settings.ts rename to apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-settings.ts index 952959fda2cac..b658a9f7604b0 100644 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-settings.ts +++ b/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-settings.ts @@ -1,12 +1,8 @@ -import type { Locator, Page } from '@playwright/test'; +import type { Locator } from '@playwright/test'; -export class OmnichannelSettings { - protected readonly page: Page; - - constructor(page: Page) { - this.page = page; - } +import { OmnichannelAdmin } from './omnichannel-admin'; +export class OmnichannelSettings extends OmnichannelAdmin { get labelLivechatLogo(): Locator { return this.page.locator('//label[@title="Assets_livechat_widget_logo"]'); } @@ -30,8 +26,4 @@ export class OmnichannelSettings { get labelHideWatermark(): Locator { return this.page.locator('label').getByText('Hide "powered by Rocket.Chat"'); } - - get btnSave(): Locator { - return this.page.locator('role=button[name="Save changes"]'); - } } diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-sla-policies.ts b/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-sla-policies.ts new file mode 100644 index 0000000000000..62a06fe2d75cf --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-sla-policies.ts @@ -0,0 +1,54 @@ +import type { Locator, Page } from '@playwright/test'; + +import { OmnichannelAdmin } from './omnichannel-admin'; +import { FlexTab } from '../fragments/flextab'; +import { Table } from '../fragments/table'; + +class OmnichannelManageSlaPolicyFlexTab extends FlexTab { + constructor(page: Page) { + super(page.getByRole('dialog', { name: 'SLA Policy' })); + } + + get inputDescription(): Locator { + return this.root.getByRole('textbox', { name: 'Description' }); + } + + get inputEstimatedWaitTime(): Locator { + return this.root.locator('[name="dueTimeInMinutes"]'); + } +} + +class OmnichannelSlaPoliciesTable extends Table { + constructor(page: Page) { + super(page.getByRole('table', { name: 'SLA Policies' })); + } +} + +export class OmnichannelSlaPolicies extends OmnichannelAdmin { + readonly manageSlaPolicy: OmnichannelManageSlaPolicyFlexTab; + + readonly table: OmnichannelSlaPoliciesTable; + + constructor(page: Page) { + super(page); + this.manageSlaPolicy = new OmnichannelManageSlaPolicyFlexTab(page); + this.table = new OmnichannelSlaPoliciesTable(page); + } + + btnRemove(name: string) { + return this.table.findRowByName(name).getByRole('button', { name: 'Remove' }); + } + + async removeSLA(name: string) { + await this.btnRemove(name).click(); + await this.deleteModal.confirmDelete(); + } + + async createNew() { + await this.getButtonByType('SLA policy').click(); + } + + get txtEmptyState() { + return this.page.locator('div >> text="No results found"'); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-tags.ts b/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-tags.ts new file mode 100644 index 0000000000000..4bf0212e25aad --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-tags.ts @@ -0,0 +1,52 @@ +import type { Locator, Page } from '@playwright/test'; + +import { OmnichannelAdmin } from './omnichannel-admin'; +import { FlexTab } from '../fragments/flextab'; +import { Listbox } from '../fragments/listbox'; +import { Table } from '../fragments/table'; + +class OmnichannelEditTagFlexTab extends FlexTab { + readonly listbox: Listbox; + + constructor(page: Page) { + super(page.getByRole('dialog', { name: 'tag' })); + this.listbox = new Listbox(page); + } + + get inputDepartments(): Locator { + return this.root.getByLabel('Departments').getByRole('textbox'); + } + + async selectDepartment(name: string) { + await this.inputDepartments.click(); + await this.inputDepartments.fill(name); + await this.listbox.selectOption(name); + } +} + +class OmnichannelTagsTable extends Table { + constructor(page: Page) { + super(page.getByRole('table', { name: 'Tags' })); + } +} + +export class OmnichannelTags extends OmnichannelAdmin { + readonly editTag: OmnichannelEditTagFlexTab; + + readonly table: OmnichannelTagsTable; + + constructor(page: Page) { + super(page); + this.editTag = new OmnichannelEditTagFlexTab(page); + this.table = new OmnichannelTagsTable(page); + } + + async createNew() { + await this.getButtonByType('tag').click(); + } + + async deleteTag(name: string) { + await this.table.findRowByName(name).getByRole('button', { name: 'Remove' }).click(); + await this.deleteModal.confirmDelete(); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-transcript.ts b/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-transcript.ts similarity index 62% rename from apps/meteor/tests/e2e/page-objects/omnichannel-transcript.ts rename to apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-transcript.ts index 60b6179852ab1..a05a185849168 100644 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-transcript.ts +++ b/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-transcript.ts @@ -1,17 +1,8 @@ -import type { Locator, Page } from '@playwright/test'; +import type { Locator } from '@playwright/test'; -import { OmnichannelSidenav } from './fragments'; - -export class OmnichannelTranscript { - private readonly page: Page; - - readonly sidenav: OmnichannelSidenav; - - constructor(page: Page) { - this.page = page; - this.sidenav = new OmnichannelSidenav(page); - } +import { OmnichannelAdmin } from './omnichannel-admin'; +export class OmnichannelTranscript extends OmnichannelAdmin { get contactCenterChats(): Locator { return this.page.locator('//button[contains(.,"Chats")]'); } diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-triggers.ts b/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-triggers.ts new file mode 100644 index 0000000000000..9619671a2c140 --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-triggers.ts @@ -0,0 +1,127 @@ +import type { Locator, Page } from '@playwright/test'; + +import { OmnichannelAdmin } from './omnichannel-admin'; +import { FlexTab } from '../fragments/flextab'; +import { Listbox } from '../fragments/listbox'; +import { Table } from '../fragments/table'; + +type TriggerConditions = 'Visitor page URL' | 'Visitor time on site' | 'Chat opened by the visitor' | 'After guest registration'; + +class OmnichannelEditTriggerFlexTab extends FlexTab { + readonly listbox: Listbox; + + constructor(page: Page) { + super(page.getByRole('dialog', { name: 'trigger' })); + this.listbox = new Listbox(page); + } + + private get inputDescription(): Locator { + return this.root.getByRole('textbox', { name: 'Description', exact: true }); + } + + private get conditionLabel(): Locator { + return this.root.getByText('Condition', { exact: true }); + } + + private get senderLabel(): Locator { + return this.root.getByText('Sender', { exact: true }); + } + + private async selectCondition(condition: string) { + await this.conditionLabel.click(); + await this.listbox.selectOption(condition); + } + + private async selectSender(sender: 'queue' | 'custom') { + await this.senderLabel.click(); + await this.listbox.selectOption(sender); + } + + private get inputAgentName(): Locator { + return this.root.locator('input[name="actions.0.params.name"]'); + } + + private get inputConditionValue(): Locator { + return this.root.locator('input[name="conditions.0.value"]'); + } + + private get inputTriggerMessage(): Locator { + return this.root.locator('textarea[name="actions.0.params.msg"]'); + } + + async fillTriggerForm( + data: Partial<{ + name: string; + description: string; + condition: TriggerConditions; + conditionValue?: string | number; + sender: 'queue' | 'custom'; + agentName?: string; + triggerMessage: string; + }>, + ) { + data.name && (await this.inputName.fill(data.name)); + data.description && (await this.inputDescription.fill(data.description)); + data.condition && (await this.selectCondition(data.condition)); + + if (data.conditionValue) { + await this.inputConditionValue.fill(data.conditionValue.toString()); + } + + data.sender && (await this.selectSender(data.sender)); + if (data.sender === 'custom' && !data.agentName) { + throw new Error('A custom agent is required for this action'); + } else { + data.agentName && (await this.inputAgentName.fill(data.agentName)); + } + + data.triggerMessage && (await this.inputTriggerMessage.fill(data.triggerMessage)); + } +} + +class OmnichannelTriggersTable extends Table { + constructor(page: Page) { + super(page.getByRole('table', { name: 'Livechat Triggers' })); + } +} + +export class OmnichannelTriggers extends OmnichannelAdmin { + readonly editTrigger: OmnichannelEditTriggerFlexTab; + + readonly table: OmnichannelTriggersTable; + + constructor(page: Page) { + super(page); + this.editTrigger = new OmnichannelEditTriggerFlexTab(page); + this.table = new OmnichannelTriggersTable(page); + } + + async removeTrigger(name: string) { + await this.table.findRowByName(name).getByRole('button', { name: 'Remove' }).click(); + await this.deleteModal.confirmDelete(); + } + + public async createTrigger(triggersName: string, triggerMessage: string, condition: TriggerConditions, conditionValue?: number | string) { + await this.getButtonByType('trigger').click(); + await this.editTrigger.fillTriggerForm({ + name: triggersName, + description: 'Creating a fresh trigger', + condition, + conditionValue, + triggerMessage, + }); + await this.editTrigger.save(); + } + + public async updateTrigger(name: string, triggerMessage: string, condition: TriggerConditions = 'Chat opened by the visitor') { + await this.editTrigger.fillTriggerForm({ + name, + description: 'Updating the existing trigger', + condition, + sender: 'custom', + agentName: 'Rocket.cat', + triggerMessage, + }); + await this.editTrigger.save(); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-units.ts b/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-units.ts new file mode 100644 index 0000000000000..1ee38399ee639 --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-units.ts @@ -0,0 +1,96 @@ +import type { Locator, Page } from '@playwright/test'; + +import { OmnichannelAdmin } from './omnichannel-admin'; +import { FlexTab } from '../fragments/flextab'; +import { Listbox } from '../fragments/listbox'; +import { Table } from '../fragments/table'; + +export class OmnichannelUnitFlexTab extends FlexTab { + readonly listbox: Listbox; + + constructor(page: Page) { + super(page.getByRole('dialog', { name: 'unit' })); + this.listbox = new Listbox(page); + } + + private get fieldDepartments() { + return this.root.getByLabel('Departments'); + } + + get inputDepartments() { + return this.fieldDepartments.getByRole('textbox'); + } + + private get fieldMonitors() { + return this.root.getByLabel('Monitors'); + } + + get inputMonitors() { + return this.fieldMonitors.getByRole('textbox'); + } + + get inputVisibility(): Locator { + return this.root.getByText('Visibility'); + } + + findDepartmentsChipOption(name: string) { + return this.fieldDepartments.getByRole('option', { name, exact: true }); + } + + findMonitorChipOption(name: string) { + return this.fieldMonitors.getByRole('option', { name, exact: true }); + } + + async selectDepartment(name: string) { + await this.inputDepartments.click(); + await this.inputDepartments.fill(name); + await this.listbox.selectOption(name); + await this.inputDepartments.click(); + } + + async selectMonitor(option: string) { + await this.inputMonitors.click(); + await this.listbox.selectOption(option); + await this.inputMonitors.click(); + } + + async removeMonitor(option: string) { + await this.findMonitorChipOption(option).click(); + } + + async selectVisibility(option: string) { + await this.inputVisibility.click(); + await this.listbox.selectOption(option); + } +} + +class OmnichannelUnitsTable extends Table { + constructor(page: Locator) { + super(page); + } + + deleteUnitByName(name: string) { + return this.findRowByName(name).getByRole('button', { name: 'Remove' }); + } +} + +export class OmnichannelUnits extends OmnichannelAdmin { + readonly manageUnit: OmnichannelUnitFlexTab; + + readonly table: OmnichannelUnitsTable; + + constructor(page: Page) { + super(page); + this.manageUnit = new OmnichannelUnitFlexTab(page); + this.table = new OmnichannelUnitsTable(page.getByRole('table', { name: 'Units' })); + } + + async createNew() { + await this.getButtonByType('unit').click(); + } + + async deleteUnit(name: string) { + await this.table.deleteUnitByName(name).click(); + await this.deleteModal.confirmDelete(); + } +} diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 5db49d7100676..e3ac97c6b6dc8 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -3903,6 +3903,7 @@ "Omnichannel_Agent": "Omnichannel Agent", "Omnichannel_Contact_Center": "Omnichannel Contact Center", "Omnichannel_Contact_Center_Chats": "Omnichannel Contact Center Chats", + "Omnichannel_Contact_Center_Contacts": "Omnichannel Contact Center Contacts", "Omnichannel_Description": "Set up Omnichannel to communicate with customers from one place, regardless of how they connect with you.", "Omnichannel_Directory": "Omnichannel Directory", "Omnichannel_External_Frame": "External Frame", From 5650b7802dd46f89e7027df97e3558159fbf0bcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Jaeger=20Foresti?= <60678893+juliajforesti@users.noreply.github.com> Date: Mon, 26 Jan 2026 12:03:51 -0300 Subject: [PATCH 016/131] fix: wrong spacing on room header toolbar Options menu (#38318) --- .changeset/little-mayflies-divide.md | 5 +++++ .../client/views/room/Header/RoomToolbox/RoomToolbox.tsx | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/little-mayflies-divide.md diff --git a/.changeset/little-mayflies-divide.md b/.changeset/little-mayflies-divide.md new file mode 100644 index 0000000000000..d5c3ee984ed47 --- /dev/null +++ b/.changeset/little-mayflies-divide.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes room header toolbar different spacing on Options menu diff --git a/apps/meteor/client/views/room/Header/RoomToolbox/RoomToolbox.tsx b/apps/meteor/client/views/room/Header/RoomToolbox/RoomToolbox.tsx index 24a056c9adb18..e1c796eca9cd1 100644 --- a/apps/meteor/client/views/room/Header/RoomToolbox/RoomToolbox.tsx +++ b/apps/meteor/client/views/room/Header/RoomToolbox/RoomToolbox.tsx @@ -51,7 +51,7 @@ const RoomToolbox = ({ className }: RoomToolboxProps) => { {featuredActions.map(mapToToolboxItem)} {featuredActions.length > 0 && } {visibleActions.map(mapToToolboxItem)} - {showKebabMenu && } + {showKebabMenu && } ); }; From aa37226dad870ff8ad7834141273711e754915ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Jaeger=20Foresti?= <60678893+juliajforesti@users.noreply.github.com> Date: Mon, 26 Jan 2026 16:16:18 -0300 Subject: [PATCH 017/131] refactor: replace legacy `Menu` with `GenericMenu` (#38287) --- .../components/forms/DateRangePicker.tsx | 76 ++++++----- .../tabs/AppLogs/Filters/AppLogsFilter.tsx | 124 +++++++++--------- .../AppLogs/Filters/AppLogsFilterOptions.tsx | 35 +++-- .../AppLogs/Filters/CompactFilterOptions.tsx | 61 ++++----- .../AppLogsFilterCompact.spec.tsx.snap | 13 +- .../AppLogsFilterExpanded.spec.tsx.snap | 17 ++- .../omnichannel/analytics/DateRangePicker.tsx | 65 +++++---- .../DepartmentsTable/DepartmentItemMenu.tsx | 71 +++++----- .../omnichannel-departaments.spec.ts | 4 +- .../tests/e2e/page-objects/fragments/menu.ts | 12 +- .../omnichannel/omnichannel-departments.ts | 14 +- packages/i18n/src/locales/en.i18n.json | 3 +- 12 files changed, 258 insertions(+), 237 deletions(-) diff --git a/apps/meteor/client/views/audit/components/forms/DateRangePicker.tsx b/apps/meteor/client/views/audit/components/forms/DateRangePicker.tsx index 9d659169622e8..9c8b7ea3010bd 100644 --- a/apps/meteor/client/views/audit/components/forms/DateRangePicker.tsx +++ b/apps/meteor/client/views/audit/components/forms/DateRangePicker.tsx @@ -1,5 +1,6 @@ -import { Box, InputBox, Menu, Margins, Option } from '@rocket.chat/fuselage'; +import { Box, InputBox, Margins } from '@rocket.chat/fuselage'; import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; +import { GenericMenu } from '@rocket.chat/ui-client'; import moment from 'moment'; import type { ReactElement, ComponentProps, SetStateAction, FormEvent } from 'react'; import { useMemo } from 'react'; @@ -141,46 +142,53 @@ const DateRangePicker = ({ value, onChange, ...props }: DateRangePickerProps): R const { t } = useTranslation(); const presets = useMemo( - () => - ({ - today: { - label: t('Today'), - action: () => dispatch('today'), - }, - yesterday: { - label: t('Yesterday'), - action: () => dispatch('yesterday'), - }, - thisWeek: { - label: t('This_week'), - action: () => dispatch('this-week'), - }, - previousWeek: { - label: t('Previous_week'), - action: () => dispatch('last-week'), - }, - thisMonth: { - label: t('This_month'), - action: () => dispatch('this-month'), - }, - lastMonth: { - label: t('Previous_month'), - action: () => dispatch('last-month'), - }, - }) as const, + () => [ + { + id: 'today', + icon: 'history' as const, + content: t('Today'), + onClick: () => dispatch('today'), + }, + { + id: 'yesterday', + icon: 'history' as const, + content: t('Yesterday'), + onClick: () => dispatch('yesterday'), + }, + { + id: 'thisWeek', + icon: 'history' as const, + content: t('This_week'), + onClick: () => dispatch('this-week'), + }, + { + id: 'previousWeek', + icon: 'history' as const, + content: t('Previous_week'), + onClick: () => dispatch('last-week'), + }, + { + id: 'thisMonth', + icon: 'history' as const, + content: t('This_month'), + onClick: () => dispatch('this-month'), + }, + { + id: 'lastMonth', + icon: 'history' as const, + content: t('Previous_month'), + onClick: () => dispatch('last-month'), + }, + ], [dispatch, t], ); return ( - + - ) =>