diff --git a/.changeset/blue-cougars-wave.md b/.changeset/blue-cougars-wave.md new file mode 100644 index 000000000000..6fa1408f9155 --- /dev/null +++ b/.changeset/blue-cougars-wave.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +fix: broken error messages on room.saveInfo & missing CF validations on omni/contact api diff --git a/.changeset/cold-years-beg.md b/.changeset/cold-years-beg.md new file mode 100644 index 000000000000..4c4cf5c03568 --- /dev/null +++ b/.changeset/cold-years-beg.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +fix: getActiveLocalUserCount query always returning 0 diff --git a/.changeset/friendly-apricots-juggle.md b/.changeset/friendly-apricots-juggle.md new file mode 100644 index 000000000000..d493f49bd66b --- /dev/null +++ b/.changeset/friendly-apricots-juggle.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +fix: Clicking uploaded file title replaces current tab diff --git a/.changeset/great-dolphins-remember.md b/.changeset/great-dolphins-remember.md new file mode 100644 index 000000000000..a69eac0040cd --- /dev/null +++ b/.changeset/great-dolphins-remember.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixed omnichannel contact form asynchronous validations diff --git a/.changeset/heavy-years-repeat.md b/.changeset/heavy-years-repeat.md new file mode 100644 index 000000000000..fc88cd86553b --- /dev/null +++ b/.changeset/heavy-years-repeat.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +fix: Admins unable to create new users if new users require manual approval diff --git a/.changeset/khaki-avocados-fix.md b/.changeset/khaki-avocados-fix.md new file mode 100644 index 000000000000..0126a11761d0 --- /dev/null +++ b/.changeset/khaki-avocados-fix.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +fix wrong %s translations diff --git a/.changeset/late-carrots-think.md b/.changeset/late-carrots-think.md new file mode 100644 index 000000000000..4a27a6c1ef77 --- /dev/null +++ b/.changeset/late-carrots-think.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': minor +--- + +fix: Hide roomLeader padding diff --git a/.changeset/mean-keys-rhyme.md b/.changeset/mean-keys-rhyme.md new file mode 100644 index 000000000000..525e289c5b79 --- /dev/null +++ b/.changeset/mean-keys-rhyme.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/meteor': minor +'@rocket.chat/rest-typings': patch +--- + +Refactored Omnichannel department pages to use best practices, also fixed existing bugs diff --git a/.changeset/nervous-masks-speak.md b/.changeset/nervous-masks-speak.md new file mode 100644 index 000000000000..2493a15469ab --- /dev/null +++ b/.changeset/nervous-masks-speak.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +fix: unable to upload files in IOS Safari browsers diff --git a/.changeset/polite-clouds-notice.md b/.changeset/polite-clouds-notice.md new file mode 100644 index 000000000000..b9e9236313b7 --- /dev/null +++ b/.changeset/polite-clouds-notice.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +chore: update room on `cleanRoomHistory` only if any message has been deleted diff --git a/.changeset/proud-apples-jog.md b/.changeset/proud-apples-jog.md new file mode 100644 index 000000000000..7a4fcd511e49 --- /dev/null +++ b/.changeset/proud-apples-jog.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +test: add missing omnichannel contact-center tests diff --git a/.changeset/quiet-rules-swim.md b/.changeset/quiet-rules-swim.md new file mode 100644 index 000000000000..005517c1d695 --- /dev/null +++ b/.changeset/quiet-rules-swim.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +fix: Add missing awaits to .count() calls diff --git a/.changeset/strange-clocks-peel.md b/.changeset/strange-clocks-peel.md new file mode 100644 index 000000000000..85ce0112652a --- /dev/null +++ b/.changeset/strange-clocks-peel.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': minor +--- + +Added ability to see attachments in the contact history message list diff --git a/.changeset/wild-lizards-guess.md b/.changeset/wild-lizards-guess.md new file mode 100644 index 000000000000..4a7b45515df7 --- /dev/null +++ b/.changeset/wild-lizards-guess.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': minor +--- + +fix: Analytics page crash diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 52147f7d2360..554d00d09ebc 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -8,11 +8,18 @@ /.github/ @RocketChat/Architecture /_templates/ @RocketChat/Architecture /apps/meteor/client/ @RocketChat/frontend -/apps/meteor/tests/ @RocketChat/Architecture +/apps/meteor/tests/e2e @RocketChat/frontend +/apps/meteor/tests/end-to-end @RocketChat/backend +/apps/meteor/tests/unit/app @RocketChat/backend +/apps/meteor/tests/unit/app/ui-utils @RocketChat/frontend +/apps/meteor/tests/unit/client @RocketChat/frontend +/apps/meteor/tests/unit/server @RocketChat/backend /apps/meteor/app/apps/ @RocketChat/apps /apps/meteor/app/livechat @RocketChat/omnichannel /apps/meteor/app/voip @RocketChat/omnichannel /apps/meteor/app/sms @RocketChat/omnichannel +/apps/meteor/server @RocketChat/backend +/apps/meteor/server/models @RocketChat/Architecture /apps/meteor/packages/rocketchat-livechat @RocketChat/omnichannel /apps/meteor/server/services/voip @RocketChat/omnichannel /apps/meteor/server/services/omnichannel-voip @RocketChat/omnichannel diff --git a/.github/workflows/ci-test-e2e.yml b/.github/workflows/ci-test-e2e.yml index 2fca0c3e36ac..a27915154277 100644 --- a/.github/workflows/ci-test-e2e.yml +++ b/.github/workflows/ci-test-e2e.yml @@ -214,6 +214,7 @@ jobs: if: inputs.type == 'api' working-directory: ./apps/meteor env: + WEBHOOK_TEST_URL: 'http://host.docker.internal:10000' IS_EE: ${{ inputs.release == 'ee' && 'true' || '' }} run: | for i in $(seq 1 2); do diff --git a/apps/meteor/app/authentication/server/startup/index.js b/apps/meteor/app/authentication/server/startup/index.js index f8d39f14b7ab..726518465e9c 100644 --- a/apps/meteor/app/authentication/server/startup/index.js +++ b/apps/meteor/app/authentication/server/startup/index.js @@ -196,7 +196,7 @@ const onCreateUserAsync = async function (options, user = {}) { if (!user.active) { const destinations = []; const usersInRole = await Roles.findUsersInRole('admin'); - await usersInRole.toArray().forEach((adminUser) => { + await usersInRole.forEach((adminUser) => { if (Array.isArray(adminUser.emails)) { adminUser.emails.forEach((email) => { destinations.push(`${adminUser.name}<${email.address}>`); diff --git a/apps/meteor/app/authorization/server/constant/permissions.ts b/apps/meteor/app/authorization/server/constant/permissions.ts new file mode 100644 index 000000000000..17bff428929f --- /dev/null +++ b/apps/meteor/app/authorization/server/constant/permissions.ts @@ -0,0 +1,219 @@ +// Note: +// 1.if we need to create a role that can only edit channel message, but not edit group message +// then we can define edit--message instead of edit-message +// 2. admin, moderator, and user roles should not be deleted as they are referenced in the code. +export const permissions = [ + { _id: 'access-permissions', roles: ['admin'] }, + { _id: 'access-setting-permissions', roles: ['admin'] }, + { _id: 'add-oauth-service', roles: ['admin'] }, + { _id: 'add-user-to-joined-room', roles: ['admin', 'owner', 'moderator'] }, + { _id: 'add-user-to-any-c-room', roles: ['admin'] }, + { _id: 'add-user-to-any-p-room', roles: [] }, + { _id: 'api-bypass-rate-limit', roles: ['admin', 'bot', 'app'] }, + { _id: 'archive-room', roles: ['admin', 'owner'] }, + { _id: 'assign-admin-role', roles: ['admin'] }, + { _id: 'assign-roles', roles: ['admin'] }, + { _id: 'ban-user', roles: ['admin', 'owner', 'moderator'] }, + { _id: 'bulk-register-user', roles: ['admin'] }, + { _id: 'change-livechat-room-visitor', roles: ['admin', 'livechat-manager', 'livechat-agent'] }, + { _id: 'create-c', roles: ['admin', 'user', 'bot', 'app'] }, + { _id: 'create-d', roles: ['admin', 'user', 'bot', 'app'] }, + { _id: 'create-p', roles: ['admin', 'user', 'bot', 'app'] }, + { _id: 'create-personal-access-tokens', roles: ['admin', 'user'] }, + { _id: 'create-user', roles: ['admin'] }, + { _id: 'clean-channel-history', roles: ['admin'] }, + { _id: 'delete-c', roles: ['admin', 'owner'] }, + { _id: 'delete-d', roles: ['admin'] }, + { _id: 'delete-message', roles: ['admin', 'owner', 'moderator'] }, + { _id: 'delete-own-message', roles: ['admin', 'user'] }, + { _id: 'delete-p', roles: ['admin', 'owner'] }, + { _id: 'delete-user', roles: ['admin'] }, + { _id: 'edit-message', roles: ['admin', 'owner', 'moderator'] }, + { _id: 'edit-other-user-active-status', roles: ['admin'] }, + { _id: 'edit-other-user-info', roles: ['admin'] }, + { _id: 'edit-other-user-password', roles: ['admin'] }, + { _id: 'edit-other-user-avatar', roles: ['admin'] }, + { _id: 'edit-other-user-e2ee', roles: ['admin'] }, + { _id: 'edit-other-user-totp', roles: ['admin'] }, + { _id: 'edit-privileged-setting', roles: ['admin'] }, + { _id: 'edit-room', roles: ['admin', 'owner', 'moderator'] }, + { _id: 'edit-room-avatar', roles: ['admin', 'owner', 'moderator'] }, + { _id: 'edit-room-retention-policy', roles: ['admin'] }, + { _id: 'force-delete-message', roles: ['admin', 'owner'] }, + { _id: 'join-without-join-code', roles: ['admin', 'bot', 'app'] }, + { _id: 'leave-c', roles: ['admin', 'user', 'bot', 'anonymous', 'app'] }, + { _id: 'leave-p', roles: ['admin', 'user', 'bot', 'anonymous', 'app'] }, + { _id: 'logout-other-user', roles: ['admin'] }, + { _id: 'manage-assets', roles: ['admin'] }, + { _id: 'manage-email-inbox', roles: ['admin'] }, + { _id: 'manage-emoji', roles: ['admin'] }, + { _id: 'manage-user-status', roles: ['admin'] }, + { _id: 'manage-outgoing-integrations', roles: ['admin'] }, + { _id: 'manage-incoming-integrations', roles: ['admin'] }, + { _id: 'manage-own-outgoing-integrations', roles: ['admin'] }, + { _id: 'manage-own-incoming-integrations', roles: ['admin'] }, + { _id: 'manage-oauth-apps', roles: ['admin'] }, + { _id: 'manage-selected-settings', roles: ['admin'] }, + { _id: 'mention-all', roles: ['admin', 'owner', 'moderator', 'user'] }, + { _id: 'mention-here', roles: ['admin', 'owner', 'moderator', 'user'] }, + { _id: 'mute-user', roles: ['admin', 'owner', 'moderator'] }, + { _id: 'remove-user', roles: ['admin', 'owner', 'moderator'] }, + { _id: 'run-import', roles: ['admin'] }, + { _id: 'run-migration', roles: ['admin'] }, + { _id: 'set-moderator', roles: ['admin', 'owner'] }, + { _id: 'set-owner', roles: ['admin', 'owner'] }, + { _id: 'send-many-messages', roles: ['admin', 'bot', 'app'] }, + { _id: 'set-leader', roles: ['admin', 'owner'] }, + { _id: 'unarchive-room', roles: ['admin'] }, + { _id: 'view-c-room', roles: ['admin', 'user', 'bot', 'app', 'anonymous'] }, + { _id: 'user-generate-access-token', roles: ['admin'] }, + { _id: 'view-d-room', roles: ['admin', 'user', 'bot', 'app', 'guest'] }, + { _id: 'view-full-other-user-info', roles: ['admin'] }, + { _id: 'view-history', roles: ['admin', 'user', 'anonymous'] }, + { _id: 'view-joined-room', roles: ['guest', 'bot', 'app', 'anonymous'] }, + { _id: 'view-join-code', roles: ['admin'] }, + { _id: 'view-logs', roles: ['admin'] }, + { _id: 'view-other-user-channels', roles: ['admin'] }, + { _id: 'view-p-room', roles: ['admin', 'user', 'anonymous', 'guest'] }, + { _id: 'view-privileged-setting', roles: ['admin'] }, + { _id: 'view-room-administration', roles: ['admin'] }, + { _id: 'view-statistics', roles: ['admin'] }, + { _id: 'view-user-administration', roles: ['admin'] }, + { _id: 'preview-c-room', roles: ['admin', 'user', 'anonymous'] }, + { _id: 'view-outside-room', roles: ['admin', 'owner', 'moderator', 'user'] }, + { _id: 'view-broadcast-member-list', roles: ['admin', 'owner', 'moderator'] }, + { _id: 'call-management', roles: ['admin', 'owner', 'moderator'] }, + { _id: 'create-invite-links', roles: ['admin', 'owner', 'moderator'] }, + { + _id: 'view-l-room', + roles: ['livechat-manager', 'livechat-monitor', 'livechat-agent', 'admin'], + }, + { _id: 'view-livechat-manager', roles: ['livechat-manager', 'livechat-monitor', 'admin'] }, + { + _id: 'view-omnichannel-contact-center', + roles: ['livechat-manager', 'livechat-agent', 'livechat-monitor', 'admin'], + }, + { _id: 'edit-omnichannel-contact', roles: ['livechat-manager', 'livechat-agent', 'admin'] }, + { _id: 'view-livechat-rooms', roles: ['livechat-manager', 'livechat-monitor', 'admin'] }, + { + _id: 'close-livechat-room', + roles: ['livechat-manager', 'livechat-monitor', 'livechat-agent', 'admin'], + }, + { _id: 'close-others-livechat-room', roles: ['livechat-manager', 'livechat-monitor', 'admin'] }, + { + _id: 'on-hold-livechat-room', + roles: ['livechat-manager', 'livechat-monitor', 'livechat-agent', 'admin'], + }, + { + _id: 'on-hold-others-livechat-room', + roles: ['livechat-manager', 'livechat-monitor', 'admin'], + }, + { _id: 'save-others-livechat-room-info', roles: ['livechat-manager', 'livechat-monitor', 'admin'] }, + { + _id: 'remove-closed-livechat-rooms', + roles: ['livechat-manager', 'livechat-monitor', 'admin'], + }, + { _id: 'view-livechat-analytics', roles: ['livechat-manager', 'livechat-monitor', 'admin'] }, + { + _id: 'view-livechat-queue', + roles: ['livechat-manager', 'livechat-monitor', 'livechat-agent', 'admin'], + }, + { _id: 'transfer-livechat-guest', roles: ['livechat-manager', 'livechat-monitor', 'admin'] }, + { _id: 'manage-livechat-managers', roles: ['livechat-manager', 'admin'] }, + { _id: 'manage-livechat-agents', roles: ['livechat-manager', 'livechat-monitor', 'admin'] }, + { + _id: 'manage-livechat-departments', + roles: ['livechat-manager', 'livechat-monitor', 'admin'], + }, + { _id: 'view-livechat-departments', roles: ['livechat-manager', 'livechat-monitor', 'admin'] }, + { + _id: 'add-livechat-department-agents', + roles: ['livechat-manager', 'livechat-monitor', 'admin'], + }, + { + _id: 'view-livechat-current-chats', + roles: ['livechat-manager', 'livechat-monitor', 'admin'], + }, + { + _id: 'view-livechat-real-time-monitoring', + roles: ['livechat-manager', 'livechat-monitor', 'admin'], + }, + { _id: 'view-livechat-triggers', roles: ['livechat-manager', 'admin'] }, + { _id: 'view-livechat-customfields', roles: ['livechat-manager', 'admin'] }, + { _id: 'view-livechat-installation', roles: ['livechat-manager', 'admin'] }, + { _id: 'view-livechat-appearance', roles: ['livechat-manager', 'admin'] }, + { _id: 'view-livechat-webhooks', roles: ['livechat-manager', 'admin'] }, + { + _id: 'view-livechat-business-hours', + roles: ['livechat-manager', 'livechat-monitor', 'admin'], + }, + { + _id: 'view-livechat-room-closed-same-department', + roles: ['livechat-manager', 'livechat-monitor', 'admin'], + }, + { + _id: 'view-livechat-room-closed-by-another-agent', + roles: ['livechat-manager', 'livechat-monitor', 'admin'], + }, + { + _id: 'view-livechat-room-customfields', + roles: ['livechat-manager', 'livechat-monitor', 'livechat-agent', 'admin'], + }, + { + _id: 'edit-livechat-room-customfields', + roles: ['livechat-manager', 'livechat-monitor', 'livechat-agent', 'admin'], + }, + { _id: 'send-omnichannel-chat-transcript', roles: ['livechat-manager', 'admin'] }, + { _id: 'mail-messages', roles: ['admin'] }, + { _id: 'toggle-room-e2e-encryption', roles: ['owner', 'admin'] }, + { _id: 'message-impersonate', roles: ['bot', 'app'] }, + { _id: 'create-team', roles: ['admin', 'user'] }, + { _id: 'delete-team', roles: ['admin', 'owner'] }, + { _id: 'convert-team', roles: ['admin', 'owner'] }, + { _id: 'edit-team', roles: ['admin', 'owner'] }, + { _id: 'add-team-member', roles: ['admin', 'owner', 'moderator'] }, + { _id: 'edit-team-member', roles: ['admin', 'owner', 'moderator'] }, + { _id: 'add-team-channel', roles: ['admin', 'owner', 'moderator'] }, + { _id: 'edit-team-channel', roles: ['admin', 'owner', 'moderator'] }, + { _id: 'remove-team-channel', roles: ['admin', 'owner', 'moderator'] }, + { _id: 'view-all-team-channels', roles: ['admin', 'owner'] }, + { _id: 'view-all-teams', roles: ['admin'] }, + { _id: 'remove-closed-livechat-room', roles: ['livechat-manager', 'admin'] }, + { _id: 'remove-livechat-department', roles: ['livechat-manager', 'admin'] }, + + // VOIP Permissions + // allows to manage voip calls configuration + { _id: 'manage-voip-call-settings', roles: ['livechat-manager', 'admin'] }, + { _id: 'manage-voip-contact-center-settings', roles: ['livechat-manager', 'admin'] }, + // allows agent-extension association. + { _id: 'manage-agent-extension-association', roles: ['admin'] }, + { _id: 'view-agent-extension-association', roles: ['livechat-manager', 'admin', 'livechat-agent'] }, + // allows to receive a voip call + { _id: 'inbound-voip-calls', roles: ['livechat-agent'] }, + + { _id: 'remove-livechat-department', roles: ['livechat-manager', 'admin'] }, + { _id: 'manage-apps', roles: ['admin'] }, + { _id: 'post-readonly', roles: ['admin', 'owner', 'moderator'] }, + { _id: 'set-readonly', roles: ['admin', 'owner'] }, + { _id: 'set-react-when-readonly', roles: ['admin', 'owner'] }, + { _id: 'manage-cloud', roles: ['admin'] }, + { _id: 'manage-sounds', roles: ['admin'] }, + { _id: 'access-mailer', roles: ['admin'] }, + { _id: 'pin-message', roles: ['owner', 'moderator', 'admin'] }, + { _id: 'mobile-upload-file', roles: ['user', 'admin'] }, + { _id: 'send-mail', roles: ['admin'] }, + { _id: 'view-federation-data', roles: ['admin'] }, + { _id: 'add-all-to-room', roles: ['admin'] }, + { _id: 'get-server-info', roles: ['admin'] }, + { _id: 'register-on-cloud', roles: ['admin'] }, + { _id: 'test-admin-options', roles: ['admin'] }, + { _id: 'sync-auth-services-users', roles: ['admin'] }, + { _id: 'restart-server', roles: ['admin'] }, + { _id: 'remove-slackbridge-links', roles: ['admin'] }, + { _id: 'view-import-operations', roles: ['admin'] }, + { _id: 'clear-oembed-cache', roles: ['admin'] }, + { _id: 'videoconf-ring-users', roles: ['admin', 'owner', 'moderator', 'user'] }, + { _id: 'view-moderation-console', roles: ['admin'] }, + { _id: 'manage-moderation-actions', roles: ['admin'] }, + { _id: 'bypass-time-limit-edit-and-delete', roles: ['bot', 'app'] }, +]; diff --git a/apps/meteor/app/authorization/server/functions/upsertPermissions.ts b/apps/meteor/app/authorization/server/functions/upsertPermissions.ts index 2236b113433d..d48de450ac4b 100644 --- a/apps/meteor/app/authorization/server/functions/upsertPermissions.ts +++ b/apps/meteor/app/authorization/server/functions/upsertPermissions.ts @@ -5,228 +5,9 @@ import { Permissions, Settings } from '@rocket.chat/models'; import { settings } from '../../../settings/server'; import { getSettingPermissionId, CONSTANTS } from '../../lib'; import { createOrUpdateProtectedRoleAsync } from '../../../../server/lib/roles/createOrUpdateProtectedRole'; +import { permissions } from '../constant/permissions'; export const upsertPermissions = async (): Promise => { - // Note: - // 1.if we need to create a role that can only edit channel message, but not edit group message - // then we can define edit--message instead of edit-message - // 2. admin, moderator, and user roles should not be deleted as they are referenced in the code. - const permissions = [ - { _id: 'access-permissions', roles: ['admin'] }, - { _id: 'access-setting-permissions', roles: ['admin'] }, - { _id: 'add-oauth-service', roles: ['admin'] }, - { _id: 'add-user-to-joined-room', roles: ['admin', 'owner', 'moderator'] }, - { _id: 'add-user-to-any-c-room', roles: ['admin'] }, - { _id: 'add-user-to-any-p-room', roles: [] }, - { _id: 'api-bypass-rate-limit', roles: ['admin', 'bot', 'app'] }, - { _id: 'archive-room', roles: ['admin', 'owner'] }, - { _id: 'assign-admin-role', roles: ['admin'] }, - { _id: 'assign-roles', roles: ['admin'] }, - { _id: 'ban-user', roles: ['admin', 'owner', 'moderator'] }, - { _id: 'bulk-register-user', roles: ['admin'] }, - { _id: 'change-livechat-room-visitor', roles: ['admin', 'livechat-manager', 'livechat-agent'] }, - { _id: 'create-c', roles: ['admin', 'user', 'bot', 'app'] }, - { _id: 'create-d', roles: ['admin', 'user', 'bot', 'app'] }, - { _id: 'create-p', roles: ['admin', 'user', 'bot', 'app'] }, - { _id: 'create-personal-access-tokens', roles: ['admin', 'user'] }, - { _id: 'create-user', roles: ['admin'] }, - { _id: 'clean-channel-history', roles: ['admin'] }, - { _id: 'delete-c', roles: ['admin', 'owner'] }, - { _id: 'delete-d', roles: ['admin'] }, - { _id: 'delete-message', roles: ['admin', 'owner', 'moderator'] }, - { _id: 'delete-own-message', roles: ['admin', 'user'] }, - { _id: 'delete-p', roles: ['admin', 'owner'] }, - { _id: 'delete-user', roles: ['admin'] }, - { _id: 'edit-message', roles: ['admin', 'owner', 'moderator'] }, - { _id: 'edit-other-user-active-status', roles: ['admin'] }, - { _id: 'edit-other-user-info', roles: ['admin'] }, - { _id: 'edit-other-user-password', roles: ['admin'] }, - { _id: 'edit-other-user-avatar', roles: ['admin'] }, - { _id: 'edit-other-user-e2ee', roles: ['admin'] }, - { _id: 'edit-other-user-totp', roles: ['admin'] }, - { _id: 'edit-privileged-setting', roles: ['admin'] }, - { _id: 'edit-room', roles: ['admin', 'owner', 'moderator'] }, - { _id: 'edit-room-avatar', roles: ['admin', 'owner', 'moderator'] }, - { _id: 'edit-room-retention-policy', roles: ['admin'] }, - { _id: 'force-delete-message', roles: ['admin', 'owner'] }, - { _id: 'join-without-join-code', roles: ['admin', 'bot', 'app'] }, - { _id: 'leave-c', roles: ['admin', 'user', 'bot', 'anonymous', 'app'] }, - { _id: 'leave-p', roles: ['admin', 'user', 'bot', 'anonymous', 'app'] }, - { _id: 'logout-other-user', roles: ['admin'] }, - { _id: 'manage-assets', roles: ['admin'] }, - { _id: 'manage-email-inbox', roles: ['admin'] }, - { _id: 'manage-emoji', roles: ['admin'] }, - { _id: 'manage-user-status', roles: ['admin'] }, - { _id: 'manage-outgoing-integrations', roles: ['admin'] }, - { _id: 'manage-incoming-integrations', roles: ['admin'] }, - { _id: 'manage-own-outgoing-integrations', roles: ['admin'] }, - { _id: 'manage-own-incoming-integrations', roles: ['admin'] }, - { _id: 'manage-oauth-apps', roles: ['admin'] }, - { _id: 'manage-selected-settings', roles: ['admin'] }, - { _id: 'mention-all', roles: ['admin', 'owner', 'moderator', 'user'] }, - { _id: 'mention-here', roles: ['admin', 'owner', 'moderator', 'user'] }, - { _id: 'mute-user', roles: ['admin', 'owner', 'moderator'] }, - { _id: 'remove-user', roles: ['admin', 'owner', 'moderator'] }, - { _id: 'run-import', roles: ['admin'] }, - { _id: 'run-migration', roles: ['admin'] }, - { _id: 'set-moderator', roles: ['admin', 'owner'] }, - { _id: 'set-owner', roles: ['admin', 'owner'] }, - { _id: 'send-many-messages', roles: ['admin', 'bot', 'app'] }, - { _id: 'set-leader', roles: ['admin', 'owner'] }, - { _id: 'unarchive-room', roles: ['admin'] }, - { _id: 'view-c-room', roles: ['admin', 'user', 'bot', 'app', 'anonymous'] }, - { _id: 'user-generate-access-token', roles: ['admin'] }, - { _id: 'view-d-room', roles: ['admin', 'user', 'bot', 'app', 'guest'] }, - { _id: 'view-full-other-user-info', roles: ['admin'] }, - { _id: 'view-history', roles: ['admin', 'user', 'anonymous'] }, - { _id: 'view-joined-room', roles: ['guest', 'bot', 'app', 'anonymous'] }, - { _id: 'view-join-code', roles: ['admin'] }, - { _id: 'view-logs', roles: ['admin'] }, - { _id: 'view-other-user-channels', roles: ['admin'] }, - { _id: 'view-p-room', roles: ['admin', 'user', 'anonymous', 'guest'] }, - { _id: 'view-privileged-setting', roles: ['admin'] }, - { _id: 'view-room-administration', roles: ['admin'] }, - { _id: 'view-statistics', roles: ['admin'] }, - { _id: 'view-user-administration', roles: ['admin'] }, - { _id: 'preview-c-room', roles: ['admin', 'user', 'anonymous'] }, - { _id: 'view-outside-room', roles: ['admin', 'owner', 'moderator', 'user'] }, - { _id: 'view-broadcast-member-list', roles: ['admin', 'owner', 'moderator'] }, - { _id: 'call-management', roles: ['admin', 'owner', 'moderator'] }, - { _id: 'create-invite-links', roles: ['admin', 'owner', 'moderator'] }, - { - _id: 'view-l-room', - roles: ['livechat-manager', 'livechat-monitor', 'livechat-agent', 'admin'], - }, - { _id: 'view-livechat-manager', roles: ['livechat-manager', 'livechat-monitor', 'admin'] }, - { - _id: 'view-omnichannel-contact-center', - roles: ['livechat-manager', 'livechat-agent', 'livechat-monitor', 'admin'], - }, - { _id: 'edit-omnichannel-contact', roles: ['livechat-manager', 'livechat-agent', 'admin'] }, - { _id: 'view-livechat-rooms', roles: ['livechat-manager', 'livechat-monitor', 'admin'] }, - { - _id: 'close-livechat-room', - roles: ['livechat-manager', 'livechat-monitor', 'livechat-agent', 'admin'], - }, - { _id: 'close-others-livechat-room', roles: ['livechat-manager', 'livechat-monitor', 'admin'] }, - { - _id: 'on-hold-livechat-room', - roles: ['livechat-manager', 'livechat-monitor', 'livechat-agent', 'admin'], - }, - { - _id: 'on-hold-others-livechat-room', - roles: ['livechat-manager', 'livechat-monitor', 'admin'], - }, - { _id: 'save-others-livechat-room-info', roles: ['livechat-manager', 'livechat-monitor', 'admin'] }, - { - _id: 'remove-closed-livechat-rooms', - roles: ['livechat-manager', 'livechat-monitor', 'admin'], - }, - { _id: 'view-livechat-analytics', roles: ['livechat-manager', 'livechat-monitor', 'admin'] }, - { - _id: 'view-livechat-queue', - roles: ['livechat-manager', 'livechat-monitor', 'livechat-agent', 'admin'], - }, - { _id: 'transfer-livechat-guest', roles: ['livechat-manager', 'livechat-monitor', 'admin'] }, - { _id: 'manage-livechat-managers', roles: ['livechat-manager', 'admin'] }, - { _id: 'manage-livechat-agents', roles: ['livechat-manager', 'livechat-monitor', 'admin'] }, - { - _id: 'manage-livechat-departments', - roles: ['livechat-manager', 'livechat-monitor', 'admin'], - }, - { _id: 'view-livechat-departments', roles: ['livechat-manager', 'livechat-monitor', 'admin'] }, - { - _id: 'add-livechat-department-agents', - roles: ['livechat-manager', 'livechat-monitor', 'admin'], - }, - { - _id: 'view-livechat-current-chats', - roles: ['livechat-manager', 'livechat-monitor', 'admin'], - }, - { - _id: 'view-livechat-real-time-monitoring', - roles: ['livechat-manager', 'livechat-monitor', 'admin'], - }, - { _id: 'view-livechat-triggers', roles: ['livechat-manager', 'admin'] }, - { _id: 'view-livechat-customfields', roles: ['livechat-manager', 'admin'] }, - { _id: 'view-livechat-installation', roles: ['livechat-manager', 'admin'] }, - { _id: 'view-livechat-appearance', roles: ['livechat-manager', 'admin'] }, - { _id: 'view-livechat-webhooks', roles: ['livechat-manager', 'admin'] }, - { - _id: 'view-livechat-business-hours', - roles: ['livechat-manager', 'livechat-monitor', 'admin'], - }, - { - _id: 'view-livechat-room-closed-same-department', - roles: ['livechat-manager', 'livechat-monitor', 'admin'], - }, - { - _id: 'view-livechat-room-closed-by-another-agent', - roles: ['livechat-manager', 'livechat-monitor', 'admin'], - }, - { - _id: 'view-livechat-room-customfields', - roles: ['livechat-manager', 'livechat-monitor', 'livechat-agent', 'admin'], - }, - { - _id: 'edit-livechat-room-customfields', - roles: ['livechat-manager', 'livechat-monitor', 'livechat-agent', 'admin'], - }, - { _id: 'send-omnichannel-chat-transcript', roles: ['livechat-manager', 'admin'] }, - { _id: 'mail-messages', roles: ['admin'] }, - { _id: 'toggle-room-e2e-encryption', roles: ['owner', 'admin'] }, - { _id: 'message-impersonate', roles: ['bot', 'app'] }, - { _id: 'create-team', roles: ['admin', 'user'] }, - { _id: 'delete-team', roles: ['admin', 'owner'] }, - { _id: 'convert-team', roles: ['admin', 'owner'] }, - { _id: 'edit-team', roles: ['admin', 'owner'] }, - { _id: 'add-team-member', roles: ['admin', 'owner', 'moderator'] }, - { _id: 'edit-team-member', roles: ['admin', 'owner', 'moderator'] }, - { _id: 'add-team-channel', roles: ['admin', 'owner', 'moderator'] }, - { _id: 'edit-team-channel', roles: ['admin', 'owner', 'moderator'] }, - { _id: 'remove-team-channel', roles: ['admin', 'owner', 'moderator'] }, - { _id: 'view-all-team-channels', roles: ['admin', 'owner'] }, - { _id: 'view-all-teams', roles: ['admin'] }, - { _id: 'remove-closed-livechat-room', roles: ['livechat-manager', 'admin'] }, - { _id: 'remove-livechat-department', roles: ['livechat-manager', 'admin'] }, - - // VOIP Permissions - // allows to manage voip calls configuration - { _id: 'manage-voip-call-settings', roles: ['livechat-manager', 'admin'] }, - { _id: 'manage-voip-contact-center-settings', roles: ['livechat-manager', 'admin'] }, - // allows agent-extension association. - { _id: 'manage-agent-extension-association', roles: ['admin'] }, - { _id: 'view-agent-extension-association', roles: ['livechat-manager', 'admin', 'livechat-agent'] }, - // allows to receive a voip call - { _id: 'inbound-voip-calls', roles: ['livechat-agent'] }, - - { _id: 'remove-livechat-department', roles: ['livechat-manager', 'admin'] }, - { _id: 'manage-apps', roles: ['admin'] }, - { _id: 'post-readonly', roles: ['admin', 'owner', 'moderator'] }, - { _id: 'set-readonly', roles: ['admin', 'owner'] }, - { _id: 'set-react-when-readonly', roles: ['admin', 'owner'] }, - { _id: 'manage-cloud', roles: ['admin'] }, - { _id: 'manage-sounds', roles: ['admin'] }, - { _id: 'access-mailer', roles: ['admin'] }, - { _id: 'pin-message', roles: ['owner', 'moderator', 'admin'] }, - { _id: 'mobile-upload-file', roles: ['user', 'admin'] }, - { _id: 'send-mail', roles: ['admin'] }, - { _id: 'view-federation-data', roles: ['admin'] }, - { _id: 'add-all-to-room', roles: ['admin'] }, - { _id: 'get-server-info', roles: ['admin'] }, - { _id: 'register-on-cloud', roles: ['admin'] }, - { _id: 'test-admin-options', roles: ['admin'] }, - { _id: 'sync-auth-services-users', roles: ['admin'] }, - { _id: 'restart-server', roles: ['admin'] }, - { _id: 'remove-slackbridge-links', roles: ['admin'] }, - { _id: 'view-import-operations', roles: ['admin'] }, - { _id: 'clear-oembed-cache', roles: ['admin'] }, - { _id: 'videoconf-ring-users', roles: ['admin', 'owner', 'moderator', 'user'] }, - { _id: 'view-moderation-console', roles: ['admin'] }, - { _id: 'manage-moderation-actions', roles: ['admin'] }, - { _id: 'bypass-time-limit-edit-and-delete', roles: ['bot', 'app'] }, - ]; - for await (const permission of permissions) { await Permissions.create(permission._id, permission.roles); } diff --git a/apps/meteor/app/importer-pending-avatars/server/importer.js b/apps/meteor/app/importer-pending-avatars/server/importer.js index b85792a406c1..912d253e808c 100644 --- a/apps/meteor/app/importer-pending-avatars/server/importer.js +++ b/apps/meteor/app/importer-pending-avatars/server/importer.js @@ -9,7 +9,7 @@ export class PendingAvatarImporter extends Base { await super.updateProgress(ProgressStep.PREPARING_STARTED); const users = await Users.findAllUsersWithPendingAvatar(); - const fileCount = users.count(); + const fileCount = await users.count(); if (fileCount === 0) { await super.updateProgress(ProgressStep.DONE); diff --git a/apps/meteor/app/importer/server/classes/ImporterBase.js b/apps/meteor/app/importer/server/classes/ImporterBase.js index f61113357385..902d58aa4271 100644 --- a/apps/meteor/app/importer/server/classes/ImporterBase.js +++ b/apps/meteor/app/importer/server/classes/ImporterBase.js @@ -13,7 +13,7 @@ import { ProgressStep } from '../../lib/ImporterProgressStep'; import { ImporterInfo } from '../../lib/ImporterInfo'; import { Logger } from '../../../logger/server'; import { ImportDataConverter } from './ImportDataConverter'; -import { t } from '../../../utils/server'; +import { t } from '../../../utils/lib/i18n'; import { Selection, SelectionChannel, SelectionUser } from '..'; /** diff --git a/apps/meteor/app/livechat/client/startup/notifyUnreadRooms.js b/apps/meteor/app/livechat/client/startup/notifyUnreadRooms.js index 51af1a47aa38..f730925f50d9 100644 --- a/apps/meteor/app/livechat/client/startup/notifyUnreadRooms.js +++ b/apps/meteor/app/livechat/client/startup/notifyUnreadRooms.js @@ -16,7 +16,7 @@ Meteor.startup(() => { return; } - const subs = Subscriptions.find({ t: 'l', ls: { $exists: 0 }, open: true }).count(); + const subs = await Subscriptions.find({ t: 'l', ls: { $exists: 0 }, open: true }).count(); if (subs === 0) { audio && audio.pause(); return; diff --git a/apps/meteor/app/livechat/server/api/v1/room.ts b/apps/meteor/app/livechat/server/api/v1/room.ts index 0da046f35e1b..aa3c125f6b43 100644 --- a/apps/meteor/app/livechat/server/api/v1/room.ts +++ b/apps/meteor/app/livechat/server/api/v1/room.ts @@ -448,7 +448,13 @@ API.v1.addRoute( delete guestData.phone; } - await Promise.allSettled([Livechat.saveGuest(guestData, this.userId), Livechat.saveRoomInfo(roomData)]); + // We want this both operations to be concurrent, so we have to go with Promise.allSettled + const result = await Promise.allSettled([Livechat.saveGuest(guestData, this.userId), Livechat.saveRoomInfo(roomData)]); + + const firstError = result.find((item) => item.status === 'rejected'); + if (firstError) { + throw new Error((firstError as PromiseRejectedResult).reason.error); + } await callbacks.run('livechat.saveInfo', await LivechatRooms.findOneById(roomData._id), { user: this.user, diff --git a/apps/meteor/app/livechat/server/lib/Contacts.ts b/apps/meteor/app/livechat/server/lib/Contacts.ts index 9ca0f2472610..7c2d84c66444 100644 --- a/apps/meteor/app/livechat/server/lib/Contacts.ts +++ b/apps/meteor/app/livechat/server/lib/Contacts.ts @@ -4,6 +4,9 @@ import type { MatchKeysAndValues, OnlyFieldsOfType } from 'mongodb'; import { LivechatVisitors, Users, LivechatRooms, LivechatCustomField, LivechatInquiry, Rooms, Subscriptions } from '@rocket.chat/models'; import type { ILivechatCustomField, ILivechatVisitor, IOmnichannelRoom } from '@rocket.chat/core-typings'; +import { trim } from '../../../../lib/utils/stringUtils'; +import { i18n } from '../../../utils/lib/i18n'; + type RegisterContactProps = { _id?: string; token: string; @@ -68,27 +71,56 @@ export const Contacts = { } } - const allowedCF = await LivechatCustomField.findByScope>('visitor', { projection: { _id: 1 } }) - .map(({ _id }) => _id) - .toArray(); + const allowedCF = LivechatCustomField.findByScope>('visitor', { + projection: { _id: 1, label: 1, regexp: 1, required: 1 }, + }); + + const livechatData: Record = {}; + + for await (const cf of allowedCF) { + if (!customFields.hasOwnProperty(cf._id)) { + if (cf.required) { + throw new Error(i18n.t('error-invalid-custom-field-value', { field: cf.label })); + } + continue; + } + const cfValue: string = trim(customFields[cf._id]); + + if (!cfValue || typeof cfValue !== 'string') { + if (cf.required) { + throw new Error(i18n.t('error-invalid-custom-field-value', { field: cf.label })); + } + continue; + } - const livechatData = Object.keys(customFields) - .filter((key) => allowedCF.includes(key) && customFields[key] !== '' && customFields[key] !== undefined) - .reduce((obj: Record, key) => { - obj[key] = customFields[key]; - return obj; - }, {}); + if (cf.regexp) { + const regex = new RegExp(cf.regexp); + if (!regex.test(cfValue)) { + throw new Error(i18n.t('error-invalid-custom-field-value', { field: cf.label })); + } + } + + livechatData[cf._id] = cfValue; + } + + const fieldsToRemove = { + // if field is explicitely set to empty string, remove + ...(phone === '' && { phone: 1 }), + ...(visitorEmail === '' && { visitorEmails: 1 }), + ...(!contactManager?.username && { contactManager: 1 }), + }; const updateUser: { $set: MatchKeysAndValues; $unset?: OnlyFieldsOfType } = { $set: { token, name, livechatData, + // if phone has some value, set ...(phone && { phone: [{ phoneNumber: phone }] }), ...(visitorEmail && { visitorEmails: [{ address: visitorEmail }] }), ...(contactManager?.username && { contactManager: { username: contactManager.username } }), }, - ...(!contactManager?.username && { $unset: { contactManager: 1 } }), + ...(Object.keys(fieldsToRemove).length && { $unset: fieldsToRemove }), }; await LivechatVisitors.updateOne({ _id: contactId }, updateUser); diff --git a/apps/meteor/app/livechat/server/lib/Helper.js b/apps/meteor/app/livechat/server/lib/Helper.js index e862ec350760..eb57dcf8e697 100644 --- a/apps/meteor/app/livechat/server/lib/Helper.js +++ b/apps/meteor/app/livechat/server/lib/Helper.js @@ -299,7 +299,7 @@ export const dispatchInquiryQueued = async (inquiry, agent) => { return; } - logger.debug(`Notifying ${onlineAgents.count()} agents of new inquiry`); + logger.debug(`Notifying ${await onlineAgents.count()} agents of new inquiry`); const notificationUserName = v && (v.name || v.username); for await (let agent of onlineAgents) { diff --git a/apps/meteor/app/livechat/server/lib/Livechat.js b/apps/meteor/app/livechat/server/lib/Livechat.js index ae39d8a21d87..fca1813820da 100644 --- a/apps/meteor/app/livechat/server/lib/Livechat.js +++ b/apps/meteor/app/livechat/server/lib/Livechat.js @@ -78,7 +78,7 @@ export const Livechat = { if (settings.get('Livechat_assign_new_conversation_to_bot')) { Livechat.logger.debug(`Fetching online bot agents for department ${department}`); const botAgents = await Livechat.getBotAgents(department); - const onlineBots = botAgents.count(); + const onlineBots = await botAgents.count(); Livechat.logger.debug(`Found ${onlineBots} online`); if (onlineBots > 0) { return true; diff --git a/apps/meteor/app/livechat/server/lib/stream/agentStatus.ts b/apps/meteor/app/livechat/server/lib/stream/agentStatus.ts index 00fa1f687557..dc105ef1a00e 100644 --- a/apps/meteor/app/livechat/server/lib/stream/agentStatus.ts +++ b/apps/meteor/app/livechat/server/lib/stream/agentStatus.ts @@ -67,11 +67,11 @@ export const onlineAgents = { try { if (action === 'close') { - return Livechat.closeOpenChats(userId, comment); + return await Livechat.closeOpenChats(userId, comment); } if (action === 'forward') { - return Livechat.forwardOpenChats(userId); + return await Livechat.forwardOpenChats(userId); } } catch (e) { logger.error({ diff --git a/apps/meteor/app/notifications/client/lib/Presence.ts b/apps/meteor/app/notifications/client/lib/Presence.ts index 29e0709c0ac9..0c04a164d936 100644 --- a/apps/meteor/app/notifications/client/lib/Presence.ts +++ b/apps/meteor/app/notifications/client/lib/Presence.ts @@ -1,13 +1,17 @@ import { Meteor } from 'meteor/meteor'; +import type { StreamerEvents } from '@rocket.chat/ui-contexts'; import { Presence, STATUS_MAP } from '../../../../client/lib/presence'; // TODO implement API on Streamer to be able to listen to all streamed data // this is a hacky way to listen to all streamed data from user-presence Streamer -(Meteor as any).StreamerCentral.on('stream-user-presence', (uid: string, args: unknown) => { + +new Meteor.Streamer('user-presence'); + +(Meteor as any).StreamerCentral.on('stream-user-presence', (uid: string, ...args: StreamerEvents['user-presence'][number]['args']) => { if (!Array.isArray(args)) { throw new Error('Presence event must be an array'); } - const [username, status, statusText] = args as [string, number, string | undefined]; - Presence.notify({ _id: uid, username, status: STATUS_MAP[status], statusText }); + const [[username, status, statusText]] = args; + Presence.notify({ _id: uid, username, status: STATUS_MAP[status ?? 0], statusText }); }); diff --git a/apps/meteor/app/notifications/server/lib/Presence.ts b/apps/meteor/app/notifications/server/lib/Presence.ts index 304e533d599a..5f258c05f998 100644 --- a/apps/meteor/app/notifications/server/lib/Presence.ts +++ b/apps/meteor/app/notifications/server/lib/Presence.ts @@ -1,6 +1,7 @@ import { Emitter } from '@rocket.chat/emitter'; import type { IPublication, IStreamerConstructor, Connection, IStreamer } from 'meteor/rocketchat:streamer'; import type { IUser } from '@rocket.chat/core-typings'; +import type { StreamerEvents } from '@rocket.chat/ui-contexts'; type UserPresenceStreamProps = { added: IUser['_id'][]; @@ -9,7 +10,7 @@ type UserPresenceStreamProps = { type UserPresenceStreamArgs = { uid: string; - args: unknown; + args: StreamerEvents['user-presence'][number]['args']; }; const e = new Emitter<{ @@ -97,6 +98,6 @@ export class StreamPresence { } } -export const emit = (uid: string, args: UserPresenceStreamArgs): void => { +export const emit = (uid: string, args: UserPresenceStreamArgs['args']): void => { e.emit(uid, { uid, args }); }; diff --git a/apps/meteor/app/slashcommands-inviteall/server/server.ts b/apps/meteor/app/slashcommands-inviteall/server/server.ts index 8478bbfba003..cd5ed9dc4932 100644 --- a/apps/meteor/app/slashcommands-inviteall/server/server.ts +++ b/apps/meteor/app/slashcommands-inviteall/server/server.ts @@ -57,11 +57,11 @@ function inviteAll(type: T): SlashCommand['callback'] { }); try { - const APIsettings = settings.get('API_User_Limit'); + const APIsettings = settings.get('API_User_Limit'); if (!APIsettings) { return; } - if (cursor.count() > APIsettings) { + if ((await cursor.count()) > APIsettings) { throw new Meteor.Error('error-user-limit-exceeded', 'User Limit Exceeded', { method: 'addAllToRoom', }); diff --git a/apps/meteor/app/theme/client/imports/general/base_old.css b/apps/meteor/app/theme/client/imports/general/base_old.css index bef6aaf8e8c6..7f924a8e1b48 100644 --- a/apps/meteor/app/theme/client/imports/general/base_old.css +++ b/apps/meteor/app/theme/client/imports/general/base_old.css @@ -998,10 +998,10 @@ .rc-old .highlight-text { padding: 0 2px 2px; - color: var(--rcx-color-font-pure-black, #2f343d); + color: var(--rcx-color-font-pure-white, #ffffff); border-radius: var(--border-radius); - background-color: var(--rcx-color-status-background-info, #d1ebfe); + background-color: var(--rcx-color-badge-background-level-4, #f5455c); } @keyframes zoomIn { diff --git a/apps/meteor/app/utils/client/lib/SDKClient.ts b/apps/meteor/app/utils/client/lib/SDKClient.ts index 785d20a93caa..43df65d8d570 100644 --- a/apps/meteor/app/utils/client/lib/SDKClient.ts +++ b/apps/meteor/app/utils/client/lib/SDKClient.ts @@ -1,5 +1,6 @@ import type { RestClientInterface } from '@rocket.chat/api-client'; import type { SDK } from '@rocket.chat/ddp-client/src/DDPSDK'; +import type { ClientStream } from '@rocket.chat/ddp-client/src/types/ClientStream'; import { Emitter } from '@rocket.chat/emitter'; import type { StreamKeys, StreamNames, StreamerCallbackArgs } from '@rocket.chat/ui-contexts/src/ServerContext/streams'; import { DDPCommon } from 'meteor/ddp-common'; @@ -14,12 +15,7 @@ declare module '@rocket.chat/ddp-client/src/DDPSDK' { streamName: N, args: [key: K, ...args: unknown[]], callback: (...args: StreamerCallbackArgs) => void, - ): { - stop: () => void; - ready: () => Promise; - isReady: boolean; - onReady: (cb: () => void) => void; - }; + ): ReturnType; } } @@ -60,11 +56,11 @@ export const createSDK = (rest: RestClientInterface) => { ev.emit(`${msg.collection}/${msg.fields.eventName}`, msg.fields.args); }); - const stream: SDK['stream'] = ( - name: string, - data: [string, ...unknown[]], - cb: (...args: unknown[]) => void, - ): { stop: () => void; ready: () => Promise; isReady: boolean; onReady: (cb: () => void) => void } => { + const stream: SDK['stream'] = >( + name: N, + data: [key: K, ...args: unknown[]], + cb: (...args: StreamerCallbackArgs) => void, + ): ReturnType => { const [key, ...args] = data; const streamName = `stream-${name}`; const streamKey = `${streamName}/${key}`; @@ -75,12 +71,44 @@ export const createSDK = (rest: RestClientInterface) => { ready: false, }; - const onReady = (cb: () => void) => { + const sub = Meteor.connection.subscribe( + streamName, + key, + { useCollection: false, args }, + { + onReady: (args: any) => { + meta.ready = true; + ee.emit('ready', [undefined, args]); + }, + onError: (err: any) => { + console.error(err); + ee.emit('ready', [err]); + }, + }, + ); + + const onChange: ReturnType['onChange'] = (cb) => { if (meta.ready) { - cb(); + cb({ + msg: 'ready', + + subs: [], + }); return; } - ee.once('ready', cb); + ee.once('ready', ([error, result]) => { + if (error) { + cb({ + msg: 'nosub', + + id: '', + error, + }); + return; + } + + cb(result); + }); }; const ready = () => { @@ -92,18 +120,6 @@ export const createSDK = (rest: RestClientInterface) => { }); }; - const sub = Meteor.connection.subscribe( - streamName, - key, - { useCollection: false, args }, - { - onReady: () => { - meta.ready = true; - ee.emit('ready'); - }, - }, - ); - const removeEv = ev.on(`${streamKey}`, (args) => cb(...args)); const stop = () => { @@ -115,9 +131,12 @@ export const createSDK = (rest: RestClientInterface) => { streams.set(`${streamKey}`, stop); return { + id: '', + name, + params: data as any, stop, ready, - onReady, + onChange, get isReady() { return meta.ready; }, diff --git a/apps/meteor/app/utils/lib/i18n.ts b/apps/meteor/app/utils/lib/i18n.ts index d70361d8e026..309585ee284b 100644 --- a/apps/meteor/app/utils/lib/i18n.ts +++ b/apps/meteor/app/utils/lib/i18n.ts @@ -9,9 +9,10 @@ export const addSprinfToI18n = function (t: (typeof i18n)['t']): typeof t & { (key: string, ...replaces: any): string; } { return function (key: string, ...replaces: any): string { - if (isObject(replaces[0])) { + if (replaces[0] === undefined || isObject(replaces[0])) { return t(key, ...replaces); } + return t(key, { postProcess: 'sprintf', sprintf: replaces, diff --git a/apps/meteor/client/components/AdministrationList/AdministrationList.tsx b/apps/meteor/client/components/AdministrationList/AdministrationList.tsx index 1de4319ed62d..919982621eb3 100644 --- a/apps/meteor/client/components/AdministrationList/AdministrationList.tsx +++ b/apps/meteor/client/components/AdministrationList/AdministrationList.tsx @@ -16,20 +16,29 @@ type AdministrationListProps = { }; const ADMIN_PERMISSIONS = [ - 'view-logs', - 'manage-emoji', - 'manage-sounds', 'view-statistics', - 'manage-oauth-apps', - 'view-privileged-setting', - 'manage-selected-settings', - 'view-room-administration', + 'run-import', 'view-user-administration', - 'access-setting-permissions', + 'view-room-administration', + 'create-invite-links', + 'manage-cloud', + 'view-logs', + 'manage-sounds', + 'view-federation-data', + 'manage-email-inbox', + 'manage-emoji', 'manage-outgoing-integrations', - 'manage-incoming-integrations', 'manage-own-outgoing-integrations', + 'manage-incoming-integrations', 'manage-own-incoming-integrations', + 'manage-oauth-apps', + 'access-mailer', + 'manage-user-status', + 'access-permissions', + 'access-setting-permissions', + 'view-privileged-setting', + 'edit-privileged-setting', + 'manage-selected-settings', 'view-engagement-dashboard', 'view-moderation-console', ]; diff --git a/apps/meteor/client/components/AdministrationList/AdministrationModelList.spec.tsx b/apps/meteor/client/components/AdministrationList/AdministrationModelList.spec.tsx index 74d751de4f9d..4d98d15f3e3c 100644 --- a/apps/meteor/client/components/AdministrationList/AdministrationModelList.spec.tsx +++ b/apps/meteor/client/components/AdministrationList/AdministrationModelList.spec.tsx @@ -67,29 +67,13 @@ describe('AdministrationModelList', () => { ); }; - it('should go to admin info', async () => { + it('should go to admin index', async () => { const AdministrationModelList = loadMock(); render(, { wrapper: ProvidersMock }); const button = screen.getByText('Workspace'); - userEvent.click(button); - await waitFor(() => expect(pushRoute).to.have.been.called.with('admin-info')); - await waitFor(() => expect(handleDismiss).to.have.been.called()); - }); - - it('should go to admin index if no permission', async () => { - const AdministrationModelList = loadMock({ - '../../../app/authorization/client': { - userHasAllPermission: () => false, - }, - }); - - render(, { wrapper: ProvidersMock }); - - const button = screen.getByText('Workspace'); - userEvent.click(button); await waitFor(() => expect(pushRoute).to.have.been.called.with('admin-index')); await waitFor(() => expect(handleDismiss).to.have.been.called()); diff --git a/apps/meteor/client/components/AdministrationList/AdministrationModelList.tsx b/apps/meteor/client/components/AdministrationList/AdministrationModelList.tsx index 3c9d7983fffc..3add4ad54128 100644 --- a/apps/meteor/client/components/AdministrationList/AdministrationModelList.tsx +++ b/apps/meteor/client/components/AdministrationList/AdministrationModelList.tsx @@ -5,7 +5,6 @@ import { FlowRouter } from 'meteor/kadira:flow-router'; import type { FC } from 'react'; import React from 'react'; -import { userHasAllPermission } from '../../../app/authorization/client'; import type { AccountBoxItem } from '../../../app/ui-utils/client/lib/AccountBox'; import { getUpgradeTabLabel, isFullyFeature } from '../../../lib/upgradeTab'; import RegisterWorkspaceModal from '../../views/admin/cloud/modals/RegisterWorkspaceModal'; @@ -19,14 +18,11 @@ type AdministrationModelListProps = { onDismiss: () => void; }; -const INFO_PERMISSIONS = ['view-statistics']; - const AdministrationModelList: FC = ({ accountBoxItems, showWorkspace, onDismiss }) => { const t = useTranslation(); const { tabType, trialEndDate, isLoading } = useUpgradeTabParams(); const shouldShowEmoji = isFullyFeature(tabType); const label = getUpgradeTabLabel(tabType); - const hasInfoPermission = userHasAllPermission(INFO_PERMISSIONS); const isAdmin = useRole('admin'); const setModal = useSetModal(); @@ -39,7 +35,6 @@ const AdministrationModelList: FC = ({ accountBoxI setModal(); }; - const infoRoute = useRoute('admin-info'); const adminRoute = useRoute('admin-index'); const upgradeRoute = useRoute('upgrade'); const cloudRoute = useRoute('cloud'); @@ -85,12 +80,6 @@ const AdministrationModelList: FC = ({ accountBoxI role='listitem' text={t('Workspace')} onClick={() => { - if (hasInfoPermission) { - infoRoute.push(); - onDismiss(); - return; - } - adminRoute.push({ context: '/' }); onDismiss(); }} diff --git a/apps/meteor/client/components/Contextualbar/Contextualbar.tsx b/apps/meteor/client/components/Contextualbar/Contextualbar.tsx new file mode 100644 index 000000000000..9908ce2cdfa6 --- /dev/null +++ b/apps/meteor/client/components/Contextualbar/Contextualbar.tsx @@ -0,0 +1,17 @@ +import { Contextualbar as ContextualbarComponent } from '@rocket.chat/fuselage'; +import { useLayoutSizes, useLayoutContextualBarPosition } from '@rocket.chat/ui-contexts'; +import type { FC, ComponentProps } from 'react'; +import React, { memo } from 'react'; + +const Contextualbar: FC> = ({ children, bg = 'room', ...props }) => { + const sizes = useLayoutSizes(); + const position = useLayoutContextualBarPosition(); + + return ( + + {children} + + ); +}; + +export default memo(Contextualbar); diff --git a/apps/meteor/client/components/Contextualbar/ContextualbarBack.tsx b/apps/meteor/client/components/Contextualbar/ContextualbarBack.tsx new file mode 100644 index 000000000000..c2ae717eda33 --- /dev/null +++ b/apps/meteor/client/components/Contextualbar/ContextualbarBack.tsx @@ -0,0 +1,13 @@ +import { ContextualbarAction } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import type { ReactElement, ComponentProps } from 'react'; +import React, { memo } from 'react'; + +type ContextualbarBackProps = Partial>; + +const ContextualbarBack = (props: ContextualbarBackProps): ReactElement => { + const t = useTranslation(); + return ; +}; + +export default memo(ContextualbarBack); diff --git a/apps/meteor/client/components/Contextualbar/ContextualbarClose.tsx b/apps/meteor/client/components/Contextualbar/ContextualbarClose.tsx new file mode 100644 index 000000000000..18e83c50c628 --- /dev/null +++ b/apps/meteor/client/components/Contextualbar/ContextualbarClose.tsx @@ -0,0 +1,13 @@ +import { ContextualbarAction } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import type { ComponentProps, ReactElement } from 'react'; +import React, { memo } from 'react'; + +type ContextualbarCloseProps = Partial>; + +const ContextualbarClose = (props: ContextualbarCloseProps): ReactElement => { + const t = useTranslation(); + return ; +}; + +export default memo(ContextualbarClose); diff --git a/apps/meteor/client/components/Contextualbar/ContextualbarHeader.tsx b/apps/meteor/client/components/Contextualbar/ContextualbarHeader.tsx new file mode 100644 index 000000000000..d757cccce0ef --- /dev/null +++ b/apps/meteor/client/components/Contextualbar/ContextualbarHeader.tsx @@ -0,0 +1,16 @@ +import { ContextualbarHeader as ContextualbarHeaderComponent } from '@rocket.chat/fuselage'; +import type { FC, ReactNode, ComponentProps } from 'react'; +import React, { memo } from 'react'; + +type ContextualbarHeaderProps = { + expanded?: boolean; + children: ReactNode; +} & ComponentProps; + +const ContextualbarHeader: FC = ({ children, expanded, ...props }) => ( + + {children} + +); + +export default memo(ContextualbarHeader); diff --git a/apps/meteor/client/components/VerticalBar/VerticalBarInnerContent.tsx b/apps/meteor/client/components/Contextualbar/ContextualbarInnerContent.tsx similarity index 65% rename from apps/meteor/client/components/VerticalBar/VerticalBarInnerContent.tsx rename to apps/meteor/client/components/Contextualbar/ContextualbarInnerContent.tsx index d32936817555..69c59f3b5db4 100644 --- a/apps/meteor/client/components/VerticalBar/VerticalBarInnerContent.tsx +++ b/apps/meteor/client/components/Contextualbar/ContextualbarInnerContent.tsx @@ -2,8 +2,8 @@ import { Box } from '@rocket.chat/fuselage'; import type { ReactElement, ComponentProps } from 'react'; import React, { memo } from 'react'; -const VerticalBarInnerContent = (props: ComponentProps): ReactElement => ( +const ContextualbarInnerContent = (props: ComponentProps): ReactElement => ( ); -export default memo(VerticalBarInnerContent); +export default memo(ContextualbarInnerContent); diff --git a/apps/meteor/client/components/VerticalBar/VerticalBarScrollableContent.tsx b/apps/meteor/client/components/Contextualbar/ContextualbarScrollableContent.tsx similarity index 58% rename from apps/meteor/client/components/VerticalBar/VerticalBarScrollableContent.tsx rename to apps/meteor/client/components/Contextualbar/ContextualbarScrollableContent.tsx index aeca831452a0..ade28661e87b 100644 --- a/apps/meteor/client/components/VerticalBar/VerticalBarScrollableContent.tsx +++ b/apps/meteor/client/components/Contextualbar/ContextualbarScrollableContent.tsx @@ -4,8 +4,8 @@ import React, { forwardRef, memo } from 'react'; import Page from '../Page'; -const VerticalBarScrollableContent = forwardRef>( - function VerticalBarScrollableContent({ children, ...props }, ref) { +const ContextualbarScrollableContent = forwardRef>( + function ContextualbarScrollableContent({ children, ...props }, ref) { return ( {children} @@ -14,4 +14,4 @@ const VerticalBarScrollableContent = forwardRef { + return ( + + + {title} + {subTitle && {subTitle}} + + {value}/{max} + + + + + ); +}; + +export default GenericResourceUsage; diff --git a/apps/meteor/client/components/GenericResourceUsage/GenericResourceUsageSkeleton.tsx b/apps/meteor/client/components/GenericResourceUsage/GenericResourceUsageSkeleton.tsx new file mode 100644 index 000000000000..9224fcd634de --- /dev/null +++ b/apps/meteor/client/components/GenericResourceUsage/GenericResourceUsageSkeleton.tsx @@ -0,0 +1,13 @@ +import { Box, Skeleton } from '@rocket.chat/fuselage'; +import React from 'react'; + +const GenericResourceUsageSkeleton = ({ title, ...props }: { title?: string }) => { + return ( + + {title ? {title} : } + + + ); +}; + +export default GenericResourceUsageSkeleton; diff --git a/apps/meteor/client/components/GenericResourceUsage/index.ts b/apps/meteor/client/components/GenericResourceUsage/index.ts new file mode 100644 index 000000000000..537183330a16 --- /dev/null +++ b/apps/meteor/client/components/GenericResourceUsage/index.ts @@ -0,0 +1,4 @@ +import GenericResourceUsage from './GenericResourceUsage'; +import GenericResourceUsageSkeleton from './GenericResourceUsageSkeleton'; + +export { GenericResourceUsage, GenericResourceUsageSkeleton }; diff --git a/apps/meteor/client/components/InfoPanel/InfoPanel.stories.tsx b/apps/meteor/client/components/InfoPanel/InfoPanel.stories.tsx index d49972df801c..9d29007f9b90 100644 --- a/apps/meteor/client/components/InfoPanel/InfoPanel.stories.tsx +++ b/apps/meteor/client/components/InfoPanel/InfoPanel.stories.tsx @@ -58,7 +58,7 @@ export const Default: ComponentStory = () => ( ); Default.storyName = 'InfoPanel'; -// export const Archived = () => +// export const Archived = () => // -// ; +// ; -// export const Broadcast = () => +// export const Broadcast = () => // -// ; +// ; diff --git a/apps/meteor/client/components/LoadingIndicator.tsx b/apps/meteor/client/components/LoadingIndicator.tsx new file mode 100644 index 000000000000..26127b122d40 --- /dev/null +++ b/apps/meteor/client/components/LoadingIndicator.tsx @@ -0,0 +1,12 @@ +import { Box, Throbber } from '@rocket.chat/fuselage'; +import React from 'react'; + +const LoadingIndicator = () => { + return ( + + + + ); +}; + +export default LoadingIndicator; diff --git a/apps/meteor/client/components/Page/PageHeader.tsx b/apps/meteor/client/components/Page/PageHeader.tsx index 916939b7bde7..b9774bb5a26d 100644 --- a/apps/meteor/client/components/Page/PageHeader.tsx +++ b/apps/meteor/client/components/Page/PageHeader.tsx @@ -22,6 +22,7 @@ const PageHeader: FC = ({ children = undefined, title, onClickB @@ -30,7 +31,7 @@ const PageHeader: FC = ({ children = undefined, title, onClickB marginInline='x24' display='flex' flexDirection='row' - flexWrap='nowrap' + flexWrap='wrap' alignItems='center' color='default' {...props} diff --git a/apps/meteor/client/components/UserInfo/UserInfo.stories.tsx b/apps/meteor/client/components/UserInfo/UserInfo.stories.tsx index 6e07b44f3fa8..5eb59c687a14 100644 --- a/apps/meteor/client/components/UserInfo/UserInfo.stories.tsx +++ b/apps/meteor/client/components/UserInfo/UserInfo.stories.tsx @@ -1,8 +1,8 @@ import type { ComponentMeta, ComponentStory } from '@storybook/react'; import React from 'react'; +import { Contextualbar } from '../Contextualbar'; import * as Status from '../UserStatus'; -import VerticalBar from '../VerticalBar'; import UserInfo from './UserInfo'; export default { @@ -12,7 +12,7 @@ export default { layout: 'fullscreen', actions: { argTypesRegex: '^on.*' }, }, - decorators: [(fn) => {fn()}], + decorators: [(fn) => {fn()}], } as ComponentMeta; const Template: ComponentStory = (args) => ; diff --git a/apps/meteor/client/components/UserInfo/UserInfo.tsx b/apps/meteor/client/components/UserInfo/UserInfo.tsx index 7f721a401ec4..142f31026d6e 100644 --- a/apps/meteor/client/components/UserInfo/UserInfo.tsx +++ b/apps/meteor/client/components/UserInfo/UserInfo.tsx @@ -8,11 +8,11 @@ import React, { memo } from 'react'; import { useTimeAgo } from '../../hooks/useTimeAgo'; import { useUserCustomFields } from '../../hooks/useUserCustomFields'; import { useUserDisplayName } from '../../hooks/useUserDisplayName'; +import { ContextualbarScrollableContent } from '../Contextualbar'; import InfoPanel from '../InfoPanel'; import MarkdownText from '../MarkdownText'; import UTCClock from '../UTCClock'; import UserCard from '../UserCard'; -import VerticalBar from '../VerticalBar'; import UserInfoAvatar from './UserInfoAvatar'; type UserInfoDataProps = Serialized< @@ -67,7 +67,7 @@ const UserInfo = ({ const userCustomFields = useUserCustomFields(customFields); return ( - + {username && ( @@ -183,7 +183,7 @@ const UserInfo = ({ )} - + ); }; diff --git a/apps/meteor/client/components/VerticalBar/VerticalBar.tsx b/apps/meteor/client/components/VerticalBar/VerticalBar.tsx deleted file mode 100644 index af000dbf6e07..000000000000 --- a/apps/meteor/client/components/VerticalBar/VerticalBar.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { Box } from '@rocket.chat/fuselage'; -import { useLayoutSizes, useLayoutContextualBarPosition } from '@rocket.chat/ui-contexts'; -import type { FC, ComponentProps } from 'react'; -import React, { memo } from 'react'; - -const VerticalBar: FC> = ({ children, bg = 'room', ...props }) => { - const sizes = useLayoutSizes(); - const position = useLayoutContextualBarPosition(); - return ( - - {children} - - ); -}; - -export default memo(VerticalBar); diff --git a/apps/meteor/client/components/VerticalBar/VerticalBarAction.tsx b/apps/meteor/client/components/VerticalBar/VerticalBarAction.tsx deleted file mode 100644 index afe5b5b15b40..000000000000 --- a/apps/meteor/client/components/VerticalBar/VerticalBarAction.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import type { Icon } from '@rocket.chat/fuselage'; -import { IconButton } from '@rocket.chat/fuselage'; -import type { ReactElement, MouseEventHandler, ComponentProps } from 'react'; -import React, { memo } from 'react'; - -type VerticalBarActionProps = { - name: ComponentProps['name']; - title?: string; - disabled?: boolean; - onClick?: MouseEventHandler; -}; - -const VerticalBarAction = ({ name, ...props }: VerticalBarActionProps): ReactElement => ( - -); - -export default memo(VerticalBarAction); diff --git a/apps/meteor/client/components/VerticalBar/VerticalBarActions.tsx b/apps/meteor/client/components/VerticalBar/VerticalBarActions.tsx deleted file mode 100644 index 8615eea2917b..000000000000 --- a/apps/meteor/client/components/VerticalBar/VerticalBarActions.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { ButtonGroup } from '@rocket.chat/fuselage'; -import type { ReactElement, ComponentProps } from 'react'; -import React, { memo } from 'react'; - -const VerticalBarActions = (props: ComponentProps): ReactElement => ; - -export default memo(VerticalBarActions); diff --git a/apps/meteor/client/components/VerticalBar/VerticalBarBack.tsx b/apps/meteor/client/components/VerticalBar/VerticalBarBack.tsx deleted file mode 100644 index 14be4e246cfe..000000000000 --- a/apps/meteor/client/components/VerticalBar/VerticalBarBack.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { useTranslation } from '@rocket.chat/ui-contexts'; -import type { ReactElement, ComponentProps } from 'react'; -import React, { memo } from 'react'; - -import VerticalBarAction from './VerticalBarAction'; - -type VerticalBarBackProps = Partial>; - -const VerticalBarBack = (props: VerticalBarBackProps): ReactElement => { - const t = useTranslation(); - return ; -}; - -export default memo(VerticalBarBack); diff --git a/apps/meteor/client/components/VerticalBar/VerticalBarButton.tsx b/apps/meteor/client/components/VerticalBar/VerticalBarButton.tsx deleted file mode 100644 index ced9e4b6bee9..000000000000 --- a/apps/meteor/client/components/VerticalBar/VerticalBarButton.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { Button } from '@rocket.chat/fuselage'; -import type { ComponentProps, ReactElement } from 'react'; -import React, { memo } from 'react'; - -const VerticalBarButton = (props: ComponentProps): ReactElement => ( - - + ); }; diff --git a/apps/meteor/client/views/admin/customSounds/AddCustomSound.tsx b/apps/meteor/client/views/admin/customSounds/AddCustomSound.tsx index 84743920d837..986422ea7c7c 100644 --- a/apps/meteor/client/views/admin/customSounds/AddCustomSound.tsx +++ b/apps/meteor/client/views/admin/customSounds/AddCustomSound.tsx @@ -3,7 +3,7 @@ import { useToastMessageDispatch, useMethod, useTranslation } from '@rocket.chat import type { ReactElement, FormEvent } from 'react'; import React, { useState, useCallback } from 'react'; -import VerticalBar from '../../../components/VerticalBar'; +import { ContextualbarScrollableContent } from '../../../components/Contextualbar'; import { useFileInput } from '../../../hooks/useFileInput'; import { validate, createSoundData } from './lib'; @@ -81,7 +81,7 @@ const AddCustomSound = ({ goToNew, close, onChange, ...props }: AddCustomSoundPr }, [dispatchToastMessage, goToNew, name, onChange, saveAction, sound, t]); return ( - + {t('Name')} @@ -115,7 +115,7 @@ const AddCustomSound = ({ goToNew, close, onChange, ...props }: AddCustomSoundPr - + ); }; diff --git a/apps/meteor/client/views/admin/customSounds/CustomSoundsRoute.tsx b/apps/meteor/client/views/admin/customSounds/CustomSoundsRoute.tsx index db39c718006d..2b36efc643df 100644 --- a/apps/meteor/client/views/admin/customSounds/CustomSoundsRoute.tsx +++ b/apps/meteor/client/views/admin/customSounds/CustomSoundsRoute.tsx @@ -6,6 +6,7 @@ import { useQuery } from '@tanstack/react-query'; import type { ReactElement } from 'react'; import React, { useMemo, useState, useCallback } from 'react'; +import { ContextualbarTitle, Contextualbar, ContextualbarClose, ContextualbarHeader } from '../../../components/Contextualbar'; import FilterByText from '../../../components/FilterByText'; import { GenericTable } from '../../../components/GenericTable/V2/GenericTable'; import { GenericTableBody } from '../../../components/GenericTable/V2/GenericTableBody'; @@ -15,7 +16,6 @@ import { GenericTableLoadingTable } from '../../../components/GenericTable/V2/Ge import { usePagination } from '../../../components/GenericTable/hooks/usePagination'; import { useSort } from '../../../components/GenericTable/hooks/useSort'; import Page from '../../../components/Page'; -import VerticalBar from '../../../components/VerticalBar'; import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; import AddCustomSound from './AddCustomSound'; import CustomSoundRow from './CustomSoundRow'; @@ -159,15 +159,15 @@ const CustomSoundsRoute = (): ReactElement => { {context && ( - - - {context === 'edit' && {t('Custom_Sound_Edit')}} - {context === 'new' && {t('Custom_Sound_Add')}} - - + + + {context === 'edit' && {t('Custom_Sound_Edit')}} + {context === 'new' && {t('Custom_Sound_Add')}} + + {context === 'edit' && } {context === 'new' && } - + )} ); diff --git a/apps/meteor/client/views/admin/customSounds/EditSound.tsx b/apps/meteor/client/views/admin/customSounds/EditSound.tsx index a48e0b1f0d6c..fdf45739cf7a 100644 --- a/apps/meteor/client/views/admin/customSounds/EditSound.tsx +++ b/apps/meteor/client/views/admin/customSounds/EditSound.tsx @@ -3,8 +3,8 @@ import { useSetModal, useToastMessageDispatch, useMethod, useTranslation } from import type { ReactElement, SyntheticEvent } from 'react'; import React, { useCallback, useState, useMemo, useEffect } from 'react'; +import { ContextualbarScrollableContent } from '../../../components/Contextualbar'; import GenericModal from '../../../components/GenericModal'; -import VerticalBar from '../../../components/VerticalBar'; import { useFileInput } from '../../../hooks/useFileInput'; import { validate, createSoundData } from './lib'; @@ -117,7 +117,7 @@ function EditSound({ close, onChange, data, ...props }: EditSoundProps): ReactEl const [clickUpload] = useFileInput(handleChangeFile, 'audio/mp3'); return ( - + {t('Name')} @@ -159,7 +159,7 @@ function EditSound({ close, onChange, data, ...props }: EditSoundProps): ReactEl - + ); } diff --git a/apps/meteor/client/views/admin/customUserStatus/CustomUserActiveConnections.tsx b/apps/meteor/client/views/admin/customUserStatus/CustomUserActiveConnections.tsx index 7e844c2b0ab5..e87107d0c263 100644 --- a/apps/meteor/client/views/admin/customUserStatus/CustomUserActiveConnections.tsx +++ b/apps/meteor/client/views/admin/customUserStatus/CustomUserActiveConnections.tsx @@ -1,7 +1,8 @@ -import { Box, ProgressBar, Skeleton } from '@rocket.chat/fuselage'; +/* eslint-disable react/no-multi-comp */ import { useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; +import { GenericResourceUsage, GenericResourceUsageSkeleton } from '../../../components/GenericResourceUsage'; import { useActiveConnections } from './hooks/useActiveConnections'; const CustomUserActiveConnections = () => { @@ -10,27 +11,12 @@ const CustomUserActiveConnections = () => { const result = useActiveConnections(); if (result.isLoading || result.isError) { - return ( - - {t('Active_connections')} - - - ); + return ; } const { current, max, percentage } = result.data; - return ( - - - {t('Active_connections')} - - {current}/{max} - - - - - ); + return ; }; export default CustomUserActiveConnections; diff --git a/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusForm.tsx b/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusForm.tsx index b328cdc0170e..2c21db402199 100644 --- a/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusForm.tsx +++ b/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusForm.tsx @@ -6,8 +6,8 @@ import type { ReactElement } from 'react'; import React, { useCallback } from 'react'; import { useForm, Controller } from 'react-hook-form'; +import { ContextualbarScrollableContent } from '../../../components/Contextualbar'; import GenericModal from '../../../components/GenericModal'; -import VerticalBar from '../../../components/VerticalBar'; type CustomUserStatusFormProps = { onClose: () => void; @@ -86,7 +86,7 @@ const CustomUserStatusForm = ({ onClose, onReload, status }: CustomUserStatusFor ]; return ( - + {t('Name')} @@ -130,7 +130,7 @@ const CustomUserStatusForm = ({ onClose, onReload, status }: CustomUserStatusFor )} - + ); }; diff --git a/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusRoute.tsx b/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusRoute.tsx index d740c00a3b04..ef73d87a0fa9 100644 --- a/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusRoute.tsx +++ b/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusRoute.tsx @@ -3,8 +3,8 @@ import { useRoute, useRouteParameter, usePermission, useTranslation, useSetting import type { ReactElement } from 'react'; import React, { useCallback, useRef, useEffect } from 'react'; +import { Contextualbar, ContextualbarHeader, ContextualbarClose } from '../../../components/Contextualbar'; import Page from '../../../components/Page'; -import VerticalBar from '../../../components/VerticalBar'; import { useIsEnterprise } from '../../../hooks/useIsEnterprise'; import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; import CustomUserActiveConnections from './CustomUserActiveConnections'; @@ -69,18 +69,18 @@ const CustomUserStatusRoute = (): ReactElement => { {context && ( - - + + {context === 'edit' && t('Custom_User_Status_Edit')} {context === 'new' && t('Custom_User_Status_Add')} {context === 'presence-service' && t('Presence_service_cap')} - - + + {context === 'presence-service' && } {(context === 'new' || context === 'edit') && ( )} - + )} ); diff --git a/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusService.tsx b/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusService.tsx index 11511031a85a..fbcfb1118308 100644 --- a/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusService.tsx +++ b/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusService.tsx @@ -16,7 +16,7 @@ import { useEndpoint, useSetting, useTranslation } from '@rocket.chat/ui-context import { useMutation } from '@tanstack/react-query'; import React from 'react'; -import VerticalBar from '../../../components/VerticalBar'; +import { ContextualbarContent, ContextualbarFooter } from '../../../components/Contextualbar'; import { useIsEnterprise } from '../../../hooks/useIsEnterprise'; import { useActiveConnections } from './hooks/useActiveConnections'; @@ -55,7 +55,7 @@ const CustomUserStatusService = () => { return ( <> - +
{t('Service_status')} @@ -102,15 +102,15 @@ const CustomUserStatusService = () => { )} - + {!license?.isEnterprise && ( - + - + )} ); diff --git a/apps/meteor/client/views/admin/moderation/MessageReportInfo.tsx b/apps/meteor/client/views/admin/moderation/MessageReportInfo.tsx index 8f956b710eb7..aaeae2d05bee 100644 --- a/apps/meteor/client/views/admin/moderation/MessageReportInfo.tsx +++ b/apps/meteor/client/views/admin/moderation/MessageReportInfo.tsx @@ -4,7 +4,7 @@ import { useQuery } from '@tanstack/react-query'; import React from 'react'; import { getUserDisplayName } from '../../../../lib/getUserDisplayName'; -import VerticalBar from '../../../components/VerticalBar'; +import { ContextualbarHeader, ContextualbarBack, ContextualbarTitle, ContextualbarClose } from '../../../components/Contextualbar'; import UserAvatar from '../../../components/avatar/UserAvatar'; import { useFormatDate } from '../../../hooks/useFormatDate'; import { useFormatDateAndTime } from '../../../hooks/useFormatDateAndTime'; @@ -59,11 +59,11 @@ const MessageReportInfo = ({ msgId }: { msgId: string }): JSX.Element => { return ( <> - - window.history.go(-1)} /> - {t('Report')} - moderationRoute.push({})} /> - + + window.history.go(-1)} /> + {t('Report')} + moderationRoute.push({})} /> + {isSuccessReportsByMessage && reportsByMessage?.reports && ( {reports.map((report) => ( diff --git a/apps/meteor/client/views/admin/moderation/ModerationConsolePage.tsx b/apps/meteor/client/views/admin/moderation/ModerationConsolePage.tsx index 27b9d80be603..34002c31b36f 100644 --- a/apps/meteor/client/views/admin/moderation/ModerationConsolePage.tsx +++ b/apps/meteor/client/views/admin/moderation/ModerationConsolePage.tsx @@ -2,8 +2,8 @@ import { useTranslation, useRouteParameter, useToastMessageDispatch } from '@roc import React from 'react'; import { MessageAction } from '../../../../app/ui-utils/client'; +import { Contextualbar } from '../../../components/Contextualbar'; import Page from '../../../components/Page'; -import VerticalBar from '../../../components/VerticalBar'; import MessageReportInfo from './MessageReportInfo'; import ModerationConsoleTable from './ModerationConsoleTable'; import UserMessages from './UserMessages'; @@ -33,10 +33,10 @@ const ModerationConsolePage = () => { {context && ( - + {context === 'info' && id && } {context === 'reports' && id && } - + )} ); diff --git a/apps/meteor/client/views/admin/moderation/UserMessages.tsx b/apps/meteor/client/views/admin/moderation/UserMessages.tsx index 9b75ab90ac7a..9556eff4acc9 100644 --- a/apps/meteor/client/views/admin/moderation/UserMessages.tsx +++ b/apps/meteor/client/views/admin/moderation/UserMessages.tsx @@ -4,7 +4,7 @@ import { useEndpoint, useRoute, useToastMessageDispatch, useTranslation } from ' import { useQuery } from '@tanstack/react-query'; import React, { useMemo } from 'react'; -import VerticalBar from '../../../components/VerticalBar'; +import { ContextualbarHeader, ContextualbarTitle, ContextualbarClose, ContextualbarFooter } from '../../../components/Contextualbar'; import { useUserDisplayName } from '../../../hooks/useUserDisplayName'; import MessageContextFooter from './MessageContextFooter'; import ContextMessage from './helpers/ContextMessage'; @@ -71,10 +71,10 @@ const UserMessages = ({ userId, onRedirect }: { userId: string; onRedirect: (mid return ( <> - - {t('Moderation_Message_context_header', { displayName })} - moderationRoute.push({})} /> - + + {t('Moderation_Message_context_header', { displayName })} + moderationRoute.push({})} /> + {isSuccessUserMessages && userMessages.messages.length > 0 && ( @@ -102,9 +102,9 @@ const UserMessages = ({ userId, onRedirect }: { userId: string; onRedirect: (mid )} - + {isSuccessUserMessages && userMessages.messages.length > 0 && } - + ); }; diff --git a/apps/meteor/client/views/admin/oauthApps/EditOauthApp.tsx b/apps/meteor/client/views/admin/oauthApps/EditOauthApp.tsx index 77879cc3d793..bf361352fcde 100644 --- a/apps/meteor/client/views/admin/oauthApps/EditOauthApp.tsx +++ b/apps/meteor/client/views/admin/oauthApps/EditOauthApp.tsx @@ -6,8 +6,8 @@ import React, { useCallback, useMemo } from 'react'; import type { SubmitHandler } from 'react-hook-form'; import { useForm, Controller } from 'react-hook-form'; +import { ContextualbarScrollableContent } from '../../../components/Contextualbar'; import GenericModal from '../../../components/GenericModal'; -import VerticalBar from '../../../components/VerticalBar'; type EditOAuthAddAppPayload = { name: string; @@ -18,7 +18,7 @@ type EditOAuthAddAppPayload = { type EditOauthAppProps = { onChange: () => void; data: Serialized; -} & Omit, 'data'>; +} & Omit, 'data'>; const EditOauthApp = ({ onChange, data, ...props }: EditOauthAppProps): ReactElement => { const t = useTranslation(); @@ -93,7 +93,7 @@ const EditOauthApp = ({ onChange, data, ...props }: EditOauthAppProps): ReactEle )); return ( - + @@ -167,7 +167,7 @@ const EditOauthApp = ({ onChange, data, ...props }: EditOauthAppProps): ReactEle - + ); }; diff --git a/apps/meteor/client/views/admin/oauthApps/OAuthAddApp.tsx b/apps/meteor/client/views/admin/oauthApps/OAuthAddApp.tsx index 6df2def0e0d5..b663517676ff 100644 --- a/apps/meteor/client/views/admin/oauthApps/OAuthAddApp.tsx +++ b/apps/meteor/client/views/admin/oauthApps/OAuthAddApp.tsx @@ -5,7 +5,7 @@ import React, { useCallback } from 'react'; import type { SubmitHandler } from 'react-hook-form'; import { useForm, Controller } from 'react-hook-form'; -import VerticalBar from '../../../components/VerticalBar'; +import { ContextualbarScrollableContent } from '../../../components/Contextualbar'; type OAuthAddAppPayload = { name: string; @@ -41,7 +41,7 @@ const OAuthAddApp = (): ReactElement => { }; return ( - + @@ -81,7 +81,7 @@ const OAuthAddApp = (): ReactElement => { - + ); }; diff --git a/apps/meteor/client/views/admin/permissions/EditRolePage.tsx b/apps/meteor/client/views/admin/permissions/EditRolePage.tsx index 3dd9fc586328..81f6e3ef7fc9 100644 --- a/apps/meteor/client/views/admin/permissions/EditRolePage.tsx +++ b/apps/meteor/client/views/admin/permissions/EditRolePage.tsx @@ -6,8 +6,8 @@ import type { ReactElement } from 'react'; import React from 'react'; import { FormProvider, useForm } from 'react-hook-form'; +import { ContextualbarFooter, ContextualbarScrollableContent } from '../../../components/Contextualbar'; import GenericModal from '../../../components/GenericModal'; -import VerticalBar from '../../../components/VerticalBar'; import RoleForm from './RoleForm'; const EditRolePage = ({ role, isEnterprise }: { role?: IRole; isEnterprise: boolean }): ReactElement => { @@ -91,7 +91,7 @@ const EditRolePage = ({ role, isEnterprise }: { role?: IRole; isEnterprise: bool return ( <> - + @@ -99,8 +99,8 @@ const EditRolePage = ({ role, isEnterprise }: { role?: IRole; isEnterprise: bool - - + + } - + ); }; diff --git a/apps/meteor/client/views/admin/permissions/PermissionsContextBar.tsx b/apps/meteor/client/views/admin/permissions/PermissionsContextBar.tsx index cbede6b0df61..442a03beef98 100644 --- a/apps/meteor/client/views/admin/permissions/PermissionsContextBar.tsx +++ b/apps/meteor/client/views/admin/permissions/PermissionsContextBar.tsx @@ -3,7 +3,7 @@ import { useRouteParameter, useRoute, useTranslation, useSetModal } from '@rocke import type { ReactElement } from 'react'; import React, { useEffect } from 'react'; -import VerticalBar from '../../../components/VerticalBar'; +import { Contextualbar, ContextualbarHeader, ContextualbarTitle, ContextualbarClose } from '../../../components/Contextualbar'; import { useIsEnterprise } from '../../../hooks/useIsEnterprise'; import CustomRoleUpsellModal from './CustomRoleUpsellModal'; import EditRolePageWithData from './EditRolePageWithData'; @@ -17,7 +17,7 @@ const PermissionsContextBar = (): ReactElement | null => { const { data } = useIsEnterprise(); const isEnterprise = !!data?.isEnterprise; - const handleCloseVerticalBar = useMutableCallback(() => { + const handleCloseContextualbar = useMutableCallback(() => { router.push({}); }); @@ -27,18 +27,18 @@ const PermissionsContextBar = (): ReactElement | null => { } setModal( setModal()} />); - handleCloseVerticalBar(); - }, [context, isEnterprise, handleCloseVerticalBar, setModal]); + handleCloseContextualbar(); + }, [context, isEnterprise, handleCloseContextualbar, setModal]); return ( (context && ( - - - {context === 'edit' ? t('Role_Editing') : t('New_role')} - - + + + {context === 'edit' ? t('Role_Editing') : t('New_role')} + + - + )) || null ); diff --git a/apps/meteor/client/views/admin/rooms/EditRoom.tsx b/apps/meteor/client/views/admin/rooms/EditRoom.tsx index 616a1fbd716d..f8dd530f17ed 100644 --- a/apps/meteor/client/views/admin/rooms/EditRoom.tsx +++ b/apps/meteor/client/views/admin/rooms/EditRoom.tsx @@ -7,8 +7,8 @@ import type { ReactElement } from 'react'; import React, { useState, useMemo } from 'react'; import { RoomSettingsEnum } from '../../../../definition/IRoomTypeConfig'; +import { ContextualbarScrollableContent } from '../../../components/Contextualbar'; import GenericModal from '../../../components/GenericModal'; -import VerticalBar from '../../../components/VerticalBar'; import RoomAvatarEditor from '../../../components/avatar/RoomAvatarEditor'; import { useEndpointAction } from '../../../hooks/useEndpointAction'; import { useForm } from '../../../hooks/useForm'; @@ -208,7 +208,7 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => }); return ( - e.preventDefault())}> + e.preventDefault())}> {room.t !== 'd' && ( @@ -357,7 +357,7 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => - + ); }; diff --git a/apps/meteor/client/views/admin/rooms/RoomsPage.tsx b/apps/meteor/client/views/admin/rooms/RoomsPage.tsx index 6694d64f0891..08d384325505 100644 --- a/apps/meteor/client/views/admin/rooms/RoomsPage.tsx +++ b/apps/meteor/client/views/admin/rooms/RoomsPage.tsx @@ -2,8 +2,8 @@ import { useRouteParameter, useRoute, useTranslation } from '@rocket.chat/ui-con import type { ReactElement } from 'react'; import React, { useRef } from 'react'; +import { Contextualbar, ContextualbarHeader, ContextualbarTitle, ContextualbarClose } from '../../../components/Contextualbar'; import Page from '../../../components/Page'; -import VerticalBar from '../../../components/VerticalBar'; import EditRoomContextBar from './EditRoomContextBar'; import RoomsTable from './RoomsTable'; @@ -15,7 +15,7 @@ const RoomsPage = (): ReactElement => { const roomsRoute = useRoute('admin-rooms'); - const handleVerticalBarCloseButtonClick = (): void => { + const handleContextualbarCloseButtonClick = (): void => { roomsRoute.push({}); }; @@ -30,13 +30,13 @@ const RoomsPage = (): ReactElement => { {context && ( - - - {t('Room_Info')} - - + + + {t('Room_Info')} + + - + )} ); diff --git a/apps/meteor/client/views/admin/users/AdminUserInfoWithData.tsx b/apps/meteor/client/views/admin/users/AdminUserInfoWithData.tsx index 87d730bd3c01..1f55fb4aee35 100644 --- a/apps/meteor/client/views/admin/users/AdminUserInfoWithData.tsx +++ b/apps/meteor/client/views/admin/users/AdminUserInfoWithData.tsx @@ -8,11 +8,11 @@ import type { ReactElement } from 'react'; import React, { useMemo } from 'react'; import { getUserEmailAddress } from '../../../../lib/getUserEmailAddress'; +import { ContextualbarContent } from '../../../components/Contextualbar'; import { FormSkeleton } from '../../../components/Skeleton'; import UserCard from '../../../components/UserCard'; import UserInfo from '../../../components/UserInfo'; import { UserStatus } from '../../../components/UserStatus'; -import VerticalBar from '../../../components/VerticalBar'; import { getUserEmailVerified } from '../../../lib/utils/getUserEmailVerified'; import AdminUserInfoActions from './AdminUserInfoActions'; @@ -96,17 +96,17 @@ const AdminUserInfoWithData = ({ uid, onReload }: AdminUserInfoWithDataProps): R if (isLoading) { return ( - + - + ); } if (error || !user || !data?.user) { return ( - + {t('User_not_found')} - + ); } diff --git a/apps/meteor/client/views/admin/users/InviteUsers.tsx b/apps/meteor/client/views/admin/users/InviteUsers.tsx index 9be818e42023..70b9878bd989 100644 --- a/apps/meteor/client/views/admin/users/InviteUsers.tsx +++ b/apps/meteor/client/views/admin/users/InviteUsers.tsx @@ -4,11 +4,11 @@ import type { ReactElement, ChangeEvent, ComponentProps } from 'react'; import React, { useCallback, useState } from 'react'; import { validateEmail } from '../../../../lib/emailValidator'; -import VerticalBar from '../../../components/VerticalBar'; +import { ContextualbarScrollableContent } from '../../../components/Contextualbar'; import { useSendInvitationEmailMutation } from './hooks/useSendInvitationEmailMutation'; import { useSmtpConfig } from './hooks/useSmtpConfig'; -type InviteUsersProps = ComponentProps; +type InviteUsersProps = ComponentProps; const InviteUsers = (props: InviteUsersProps): ReactElement => { const t = useTranslation(); @@ -24,7 +24,7 @@ const InviteUsers = (props: InviteUsersProps): ReactElement => { if (!isSmtpEnabled) { return ( - + {t('SMTP_Server_Not_Setup_Title')} {t('SMTP_Server_Not_Setup_Description')} @@ -34,12 +34,12 @@ const InviteUsers = (props: InviteUsersProps): ReactElement => { - + ); } return ( - + {t('Send_invitation_email')} @@ -50,7 +50,7 @@ const InviteUsers = (props: InviteUsersProps): ReactElement => { - + ); }; diff --git a/apps/meteor/client/views/admin/users/UserForm.js b/apps/meteor/client/views/admin/users/UserForm.js index cc6c86a9aaf3..d530a0b49816 100644 --- a/apps/meteor/client/views/admin/users/UserForm.js +++ b/apps/meteor/client/views/admin/users/UserForm.js @@ -14,8 +14,8 @@ import { useTranslation } from '@rocket.chat/ui-contexts'; import React, { useCallback, useMemo, useState } from 'react'; import { validateEmail } from '../../../../lib/emailValidator'; +import { ContextualbarScrollableContent } from '../../../components/Contextualbar'; import CustomFieldsForm from '../../../components/CustomFieldsForm'; -import VerticalBar from '../../../components/VerticalBar'; export default function UserForm({ formValues, formHandlers, availableRoles, append, prepend, errors, isSmtpEnabled, ...props }) { const t = useTranslation(); @@ -58,7 +58,7 @@ export default function UserForm({ formValues, formHandlers, availableRoles, app const onLoadCustomFields = useCallback((hasCustomFields) => setHasCustomFields(hasCustomFields), []); return ( - e.preventDefault(), [])} autoComplete='off'> + e.preventDefault(), [])} autoComplete='off'> {prepend} {useMemo( @@ -283,6 +283,6 @@ export default function UserForm({ formValues, formHandlers, availableRoles, app {append} - + ); } diff --git a/apps/meteor/client/views/admin/users/UsersPage.tsx b/apps/meteor/client/views/admin/users/UsersPage.tsx index 224bcdca83d8..2eacbd32dc47 100644 --- a/apps/meteor/client/views/admin/users/UsersPage.tsx +++ b/apps/meteor/client/views/admin/users/UsersPage.tsx @@ -5,8 +5,8 @@ import React, { useEffect, useRef } from 'react'; import UserPageHeaderContentWithSeatsCap from '../../../../ee/client/views/admin/users/UserPageHeaderContentWithSeatsCap'; import { useSeatsCap } from '../../../../ee/client/views/admin/users/useSeatsCap'; +import { Contextualbar, ContextualbarHeader, ContextualbarTitle, ContextualbarClose } from '../../../components/Contextualbar'; import Page from '../../../components/Page'; -import VerticalBar from '../../../components/VerticalBar'; import AddUser from './AddUser'; import AdminUserInfoWithData from './AdminUserInfoWithData'; import EditUserWithData from './EditUserWithData'; @@ -34,7 +34,7 @@ const UsersPage = (): ReactElement => { } }, [context, seatsCap, usersRoute]); - const handleCloseVerticalBar = (): void => { + const handleCloseContextualbar = (): void => { usersRoute.push({}); }; @@ -77,21 +77,21 @@ const UsersPage = (): ReactElement => { {context && ( - - - + + + {context === 'info' && t('User_Info')} {context === 'edit' && t('Edit_User')} {context === 'new' && t('Add_User')} {context === 'invite' && t('Invite_Users')} - - - + + + {context === 'info' && id && } {context === 'edit' && id && } {context === 'new' && } {context === 'invite' && } - + )} ); diff --git a/apps/meteor/client/views/marketplace/AppDetailsPage/AppDetailsPage.tsx b/apps/meteor/client/views/marketplace/AppDetailsPage/AppDetailsPage.tsx index 4272db0522d9..d1688b22c6ab 100644 --- a/apps/meteor/client/views/marketplace/AppDetailsPage/AppDetailsPage.tsx +++ b/apps/meteor/client/views/marketplace/AppDetailsPage/AppDetailsPage.tsx @@ -53,7 +53,8 @@ const AppDetailsPage = ({ id }: { id: App['id'] }): ReactElement => { context && router.push({ context, page: 'list' }); }); - const { installed, settings, privacyPolicySummary, permissions, tosLink, privacyLink, marketplace, name } = appData || {}; + const { installed, settings, privacyPolicySummary, permissions, tosLink, privacyLink, name } = appData || {}; + const isSecurityVisible = Boolean(privacyPolicySummary || permissions || tosLink || privacyLink); const saveAppSettings = useCallback(async () => { @@ -94,12 +95,11 @@ const AppDetailsPage = ({ id }: { id: App['id'] }): ReactElement => { <> {Boolean(!tab || tab === 'details') && } {tab === 'requests' && } diff --git a/apps/meteor/client/views/marketplace/AppDetailsPage/AppDetailsPageTabs.tsx b/apps/meteor/client/views/marketplace/AppDetailsPage/AppDetailsPageTabs.tsx index ff3ddef243a0..d93af7b44d19 100644 --- a/apps/meteor/client/views/marketplace/AppDetailsPage/AppDetailsPageTabs.tsx +++ b/apps/meteor/client/views/marketplace/AppDetailsPage/AppDetailsPageTabs.tsx @@ -6,22 +6,14 @@ import React from 'react'; import type { ISettings } from '../../../../ee/client/apps/@types/IOrchestrator'; type AppDetailsPageTabsProps = { + context: string; installed: boolean | undefined; isSecurityVisible: boolean; - marketplace: unknown; settings: ISettings | undefined; tab: string | undefined; - context: string; }; -const AppDetailsPageTabs = ({ - installed, - isSecurityVisible, - marketplace, - settings, - tab, - context, -}: AppDetailsPageTabsProps): ReactElement => { +const AppDetailsPageTabs = ({ context, installed, isSecurityVisible, settings, tab }: AppDetailsPageTabsProps): ReactElement => { const t = useTranslation(); const isAdminUser = usePermission('manage-apps'); @@ -51,7 +43,7 @@ const AppDetailsPageTabs = ({ {t('Security')} )} - {marketplace !== false && ( + {context !== 'private' && ( handleTabClick('releases')} selected={tab === 'releases'}> {t('Releases')} diff --git a/apps/meteor/client/views/marketplace/AppsPage/AppsFilters.tsx b/apps/meteor/client/views/marketplace/AppsPage/AppsFilters.tsx index 7c9b4ece01cb..9a2441f20210 100644 --- a/apps/meteor/client/views/marketplace/AppsPage/AppsFilters.tsx +++ b/apps/meteor/client/views/marketplace/AppsPage/AppsFilters.tsx @@ -1,5 +1,5 @@ import { Box } from '@rocket.chat/fuselage'; -import { useMediaQuery } from '@rocket.chat/fuselage-hooks'; +import { useBreakpoints } from '@rocket.chat/fuselage-hooks'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; @@ -46,8 +46,8 @@ const AppsFilters = ({ const t = useTranslation(); const isPrivateAppsPage = context === 'private'; - - const shouldFiltersStack = useMediaQuery('(max-width: 1060px)'); + const breakpoints = useBreakpoints(); + const shouldFiltersStack = ['xs', 'sm', 'md'].some((size) => breakpoints.includes(size)); const hasFilterStackMargin = shouldFiltersStack ? '' : 'x8'; const hasNotFilterStackMargin = shouldFiltersStack ? 'x8' : ''; @@ -59,6 +59,8 @@ const AppsFilters = ({ private: t('Search_Private_apps'), }; + const fixFiltersSize = breakpoints.includes('lg') ? { maxWidth: 'x200', minWidth: 'x200' } : null; + return ( )} {!isPrivateAppsPage && ( )} - + diff --git a/apps/meteor/client/views/marketplace/AppsProvider.tsx b/apps/meteor/client/views/marketplace/AppsProvider.tsx index 0f0b809adb21..0f1868ba375c 100644 --- a/apps/meteor/client/views/marketplace/AppsProvider.tsx +++ b/apps/meteor/client/views/marketplace/AppsProvider.tsx @@ -1,15 +1,13 @@ -import type { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; -import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { usePermission } from '@rocket.chat/ui-contexts'; -import type { FC, Reducer } from 'react'; -import React, { useEffect, useReducer, useCallback } from 'react'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import type { FC } from 'react'; +import React, { useEffect } from 'react'; import { AppEvents } from '../../../ee/client/apps/communication'; import { Apps } from '../../../ee/client/apps/orchestrator'; -import type { AsyncState } from '../../lib/asyncState'; +import PageSkeleton from '../../components/PageSkeleton'; import { AsyncStatePhase } from '../../lib/asyncState'; import { AppsContext } from './AppsContext'; -import { handleAPIError } from './helpers'; import { useInvalidateAppsCountQueryCallback } from './hooks/useAppsCountQuery'; import type { App } from './types'; @@ -34,501 +32,143 @@ const registerListeners = (listeners: ListenersMapping): (() => void) => { }; }; -type Action = - | { type: 'request'; reload: () => Promise } - | { type: 'update'; app: App; reload: () => Promise } - | { type: 'delete'; appId: string; reload: () => Promise } - | { type: 'invalidate'; appId: string; reload: () => Promise } - | { type: 'success'; apps: App[]; reload: () => Promise } - | { type: 'failure'; error: Error; reload: () => Promise }; - const sortByName = (apps: App[]): App[] => apps.sort((a, b) => (a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1)); -const reducer = ( - state: AsyncState<{ apps: App[] }> & { - reload: () => Promise; - }, - action: Action, -): AsyncState<{ apps: App[] }> & { - reload: () => Promise; -} => { - switch (action.type) { - case 'invalidate': - if (state.phase !== AsyncStatePhase.RESOLVED) { - return state; - } - return { - phase: AsyncStatePhase.RESOLVED, - reload: action.reload, - value: { - apps: sortByName( - state.value.apps.map((app) => { - if (app.id === action.appId) { - return { ...app }; - } - return app; - }), - ), - }, - error: undefined, - }; - case 'update': - if (state.phase !== AsyncStatePhase.RESOLVED) { - return state; - } - return { - phase: AsyncStatePhase.RESOLVED, - reload: async (): Promise => undefined, - value: { - apps: sortByName( - state.value.apps.map((app) => { - if (app.id === action.app.id) { - return action.app; - } - return app; - }), - ), - }, - error: undefined, - }; - case 'request': - return { - reload: async (): Promise => undefined, - phase: AsyncStatePhase.LOADING, - value: undefined, - error: undefined, - }; - case 'success': - return { - reload: action.reload, - phase: AsyncStatePhase.RESOLVED, - value: { apps: sortByName(action.apps) }, - error: undefined, - }; - case 'delete': - if (state.phase !== AsyncStatePhase.RESOLVED) { - return state; - } - return { - reload: action.reload, - phase: AsyncStatePhase.RESOLVED, - value: { apps: state.value.apps.filter(({ id }) => id !== action.appId) }, - error: undefined, - }; - case 'failure': - return { - reload: action.reload, - phase: AsyncStatePhase.REJECTED, - value: undefined, - error: action.error, - }; - default: - return state; - } -}; - const AppsProvider: FC = ({ children }) => { - const [marketplaceAppsState, dispatchMarketplaceApps] = useReducer< - Reducer< - AsyncState<{ apps: App[] }> & { - reload: () => Promise; - }, - Action - > - >(reducer, { - phase: AsyncStatePhase.LOADING, - value: undefined, - error: undefined, - reload: async () => undefined, - }); - - const [installedAppsState, dispatchInstalledApps] = useReducer< - Reducer< - AsyncState<{ apps: App[] }> & { - reload: () => Promise; - }, - Action - > - >(reducer, { - phase: AsyncStatePhase.LOADING, - value: undefined, - error: undefined, - reload: async () => undefined, - }); - - const [privateAppsState, dispatchPrivateApps] = useReducer< - Reducer< - AsyncState<{ apps: App[] }> & { - reload: () => Promise; - }, - Action - > - >(reducer, { - phase: AsyncStatePhase.LOADING, - value: undefined, - error: undefined, - reload: async () => undefined, - }); - const isAdminUser = usePermission('manage-apps'); - const fetch = useCallback(async (isAdminUser?: string): Promise => { - dispatchMarketplaceApps({ type: 'request', reload: async () => undefined }); - dispatchInstalledApps({ type: 'request', reload: async () => undefined }); - dispatchPrivateApps({ type: 'request', reload: async () => undefined }); + const queryClient = useQueryClient(); - let allInstalledApps: App[] = []; - let installedApps: App[] = []; - let marketplaceApps: App[] = []; - let privateApps: App[] = []; - let marketplaceError = false; - let installedAppsError = false; - let privateAppsError = false; + const invalidateAppsCountQuery = useInvalidateAppsCountQueryCallback(); - try { - marketplaceApps = (await Apps.getAppsFromMarketplace(isAdminUser)) as unknown as App[]; - } catch (e) { - dispatchMarketplaceApps({ - type: 'failure', - error: e instanceof Error ? e : new Error(String(e)), - reload: fetch, - }); - marketplaceError = true; - } + useEffect(() => { + const listeners = { + APP_ADDED: (): void => { + queryClient.invalidateQueries(['marketplace', 'apps-instance']); + }, + APP_UPDATED: (): void => { + queryClient.invalidateQueries(['marketplace', 'apps-instance']); + }, + APP_REMOVED: (): void => { + queryClient.invalidateQueries(['marketplace', 'apps-instance']); + }, + APP_STATUS_CHANGE: (): void => { + queryClient.invalidateQueries(['marketplace', 'apps-instance']); + }, + APP_SETTING_UPDATED: (): void => { + queryClient.invalidateQueries(['marketplace', 'apps-instance']); + }, + }; + const unregisterListeners = registerListeners(listeners); - try { - allInstalledApps = await Apps.getInstalledApps().then((result: App[]) => + // eslint-disable-next-line no-unsafe-finally + return unregisterListeners; + }, [invalidateAppsCountQuery, isAdminUser, queryClient]); + + const marketplace = useQuery( + ['marketplace', 'apps-marketplace', isAdminUser], + () => { + const result = Apps.getAppsFromMarketplace(isAdminUser ? 'true' : 'false'); + queryClient.invalidateQueries(['marketplace', 'apps-stored']); + return result; + }, + { + staleTime: Infinity, + refetchOnWindowFocus: false, + keepPreviousData: true, + onSettled: () => queryClient.invalidateQueries(['marketplace', 'apps-stored']), + }, + ); + + const instance = useQuery( + ['marketplace', 'apps-instance', isAdminUser], + async () => { + const result = await Apps.getInstalledApps().then((result: App[]) => result.map((current: App) => ({ ...current, installed: true, })), ); - } catch (e) { - dispatchInstalledApps({ - type: 'failure', - error: e instanceof Error ? e : new Error(String(e)), - reload: fetch, - }); - installedAppsError = true; - } - - try { - installedApps = allInstalledApps.filter((app: App) => !app.private); - } catch (e) { - dispatchInstalledApps({ - type: 'failure', - error: e instanceof Error ? e : new Error(String(e)), - reload: fetch, - }); - installedAppsError = true; - } + return result; + }, + { + staleTime: Infinity, + refetchOnWindowFocus: false, + keepPreviousData: true, + onSettled: () => queryClient.invalidateQueries(['marketplace', 'apps-stored']), + }, + ); - try { - privateApps = allInstalledApps.filter((app: App) => app.private); - } catch (e) { - dispatchPrivateApps({ - type: 'failure', - error: e instanceof Error ? e : new Error(String(e)), - reload: fetch, - }); + const store = useQuery( + ['marketplace', 'apps-stored', isAdminUser], + () => { + if (!marketplace.isSuccess || !instance.isSuccess) { + throw new Error('Apps not loaded'); + } - privateAppsError = true; - } + const marketplaceApps: App[] = []; + const installedApps: App[] = []; + const privateApps: App[] = []; - const installedAppsData: App[] = []; - const marketplaceAppsData: App[] = []; - const privateAppsData: App[] = []; + const clonedData = [...instance.data]; - if (!marketplaceError) { - marketplaceApps.forEach((app) => { - const appIndex = installedApps.findIndex(({ id }) => id === app.id); - if (!installedApps[appIndex]) { - marketplaceAppsData.push({ - ...app, - status: undefined, - marketplaceVersion: app.version, - bundledIn: app.bundledIn, - }); + sortByName(marketplace.data).forEach((app) => { + const appIndex = clonedData.findIndex(({ id }) => id === app.id); + const [installedApp] = appIndex > -1 ? clonedData.splice(appIndex, 1) : []; - return; - } - const [installedApp] = installedApps.splice(appIndex, 1); - const appData = { + const record = { ...app, - installed: true, ...(installedApp && { + private: installedApp.private, + installed: true, status: installedApp.status, version: installedApp.version, licenseValidation: installedApp.licenseValidation, + migrated: installedApp.migrated, }), bundledIn: app.bundledIn, marketplaceVersion: app.version, - migrated: installedApp.migrated, - }; - - installedAppsData.push(appData); - marketplaceAppsData.push(appData); - }); - dispatchMarketplaceApps({ - type: 'success', - reload: fetch, - apps: marketplaceAppsData, - }); - } - - if (!installedAppsError) { - if (installedApps.length > 0) { - installedAppsData.push(...installedApps); - } - - dispatchInstalledApps({ - type: 'success', - reload: fetch, - apps: installedAppsData, - }); - } - - if (!privateAppsError) { - if (privateApps.length > 0) { - privateAppsData.push(...privateApps); - } - - dispatchPrivateApps({ - type: 'success', - reload: fetch, - apps: privateAppsData, - }); - } - }, []); - - const getCurrentData = useMutableCallback(function getCurrentData() { - return [marketplaceAppsState, installedAppsState, privateAppsState]; - }); - - const invalidateAppsCountQuery = useInvalidateAppsCountQueryCallback(); - - useEffect(() => { - const handleAppAddedOrUpdated = async (appId: string): Promise => { - let marketplaceApp: { app: App; success: boolean } | undefined; - let installedApp: App; - let privateApp: App | undefined; - - invalidateAppsCountQuery(); - - try { - const app = await Apps.getApp(appId); - - if (app.private) { - privateApp = app; - } - - installedApp = app; - } catch (error: any) { - handleAPIError(error); - throw error; - } - - try { - marketplaceApp = await Apps.getAppFromMarketplace(appId, installedApp.version); - } catch (error: any) { - handleAPIError(error); - } - - const [, installedApps, privateApps] = getCurrentData(); - - if (marketplaceApp !== undefined) { - const { status, version, licenseValidation } = installedApp; - const record = { - ...marketplaceApp.app, - success: marketplaceApp.success, - installed: true, - status, - version, - licenseValidation, - marketplaceVersion: marketplaceApp.app.version, }; - dispatchMarketplaceApps({ - type: 'update', - app: record, - reload: fetch, - }); - - if (installedApps.value) { - if (installedApps.value.apps.some((app) => app.id === appId)) { - dispatchInstalledApps({ - type: 'update', - app: record, - reload: fetch, - }); - return; - } - dispatchInstalledApps({ - type: 'success', - apps: [...installedApps.value.apps, record], - reload: fetch, - }); - return; + if (installedApp?.private) { + privateApps.push(record); } - dispatchInstalledApps({ - type: 'success', - apps: [record], - reload: fetch, - }); - - return; - } - - if (privateApp !== undefined) { - const { status, version } = privateApp; - - const record = { - ...privateApp, - success: true, - installed: true, - status, - version, - }; - - if (privateApps.value) { - if (privateApps.value.apps.some((app) => app.id === appId)) { - dispatchPrivateApps({ - type: 'update', - app: record, - reload: fetch, - }); - return; - } - dispatchPrivateApps({ - type: 'success', - apps: [...privateApps.value.apps, record], - reload: fetch, - }); - return; + if (installedApp && !installedApp.private) { + installedApps.push(record); } + marketplaceApps.push(record); + }); - dispatchPrivateApps({ type: 'success', apps: [record], reload: fetch }); - return; - } - - // TODO: Reevaluate the necessity of this dispatch - dispatchInstalledApps({ type: 'update', app: installedApp, reload: fetch }); - }; - const listeners = { - APP_ADDED: handleAppAddedOrUpdated, - APP_UPDATED: handleAppAddedOrUpdated, - APP_REMOVED: (appId: string): void => { - const updatedData = getCurrentData(); - - // TODO: This forEach is not ideal, it will be improved in the future during the refactor of this provider; - updatedData.forEach((appsList) => { - const app = appsList.value?.apps.find(({ id }: { id: string }) => id === appId); - - dispatchInstalledApps({ - type: 'delete', - appId, - reload: fetch, - }); - - if (!app) { - return; - } - - if (app.private) { - dispatchPrivateApps({ - type: 'delete', - appId, - reload: fetch, - }); - } - - dispatchMarketplaceApps({ - type: 'update', - reload: fetch, - app: { - ...app, - version: app?.marketplaceVersion, - installed: false, - marketplaceVersion: app?.marketplaceVersion, - }, - }); - }); - - invalidateAppsCountQuery(); - }, - APP_STATUS_CHANGE: ({ appId, status }: { appId: string; status: AppStatus }): void => { - const updatedData = getCurrentData(); - - if (!Array.isArray(updatedData)) { - return; + sortByName(clonedData).forEach((app) => { + if (app.private) { + privateApps.push(app); } + }); - // TODO: This forEach is not ideal, it will be improved in the future during the refactor of this provider; - updatedData.forEach((appsList) => { - const app = appsList.value?.apps.find(({ id }: { id: string }) => id === appId); - - if (!app) { - return; - } - - app.status = status; - - dispatchInstalledApps({ - type: 'update', - app: { - ...app, - status, - }, - reload: fetch, - }); - - if (app.private) { - dispatchPrivateApps({ - type: 'update', - app: { - ...app, - status, - }, - reload: fetch, - }); - } - - dispatchMarketplaceApps({ - type: 'update', - app: { - ...app, - status, - }, - reload: fetch, - }); - }); + return [marketplaceApps, installedApps, privateApps]; + }, + { + enabled: marketplace.isSuccess && instance.isSuccess && !instance.isRefetching, + refetchOnWindowFocus: false, + keepPreviousData: true, + }, + ); - invalidateAppsCountQuery(); - }, - APP_SETTING_UPDATED: ({ appId }: { appId: string }): void => { - dispatchInstalledApps({ type: 'invalidate', appId, reload: fetch }); - dispatchMarketplaceApps({ type: 'invalidate', appId, reload: fetch }); - dispatchPrivateApps({ type: 'invalidate', appId, reload: fetch }); - }, - }; - const unregisterListeners = registerListeners(listeners); - try { - fetch(isAdminUser ? 'true' : 'false'); - } finally { - // eslint-disable-next-line no-unsafe-finally - return unregisterListeners; - } - }, [fetch, getCurrentData, invalidateAppsCountQuery, isAdminUser]); + if (!store.isSuccess) { + return ; + } return ( { + await Promise.all([queryClient.invalidateQueries(['marketplace'])]); + }, }} /> ); diff --git a/apps/meteor/client/views/marketplace/components/EnabledAppsCount.tsx b/apps/meteor/client/views/marketplace/components/EnabledAppsCount.tsx index 8fcf4ce306a3..72cfa5474346 100644 --- a/apps/meteor/client/views/marketplace/components/EnabledAppsCount.tsx +++ b/apps/meteor/client/views/marketplace/components/EnabledAppsCount.tsx @@ -1,8 +1,9 @@ -import { Box, ProgressBar } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; +import { GenericResourceUsage } from '../../../components/GenericResourceUsage'; + const EnabledAppsCount = ({ variant, percentage, @@ -22,28 +23,14 @@ const EnabledAppsCount = ({ const marketplaceAppsCountText: string = t('Apps_Count_Enabled', { count: enabled }); return ( - - - {context === 'private' ? privateAppsCountText : marketplaceAppsCountText} - - - {`${enabled} / ${limit}`} - - - - - - + ); }; diff --git a/apps/meteor/client/views/marketplace/components/MarketplaceHeader.tsx b/apps/meteor/client/views/marketplace/components/MarketplaceHeader.tsx index 3b54591e219e..ec06f1afff13 100644 --- a/apps/meteor/client/views/marketplace/components/MarketplaceHeader.tsx +++ b/apps/meteor/client/views/marketplace/components/MarketplaceHeader.tsx @@ -1,8 +1,10 @@ -import { Box, Button, ButtonGroup, Skeleton } from '@rocket.chat/fuselage'; +import { Button, ButtonGroup } from '@rocket.chat/fuselage'; import { usePermission, useRoute, useRouteParameter, useSetModal, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useCallback } from 'react'; +import { GenericResourceUsageSkeleton } from '../../../components/GenericResourceUsage'; +import Page from '../../../components/Page'; import UnlimitedAppsUpsellModal from '../UnlimitedAppsUpsellModal'; import { useAppsCountQuery } from '../hooks/useAppsCountQuery'; import EnabledAppsCount from './EnabledAppsCount'; @@ -20,40 +22,27 @@ const MarketplaceHeader = ({ title }: { title: string }): ReactElement | null => route.push({ context, page: 'install' }); }, [context, route]); - if (result.isLoading) { - return ( - - {t('Active_connections')} - - - ); - } - if (result.isError) { return null; } return ( - - {title} - - {!result.data.hasUnlimitedApps && } - {isAdmin && ( - - {!result.data.hasUnlimitedApps && ( - - )} - {context === 'private' && } - + + + {result.isLoading && } + {result.isSuccess && !result.data.hasUnlimitedApps && } + {isAdmin && result.isSuccess && !result.data.hasUnlimitedApps && ( + )} - - + {isAdmin && context === 'private' && } + + ); }; diff --git a/apps/meteor/client/views/marketplace/hooks/useAppsCountQuery.ts b/apps/meteor/client/views/marketplace/hooks/useAppsCountQuery.ts index d9ffd6a24051..10689c773479 100644 --- a/apps/meteor/client/views/marketplace/hooks/useAppsCountQuery.ts +++ b/apps/meteor/client/views/marketplace/hooks/useAppsCountQuery.ts @@ -21,7 +21,7 @@ export const useAppsCountQuery = (context: MarketplaceRouteContext) => { const getAppsCount = useEndpoint('GET', '/apps/count'); return useQuery( - ['apps/count', { context }], + ['apps/count', context], async () => { const data = await getAppsCount(); diff --git a/apps/meteor/client/views/omnichannel/agents/AgentEdit.tsx b/apps/meteor/client/views/omnichannel/agents/AgentEdit.tsx index d9c1321e93c6..8d796e25e79f 100644 --- a/apps/meteor/client/views/omnichannel/agents/AgentEdit.tsx +++ b/apps/meteor/client/views/omnichannel/agents/AgentEdit.tsx @@ -6,8 +6,8 @@ import type { FC, ReactElement } from 'react'; import React, { useMemo, useRef, useState } from 'react'; import { getUserEmailAddress } from '../../../../lib/getUserEmailAddress'; +import { ContextualbarScrollableContent } from '../../../components/Contextualbar'; import UserInfo from '../../../components/UserInfo'; -import VerticalBar from '../../../components/VerticalBar'; import { useForm } from '../../../hooks/useForm'; import { useFormsSubscription } from '../additionalForms'; @@ -113,7 +113,7 @@ const AgentEdit: FC = ({ data, userDepartments, availableDepartm }); return ( - + {username && ( @@ -207,7 +207,7 @@ const AgentEdit: FC = ({ data, userDepartments, availableDepartm - + ); }; diff --git a/apps/meteor/client/views/omnichannel/agents/AgentInfo.tsx b/apps/meteor/client/views/omnichannel/agents/AgentInfo.tsx index c9f35302009b..521c449f55c8 100644 --- a/apps/meteor/client/views/omnichannel/agents/AgentInfo.tsx +++ b/apps/meteor/client/views/omnichannel/agents/AgentInfo.tsx @@ -3,10 +3,10 @@ import { useTranslation } from '@rocket.chat/ui-contexts'; import type { HTMLAttributes } from 'react'; import React, { memo } from 'react'; +import { ContextualbarScrollableContent } from '../../../components/Contextualbar'; import { FormSkeleton } from '../../../components/Skeleton'; import UserInfo from '../../../components/UserInfo'; import { UserStatus } from '../../../components/UserStatus'; -import VerticalBar from '../../../components/VerticalBar'; import { AsyncStatePhase } from '../../../hooks/useAsyncState'; import { useEndpointData } from '../../../hooks/useEndpointData'; import { useFormsSubscription } from '../additionalForms'; @@ -36,7 +36,7 @@ const AgentInfo = memo(function AgentInfo({ uid, children, ...pr const { username, statusLivechat, status: userStatus } = user; return ( - + {username && ( @@ -61,7 +61,7 @@ const AgentInfo = memo(function AgentInfo({ uid, children, ...pr {MaxChats && } - + ); }); diff --git a/apps/meteor/client/views/omnichannel/agents/AgentsTab.tsx b/apps/meteor/client/views/omnichannel/agents/AgentsTab.tsx index 01c633f60b11..f6ea3612e909 100644 --- a/apps/meteor/client/views/omnichannel/agents/AgentsTab.tsx +++ b/apps/meteor/client/views/omnichannel/agents/AgentsTab.tsx @@ -2,7 +2,7 @@ import { useRoute, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useCallback } from 'react'; -import VerticalBar from '../../../components/VerticalBar'; +import { Contextualbar, ContextualbarHeader, ContextualbarClose } from '../../../components/Contextualbar'; import AgentEditWithData from './AgentEditWithData'; import AgentInfo from './AgentInfo'; import AgentInfoActions from './AgentInfoActions'; @@ -11,17 +11,17 @@ const AgentsTab = ({ reload, context, id }: { reload: () => void; context: strin const t = useTranslation(); const agentsRoute = useRoute('omnichannel-agents'); - const handleVerticalBarCloseButtonClick = useCallback((): void => { + const handleContextualbarCloseButtonClick = useCallback((): void => { agentsRoute.push({}); }, [agentsRoute]); return ( - - + + {context === 'edit' && t('Edit_User')} {context === 'info' && t('User_Info')} - - + + {context === 'edit' && } {context === 'info' && ( @@ -29,7 +29,7 @@ const AgentsTab = ({ reload, context, id }: { reload: () => void; context: strin )} - + ); }; diff --git a/apps/meteor/client/views/omnichannel/contactHistory/ContactHistory.tsx b/apps/meteor/client/views/omnichannel/contactHistory/ContactHistory.tsx index f453e449d9d6..c9a5b55e4e48 100644 --- a/apps/meteor/client/views/omnichannel/contactHistory/ContactHistory.tsx +++ b/apps/meteor/client/views/omnichannel/contactHistory/ContactHistory.tsx @@ -1,17 +1,17 @@ import type { ReactElement } from 'react'; import React, { useState } from 'react'; -import ContactHistoryVerticalBar from './ContactHistoryVerticalBar'; -import ContactHistoryMessagesVerticalBar from './MessageList/ContactHistoryMessagesVerticalBar'; +import ContactHistoryList from './ContactHistoryList'; +import ContactHistoryMessagesList from './MessageList/ContactHistoryMessagesList'; const ContactHistory = ({ tabBar: { close } }: any): ReactElement => { const [chatId, setChatId] = useState(''); return ( <> {chatId && chatId !== '' ? ( - + ) : ( - + )} ); diff --git a/apps/meteor/client/views/omnichannel/contactHistory/ContactHistoryVerticalBar.tsx b/apps/meteor/client/views/omnichannel/contactHistory/ContactHistoryList.tsx similarity index 80% rename from apps/meteor/client/views/omnichannel/contactHistory/ContactHistoryVerticalBar.tsx rename to apps/meteor/client/views/omnichannel/contactHistory/ContactHistoryList.tsx index dc07ac2f2f9a..5d84aebf2549 100644 --- a/apps/meteor/client/views/omnichannel/contactHistory/ContactHistoryVerticalBar.tsx +++ b/apps/meteor/client/views/omnichannel/contactHistory/ContactHistoryList.tsx @@ -4,21 +4,22 @@ import type { ChangeEvent, Dispatch, ReactElement, SetStateAction } from 'react' import React, { useMemo, useState } from 'react'; import { Virtuoso } from 'react-virtuoso'; +import { + ContextualbarHeader, + ContextualbarContent, + ContextualbarTitle, + ContextualbarIcon, + ContextualbarClose, + ContextualbarEmptyContent, +} from '../../../components/Contextualbar'; import ScrollableContentWrapper from '../../../components/ScrollableContentWrapper'; -import VerticalBar from '../../../components/VerticalBar'; import { useRecordList } from '../../../hooks/lists/useRecordList'; import { AsyncStatePhase } from '../../../lib/asyncState'; import { useOmnichannelRoom } from '../../room/contexts/RoomContext'; import ContactHistoryItem from './ContactHistoryItem'; import { useHistoryList } from './useHistoryList'; -const ContactHistoryVerticalBar = ({ - setChatId, - close, -}: { - setChatId: Dispatch>; - close: () => void; -}): ReactElement => { +const ContactHistoryList = ({ setChatId, close }: { setChatId: Dispatch>; close: () => void }): ReactElement => { const [text, setText] = useState(''); const t = useTranslation(); const room = useOmnichannelRoom(); @@ -34,13 +35,13 @@ const ContactHistoryVerticalBar = ({ return ( <> - - - {t('Contact_Chat_History')} - - + + + {t('Contact_Chat_History')} + + - + {error.toString()} )} - {phase !== AsyncStatePhase.LOADING && totalItemCount === 0 && ( - - - {t('No_results_found')} - - )} + {phase !== AsyncStatePhase.LOADING && totalItemCount === 0 && } {!error && totalItemCount > 0 && history.length > 0 && ( )} - + ); }; -export default ContactHistoryVerticalBar; +export default ContactHistoryList; diff --git a/apps/meteor/client/views/omnichannel/contactHistory/MessageList/ContactHistoryMessage.tsx b/apps/meteor/client/views/omnichannel/contactHistory/MessageList/ContactHistoryMessage.tsx index 421e73246458..7978903afe9c 100644 --- a/apps/meteor/client/views/omnichannel/contactHistory/MessageList/ContactHistoryMessage.tsx +++ b/apps/meteor/client/views/omnichannel/contactHistory/MessageList/ContactHistoryMessage.tsx @@ -25,6 +25,7 @@ import { getUserDisplayName } from '../../../../../lib/getUserDisplayName'; import UserAvatar from '../../../../components/avatar/UserAvatar'; import MessageContentBody from '../../../../components/message/MessageContentBody'; import StatusIndicators from '../../../../components/message/StatusIndicators'; +import Attachments from '../../../../components/message/content/Attachments'; import UiKitSurface from '../../../../components/message/content/UiKitSurface'; import { useFormatDate } from '../../../../hooks/useFormatDate'; import { useFormatTime } from '../../../../hooks/useFormatTime'; @@ -105,6 +106,7 @@ const ContactHistoryMessage: FC<{ )} {message.blocks && } + {message.attachments && } diff --git a/apps/meteor/client/views/omnichannel/contactHistory/MessageList/ContactHistoryMessagesVerticalBar.tsx b/apps/meteor/client/views/omnichannel/contactHistory/MessageList/ContactHistoryMessagesList.tsx similarity index 82% rename from apps/meteor/client/views/omnichannel/contactHistory/MessageList/ContactHistoryMessagesVerticalBar.tsx rename to apps/meteor/client/views/omnichannel/contactHistory/MessageList/ContactHistoryMessagesList.tsx index 3a5b151cdea0..df8dac890828 100644 --- a/apps/meteor/client/views/omnichannel/contactHistory/MessageList/ContactHistoryMessagesVerticalBar.tsx +++ b/apps/meteor/client/views/omnichannel/contactHistory/MessageList/ContactHistoryMessagesList.tsx @@ -4,8 +4,16 @@ import type { ChangeEvent, Dispatch, ReactElement, SetStateAction } from 'react' import React, { useMemo, useState } from 'react'; import { Virtuoso } from 'react-virtuoso'; +import { + ContextualbarHeader, + ContextualbarAction, + ContextualbarIcon, + ContextualbarTitle, + ContextualbarClose, + ContextualbarContent, + ContextualbarEmptyContent, +} from '../../../../components/Contextualbar'; import ScrollableContentWrapper from '../../../../components/ScrollableContentWrapper'; -import VerticalBar from '../../../../components/VerticalBar'; import { useRecordList } from '../../../../hooks/lists/useRecordList'; import { AsyncStatePhase } from '../../../../lib/asyncState'; import { isMessageNewDay } from '../../../room/MessageList/lib/isMessageNewDay'; @@ -13,7 +21,7 @@ import { isMessageSequential } from '../../../room/MessageList/lib/isMessageSequ import ContactHistoryMessage from './ContactHistoryMessage'; import { useHistoryMessageList } from './useHistoryMessageList'; -const ContactHistoryMessagesVerticalBar = ({ +const ContactHistoryMessagesList = ({ chatId, setChatId, close, @@ -38,14 +46,14 @@ const ContactHistoryMessagesVerticalBar = ({ return ( <> - - setChatId('')} title={t('Back')} name='arrow-back' /> - - {t('Chat_History')} - - + + setChatId('')} title={t('Back')} name='arrow-back' /> + + {t('Chat_History')} + + - + {error.toString()} )} - {phase !== AsyncStatePhase.LOADING && totalItemCount === 0 && ( - - - {t('No_results_found')} - - )} + {phase !== AsyncStatePhase.LOADING && totalItemCount === 0 && } {!error && totalItemCount > 0 && history.length > 0 && ( )} - + ); }; -export default ContactHistoryMessagesVerticalBar; +export default ContactHistoryMessagesList; diff --git a/apps/meteor/client/views/omnichannel/currentChats/CurrentChatsRoute.tsx b/apps/meteor/client/views/omnichannel/currentChats/CurrentChatsRoute.tsx index feef97ce0b87..7bf4eda651e0 100644 --- a/apps/meteor/client/views/omnichannel/currentChats/CurrentChatsRoute.tsx +++ b/apps/meteor/client/views/omnichannel/currentChats/CurrentChatsRoute.tsx @@ -22,7 +22,7 @@ import { useSort } from '../../../components/GenericTable/hooks/useSort'; import Page from '../../../components/Page'; import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; import Chat from '../directory/chats/Chat'; -import CustomFieldsVerticalBar from './CustomFieldsVerticalBar'; +import CustomFieldsList from './CustomFieldsList'; import FilterByText from './FilterByText'; import RemoveChatButton from './RemoveChatButton'; import { useAllCustomFields } from './hooks/useAllCustomFields'; @@ -326,7 +326,7 @@ const CurrentChatsRoute = (): ReactElement => { {id === 'custom-fields' && hasCustomFields && ( - + )} ); diff --git a/apps/meteor/client/views/omnichannel/currentChats/CustomFieldsVerticalBar.tsx b/apps/meteor/client/views/omnichannel/currentChats/CustomFieldsList.tsx similarity index 76% rename from apps/meteor/client/views/omnichannel/currentChats/CustomFieldsVerticalBar.tsx rename to apps/meteor/client/views/omnichannel/currentChats/CustomFieldsList.tsx index 749be5a800da..b3d5b22472a4 100644 --- a/apps/meteor/client/views/omnichannel/currentChats/CustomFieldsVerticalBar.tsx +++ b/apps/meteor/client/views/omnichannel/currentChats/CustomFieldsList.tsx @@ -5,14 +5,14 @@ import type { ReactElement, Dispatch, SetStateAction } from 'react'; import React, { useEffect } from 'react'; import { Controller, useForm } from 'react-hook-form'; -import VerticalBar from '../../../components/VerticalBar'; +import { Contextualbar, ContextualbarScrollableContent, ContextualbarHeader, ContextualbarClose } from '../../../components/Contextualbar'; -type CustomFieldsVerticalBarProps = { +type CustomFieldsListProps = { setCustomFields: Dispatch>; allCustomFields: ILivechatCustomField[]; }; -const CustomFieldsVerticalBar = ({ setCustomFields, allCustomFields }: CustomFieldsVerticalBarProps): ReactElement => { +const CustomFieldsList = ({ setCustomFields, allCustomFields }: CustomFieldsListProps): ReactElement => { const { register, watch, control } = useForm({ mode: 'onChange' }); // TODO: When we refactor the other CurrentChat's fields to use react-hook-form, we need to change this to use the form controller @@ -26,12 +26,12 @@ const CustomFieldsVerticalBar = ({ setCustomFields, allCustomFields }: CustomFie const currentChatsRoute = useRoute('omnichannel-current-chats'); return ( - - + + {t('Filter_by_Custom_Fields')} - currentChatsRoute.push({ context: '' })} /> - - + currentChatsRoute.push({ context: '' })} /> + + {/* TODO: REMOVE FILTER ONCE THE ENDPOINT SUPPORTS A SCOPE PARAMETER */} {allCustomFields .filter((customField) => customField.scope !== 'visitor') @@ -58,9 +58,9 @@ const CustomFieldsVerticalBar = ({ setCustomFields, allCustomFields }: CustomFie ), )} - - + + ); }; -export default CustomFieldsVerticalBar; +export default CustomFieldsList; diff --git a/apps/meteor/client/views/omnichannel/departments/AgentRow.js b/apps/meteor/client/views/omnichannel/departments/AgentRow.js deleted file mode 100644 index bce5751dbde9..000000000000 --- a/apps/meteor/client/views/omnichannel/departments/AgentRow.js +++ /dev/null @@ -1,41 +0,0 @@ -import { Box, Table } from '@rocket.chat/fuselage'; -import React, { memo } from 'react'; - -import UserAvatar from '../../../components/avatar/UserAvatar'; -import Count from './Count'; -import Order from './Order'; -import RemoveAgentButton from './RemoveAgentButton'; - -const AgentRow = ({ agentId, username, name, avatarETag, mediaQuery, agentList, setAgentList, setAgentsRemoved }) => ( - - - - - - - - {name || username} - - {!mediaQuery && name && ( - - {' '} - {`@${username}`}{' '} - - )} - - - - - - - - - - - - - - -); - -export default memo(AgentRow); diff --git a/apps/meteor/client/views/omnichannel/departments/ArchivedDepartmentsPageWithData.tsx b/apps/meteor/client/views/omnichannel/departments/ArchivedDepartmentsPageWithData.tsx index 0f2a9b1edf78..e8cd8d7563d6 100644 --- a/apps/meteor/client/views/omnichannel/departments/ArchivedDepartmentsPageWithData.tsx +++ b/apps/meteor/client/views/omnichannel/departments/ArchivedDepartmentsPageWithData.tsx @@ -13,7 +13,7 @@ import DepartmentsTable from './DepartmentsTable'; const ArchivedDepartmentsPageWithData = (): ReactElement => { const [text, setText] = useState(''); - const [debouncedText = ''] = useDebouncedValue(text, 500); + const debouncedText = useDebouncedValue(text, 500) || ''; const pagination = usePagination(); const sort = useSort<'name' | 'email' | 'active'>('name'); diff --git a/apps/meteor/client/views/omnichannel/departments/Count.js b/apps/meteor/client/views/omnichannel/departments/Count.js deleted file mode 100644 index 8897c01eb723..000000000000 --- a/apps/meteor/client/views/omnichannel/departments/Count.js +++ /dev/null @@ -1,30 +0,0 @@ -import { Box, NumberInput } from '@rocket.chat/fuselage'; -import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { useTranslation } from '@rocket.chat/ui-contexts'; -import React, { useState } from 'react'; - -function Count({ agentId, setAgentList, agentList }) { - const t = useTranslation(); - const [agentCount, setAgentCount] = useState(agentList.find((agent) => agent.agentId === agentId).count || 0); - - const handleCount = useMutableCallback(async (e) => { - const countValue = Number(e.currentTarget.value); - setAgentCount(countValue); - setAgentList( - agentList.map((agent) => { - if (agent.agentId === agentId) { - agent.count = countValue; - } - return agent; - }), - ); - }); - - return ( - - - - ); -} - -export default Count; diff --git a/apps/meteor/client/views/omnichannel/departments/AddAgent.js b/apps/meteor/client/views/omnichannel/departments/DepartmentAgentsTable/AddAgent.tsx similarity index 54% rename from apps/meteor/client/views/omnichannel/departments/AddAgent.js rename to apps/meteor/client/views/omnichannel/departments/DepartmentAgentsTable/AddAgent.tsx index b2964ae5bbba..0186e1438dfd 100644 --- a/apps/meteor/client/views/omnichannel/departments/AddAgent.js +++ b/apps/meteor/client/views/omnichannel/departments/DepartmentAgentsTable/AddAgent.tsx @@ -3,13 +3,17 @@ import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; import React, { useState } from 'react'; -import AutoCompleteAgent from '../../../components/AutoCompleteAgent'; -import { useEndpointAction } from '../../../hooks/useEndpointAction'; +import AutoCompleteAgent from '../../../../components/AutoCompleteAgent'; +import { useEndpointAction } from '../../../../hooks/useEndpointAction'; +import type { IDepartmentAgent } from '../EditDepartment'; -function AddAgent({ agentList, setAgentsAdded, setAgentList, ...props }) { +function AddAgent({ agentList, onAdd }: { agentList: IDepartmentAgent[]; onAdd: (agent: IDepartmentAgent) => void }) { const t = useTranslation(); - const [userId, setUserId] = useState(); + + const [userId, setUserId] = useState(''); + const getAgent = useEndpointAction('GET', '/v1/livechat/users/agent/:_id', { keys: { _id: userId } }); + const dispatchToastMessage = useToastMessageDispatch(); const handleAgent = useMutableCallback((e) => setUserId(e)); @@ -18,19 +22,22 @@ function AddAgent({ agentList, setAgentsAdded, setAgentList, ...props }) { if (!userId) { return; } - const { user } = await getAgent(); - if (agentList.filter((e) => e.agentId === user._id).length === 0) { - setAgentList([{ ...user, agentId: user._id }, ...agentList]); - setUserId(); - setAgentsAdded((agents) => [...agents, { agentId: user._id }]); + const { + user: { _id, username, name }, + } = await getAgent(); + + if (!agentList.some(({ agentId }) => agentId === _id)) { + setUserId(''); + onAdd({ agentId: _id, username: username ?? '', name, count: 0, order: 0 }); } else { dispatchToastMessage({ type: 'error', message: t('This_agent_was_already_selected') }); } }); + return ( - - + + diff --git a/apps/meteor/client/views/omnichannel/departments/DepartmentAgentsTable/AgentAvatar.tsx b/apps/meteor/client/views/omnichannel/departments/DepartmentAgentsTable/AgentAvatar.tsx new file mode 100644 index 000000000000..2893711b9a13 --- /dev/null +++ b/apps/meteor/client/views/omnichannel/departments/DepartmentAgentsTable/AgentAvatar.tsx @@ -0,0 +1,30 @@ +import { Box } from '@rocket.chat/fuselage'; +import { useMediaQuery } from '@rocket.chat/fuselage-hooks'; +import React, { memo } from 'react'; + +import UserAvatar from '../../../../components/avatar/UserAvatar'; + +const AgentAvatar = ({ name, username, eTag }: { name: string; username: string; eTag?: string }) => { + const mediaQuery = useMediaQuery('(min-width: 1024px)'); + + return ( + + + + + + {name || username} + + {!mediaQuery && name && ( + + {' '} + {`@${username}`}{' '} + + )} + + + + ); +}; + +export default memo(AgentAvatar); diff --git a/apps/meteor/client/views/omnichannel/departments/DepartmentAgentsTable/AgentRow.tsx b/apps/meteor/client/views/omnichannel/departments/DepartmentAgentsTable/AgentRow.tsx new file mode 100644 index 000000000000..dfa14374dc01 --- /dev/null +++ b/apps/meteor/client/views/omnichannel/departments/DepartmentAgentsTable/AgentRow.tsx @@ -0,0 +1,37 @@ +import { NumberInput, TableCell, TableRow } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import React, { memo } from 'react'; +import type { UseFormRegister } from 'react-hook-form'; + +import type { FormValues, IDepartmentAgent } from '../EditDepartment'; +import AgentAvatar from './AgentAvatar'; +import RemoveAgentButton from './RemoveAgentButton'; + +type AgentRowProps = { + agent: IDepartmentAgent; + index: number; + register: UseFormRegister; + onRemove: (agentId: string) => void; +}; + +const AgentRow = ({ index, agent, register, onRemove }: AgentRowProps) => { + const t = useTranslation(); + + return ( + + + + + + + + + + + + + + + ); +}; +export default memo(AgentRow); diff --git a/apps/meteor/client/views/omnichannel/departments/DepartmentAgentsTable/DepartmentAgentsTable.tsx b/apps/meteor/client/views/omnichannel/departments/DepartmentAgentsTable/DepartmentAgentsTable.tsx new file mode 100644 index 000000000000..2f2f8d39e995 --- /dev/null +++ b/apps/meteor/client/views/omnichannel/departments/DepartmentAgentsTable/DepartmentAgentsTable.tsx @@ -0,0 +1,43 @@ +import { useTranslation } from '@rocket.chat/ui-contexts'; +import React from 'react'; +import type { Control, UseFormRegister } from 'react-hook-form'; +import { useWatch, useFieldArray } from 'react-hook-form'; + +import { GenericTable, GenericTableBody, GenericTableHeader, GenericTableHeaderCell } from '../../../../components/GenericTable'; +import type { FormValues } from '../EditDepartment'; +import AddAgent from './AddAgent'; +import AgentRow from './AgentRow'; + +type DepartmentAgentsTableProps = { + control: Control; + register: UseFormRegister; +}; + +function DepartmentAgentsTable({ control, register }: DepartmentAgentsTableProps) { + const t = useTranslation(); + const { fields, append, remove } = useFieldArray({ control, name: 'agentList' }); + const agentList = useWatch({ control, name: 'agentList' }); + + return ( + <> + + + + + {t('Name')} + {t('Count')} + {t('Order')} + {t('Remove')} + + + + {fields.map((agent, index) => ( + remove(index)} /> + ))} + + + + ); +} + +export default DepartmentAgentsTable; diff --git a/apps/meteor/client/views/omnichannel/departments/RemoveAgentButton.js b/apps/meteor/client/views/omnichannel/departments/DepartmentAgentsTable/RemoveAgentButton.tsx similarity index 63% rename from apps/meteor/client/views/omnichannel/departments/RemoveAgentButton.js rename to apps/meteor/client/views/omnichannel/departments/DepartmentAgentsTable/RemoveAgentButton.tsx index d79c3d214a7e..7d574b6a23e3 100644 --- a/apps/meteor/client/views/omnichannel/departments/RemoveAgentButton.js +++ b/apps/meteor/client/views/omnichannel/departments/DepartmentAgentsTable/RemoveAgentButton.tsx @@ -3,24 +3,23 @@ import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useSetModal, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; -import GenericModal from '../../../components/GenericModal'; +import GenericModal from '../../../../components/GenericModal'; -function RemoveAgentButton({ agentId, setAgentList, agentList, setAgentsRemoved }) { +function RemoveAgentButton({ agentId, onRemove }: { agentId: string; onRemove: (agentId: string) => void }) { const setModal = useSetModal(); const dispatchToastMessage = useToastMessageDispatch(); const t = useTranslation(); const handleDelete = useMutableCallback((e) => { e.stopPropagation(); - const onDeleteAgent = async () => { - const newList = agentList.filter((listItem) => listItem.agentId !== agentId); - setAgentList(newList); + + const onRemoveAgent = async () => { + onRemove(agentId); dispatchToastMessage({ type: 'success', message: t('Agent_removed') }); setModal(); - setAgentsRemoved((agents) => [...agents, { agentId }]); }; - setModal( setModal()} confirmText={t('Delete')} />); + setModal( setModal()} confirmText={t('Delete')} />); }); return ; diff --git a/apps/meteor/client/views/omnichannel/departments/DepartmentTags/index.tsx b/apps/meteor/client/views/omnichannel/departments/DepartmentTags/index.tsx new file mode 100644 index 000000000000..0607645e1696 --- /dev/null +++ b/apps/meteor/client/views/omnichannel/departments/DepartmentTags/index.tsx @@ -0,0 +1,63 @@ +import { Button, Chip, Field, TextInput } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import type { FormEvent } from 'react'; +import React, { useCallback, useState } from 'react'; + +type DepartmentTagsProps = { + error: string; + value: string[]; + onChange: (tags: string[]) => void; +}; + +export const DepartmentTags = ({ error, value: tags, onChange }: DepartmentTagsProps) => { + const t = useTranslation(); + const [tagText, setTagText] = useState(''); + + const handleAddTag = useCallback(() => { + if (tags.includes(tagText)) { + return; + } + + setTagText(''); + onChange([...tags, tagText]); + }, [onChange, tagText, tags]); + + const handleTagChipClick = (tag: string) => () => { + onChange(tags.filter((_tag) => _tag !== tag)); + }; + + return ( + <> + + ) => setTagText(e.currentTarget.value)} + /> + + + + {t('Conversation_closing_tags_description')} + + {tags?.length > 0 && ( + + {tags.map((tag, i) => ( + + {tag} + + ))} + + )} + + ); +}; diff --git a/apps/meteor/client/views/omnichannel/departments/DepartmentsAgentsTable.js b/apps/meteor/client/views/omnichannel/departments/DepartmentsAgentsTable.js deleted file mode 100644 index 09cef271a255..000000000000 --- a/apps/meteor/client/views/omnichannel/departments/DepartmentsAgentsTable.js +++ /dev/null @@ -1,56 +0,0 @@ -import { useMediaQuery } from '@rocket.chat/fuselage-hooks'; -import { useTranslation } from '@rocket.chat/ui-contexts'; -import React, { useState, useEffect } from 'react'; - -import GenericTable from '../../../components/GenericTable'; -import AddAgent from './AddAgent'; -import AgentRow from './AgentRow'; - -function DepartmentsAgentsTable({ agents, setAgentListFinal, setAgentsAdded, setAgentsRemoved }) { - const t = useTranslation(); - const [agentList, setAgentList] = useState((agents && JSON.parse(JSON.stringify(agents))) || []); - - useEffect(() => setAgentListFinal(agentList), [agentList, setAgentListFinal]); - - const mediaQuery = useMediaQuery('(min-width: 1024px)'); - - return ( - <> - - - - {t('Name')} - - - {t('Count')} - - - {t('Order')} - - - {t('Remove')} - - - } - results={agentList} - total={agentList?.length} - pi='x24' - > - {(props) => ( - - )} - - - ); -} - -export default DepartmentsAgentsTable; diff --git a/apps/meteor/client/views/omnichannel/departments/DepartmentsPageWithData.tsx b/apps/meteor/client/views/omnichannel/departments/DepartmentsPageWithData.tsx index fa87d2c3aee6..b042aa93134d 100644 --- a/apps/meteor/client/views/omnichannel/departments/DepartmentsPageWithData.tsx +++ b/apps/meteor/client/views/omnichannel/departments/DepartmentsPageWithData.tsx @@ -13,7 +13,7 @@ import DepartmentsTable from './DepartmentsTable'; const DepartmentsPageWithData = (): ReactElement => { const [text, setText] = useState(''); - const [debouncedText = ''] = useDebouncedValue(text, 500); + const debouncedText = useDebouncedValue(text, 500) || ''; const pagination = usePagination(); const sort = useSort<'name' | 'email' | 'active'>('name'); diff --git a/apps/meteor/client/views/omnichannel/departments/EditDepartment.js b/apps/meteor/client/views/omnichannel/departments/EditDepartment.js deleted file mode 100644 index 045cf59f0568..000000000000 --- a/apps/meteor/client/views/omnichannel/departments/EditDepartment.js +++ /dev/null @@ -1,498 +0,0 @@ -import { - FieldGroup, - Field, - TextInput, - Chip, - Box, - Icon, - Divider, - ToggleSwitch, - TextAreaInput, - ButtonGroup, - Button, - PaginatedSelectFiltered, -} from '@rocket.chat/fuselage'; -import { useMutableCallback, useUniqueId } from '@rocket.chat/fuselage-hooks'; -import { useToastMessageDispatch, useRoute, useMethod, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; -import React, { useMemo, useState, useRef, useCallback } from 'react'; - -import { validateEmail } from '../../../../lib/emailValidator'; -import Page from '../../../components/Page'; -import { useRecordList } from '../../../hooks/lists/useRecordList'; -import { useComponentDidUpdate } from '../../../hooks/useComponentDidUpdate'; -import { useForm } from '../../../hooks/useForm'; -import { useRoomsList } from '../../../hooks/useRoomsList'; -import { AsyncStatePhase } from '../../../lib/asyncState'; -import { useFormsSubscription } from '../additionalForms'; -import DepartmentsAgentsTable from './DepartmentsAgentsTable'; - -function withDefault(key, defaultValue) { - return key || defaultValue; -} - -function EditDepartment({ data, id, title, allowedToForwardData }) { - const t = useTranslation(); - const departmentsRoute = useRoute('omnichannel-departments'); - - const { - useEeNumberInput = () => {}, - useEeTextInput = () => {}, - useEeTextAreaInput = () => {}, - useDepartmentForwarding = () => {}, - useDepartmentBusinessHours = () => {}, - useSelectForwardDepartment = () => {}, - } = useFormsSubscription(); - - const { agents } = data || { agents: [] }; - - const initialAgents = useRef(agents); - - const MaxChats = useEeNumberInput(); - const VisitorInactivity = useEeNumberInput(); - const WaitingQueueMessageInput = useEeTextAreaInput(); - const AbandonedMessageInput = useEeTextInput(); - const DepartmentForwarding = useDepartmentForwarding(); - const DepartmentBusinessHours = useDepartmentBusinessHours(); - const AutoCompleteDepartment = useSelectForwardDepartment(); - const [agentList, setAgentList] = useState([]); - const [agentsRemoved, setAgentsRemoved] = useState([]); - const [agentsAdded, setAgentsAdded] = useState([]); - - const { department } = data || { department: {} }; - - const [initialTags] = useState(() => department?.chatClosingTags ?? []); - const [[tags, tagsText], setTagsState] = useState(() => [initialTags, '']); - const hasTagChanges = useMemo(() => tags.toString() !== initialTags.toString(), [tags, initialTags]); - - const { values, handlers, hasUnsavedChanges } = useForm({ - name: withDefault(department?.name, ''), - email: withDefault(department?.email, ''), - description: withDefault(department?.description, ''), - enabled: !!department?.enabled, - maxNumberSimultaneousChat: department?.maxNumberSimultaneousChat, - showOnRegistration: !!department?.showOnRegistration, - showOnOfflineForm: !!department?.showOnOfflineForm, - abandonedRoomsCloseCustomMessage: withDefault(department?.abandonedRoomsCloseCustomMessage, ''), - requestTagBeforeClosingChat: !!department?.requestTagBeforeClosingChat, - offlineMessageChannelName: withDefault(department?.offlineMessageChannelName, ''), - visitorInactivityTimeoutInSeconds: department?.visitorInactivityTimeoutInSeconds, - waitingQueueMessage: withDefault(department?.waitingQueueMessage, ''), - departmentsAllowedToForward: allowedToForwardData?.departments?.map((dep) => ({ label: dep.name, value: dep._id })) || [], - fallbackForwardDepartment: withDefault(department?.fallbackForwardDepartment, ''), - }); - const { - handleName, - handleEmail, - handleDescription, - handleEnabled, - handleMaxNumberSimultaneousChat, - handleShowOnRegistration, - handleShowOnOfflineForm, - handleAbandonedRoomsCloseCustomMessage, - handleRequestTagBeforeClosingChat, - handleOfflineMessageChannelName, - handleVisitorInactivityTimeoutInSeconds, - handleWaitingQueueMessage, - handleDepartmentsAllowedToForward, - handleFallbackForwardDepartment, - } = handlers; - - const { - name, - email, - description, - enabled, - maxNumberSimultaneousChat, - showOnRegistration, - showOnOfflineForm, - abandonedRoomsCloseCustomMessage, - requestTagBeforeClosingChat, - offlineMessageChannelName, - visitorInactivityTimeoutInSeconds, - waitingQueueMessage, - departmentsAllowedToForward, - fallbackForwardDepartment, - } = values; - - const { itemsList: RoomsList, loadMoreItems: loadMoreRooms } = useRoomsList( - useMemo(() => ({ text: offlineMessageChannelName }), [offlineMessageChannelName]), - ); - - const { phase: roomsPhase, items: roomsItems, itemCount: roomsTotal } = useRecordList(RoomsList); - - const handleTagChipClick = (tag) => () => { - setTagsState(([tags, tagsText]) => [tags.filter((_tag) => _tag !== tag), tagsText]); - }; - - const handleTagTextSubmit = useCallback(() => { - setTagsState((state) => { - const [tags, tagsText] = state; - - if (tags.includes(tagsText)) { - return state; - } - - return [[...tags, tagsText], '']; - }); - }, []); - - const handleTagTextChange = (e) => { - setTagsState(([tags]) => [tags, e.target.value]); - }; - - const saveDepartmentInfo = useMethod('livechat:saveDepartment'); - const saveDepartmentAgentsInfoOnEdit = useEndpoint('POST', `/v1/livechat/department/${id}/agents`); - - const dispatchToastMessage = useToastMessageDispatch(); - - const [nameError, setNameError] = useState(); - const [emailError, setEmailError] = useState(); - const [tagError, setTagError] = useState(); - - useComponentDidUpdate(() => { - setNameError(!name ? t('The_field_is_required', 'name') : ''); - }, [t, name]); - useComponentDidUpdate(() => { - setEmailError(!email ? t('The_field_is_required', 'email') : ''); - }, [t, email]); - useComponentDidUpdate(() => { - setEmailError(!validateEmail(email) ? t('Validate_email_address') : ''); - }, [t, email]); - useComponentDidUpdate(() => { - setTagError(requestTagBeforeClosingChat && (!tags || tags.length === 0) ? t('The_field_is_required', 'name') : ''); - }, [requestTagBeforeClosingChat, t, tags]); - - const handleSubmit = useMutableCallback(async (e) => { - e.preventDefault(); - let error = false; - if (!name) { - setNameError(t('The_field_is_required', 'name')); - error = true; - } - if (!email) { - setEmailError(t('The_field_is_required', 'email')); - error = true; - } - if (!validateEmail(email)) { - setEmailError(t('Validate_email_address')); - error = true; - } - if (requestTagBeforeClosingChat && (!tags || tags.length === 0)) { - setTagError(t('The_field_is_required', 'tags')); - error = true; - } - - if (error) { - return; - } - - const payload = { - enabled, - name, - description, - showOnRegistration, - showOnOfflineForm, - requestTagBeforeClosingChat, - email, - chatClosingTags: tags, - offlineMessageChannelName, - maxNumberSimultaneousChat, - visitorInactivityTimeoutInSeconds, - abandonedRoomsCloseCustomMessage, - waitingQueueMessage, - departmentsAllowedToForward: departmentsAllowedToForward?.map((dep) => dep.value), - fallbackForwardDepartment, - }; - - const agentListPayload = { - upsert: agentList.filter( - (agent) => - !initialAgents.current.some( - (initialAgent) => initialAgent._id === agent._id && agent.count === initialAgent.count && agent.order === initialAgent.order, - ), - ), - remove: initialAgents.current.filter((initialAgent) => !agentList.some((agent) => initialAgent._id === agent._id)), - }; - - try { - if (id) { - await saveDepartmentInfo(id, payload, []); - if (agentListPayload.upsert.length > 0 || agentListPayload.remove.length > 0) { - await saveDepartmentAgentsInfoOnEdit(agentListPayload); - } - } else { - await saveDepartmentInfo(id, payload, agentList); - } - dispatchToastMessage({ type: 'success', message: t('Saved') }); - departmentsRoute.push({}); - } catch (error) { - dispatchToastMessage({ type: 'error', message: error }); - } - }); - - const handleReturn = useMutableCallback(() => { - departmentsRoute.push({}); - }); - - const invalidForm = - !name || - !email || - !validateEmail(email) || - !(hasUnsavedChanges || hasTagChanges) || - (requestTagBeforeClosingChat && (!tags || tags.length === 0)); - - const formId = useUniqueId(); - - const hasNewAgent = useMemo(() => agents.length === agentList.length, [agents, agentList]); - - const agentsHaveChanged = () => { - let hasChanges = false; - if (agentList.length !== initialAgents.current.length) { - hasChanges = true; - } - - if (agentsAdded.length > 0 && agentsRemoved.length > 0) { - hasChanges = true; - } - - agentList.forEach((agent) => { - const existingAgent = initialAgents.current.find((initial) => initial.agentId === agent.agentId); - if (existingAgent) { - if (agent.count !== existingAgent.count) { - hasChanges = true; - } - if (agent.order !== existingAgent.order) { - hasChanges = true; - } - } - }); - - return hasChanges; - }; - - return ( - - - - - - - - - - - - - {t('Enabled')} - - - - - - - {t('Name')}* - - - - - - {t('Description')} - - - - - - - {t('Show_on_registration_page')} - - - - - - - {t('Email')}* - - } - onChange={handleEmail} - placeholder={t('Email')} - /> - - - - - {t('Show_on_offline_page')} - - - - - - - {t('Livechat_DepartmentOfflineMessageToChannel')} - - {} : (start) => loadMoreRooms(start, Math.min(50, roomsTotal))} - /> - - - {MaxChats && ( - - - - )} - {VisitorInactivity && ( - - - - )} - {AbandonedMessageInput && ( - - - - )} - {WaitingQueueMessageInput && ( - - - - )} - {DepartmentForwarding && ( - - - - )} - {AutoCompleteDepartment && ( - - {t('Fallback_forward_department')} - - - )} - - - {t('Request_tag_before_closing_chat')} - - - - - - {requestTagBeforeClosingChat && ( - - {t('Conversation_closing_tags')}* - - - - - {t('Conversation_closing_tags_description')} - {tags?.length > 0 && ( - - {tags.map((tag, i) => ( - - {tag} - - ))} - - )} - - )} - {DepartmentBusinessHours && ( - - - - )} - - - {t('Agents')}: - - - - - - - - - ); -} - -export default EditDepartment; diff --git a/apps/meteor/client/views/omnichannel/departments/EditDepartment.tsx b/apps/meteor/client/views/omnichannel/departments/EditDepartment.tsx new file mode 100644 index 000000000000..f02ef2ed150f --- /dev/null +++ b/apps/meteor/client/views/omnichannel/departments/EditDepartment.tsx @@ -0,0 +1,487 @@ +import type { ILivechatDepartment, ILivechatDepartmentAgents, Serialized } from '@rocket.chat/core-typings'; +import { + FieldGroup, + Field, + TextInput, + Box, + Icon, + Divider, + ToggleSwitch, + TextAreaInput, + ButtonGroup, + Button, + PaginatedSelectFiltered, +} from '@rocket.chat/fuselage'; +import { useMutableCallback, useUniqueId } from '@rocket.chat/fuselage-hooks'; +import { useToastMessageDispatch, useRoute, useMethod, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; +import React, { useMemo } from 'react'; +import { Controller, useForm } from 'react-hook-form'; + +import { validateEmail } from '../../../../lib/emailValidator'; +import Page from '../../../components/Page'; +import { useRecordList } from '../../../hooks/lists/useRecordList'; +import { useRoomsList } from '../../../hooks/useRoomsList'; +import { AsyncStatePhase } from '../../../lib/asyncState'; +import { useFormsSubscription } from '../additionalForms'; +import DepartmentsAgentsTable from './DepartmentAgentsTable/DepartmentAgentsTable'; +import { DepartmentTags } from './DepartmentTags'; + +export type EditDepartmentProps = { + id?: string; + title: string; + data?: Serialized<{ + department: ILivechatDepartment | null; + agents?: ILivechatDepartmentAgents[]; + }>; + allowedToForwardData?: Serialized<{ + departments: ILivechatDepartment[]; + }>; +}; + +type InitialValueParams = { + department?: Serialized | null; + agents?: Serialized[]; + allowedToForwardData?: EditDepartmentProps['allowedToForwardData']; +}; + +export type IDepartmentAgent = Pick & { + _id?: string; + name?: string; +}; + +export type FormValues = { + name: string; + email: string; + description: string; + enabled: boolean; + maxNumberSimultaneousChat: number; + showOnRegistration: boolean; + showOnOfflineForm: boolean; + abandonedRoomsCloseCustomMessage: string; + requestTagBeforeClosingChat: boolean; + offlineMessageChannelName: string; + visitorInactivityTimeoutInSeconds: number; + waitingQueueMessage: string; + departmentsAllowedToForward: { label: string; value: string }[]; + fallbackForwardDepartment: string; + agentList: IDepartmentAgent[]; + chatClosingTags: string[]; +}; + +function withDefault(key: T | undefined | null, defaultValue: T) { + return key || defaultValue; +} + +const getInitialValues = ({ department, agents, allowedToForwardData }: InitialValueParams) => ({ + name: withDefault(department?.name, ''), + email: withDefault(department?.email, ''), + description: withDefault(department?.description, ''), + enabled: !!department?.enabled, + maxNumberSimultaneousChat: department?.maxNumberSimultaneousChat, + showOnRegistration: !!department?.showOnRegistration, + showOnOfflineForm: !!department?.showOnOfflineForm, + abandonedRoomsCloseCustomMessage: withDefault(department?.abandonedRoomsCloseCustomMessage, ''), + requestTagBeforeClosingChat: !!department?.requestTagBeforeClosingChat, + offlineMessageChannelName: withDefault(department?.offlineMessageChannelName, ''), + visitorInactivityTimeoutInSeconds: department?.visitorInactivityTimeoutInSeconds, + waitingQueueMessage: withDefault(department?.waitingQueueMessage, ''), + departmentsAllowedToForward: allowedToForwardData?.departments?.map((dep) => ({ label: dep.name, value: dep._id })) || [], + fallbackForwardDepartment: withDefault(department?.fallbackForwardDepartment, ''), + chatClosingTags: department?.chatClosingTags ?? [], + agentList: agents || [], +}); + +function EditDepartment({ data, id, title, allowedToForwardData }: EditDepartmentProps) { + const t = useTranslation(); + const departmentsRoute = useRoute('omnichannel-departments'); + + const { + useEeNumberInput = () => null, + useEeTextInput = () => null, + useEeTextAreaInput = () => null, + useDepartmentForwarding = () => null, + useDepartmentBusinessHours = () => null, + useSelectForwardDepartment = () => null, + } = useFormsSubscription(); + + const { department, agents = [] } = data || {}; + + const MaxChats = useEeNumberInput(); + const VisitorInactivity = useEeNumberInput(); + const WaitingQueueMessageInput = useEeTextAreaInput(); + const AbandonedMessageInput = useEeTextInput(); + const DepartmentForwarding = useDepartmentForwarding(); + const DepartmentBusinessHours = useDepartmentBusinessHours(); + const AutoCompleteDepartment = useSelectForwardDepartment(); + + const initialValues = getInitialValues({ department, agents, allowedToForwardData }); + + const { + register, + control, + handleSubmit, + watch, + formState: { errors, isValid, isDirty }, + } = useForm({ mode: 'onChange', defaultValues: initialValues }); + + const requestTagBeforeClosingChat = watch('requestTagBeforeClosingChat'); + const offlineMessageChannelName = watch('offlineMessageChannelName'); + + const { itemsList: RoomsList, loadMoreItems: loadMoreRooms } = useRoomsList( + useMemo(() => ({ text: offlineMessageChannelName }), [offlineMessageChannelName]), + ); + + const { phase: roomsPhase, items: roomsItems, itemCount: roomsTotal } = useRecordList(RoomsList); + + const saveDepartmentInfo = useMethod('livechat:saveDepartment'); + const saveDepartmentAgentsInfoOnEdit = useEndpoint('POST', `/v1/livechat/department/:_id/agents`, { _id: id || '' }); + + const dispatchToastMessage = useToastMessageDispatch(); + + const handleSave = useMutableCallback(async (data: FormValues) => { + const { + agentList, + enabled, + name, + description, + showOnRegistration, + showOnOfflineForm, + email, + chatClosingTags, + offlineMessageChannelName, + maxNumberSimultaneousChat, + visitorInactivityTimeoutInSeconds, + abandonedRoomsCloseCustomMessage, + waitingQueueMessage, + departmentsAllowedToForward, + fallbackForwardDepartment, + } = data; + + const payload = { + enabled, + name, + description, + showOnRegistration, + showOnOfflineForm, + requestTagBeforeClosingChat, + email, + chatClosingTags, + offlineMessageChannelName, + maxNumberSimultaneousChat, + visitorInactivityTimeoutInSeconds, + abandonedRoomsCloseCustomMessage, + waitingQueueMessage, + departmentsAllowedToForward: departmentsAllowedToForward?.map((dep) => dep.value), + fallbackForwardDepartment, + }; + + try { + if (id) { + const { agentList: initialAgentList } = initialValues; + + const agentListPayload = { + upsert: agentList.filter( + (agent) => + !initialAgentList.some( + (initialAgent) => + initialAgent._id === agent._id && agent.count === initialAgent.count && agent.order === initialAgent.order, + ), + ), + remove: initialAgentList.filter((initialAgent) => !agentList.some((agent) => initialAgent._id === agent._id)), + }; + + await saveDepartmentInfo(id, payload, []); + if (agentListPayload.upsert.length > 0 || agentListPayload.remove.length > 0) { + await saveDepartmentAgentsInfoOnEdit(agentListPayload); + } + } else { + await saveDepartmentInfo(id ?? null, payload, agentList); + } + dispatchToastMessage({ type: 'success', message: t('Saved') }); + departmentsRoute.push({}); + } catch (error) { + dispatchToastMessage({ type: 'error', message: error }); + } + }); + + const handleReturn = useMutableCallback(() => { + departmentsRoute.push({}); + }); + + const isFormValid = isValid && isDirty; + + const formId = useUniqueId(); + + return ( + + + + + + + + + + + + + {t('Enabled')} + + + + + + + + {t('Name')}* + + + + {errors.name && {errors.name?.message}} + + + + {t('Description')} + + + + + + + + {t('Show_on_registration_page')} + + + + + + + + {t('Email')}* + + } + placeholder={t('Email')} + {...register('email', { + required: t('The_field_is_required', 'email'), + validate: (email) => validateEmail(email) || t('error-invalid-email-address'), + })} + /> + + {errors.email && {errors.email?.message}} + + + + + {t('Show_on_offline_page')} + + + + + + + + {t('Livechat_DepartmentOfflineMessageToChannel')} + + ( + undefined : (start) => loadMoreRooms(start, Math.min(50, roomsTotal)) + } + /> + )} + /> + + + + {MaxChats && ( + + ( + + )} + /> + + )} + + {VisitorInactivity && ( + + ( + + )} + /> + + )} + + {AbandonedMessageInput && ( + + ( + + )} + /> + + )} + + {WaitingQueueMessageInput && ( + + ( + + )} + /> + + )} + + {DepartmentForwarding && ( + + ( + + )} + /> + + )} + + {AutoCompleteDepartment && ( + + {t('Fallback_forward_department')} + ( + + )} + /> + + )} + + + + {t('Request_tag_before_closing_chat')} + + + + + + + {requestTagBeforeClosingChat && ( + + {t('Conversation_closing_tags')}* + ( + + )} + /> + {errors.chatClosingTags && {errors.chatClosingTags?.message}} + + )} + + {DepartmentBusinessHours && ( + + + + )} + + + + {t('Agents')}: + + + + + + + + + ); +} + +export default EditDepartment; diff --git a/apps/meteor/client/views/omnichannel/departments/EditDepartmentWithAllowedForwardData.js b/apps/meteor/client/views/omnichannel/departments/EditDepartmentWithAllowedForwardData.js deleted file mode 100644 index ba2aff79bb74..000000000000 --- a/apps/meteor/client/views/omnichannel/departments/EditDepartmentWithAllowedForwardData.js +++ /dev/null @@ -1,36 +0,0 @@ -import { Box } from '@rocket.chat/fuselage'; -import { useTranslation } from '@rocket.chat/ui-contexts'; -import React, { useMemo } from 'react'; - -import { FormSkeleton } from '../../../components/Skeleton'; -import { AsyncStatePhase } from '../../../hooks/useAsyncState'; -import { useEndpointData } from '../../../hooks/useEndpointData'; -import EditDepartment from './EditDepartment'; - -function EditDepartmentWithAllowedForwardData({ data, ...props }) { - const t = useTranslation(); - - const { - value: allowedToForwardData, - phase: allowedToForwardState, - error: allowedToForwardError, - } = useEndpointData('/v1/livechat/department.listByIds', { - params: useMemo( - () => ({ - ids: data?.department?.departmentsAllowedToForward ?? [], - }), - [data], - ), - }); - - if ([allowedToForwardState].includes(AsyncStatePhase.LOADING)) { - return ; - } - - if (allowedToForwardError) { - return {t('Not_Available')}; - } - return ; -} - -export default EditDepartmentWithAllowedForwardData; diff --git a/apps/meteor/client/views/omnichannel/departments/EditDepartmentWithAllowedForwardData.tsx b/apps/meteor/client/views/omnichannel/departments/EditDepartmentWithAllowedForwardData.tsx new file mode 100644 index 000000000000..1b9797f2dec4 --- /dev/null +++ b/apps/meteor/client/views/omnichannel/departments/EditDepartmentWithAllowedForwardData.tsx @@ -0,0 +1,35 @@ +import { Box } from '@rocket.chat/fuselage'; +import { useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; +import { useQuery } from '@tanstack/react-query'; +import React from 'react'; + +import { FormSkeleton } from '../../../components/Skeleton'; +import type { EditDepartmentProps } from './EditDepartment'; +import EditDepartment from './EditDepartment'; + +const EditDepartmentWithAllowedForwardData = ({ data, ...props }: Omit) => { + const t = useTranslation(); + const getDepartmentListByIds = useEndpoint('GET', '/v1/livechat/department.listByIds'); + + const { + data: allowedToForwardData, + isInitialLoading, + isError, + } = useQuery(['/v1/livechat/department.listByIds', data?.department?.departmentsAllowedToForward], () => + getDepartmentListByIds({ + ids: data?.department?.departmentsAllowedToForward ?? [], + }), + ); + + if (isInitialLoading) { + return ; + } + + if (isError) { + return {t('Not_Available')}; + } + + return ; +}; + +export default EditDepartmentWithAllowedForwardData; diff --git a/apps/meteor/client/views/omnichannel/departments/EditDepartmentWithData.js b/apps/meteor/client/views/omnichannel/departments/EditDepartmentWithData.js deleted file mode 100644 index be340125dfaa..000000000000 --- a/apps/meteor/client/views/omnichannel/departments/EditDepartmentWithData.js +++ /dev/null @@ -1,39 +0,0 @@ -import { Box } from '@rocket.chat/fuselage'; -import { useTranslation } from '@rocket.chat/ui-contexts'; -import React from 'react'; - -import { FormSkeleton } from '../../../components/Skeleton'; -import { AsyncStatePhase } from '../../../hooks/useAsyncState'; -import { useEndpointData } from '../../../hooks/useEndpointData'; -import EditDepartment from './EditDepartment'; -import EditDepartmentWithAllowedForwardData from './EditDepartmentWithAllowedForwardData'; - -const params = { onlyMyDepartments: true }; -function EditDepartmentWithData({ id, title }) { - const t = useTranslation(); - const { value: data, phase: state, error } = useEndpointData('/v1/livechat/department/:_id', { keys: { _id: id }, params }); - - if ([state].includes(AsyncStatePhase.LOADING)) { - return ; - } - - if (error || (id && !data?.department)) { - return {t('Department_not_found')}; - } - - if (data.department.archived === true) { - return {t('Department_archived')}; - } - - return ( - <> - {data && data.department && data.department.departmentsAllowedToForward && data.department.departmentsAllowedToForward.length > 0 ? ( - - ) : ( - - )} - - ); -} - -export default EditDepartmentWithData; diff --git a/apps/meteor/client/views/omnichannel/departments/EditDepartmentWithData.tsx b/apps/meteor/client/views/omnichannel/departments/EditDepartmentWithData.tsx new file mode 100644 index 000000000000..47f24247997b --- /dev/null +++ b/apps/meteor/client/views/omnichannel/departments/EditDepartmentWithData.tsx @@ -0,0 +1,45 @@ +import { Box } from '@rocket.chat/fuselage'; +import { useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; +import { useQuery } from '@tanstack/react-query'; +import React from 'react'; + +import { FormSkeleton } from '../../../components/Skeleton'; +import EditDepartment from './EditDepartment'; +import EditDepartmentWithAllowedForwardData from './EditDepartmentWithAllowedForwardData'; + +const params = { onlyMyDepartments: 'true' } as const; + +type EditDepartmentWithDataProps = { + id?: string; + title: string; +}; + +const EditDepartmentWithData = ({ id, title }: EditDepartmentWithDataProps) => { + const t = useTranslation(); + const getDepartment = useEndpoint('GET', '/v1/livechat/department/:_id', { _id: id ?? '' }); + const { data, isInitialLoading, isError } = useQuery(['/v1/livechat/department/:_id'], () => getDepartment(params), { enabled: !!id }); + + if (isInitialLoading) { + return ; + } + + if (isError || (id && !data?.department)) { + return {t('Department_not_found')}; + } + + if (data?.department?.archived === true) { + return {t('Department_archived')}; + } + + return ( + <> + {data?.department?.departmentsAllowedToForward && data.department.departmentsAllowedToForward.length > 0 ? ( + + ) : ( + + )} + + ); +}; + +export default EditDepartmentWithData; diff --git a/apps/meteor/client/views/omnichannel/departments/NewDepartment.tsx b/apps/meteor/client/views/omnichannel/departments/NewDepartment.tsx index e7d80fd4627f..85d72cf26bfa 100644 --- a/apps/meteor/client/views/omnichannel/departments/NewDepartment.tsx +++ b/apps/meteor/client/views/omnichannel/departments/NewDepartment.tsx @@ -12,9 +12,10 @@ type NewDepartmentProps = { }; const NewDepartment = ({ id }: NewDepartmentProps) => { + const t = useTranslation(); const setModal = useSetModal(); const getDepartmentCreationAvailable = useEndpoint('GET', '/v1/livechat/department/isDepartmentCreationAvailable'); - const { data, isLoading, error } = useQuery(['getDepartments'], () => getDepartmentCreationAvailable(), { + const { data, isLoading, isError } = useQuery(['getDepartments'], () => getDepartmentCreationAvailable(), { onSuccess: (data) => { if (data.isDepartmentCreationAvailable === false) { setModal( setModal(null)} />); @@ -22,9 +23,7 @@ const NewDepartment = ({ id }: NewDepartmentProps) => { }, }); - const t = useTranslation(); - - if (error) { + if (isError) { return {t('Unavailable')}; } @@ -32,8 +31,7 @@ const NewDepartment = ({ id }: NewDepartmentProps) => { return ; } - // TODO: remove allowedToForwardData and data props once the EditDepartment component is migrated to TS - return ; + return ; }; export default NewDepartment; diff --git a/apps/meteor/client/views/omnichannel/departments/Order.js b/apps/meteor/client/views/omnichannel/departments/Order.js deleted file mode 100644 index 5821a44a6c8a..000000000000 --- a/apps/meteor/client/views/omnichannel/departments/Order.js +++ /dev/null @@ -1,30 +0,0 @@ -import { Box, NumberInput } from '@rocket.chat/fuselage'; -import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { useTranslation } from '@rocket.chat/ui-contexts'; -import React, { useState } from 'react'; - -function Order({ agentId, setAgentList, agentList }) { - const t = useTranslation(); - const [agentOrder, setAgentOrder] = useState(agentList.find((agent) => agent.agentId === agentId).order || 0); - - const handleOrder = useMutableCallback(async (e) => { - const orderValue = Number(e.currentTarget.value); - setAgentOrder(orderValue); - setAgentList( - agentList.map((agent) => { - if (agent.agentId === agentId) { - agent.order = orderValue; - } - return agent; - }), - ); - }); - - return ( - - - - ); -} - -export default Order; diff --git a/apps/meteor/client/views/omnichannel/directory/CallsContextualBarDirectory.tsx b/apps/meteor/client/views/omnichannel/directory/CallsContextualBarDirectory.tsx index b7691b22bfd1..b330fd1bbee7 100644 --- a/apps/meteor/client/views/omnichannel/directory/CallsContextualBarDirectory.tsx +++ b/apps/meteor/client/views/omnichannel/directory/CallsContextualBarDirectory.tsx @@ -4,7 +4,7 @@ import { useRoute, useRouteParameter, useQueryStringParameter, useTranslation } import type { FC } from 'react'; import React, { useMemo } from 'react'; -import VerticalBar from '../../../components/VerticalBar'; +import { Contextualbar } from '../../../components/Contextualbar'; import { AsyncStatePhase } from '../../../hooks/useAsyncState'; import { useEndpointData } from '../../../hooks/useEndpointData'; import Call from './calls/Call'; @@ -20,7 +20,7 @@ const CallsContextualBarDirectory: FC = () => { const t = useTranslation(); - const handleCallsVerticalBarCloseButtonClick = (): void => { + const handleCallsContextualbarCloseButtonClick = (): void => { directoryRoute.push({ page: 'calls' }); }; @@ -52,7 +52,9 @@ const CallsContextualBarDirectory: FC = () => { const room = data.room as unknown as IVoipRoom; // TODO Check why types are incompatible even though the endpoint returns an IVoipRooms - return {bar === 'info' && }; + return ( + {bar === 'info' && } + ); }; export default CallsContextualBarDirectory; diff --git a/apps/meteor/client/views/omnichannel/directory/ChatsContextualBar.tsx b/apps/meteor/client/views/omnichannel/directory/ChatsContextualBar.tsx index 45f278924c6e..aa92b72310eb 100644 --- a/apps/meteor/client/views/omnichannel/directory/ChatsContextualBar.tsx +++ b/apps/meteor/client/views/omnichannel/directory/ChatsContextualBar.tsx @@ -3,7 +3,14 @@ import { useRoute, useRouteParameter, useTranslation } from '@rocket.chat/ui-con import type { FC } from 'react'; import React from 'react'; -import VerticalBar from '../../../components/VerticalBar'; +import { + Contextualbar, + ContextualbarHeader, + ContextualbarIcon, + ContextualbarTitle, + ContextualbarAction, + ContextualbarClose, +} from '../../../components/Contextualbar'; import Chat from './chats/Chat'; import ChatInfoDirectory from './chats/contextualBar/ChatInfoDirectory'; import { RoomEditWithData } from './chats/contextualBar/RoomEdit'; @@ -22,11 +29,11 @@ const ChatsContextualBar: FC<{ chatReload?: () => void }> = ({ chatReload }) => id && directoryRoute.push({ page: 'chats', id, bar: 'view' }); }; - const handleChatsVerticalBarCloseButtonClick = (): void => { + const handleChatsContextualbarCloseButtonClick = (): void => { directoryRoute.push({ page: 'chats' }); }; - const handleChatsVerticalBarBackButtonClick = (): void => { + const handleChatsContextualbarBackButtonClick = (): void => { id && directoryRoute.push({ page: 'chats', id, bar: 'info' }); }; @@ -49,28 +56,28 @@ const ChatsContextualBar: FC<{ chatReload?: () => void }> = ({ chatReload }) => } return ( - - + + {bar === 'info' && ( <> - - {t('Room_Info')} - + + {t('Room_Info')} + )} {bar === 'edit' && ( <> - - {t('edit-room')} + + {t('edit-room')} )} - - + + {bar === 'info' && } {bar === 'edit' && ( - + )} - + ); }; diff --git a/apps/meteor/client/views/omnichannel/directory/ContactContextualBar.tsx b/apps/meteor/client/views/omnichannel/directory/ContactContextualBar.tsx index c82679c55b0c..6a51b6fa4b03 100644 --- a/apps/meteor/client/views/omnichannel/directory/ContactContextualBar.tsx +++ b/apps/meteor/client/views/omnichannel/directory/ContactContextualBar.tsx @@ -1,7 +1,13 @@ import { useRoute, useRouteParameter, useTranslation } from '@rocket.chat/ui-contexts'; import React, { useMemo } from 'react'; -import VerticalBar from '../../../components/VerticalBar'; +import { + Contextualbar, + ContextualbarHeader, + ContextualbarIcon, + ContextualbarTitle, + ContextualbarClose, +} from '../../../components/Contextualbar'; import ContactEditWithData from './contacts/contextualBar/ContactEditWithData'; import ContactInfo from './contacts/contextualBar/ContactInfo'; import ContactNewEdit from './contacts/contextualBar/ContactNewEdit'; @@ -21,27 +27,27 @@ const ContactContextualBar = () => { const t = useTranslation(); - const handleContactsVerticalBarCloseButtonClick = () => { + const handleContactsContextualbarCloseButtonClick = () => { directoryRoute.push({ page: 'contacts' }); }; - const handleContactsVerticalBarBackButtonClick = () => { + const handleContactsContextualbarBackButtonClick = () => { directoryRoute.push({ page: 'contacts', id: contactId, bar: 'info' }); }; const header = useMemo(() => HEADER_OPTIONS[bar] || HEADER_OPTIONS.info, [bar]); return ( - - - - {t(header.title)} - - - {bar === 'new' && } + + + + {t(header.title)} + + + {bar === 'new' && } {bar === 'info' && } - {bar === 'edit' && } - + {bar === 'edit' && } + ); }; diff --git a/apps/meteor/client/views/omnichannel/directory/calls/contextualBar/VoipInfo.tsx b/apps/meteor/client/views/omnichannel/directory/calls/contextualBar/VoipInfo.tsx index 7959e070929c..473d86a8b002 100644 --- a/apps/meteor/client/views/omnichannel/directory/calls/contextualBar/VoipInfo.tsx +++ b/apps/meteor/client/views/omnichannel/directory/calls/contextualBar/VoipInfo.tsx @@ -6,9 +6,16 @@ import type { ReactElement } from 'react'; import React, { useMemo } from 'react'; import { parseOutboundPhoneNumber } from '../../../../../../ee/client/lib/voip/parseOutboundPhoneNumber'; +import { + ContextualbarIcon, + ContextualbarHeader, + ContextualbarTitle, + ContextualbarClose, + ContextualbarScrollableContent, + ContextualbarFooter, +} from '../../../../../components/Contextualbar'; import InfoPanel from '../../../../../components/InfoPanel'; import { UserStatus } from '../../../../../components/UserStatus'; -import VerticalBar from '../../../../../components/VerticalBar'; import UserAvatar from '../../../../../components/avatar/UserAvatar'; import { useIsCallReady } from '../../../../../contexts/CallContext'; import AgentInfoDetails from '../../../components/AgentInfoDetails'; @@ -38,12 +45,12 @@ export const VoipInfo = ({ room, onClickClose /* , onClickReport */ }: VoipInfo return ( <> - - - {t('Call_Information')} - - - + + + {t('Call_Information')} + + + {t('Channel')} @@ -84,8 +91,8 @@ export const VoipInfo = ({ room, onClickClose /* , onClickReport */ }: VoipInfo )} - - + + {/* TODO: Introduce this buttons [Not part of MVP] */} {/* */} {isCallReady && } - + ); }; diff --git a/apps/meteor/client/views/omnichannel/directory/chats/contextualBar/ChatInfo.js b/apps/meteor/client/views/omnichannel/directory/chats/contextualBar/ChatInfo.js index 7999794a52be..7c32188e7e34 100644 --- a/apps/meteor/client/views/omnichannel/directory/chats/contextualBar/ChatInfo.js +++ b/apps/meteor/client/views/omnichannel/directory/chats/contextualBar/ChatInfo.js @@ -5,7 +5,7 @@ import { Meteor } from 'meteor/meteor'; import moment from 'moment'; import React, { useEffect, useState } from 'react'; -import VerticalBar from '../../../../../components/VerticalBar'; +import { ContextualbarScrollableContent, ContextualbarFooter } from '../../../../../components/Contextualbar'; import { useEndpointData } from '../../../../../hooks/useEndpointData'; import { useFormatDateAndTime } from '../../../../../hooks/useFormatDateAndTime'; import { useFormatDuration } from '../../../../../hooks/useFormatDuration'; @@ -94,7 +94,7 @@ function ChatInfo({ id, route }) { return ( <> - + {source && } {room && v && } @@ -171,14 +171,14 @@ function ChatInfo({ id, route }) { {slaId && } {priorityId && } - - + + - + ); } diff --git a/apps/meteor/client/views/omnichannel/directory/chats/contextualBar/ChatInfoDirectory.js b/apps/meteor/client/views/omnichannel/directory/chats/contextualBar/ChatInfoDirectory.js index d8a8a39232ba..ab862dd3fb2d 100644 --- a/apps/meteor/client/views/omnichannel/directory/chats/contextualBar/ChatInfoDirectory.js +++ b/apps/meteor/client/views/omnichannel/directory/chats/contextualBar/ChatInfoDirectory.js @@ -6,7 +6,7 @@ import moment from 'moment'; import React, { useEffect, useState } from 'react'; import { hasPermission } from '../../../../../../app/authorization/client'; -import VerticalBar from '../../../../../components/VerticalBar'; +import { ContextualbarScrollableContent, ContextualbarFooter } from '../../../../../components/Contextualbar'; import { useEndpointData } from '../../../../../hooks/useEndpointData'; import { useFormatDateAndTime } from '../../../../../hooks/useFormatDateAndTime'; import { useFormatDuration } from '../../../../../hooks/useFormatDuration'; @@ -91,7 +91,7 @@ function ChatInfoDirectory({ id, route = undefined, room }) { return ( <> - + {room && v && } {visitorId && } @@ -171,14 +171,14 @@ function ChatInfoDirectory({ id, route = undefined, room }) { {slaId && } {priorityId && } - - + + - + ); } diff --git a/apps/meteor/client/views/omnichannel/directory/chats/contextualBar/ChatsContextualBar.tsx b/apps/meteor/client/views/omnichannel/directory/chats/contextualBar/ChatsContextualBar.tsx index 9d82a22f1f89..f5d48fd39e22 100644 --- a/apps/meteor/client/views/omnichannel/directory/chats/contextualBar/ChatsContextualBar.tsx +++ b/apps/meteor/client/views/omnichannel/directory/chats/contextualBar/ChatsContextualBar.tsx @@ -1,7 +1,7 @@ import { useRoute, useRouteParameter, useTranslation } from '@rocket.chat/ui-contexts'; import React, { useMemo } from 'react'; -import VerticalBar from '../../../../../components/VerticalBar'; +import { ContextualbarHeader, ContextualbarIcon, ContextualbarTitle, ContextualbarClose } from '../../../../../components/Contextualbar'; import { useTabBarClose } from '../../../../room/contexts/ToolboxContext'; import ChatInfo from './ChatInfo'; import { RoomEditWithData } from './RoomEdit'; @@ -32,13 +32,11 @@ const ChatsContextualBar = ({ rid }: ChatsContextualBarProps) => { return ( <> - - - {t(title)} - - - - + + + {t(title)} + + {context === 'edit' ? : } ); diff --git a/apps/meteor/client/views/omnichannel/directory/chats/contextualBar/RoomEdit/RoomEdit.tsx b/apps/meteor/client/views/omnichannel/directory/chats/contextualBar/RoomEdit/RoomEdit.tsx index 7bea7ef2146e..71e7ec57ca42 100644 --- a/apps/meteor/client/views/omnichannel/directory/chats/contextualBar/RoomEdit/RoomEdit.tsx +++ b/apps/meteor/client/views/omnichannel/directory/chats/contextualBar/RoomEdit/RoomEdit.tsx @@ -7,9 +7,9 @@ import { useController, useForm } from 'react-hook-form'; import { hasAtLeastOnePermission } from '../../../../../../../app/authorization/client'; import { useOmnichannelPriorities } from '../../../../../../../ee/client/omnichannel/hooks/useOmnichannelPriorities'; +import { ContextualbarFooter, ContextualbarScrollableContent } from '../../../../../../components/Contextualbar'; import { CustomFieldsForm } from '../../../../../../components/CustomFieldsFormV2'; import Tags from '../../../../../../components/Omnichannel/Tags'; -import VerticalBar from '../../../../../../components/VerticalBar'; import { useFormsSubscription } from '../../../../additionalForms'; import { FormSkeleton } from '../../../components/FormSkeleton'; import { useCustomFieldsMetadata } from '../../../hooks/useCustomFieldsMetadata'; @@ -113,15 +113,15 @@ function RoomEdit({ room, visitor, reload, reloadInfo, onClose }: RoomEditProps) if (isCustomFieldsLoading || isSlaPoliciesLoading || isPrioritiesLoading) { return ( - + - + ); } return ( <> - + {canViewCustomFields && customFieldsMetadata && ( )} @@ -144,9 +144,8 @@ function RoomEdit({ room, visitor, reload, reloadInfo, onClose }: RoomEditProps) {PrioritiesSelect && !!priorities?.length && ( )} - - - + + - + ); } diff --git a/apps/meteor/client/views/omnichannel/directory/contacts/contextualBar/ContactInfo.tsx b/apps/meteor/client/views/omnichannel/directory/contacts/contextualBar/ContactInfo.tsx index 2a419cd2e73d..0ee7df68b6c9 100644 --- a/apps/meteor/client/views/omnichannel/directory/contacts/contextualBar/ContactInfo.tsx +++ b/apps/meteor/client/views/omnichannel/directory/contacts/contextualBar/ContactInfo.tsx @@ -6,8 +6,8 @@ import React from 'react'; import { parseOutboundPhoneNumber } from '../../../../../../ee/client/lib/voip/parseOutboundPhoneNumber'; import ContactManagerInfo from '../../../../../../ee/client/omnichannel/ContactManagerInfo'; +import { ContextualbarScrollableContent, ContextualbarFooter } from '../../../../../components/Contextualbar'; import { UserStatus } from '../../../../../components/UserStatus'; -import VerticalBar from '../../../../../components/VerticalBar'; import UserAvatar from '../../../../../components/avatar/UserAvatar'; import { useIsCallReady } from '../../../../../contexts/CallContext'; import { useFormatDate } from '../../../../../hooks/useFormatDate'; @@ -107,7 +107,7 @@ const ContactInfo = ({ id: contactId, rid: roomId = '', route }: ContactInfoProp return ( <> - + {username && ( @@ -156,8 +156,8 @@ const ContactInfo = ({ id: contactId, rid: roomId = '', route }: ContactInfoProp )} - - + + {isCallReady && ( <> @@ -175,7 +175,7 @@ const ContactInfo = ({ id: contactId, rid: roomId = '', route }: ContactInfoProp {t('Edit')} - + ); }; diff --git a/apps/meteor/client/views/omnichannel/directory/contacts/contextualBar/ContactNewEdit.tsx b/apps/meteor/client/views/omnichannel/directory/contacts/contextualBar/ContactNewEdit.tsx index dc4d7d839e63..07f9024b4248 100644 --- a/apps/meteor/client/views/omnichannel/directory/contacts/contextualBar/ContactNewEdit.tsx +++ b/apps/meteor/client/views/omnichannel/directory/contacts/contextualBar/ContactNewEdit.tsx @@ -7,9 +7,8 @@ import { useForm } from 'react-hook-form'; import { hasAtLeastOnePermission } from '../../../../../../app/authorization/client'; import { validateEmail } from '../../../../../../lib/emailValidator'; -import { withDebouncing } from '../../../../../../lib/utils/highOrderFunctions'; +import { ContextualbarScrollableContent, ContextualbarFooter } from '../../../../../components/Contextualbar'; import { CustomFieldsForm } from '../../../../../components/CustomFieldsFormV2'; -import VerticalBar from '../../../../../components/VerticalBar'; import { createToken } from '../../../../../lib/utils/createToken'; import { useFormsSubscription } from '../../../additionalForms'; import { FormSkeleton } from '../../components/FormSkeleton'; @@ -82,19 +81,17 @@ const ContactNewEdit = ({ id, data, close }: ContactNewEditProps): ReactElement const { register, - formState: { errors, isValid: isFormValid, isDirty }, + formState: { errors, isValid, isDirty }, control, setValue, handleSubmit, - trigger, + setError, } = useForm({ - mode: 'onSubmit', - reValidateMode: 'onSubmit', + mode: 'onChange', + reValidateMode: 'onChange', defaultValues: initialValue, }); - const isValid = isDirty && isFormValid; - useEffect(() => { if (!initialUsername) { return; @@ -105,8 +102,8 @@ const ContactNewEdit = ({ id, data, close }: ContactNewEditProps): ReactElement }); }, [getUserData, initialUsername]); - const isEmailValid = async (email: string): Promise => { - if (email === initialValue.email) { + const validateEmailFormat = (email: string): boolean | string => { + if (!email || email === initialValue.email) { return true; } @@ -114,20 +111,20 @@ const ContactNewEdit = ({ id, data, close }: ContactNewEditProps): ReactElement return t('error-invalid-email-address'); } - const { contact } = await getContactBy({ email }); - return !contact || contact._id === id || t('Email_already_exists'); + return true; }; - const isPhoneValid = async (phone: string): Promise => { - if (!phone || initialValue.phone === phone) { + const validateContactField = async (name: 'phone' | 'email', value: string, optional = true) => { + if ((optional && !value) || value === initialValue[name]) { return true; } - const { contact } = await getContactBy({ phone }); - return !contact || contact._id === id || t('Phone_already_exists'); + const query = { [name]: value } as Record<'phone' | 'email', string>; + const { contact } = await getContactBy(query); + return !contact || contact._id === id; }; - const isNameValid = (v: string): string | boolean => (!v.trim() ? t('The_field_is_required', t('Name')) : true); + const validateName = (v: string): string | boolean => (!v.trim() ? t('The_field_is_required', t('Name')) : true); const handleContactManagerChange = async (userId: string): Promise => { setUserId(userId); @@ -141,9 +138,21 @@ const ContactNewEdit = ({ id, data, close }: ContactNewEditProps): ReactElement setValue('username', user.username || '', { shouldDirty: true }); }; - const validate = (fieldName: keyof ContactFormData): (() => void) => withDebouncing({ wait: 500 })(() => trigger(fieldName)); + const validateAsync = async ({ phone = '', email = '' } = {}) => { + const isEmailValid = await validateContactField('email', email); + const isPhoneValid = await validateContactField('phone', phone); + + !isEmailValid && setError('email', { message: t('Email_already_exists') }); + !isPhoneValid && setError('phone', { message: t('Phone_already_exists') }); + + return isEmailValid && isPhoneValid; + }; const handleSave = async (data: ContactFormData): Promise => { + if (!(await validateAsync(data))) { + return; + } + const { name, phone, email, customFields, username, token } = data; const payload = { @@ -171,49 +180,41 @@ const ContactNewEdit = ({ id, data, close }: ContactNewEditProps): ReactElement return ( <> - + {t('Name')}* - + {errors.name?.message} {t('Email')} - + {errors.email?.message} {t('Phone')} - + {errors.phone?.message} {canViewCustomFields() && } {ContactManager && } - - + + - - + ); }; diff --git a/apps/meteor/client/views/omnichannel/directory/contacts/contextualBar/ContactsContextualBar.tsx b/apps/meteor/client/views/omnichannel/directory/contacts/contextualBar/ContactsContextualBar.tsx index 02cf97861ca1..4b7e73386df8 100644 --- a/apps/meteor/client/views/omnichannel/directory/contacts/contextualBar/ContactsContextualBar.tsx +++ b/apps/meteor/client/views/omnichannel/directory/contacts/contextualBar/ContactsContextualBar.tsx @@ -3,7 +3,7 @@ import { useRoute, useRouteParameter, useTranslation } from '@rocket.chat/ui-con import type { FC } from 'react'; import React from 'react'; -import VerticalBar from '../../../../../components/VerticalBar'; +import { ContextualbarHeader, ContextualbarIcon, ContextualbarTitle, ContextualbarClose } from '../../../../../components/Contextualbar'; import { useOmnichannelRoom } from '../../../../room/contexts/RoomContext'; import { useTabBarClose } from '../../../../room/contexts/ToolboxContext'; import ContactEditWithData from './ContactEditWithData'; @@ -32,21 +32,21 @@ const ContactsContextualBar: FC<{ rid: IOmnichannelRoom['_id'] }> = ({ rid }) => return ( <> - + {(context === 'info' || !context) && ( <> - - {t('Contact_Info')} + + {t('Contact_Info')} )} {context === 'edit' && ( <> - - {t('Edit_Contact_Profile')} + + {t('Edit_Contact_Profile')} )} - - + + {context === 'edit' ? ( ) : ( diff --git a/apps/meteor/client/views/omnichannel/triggers/TriggersPage.js b/apps/meteor/client/views/omnichannel/triggers/TriggersPage.js index d459606d9f2e..fa1e86f3bfa4 100644 --- a/apps/meteor/client/views/omnichannel/triggers/TriggersPage.js +++ b/apps/meteor/client/views/omnichannel/triggers/TriggersPage.js @@ -3,8 +3,8 @@ import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useRoute, useRouteParameter, usePermission, useTranslation } from '@rocket.chat/ui-contexts'; import React, { useRef } from 'react'; +import { Contextualbar, ContextualbarHeader, ContextualbarClose, ContextualbarScrollableContent } from '../../../components/Contextualbar'; import Page from '../../../components/Page'; -import VerticalBar from '../../../components/VerticalBar'; import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; import EditTriggerPageContainer from './EditTriggerPageContainer'; import NewTriggerPage from './NewTriggerPage'; @@ -26,7 +26,7 @@ const MonitorsPage = () => { router.push({ context: 'new' }); }); - const handleCloseVerticalBar = useMutableCallback(() => { + const handleCloseContextualbar = useMutableCallback(() => { router.push({}); }); @@ -47,16 +47,16 @@ const MonitorsPage = () => { {context && ( - - + + {t('Trigger')} - - - + + + {context === 'edit' && } {context === 'new' && } - - + + )} ); diff --git a/apps/meteor/client/views/room/Header/ToolBox/ToolBox.tsx b/apps/meteor/client/views/room/Header/ToolBox/ToolBox.tsx index c9a654f63d64..6fadb9041948 100644 --- a/apps/meteor/client/views/room/Header/ToolBox/ToolBox.tsx +++ b/apps/meteor/client/views/room/Header/ToolBox/ToolBox.tsx @@ -76,7 +76,6 @@ const ToolBox = ({ className }: ToolBoxProps): ReactElement => { // }, [visibleActions.length, open]); // TODO: Create helper for render Actions - // TODO: Add proper Vertical Divider Component return ( <> @@ -87,7 +86,7 @@ const ToolBox = ({ className }: ToolBoxProps): ReactElement => { title: t(title), className, index, - info: id === tab?.id, + pressed: id === tab?.id, action, key: id, disabled, @@ -106,7 +105,7 @@ const ToolBox = ({ className }: ToolBoxProps): ReactElement => { title: t(title), className, index, - info: id === tab?.id, + pressed: id === tab?.id, action, key: id, disabled, diff --git a/apps/meteor/client/views/room/Room/Room.tsx b/apps/meteor/client/views/room/Room/Room.tsx index bcde1026d8d1..aef502a16a0b 100644 --- a/apps/meteor/client/views/room/Room/Room.tsx +++ b/apps/meteor/client/views/room/Room/Room.tsx @@ -3,7 +3,7 @@ import type { ReactElement } from 'react'; import React, { createElement, memo, Suspense } from 'react'; import { ErrorBoundary } from 'react-error-boundary'; -import VerticalBarSkeleton from '../../../components/VerticalBar/VerticalBarSkeleton'; +import { ContextualbarSkeleton } from '../../../components/Contextualbar'; import Header from '../Header'; import MessageHighlightProvider from '../MessageList/providers/MessageHighlightProvider'; import RoomBody from '../components/body/RoomBody'; @@ -38,7 +38,7 @@ const Room = (): ReactElement => { {typeof tab.template !== 'string' && typeof tab.template !== 'undefined' && ( - }> + }> {createElement(tab.template, { tabBar: toolbox, _id: room._id, rid: room._id, teamId: room.teamId })} )} @@ -48,7 +48,7 @@ const Room = (): ReactElement => { (appsContextualBarContext && ( - }> + }> ( } - aside={} + aside={} /> ); export default RoomSkeleton; diff --git a/apps/meteor/client/views/room/components/body/LoadingMessagesIndicator.tsx b/apps/meteor/client/views/room/components/body/LoadingMessagesIndicator.tsx index 701a20423e48..22598fe01c07 100644 --- a/apps/meteor/client/views/room/components/body/LoadingMessagesIndicator.tsx +++ b/apps/meteor/client/views/room/components/body/LoadingMessagesIndicator.tsx @@ -1,12 +1,8 @@ import type { ReactElement } from 'react'; import React from 'react'; -const LoadingMessagesIndicator = (): ReactElement => ( -
-
-
-
-
-); +import LoadingIndicator from '../../../../components/LoadingIndicator'; + +const LoadingMessagesIndicator = (): ReactElement => ; export default LoadingMessagesIndicator; diff --git a/apps/meteor/client/views/room/components/body/RoomBody.tsx b/apps/meteor/client/views/room/components/body/RoomBody.tsx index 40a5199eecda..2859395ac2cc 100644 --- a/apps/meteor/client/views/room/components/body/RoomBody.tsx +++ b/apps/meteor/client/views/room/components/body/RoomBody.tsx @@ -563,7 +563,10 @@ const RoomBody = (): ReactElement => { /> ))}
-
+
{ fileInputRef.current?.click(); - - // Simple hack for iOS aka codegueira - if (navigator.userAgent.match(/(iPad|iPhone|iPod)/g)) { - fileInputRef.current?.click(); - } }; if (collapsed) { diff --git a/apps/meteor/client/views/room/components/contextualBar/MessageListTab.tsx b/apps/meteor/client/views/room/components/contextualBar/MessageListTab.tsx index a1a63ac2dddd..eb4a7c8dbc60 100644 --- a/apps/meteor/client/views/room/components/contextualBar/MessageListTab.tsx +++ b/apps/meteor/client/views/room/components/contextualBar/MessageListTab.tsx @@ -9,12 +9,15 @@ import { Virtuoso } from 'react-virtuoso'; import { MessageTypes } from '../../../../../app/ui-utils/client'; import type { MessageActionContext } from '../../../../../app/ui-utils/client/lib/MessageAction'; +import { + ContextualbarContent, + ContextualbarHeader, + ContextualbarIcon, + ContextualbarTitle, + ContextualbarClose, + ContextualbarEmptyContent, +} from '../../../../components/Contextualbar'; import ScrollableContentWrapper from '../../../../components/ScrollableContentWrapper'; -import VerticalBarClose from '../../../../components/VerticalBar/VerticalBarClose'; -import VerticalBarContent from '../../../../components/VerticalBar/VerticalBarContent'; -import VerticalBarHeader from '../../../../components/VerticalBar/VerticalBarHeader'; -import VerticalBarIcon from '../../../../components/VerticalBar/VerticalBarIcon'; -import VerticalBarText from '../../../../components/VerticalBar/VerticalBarText'; import RoomMessage from '../../../../components/message/variants/RoomMessage'; import SystemMessage from '../../../../components/message/variants/SystemMessage'; import { useFormatDate } from '../../../../hooks/useFormatDate'; @@ -28,7 +31,7 @@ import { useTabBarClose } from '../../contexts/ToolboxContext'; type MessageListTabProps = { iconName: ComponentProps['name']; title: ReactNode; - emptyResultMessage: ReactNode; + emptyResultMessage: string; context: MessageActionContext; queryResult: UseQueryResult; }; @@ -47,12 +50,12 @@ const MessageListTab = ({ iconName, title, emptyResultMessage, context, queryRes return ( <> - - - {title} - - - + + + {title} + + + {queryResult.isLoading && ( @@ -60,11 +63,7 @@ const MessageListTab = ({ iconName, title, emptyResultMessage, context, queryRes )} {queryResult.isSuccess && ( <> - {queryResult.data.length === 0 && ( - - {emptyResultMessage} - - )} + {queryResult.data.length === 0 && } {queryResult.data.length > 0 && ( @@ -119,7 +118,7 @@ const MessageListTab = ({ iconName, title, emptyResultMessage, context, queryRes )} )} - + ); }; diff --git a/apps/meteor/client/views/room/contextualBar/Apps/Apps.tsx b/apps/meteor/client/views/room/contextualBar/Apps/Apps.tsx index d8d722d2d13e..1c353ff9a323 100644 --- a/apps/meteor/client/views/room/contextualBar/Apps/Apps.tsx +++ b/apps/meteor/client/views/room/contextualBar/Apps/Apps.tsx @@ -6,7 +6,13 @@ import { BlockContext } from '@rocket.chat/ui-kit'; import React from 'react'; import { getURL } from '../../../../../app/utils/client/getURL'; -import VerticalBar from '../../../../components/VerticalBar'; +import { + ContextualbarHeader, + ContextualbarTitle, + ContextualbarScrollableContent, + ContextualbarFooter, + ContextualbarClose, +} from '../../../../components/Contextualbar'; import { getButtonStyle } from '../../../modal/uikit/getButtonStyle'; type AppsProps = { @@ -19,17 +25,17 @@ type AppsProps = { const Apps = ({ view, onSubmit, onClose, onCancel, appId }: AppsProps): JSX.Element => ( <> - + - {modalParser.text(view.title, BlockContext.NONE, 0)} - {onClose && } - - + {modalParser.text(view.title, BlockContext.NONE, 0)} + {onClose && } + + - - + + {view.close && ( )} - + ); diff --git a/apps/meteor/client/views/room/contextualBar/AutoTranslate/AutoTranslate.stories.tsx b/apps/meteor/client/views/room/contextualBar/AutoTranslate/AutoTranslate.stories.tsx index c36837ca257f..d29b118d3df9 100644 --- a/apps/meteor/client/views/room/contextualBar/AutoTranslate/AutoTranslate.stories.tsx +++ b/apps/meteor/client/views/room/contextualBar/AutoTranslate/AutoTranslate.stories.tsx @@ -1,7 +1,7 @@ import type { ComponentMeta, ComponentStory } from '@storybook/react'; import React from 'react'; -import VerticalBar from '../../../../components/VerticalBar'; +import { Contextualbar } from '../../../../components/Contextualbar'; import AutoTranslate from './AutoTranslate'; export default { @@ -10,7 +10,7 @@ export default { parameters: { layout: 'fullscreen', }, - decorators: [(fn) => {fn()}], + decorators: [(fn) => {fn()}], } as ComponentMeta; export const Default: ComponentStory = (args) => ; diff --git a/apps/meteor/client/views/room/contextualBar/AutoTranslate/AutoTranslate.tsx b/apps/meteor/client/views/room/contextualBar/AutoTranslate/AutoTranslate.tsx index 78060f57a52c..bf539c48ad04 100644 --- a/apps/meteor/client/views/room/contextualBar/AutoTranslate/AutoTranslate.tsx +++ b/apps/meteor/client/views/room/contextualBar/AutoTranslate/AutoTranslate.tsx @@ -4,7 +4,13 @@ import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement, ChangeEvent } from 'react'; import React from 'react'; -import VerticalBar from '../../../../components/VerticalBar'; +import { + ContextualbarClose, + ContextualbarTitle, + ContextualbarHeader, + ContextualbarIcon, + ContextualbarContent, +} from '../../../../components/Contextualbar'; type AutoTranslateProps = { language: string; @@ -27,12 +33,12 @@ const AutoTranslate = ({ return ( <> - - - {t('Auto_Translate')} - {handleClose && } - - + + + {t('Auto_Translate')} + {handleClose && } + + @@ -47,7 +53,7 @@ const AutoTranslate = ({ - + ); }; diff --git a/apps/meteor/client/views/room/contextualBar/Discussions/DiscussionsList.tsx b/apps/meteor/client/views/room/contextualBar/Discussions/DiscussionsList.tsx index bbdaabfcd2cf..9ced5a8ed4cd 100644 --- a/apps/meteor/client/views/room/contextualBar/Discussions/DiscussionsList.tsx +++ b/apps/meteor/client/views/room/contextualBar/Discussions/DiscussionsList.tsx @@ -6,8 +6,14 @@ import type { RefObject } from 'react'; import React, { useCallback } from 'react'; import { Virtuoso } from 'react-virtuoso'; +import { + ContextualbarHeader, + ContextualbarIcon, + ContextualbarContent, + ContextualbarClose, + ContextualbarEmptyContent, +} from '../../../../components/Contextualbar'; import ScrollableContentWrapper from '../../../../components/ScrollableContentWrapper'; -import VerticalBar from '../../../../components/VerticalBar'; import { goToRoomById } from '../../../../lib/utils/goToRoomById'; import DiscussionsListRow from './DiscussionsListRow'; @@ -47,15 +53,15 @@ function DiscussionsList({ }); return ( <> - - + + {t('Discussions')} - - + + - + )} - {!loading && total === 0 && ( - - {t('No_Discussions_found')} - - )} + {!loading && total === 0 && } {!error && total > 0 && discussions.length > 0 && ( @@ -111,7 +113,7 @@ function DiscussionsList({ /> )} - + ); } diff --git a/apps/meteor/client/views/room/contextualBar/ExportMessages/ExportMessages.stories.tsx b/apps/meteor/client/views/room/contextualBar/ExportMessages/ExportMessages.stories.tsx index a280db56df81..ad218cb64409 100644 --- a/apps/meteor/client/views/room/contextualBar/ExportMessages/ExportMessages.stories.tsx +++ b/apps/meteor/client/views/room/contextualBar/ExportMessages/ExportMessages.stories.tsx @@ -1,7 +1,7 @@ import type { ComponentMeta, ComponentStory } from '@storybook/react'; import React from 'react'; -import VerticalBar from '../../../../components/VerticalBar/VerticalBar'; +import { Contextualbar } from '../../../../components/Contextualbar'; import ExportMessages from './index'; export default { @@ -10,7 +10,7 @@ export default { parameters: { layout: 'fullscreen', }, - decorators: [(fn) => {fn()}], + decorators: [(fn) => {fn()}], } as ComponentMeta; export const Default: ComponentStory = (args) => ; diff --git a/apps/meteor/client/views/room/contextualBar/ExportMessages/ExportMessages.tsx b/apps/meteor/client/views/room/contextualBar/ExportMessages/ExportMessages.tsx index c39b1a845e2c..d03b96450410 100644 --- a/apps/meteor/client/views/room/contextualBar/ExportMessages/ExportMessages.tsx +++ b/apps/meteor/client/views/room/contextualBar/ExportMessages/ExportMessages.tsx @@ -5,7 +5,13 @@ import { useTranslation } from '@rocket.chat/ui-contexts'; import type { FC } from 'react'; import React, { useState, useMemo } from 'react'; -import VerticalBar from '../../../../components/VerticalBar'; +import { + ContextualbarHeader, + ContextualbarIcon, + ContextualbarTitle, + ContextualbarClose, + ContextualbarScrollableContent, +} from '../../../../components/Contextualbar'; import { useTabBarClose } from '../../contexts/ToolboxContext'; import FileExport from './FileExport'; import MailExportForm from './MailExportForm'; @@ -29,12 +35,12 @@ const ExportMessages: FC = ({ rid }) => { return ( <> - - - {t('Export_Messages')} - - - + + + {t('Export_Messages')} + + + {t('Method')} @@ -45,7 +51,7 @@ const ExportMessages: FC = ({ rid }) => { {type && type === 'file' && } {type && type === 'email' && } - + ); }; diff --git a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditChannel.js b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditChannel.js index b40f16712e89..eb344108278b 100644 --- a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditChannel.js +++ b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditChannel.js @@ -31,9 +31,15 @@ import React, { useCallback, useMemo, useRef } from 'react'; import { e2e } from '../../../../../../app/e2e/client/rocketchat.e2e'; import { MessageTypesValues } from '../../../../../../app/lib/lib/MessageTypes'; import { RoomSettingsEnum } from '../../../../../../definition/IRoomTypeConfig'; +import { + ContextualbarHeader, + ContextualbarBack, + ContextualbarTitle, + ContextualbarClose, + ContextualbarScrollableContent, +} from '../../../../../components/Contextualbar'; import GenericModal from '../../../../../components/GenericModal'; import RawText from '../../../../../components/RawText'; -import VerticalBar from '../../../../../components/VerticalBar'; import RoomAvatarEditor from '../../../../../components/avatar/RoomAvatarEditor'; import { useEndpointAction } from '../../../../../hooks/useEndpointAction'; import { useForm } from '../../../../../hooks/useForm'; @@ -293,13 +299,13 @@ function EditChannel({ room, onClickClose, onClickBack }) { return ( <> - - {onClickBack && } - {room.teamId ? t('edit-team') : t('edit-room')} - {onClickClose && } - + + {onClickBack && } + {room.teamId ? t('edit-team') : t('edit-room')} + {onClickClose && } + - e.preventDefault())}> + e.preventDefault())}> @@ -505,7 +511,7 @@ function EditChannel({ room, onClickClose, onClickBack }) { - + ); } diff --git a/apps/meteor/client/views/room/contextualBar/Info/RoomInfo/RoomInfo.stories.tsx b/apps/meteor/client/views/room/contextualBar/Info/RoomInfo/RoomInfo.stories.tsx index e1b12e7d3184..4554447d4ec2 100644 --- a/apps/meteor/client/views/room/contextualBar/Info/RoomInfo/RoomInfo.stories.tsx +++ b/apps/meteor/client/views/room/contextualBar/Info/RoomInfo/RoomInfo.stories.tsx @@ -2,7 +2,7 @@ import type { RoomType } from '@rocket.chat/core-typings'; import type { ComponentMeta, ComponentStory } from '@storybook/react'; import React from 'react'; -import VerticalBar from '../../../../../components/VerticalBar'; +import { Contextualbar } from '../../../../../components/Contextualbar'; import RoomInfo from './RoomInfo'; export default { @@ -12,7 +12,7 @@ export default { layout: 'fullscreen', actions: { argTypesRegex: '^on[A-Z].*' }, }, - decorators: [(fn) => {fn()}], + decorators: [(fn) => {fn()}], args: { icon: 'lock', }, diff --git a/apps/meteor/client/views/room/contextualBar/Info/RoomInfo/RoomInfo.tsx b/apps/meteor/client/views/room/contextualBar/Info/RoomInfo/RoomInfo.tsx index 8b65bfd62b82..bcfe97d6b816 100644 --- a/apps/meteor/client/views/room/contextualBar/Info/RoomInfo/RoomInfo.tsx +++ b/apps/meteor/client/views/room/contextualBar/Info/RoomInfo/RoomInfo.tsx @@ -3,10 +3,17 @@ import { Box, Callout, Menu, Option } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; import React, { useMemo } from 'react'; +import { + ContextualbarHeader, + ContextualbarScrollableContent, + ContextualbarBack, + ContextualbarIcon, + ContextualbarClose, + ContextualbarTitle, +} from '../../../../../components/Contextualbar'; import InfoPanel from '../../../../../components/InfoPanel'; import RetentionPolicyCallout from '../../../../../components/InfoPanel/RetentionPolicyCallout'; import MarkdownText from '../../../../../components/MarkdownText'; -import VerticalBar from '../../../../../components/VerticalBar'; import RoomAvatar from '../../../../../components/avatar/RoomAvatar'; import type { Action } from '../../../../hooks/useActionSpread'; import { useActionSpread } from '../../../../hooks/useActionSpread'; @@ -61,13 +68,13 @@ const RoomInfo = ({ room, icon, onClickBack, onClickClose, onClickEnterRoom, onC return ( <> - - {onClickBack ? : } - {t('Room_Info')} - {onClickClose && } - + + {onClickBack ? : } + {t('Room_Info')} + {onClickClose && } + - + @@ -134,7 +141,7 @@ const RoomInfo = ({ room, icon, onClickBack, onClickClose, onClickEnterRoom, onC )} - + ); }; diff --git a/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/KeyboardShortcuts.stories.tsx b/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/KeyboardShortcuts.stories.tsx index c0dfe69c07b4..58f4e65e5de6 100644 --- a/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/KeyboardShortcuts.stories.tsx +++ b/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/KeyboardShortcuts.stories.tsx @@ -1,7 +1,7 @@ import type { ComponentMeta, ComponentStory } from '@storybook/react'; import React from 'react'; -import VerticalBar from '../../../../components/VerticalBar'; +import { Contextualbar } from '../../../../components/Contextualbar'; import KeyboardShortcutsWithData from './KeyboardShortcutsWithData'; export default { @@ -10,7 +10,7 @@ export default { parameters: { layout: 'fullscreen', }, - decorators: [(fn) => {fn()}], + decorators: [(fn) => {fn()}], } as ComponentMeta; export const Default: ComponentStory = (args) => ; diff --git a/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/KeyboardShortcuts.tsx b/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/KeyboardShortcuts.tsx index d2ad13592352..505a7cbc8590 100644 --- a/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/KeyboardShortcuts.tsx +++ b/apps/meteor/client/views/room/contextualBar/KeyboardShortcuts/KeyboardShortcuts.tsx @@ -2,7 +2,13 @@ import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { memo } from 'react'; -import VerticalBar from '../../../../components/VerticalBar'; +import { + ContextualbarHeader, + ContextualbarIcon, + ContextualbarTitle, + ContextualbarClose, + ContextualbarScrollableContent, +} from '../../../../components/Contextualbar'; import KeyboardShortcutSection from './KeyboardShortcutSection'; const KeyboardShortcuts = ({ handleClose }: { handleClose: () => void }): ReactElement => { @@ -10,12 +16,12 @@ const KeyboardShortcuts = ({ handleClose }: { handleClose: () => void }): ReactE return ( <> - - - {t('Keyboard_Shortcuts_Title')} - {handleClose && } - - + + + {t('Keyboard_Shortcuts_Title')} + {handleClose && } + + @@ -24,7 +30,7 @@ const KeyboardShortcuts = ({ handleClose }: { handleClose: () => void }): ReactE - + ); }; diff --git a/apps/meteor/client/views/room/contextualBar/MessageSearchTab/MessageSearchTab.tsx b/apps/meteor/client/views/room/contextualBar/MessageSearchTab/MessageSearchTab.tsx index 872f6b1ffcbc..b67ff96c34ec 100644 --- a/apps/meteor/client/views/room/contextualBar/MessageSearchTab/MessageSearchTab.tsx +++ b/apps/meteor/client/views/room/contextualBar/MessageSearchTab/MessageSearchTab.tsx @@ -2,11 +2,13 @@ import { Callout } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; import React, { useCallback, useState } from 'react'; -import VerticalBarClose from '../../../../components/VerticalBar/VerticalBarClose'; -import VerticalBarContent from '../../../../components/VerticalBar/VerticalBarContent'; -import VerticalBarHeader from '../../../../components/VerticalBar/VerticalBarHeader'; -import VerticalBarIcon from '../../../../components/VerticalBar/VerticalBarIcon'; -import VerticalBarText from '../../../../components/VerticalBar/VerticalBarText'; +import { + ContextualbarClose, + ContextualbarContent, + ContextualbarHeader, + ContextualbarTitle, + ContextualbarIcon, +} from '../../../../components/Contextualbar'; import { useTabBarClose } from '../../contexts/ToolboxContext'; import MessageSearch from './components/MessageSearch'; import MessageSearchForm from './components/MessageSearchForm'; @@ -26,12 +28,12 @@ const MessageSearchTab = () => { return ( <> - - - {t('Search_Messages')} - - - + + + {t('Search_Messages')} + + + {providerQuery.isSuccess && ( <> @@ -43,7 +45,7 @@ const MessageSearchTab = () => { {t('Search_current_provider_not_active')} )} - + ); }; diff --git a/apps/meteor/client/views/room/contextualBar/MessageSearchTab/components/MessageSearch.tsx b/apps/meteor/client/views/room/contextualBar/MessageSearchTab/components/MessageSearch.tsx index b8c38b9086ad..4866a749a708 100644 --- a/apps/meteor/client/views/room/contextualBar/MessageSearchTab/components/MessageSearch.tsx +++ b/apps/meteor/client/views/room/contextualBar/MessageSearchTab/components/MessageSearch.tsx @@ -5,6 +5,7 @@ import React, { Fragment, memo, useState } from 'react'; import { Virtuoso } from 'react-virtuoso'; import { MessageTypes } from '../../../../../../app/ui-utils/client'; +import { ContextualbarEmptyContent } from '../../../../../components/Contextualbar'; import ScrollableContentWrapper from '../../../../../components/ScrollableContentWrapper'; import RoomMessage from '../../../../../components/message/variants/RoomMessage'; import SystemMessage from '../../../../../components/message/variants/SystemMessage'; @@ -36,11 +37,7 @@ const MessageSearch = ({ searchText, globalSearch }: MessageSearchProps): ReactE {messageSearchQuery.data && ( <> - {messageSearchQuery.data.length === 0 && ( - - {t('No_results_found')} - - )} + {messageSearchQuery.data.length === 0 && } {messageSearchQuery.data.length > 0 && ( diff --git a/apps/meteor/client/views/room/contextualBar/NotificationPreferences/NotificationPreferences.stories.tsx b/apps/meteor/client/views/room/contextualBar/NotificationPreferences/NotificationPreferences.stories.tsx index ba43a8fb06e0..52db06378c9a 100644 --- a/apps/meteor/client/views/room/contextualBar/NotificationPreferences/NotificationPreferences.stories.tsx +++ b/apps/meteor/client/views/room/contextualBar/NotificationPreferences/NotificationPreferences.stories.tsx @@ -2,7 +2,7 @@ import { action } from '@storybook/addon-actions'; import type { ComponentMeta, ComponentStory } from '@storybook/react'; import React from 'react'; -import VerticalBar from '../../../../components/VerticalBar'; +import { Contextualbar } from '../../../../components/Contextualbar'; import NotificationsPreferences from './NotificationPreferences'; export default { @@ -11,7 +11,7 @@ export default { parameters: { layout: 'fullscreen', }, - decorators: [(fn) => {fn()}], + decorators: [(fn) => {fn()}], } as ComponentMeta; export const Default: ComponentStory = (args) => ; diff --git a/apps/meteor/client/views/room/contextualBar/NotificationPreferences/NotificationPreferences.tsx b/apps/meteor/client/views/room/contextualBar/NotificationPreferences/NotificationPreferences.tsx index 4d1a50b23d8f..1dbad165d473 100644 --- a/apps/meteor/client/views/room/contextualBar/NotificationPreferences/NotificationPreferences.tsx +++ b/apps/meteor/client/views/room/contextualBar/NotificationPreferences/NotificationPreferences.tsx @@ -5,7 +5,14 @@ import type { ReactElement } from 'react'; import React from 'react'; import { useFormContext } from 'react-hook-form'; -import VerticalBar from '../../../../components/VerticalBar'; +import { + ContextualbarHeader, + ContextualbarIcon, + ContextualbarTitle, + ContextualbarClose, + ContextualbarScrollableContent, + ContextualbarFooter, +} from '../../../../components/Contextualbar'; import NotificationPreferencesForm from './NotificationPreferencesForm'; type NotificationPreferencesProps = { @@ -30,22 +37,22 @@ const NotificationPreferences = ({ return ( <> - - - {t('Notifications_Preferences')} - {handleClose && } - - + + + {t('Notifications_Preferences')} + {handleClose && } + + - - + + {handleClose && } - + ); }; diff --git a/apps/meteor/client/views/room/contextualBar/OTR/OTR.stories.tsx b/apps/meteor/client/views/room/contextualBar/OTR/OTR.stories.tsx index d8d0dd34c4f4..c58266652847 100644 --- a/apps/meteor/client/views/room/contextualBar/OTR/OTR.stories.tsx +++ b/apps/meteor/client/views/room/contextualBar/OTR/OTR.stories.tsx @@ -2,7 +2,7 @@ import type { ComponentMeta, ComponentStory } from '@storybook/react'; import React from 'react'; import { OtrRoomState } from '../../../../../app/otr/lib/OtrRoomState'; -import VerticalBar from '../../../../components/VerticalBar'; +import { Contextualbar } from '../../../../components/Contextualbar'; import OTR from './OTR'; export default { @@ -12,7 +12,7 @@ export default { layout: 'fullscreen', actions: { argTypesRegex: '^on.*' }, }, - decorators: [(fn) => {fn()}], + decorators: [(fn) => {fn()}], } as ComponentMeta; const Template: ComponentStory = (args) => ; diff --git a/apps/meteor/client/views/room/contextualBar/OTR/OTR.tsx b/apps/meteor/client/views/room/contextualBar/OTR/OTR.tsx index 12c6df8782d8..c9af00d7cdef 100644 --- a/apps/meteor/client/views/room/contextualBar/OTR/OTR.tsx +++ b/apps/meteor/client/views/room/contextualBar/OTR/OTR.tsx @@ -5,7 +5,13 @@ import type { MouseEventHandler, ReactElement } from 'react'; import React from 'react'; import { OtrRoomState } from '../../../../../app/otr/lib/OtrRoomState'; -import VerticalBar from '../../../../components/VerticalBar'; +import { + ContextualbarHeader, + ContextualbarIcon, + ContextualbarTitle, + ContextualbarClose, + ContextualbarScrollableContent, +} from '../../../../components/Contextualbar'; import OTREstablished from './components/OTREstablished'; import OTRStates from './components/OTRStates'; @@ -73,16 +79,15 @@ const OTR = ({ isOnline, onClickClose, onClickStart, onClickEnd, onClickRefresh, return ( <> - - - {t('OTR')} - {onClickClose && } - - - + + + {t('OTR')} + {onClickClose && } + + {t('Off_the_record_conversation')} {isOnline ? renderOTRState() : {t('OTR_is_only_available_when_both_users_are_online')}} - + ); }; diff --git a/apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessages.stories.tsx b/apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessages.stories.tsx index d74711ec867e..a894bca693ab 100644 --- a/apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessages.stories.tsx +++ b/apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessages.stories.tsx @@ -1,7 +1,7 @@ import type { ComponentMeta, ComponentStory } from '@storybook/react'; import React from 'react'; -import VerticalBar from '../../../../components/VerticalBar'; +import { Contextualbar } from '../../../../components/Contextualbar'; import PruneMessages from './PruneMessages'; export default { @@ -11,7 +11,7 @@ export default { layout: 'fullscreen', actions: { argTypesRegex: '^on.*' }, }, - decorators: [(fn) => {fn()}], + decorators: [(fn) => {fn()}], } as ComponentMeta; const Template: ComponentStory = (args) => ; diff --git a/apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessages.tsx b/apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessages.tsx index 3ab5c428d54e..3dd4f7862609 100644 --- a/apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessages.tsx +++ b/apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessages.tsx @@ -4,8 +4,15 @@ import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; +import { + ContextualbarHeader, + ContextualbarIcon, + ContextualbarTitle, + ContextualbarScrollableContent, + ContextualbarFooter, + ContextualbarClose, +} from '../../../../components/Contextualbar'; import UserAutoCompleteMultiple from '../../../../components/UserAutoCompleteMultiple'; -import VerticalBar from '../../../../components/VerticalBar'; import PruneMessagesDateTimeRow from './PruneMessagesDateTimeRow'; import type { initialValues } from './PruneMessagesWithData'; @@ -46,12 +53,12 @@ const PruneMessages = ({ callOutText, validateText, values, handlers, onClickClo return ( <> - - - {t('Prune_Messages')} - {onClickClose && } - - + + + {t('Prune_Messages')} + {onClickClose && } + + {callOutText && !validateText && {callOutText}} {validateText && {validateText}} - - + + - + ); }; diff --git a/apps/meteor/client/views/room/contextualBar/RoomFiles/RoomFiles.js b/apps/meteor/client/views/room/contextualBar/RoomFiles/RoomFiles.js index b39df2bc22cd..66ec3a00d7e2 100644 --- a/apps/meteor/client/views/room/contextualBar/RoomFiles/RoomFiles.js +++ b/apps/meteor/client/views/room/contextualBar/RoomFiles/RoomFiles.js @@ -4,8 +4,15 @@ import { useTranslation } from '@rocket.chat/ui-contexts'; import React, { useMemo } from 'react'; import { Virtuoso } from 'react-virtuoso'; +import { + ContextualbarHeader, + ContextualbarIcon, + ContextualbarTitle, + ContextualbarClose, + ContextualbarContent, + ContextualbarEmptyContent, +} from '../../../../components/Contextualbar'; import ScrollableContentWrapper from '../../../../components/ScrollableContentWrapper'; -import VerticalBar from '../../../../components/VerticalBar'; import Row from './Row'; function RoomFiles({ @@ -47,13 +54,13 @@ function RoomFiles({ return ( <> - - - {t('Files')} - {onClickClose && } - + + + {t('Files')} + {onClickClose && } + - + @@ -77,27 +84,25 @@ function RoomFiles({ )} - {!loading && filesItems.length <= 0 && ( - - {t('No_files_found')} + {!loading && filesItems.length <= 0 && } + + {!loading && filesItems.length > 0 && ( + + {} : (start) => loadMoreItems(start, Math.min(50, total - start))} + overscan={50} + data={filesItems} + components={{ Scroller: ScrollableContentWrapper }} + itemContent={(index, data) => } + /> )} - - - {} : (start) => loadMoreItems(start, Math.min(50, total - start))} - overscan={50} - data={filesItems} - components={{ Scroller: ScrollableContentWrapper }} - itemContent={(index, data) => } - /> - - + ); } diff --git a/apps/meteor/client/views/room/contextualBar/RoomFiles/RoomFiles.stories.tsx b/apps/meteor/client/views/room/contextualBar/RoomFiles/RoomFiles.stories.tsx index e744e93c72a5..b71e09de5d4b 100644 --- a/apps/meteor/client/views/room/contextualBar/RoomFiles/RoomFiles.stories.tsx +++ b/apps/meteor/client/views/room/contextualBar/RoomFiles/RoomFiles.stories.tsx @@ -2,7 +2,7 @@ import { action } from '@storybook/addon-actions'; import type { ComponentMeta, ComponentStory } from '@storybook/react'; import React from 'react'; -import VerticalBar from '../../../../components/VerticalBar'; +import { Contextualbar } from '../../../../components/Contextualbar'; import RoomFiles from './RoomFiles'; export default { @@ -12,7 +12,7 @@ export default { layout: 'fullscreen', actions: { argTypesRegex: '^on.*' }, }, - decorators: [(fn) => {fn()}], + decorators: [(fn) => {fn()}], } as ComponentMeta; const Template: ComponentStory = (args) => ; diff --git a/apps/meteor/client/views/room/contextualBar/RoomFiles/components/FileItem.stories.tsx b/apps/meteor/client/views/room/contextualBar/RoomFiles/components/FileItem.stories.tsx index 555acb5bf8bb..5734dce90d5e 100644 --- a/apps/meteor/client/views/room/contextualBar/RoomFiles/components/FileItem.stories.tsx +++ b/apps/meteor/client/views/room/contextualBar/RoomFiles/components/FileItem.stories.tsx @@ -1,7 +1,7 @@ import type { ComponentMeta, ComponentStory } from '@storybook/react'; import React from 'react'; -import VerticalBar from '../../../../../components/VerticalBar'; +import { Contextualbar } from '../../../../../components/Contextualbar'; import FileItem from './FileItem'; export default { @@ -10,7 +10,7 @@ export default { parameters: { layout: 'fullscreen', }, - decorators: [(fn) => {fn()}], + decorators: [(fn) => {fn()}], } as ComponentMeta; export const Default: ComponentStory = (args) => ; diff --git a/apps/meteor/client/views/room/contextualBar/RoomMembers/AddUsers/AddUsers.stories.tsx b/apps/meteor/client/views/room/contextualBar/RoomMembers/AddUsers/AddUsers.stories.tsx index 60d0e5dc1352..8ba43abf2ce3 100644 --- a/apps/meteor/client/views/room/contextualBar/RoomMembers/AddUsers/AddUsers.stories.tsx +++ b/apps/meteor/client/views/room/contextualBar/RoomMembers/AddUsers/AddUsers.stories.tsx @@ -1,7 +1,7 @@ import type { ComponentMeta, ComponentStory } from '@storybook/react'; import React from 'react'; -import VerticalBar from '../../../../../components/VerticalBar'; +import { Contextualbar } from '../../../../../components/Contextualbar'; import AddUsers from './AddUsers'; export default { @@ -11,7 +11,7 @@ export default { layout: 'fullscreen', actions: { argTypesRegex: '^on.*' }, }, - decorators: [(fn) => {fn()}], + decorators: [(fn) => {fn()}], } as ComponentMeta; export const Default: ComponentStory = (args) => ; diff --git a/apps/meteor/client/views/room/contextualBar/RoomMembers/AddUsers/AddUsers.tsx b/apps/meteor/client/views/room/contextualBar/RoomMembers/AddUsers/AddUsers.tsx index 078f6651c5cf..926abd031ee5 100644 --- a/apps/meteor/client/views/room/contextualBar/RoomMembers/AddUsers/AddUsers.tsx +++ b/apps/meteor/client/views/room/contextualBar/RoomMembers/AddUsers/AddUsers.tsx @@ -4,9 +4,16 @@ import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; +import { + ContextualbarHeader, + ContextualbarBack, + ContextualbarTitle, + ContextualbarClose, + ContextualbarScrollableContent, + ContextualbarFooter, +} from '../../../../../components/Contextualbar'; import UserAutoCompleteMultiple from '../../../../../components/UserAutoCompleteMultiple'; import UserAutoCompleteMultipleFederated from '../../../../../components/UserAutoCompleteMultiple/UserAutoCompleteMultipleFederated'; -import VerticalBar from '../../../../../components/VerticalBar'; type AddUsersProps = { onClickClose?: () => void; @@ -22,12 +29,12 @@ const AddUsers = ({ onClickClose, onClickBack, onClickSave, users, isRoomFederat return ( <> - - {onClickBack && } - {t('Add_users')} - {onClickClose && } - - + + {onClickBack && } + {t('Add_users')} + {onClickClose && } + + {t('Choose_users')} @@ -38,14 +45,14 @@ const AddUsers = ({ onClickClose, onClickBack, onClickSave, users, isRoomFederat )} - - + + - + ); }; diff --git a/apps/meteor/client/views/room/contextualBar/RoomMembers/InviteUsers/InviteUsers.stories.tsx b/apps/meteor/client/views/room/contextualBar/RoomMembers/InviteUsers/InviteUsers.stories.tsx index 1707a49fceff..be46592cde42 100644 --- a/apps/meteor/client/views/room/contextualBar/RoomMembers/InviteUsers/InviteUsers.stories.tsx +++ b/apps/meteor/client/views/room/contextualBar/RoomMembers/InviteUsers/InviteUsers.stories.tsx @@ -1,7 +1,7 @@ import type { ComponentMeta, ComponentStory } from '@storybook/react'; import React from 'react'; -import VerticalBar from '../../../../../components/VerticalBar'; +import { Contextualbar } from '../../../../../components/Contextualbar'; import InviteUsers from './InviteUsers'; export default { @@ -11,7 +11,7 @@ export default { layout: 'fullscreen', actions: { argTypesRegex: '^on.*' }, }, - decorators: [(fn) => {fn()}], + decorators: [(fn) => {fn()}], } as ComponentMeta; export const Default: ComponentStory = (args) => ; diff --git a/apps/meteor/client/views/room/contextualBar/RoomMembers/InviteUsers/InviteUsers.tsx b/apps/meteor/client/views/room/contextualBar/RoomMembers/InviteUsers/InviteUsers.tsx index 2a3226380efa..fa7a915a7285 100644 --- a/apps/meteor/client/views/room/contextualBar/RoomMembers/InviteUsers/InviteUsers.tsx +++ b/apps/meteor/client/views/room/contextualBar/RoomMembers/InviteUsers/InviteUsers.tsx @@ -3,7 +3,13 @@ import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; -import VerticalBar from '../../../../../components/VerticalBar'; +import { + ContextualbarHeader, + ContextualbarTitle, + ContextualbarBack, + ContextualbarClose, + ContextualbarScrollableContent, +} from '../../../../../components/Contextualbar'; import EditInviteLink from './EditInviteLink'; import InviteLink from './InviteLink'; @@ -36,16 +42,16 @@ const InviteUsers = ({ return ( <> - - {(onClickBackMembers || onClickBackLink) && } - {t('Invite_Users')} - {onClose && } - - + + {(onClickBackMembers || onClickBackLink) && } + {t('Invite_Users')} + {onClose && } + + {error && {error.toString()}} {isEditing && !error && } {!isEditing && !error && } - + ); }; diff --git a/apps/meteor/client/views/room/contextualBar/RoomMembers/RoomMembers.stories.tsx b/apps/meteor/client/views/room/contextualBar/RoomMembers/RoomMembers.stories.tsx index 0e1f9fbc5dc6..d92365552673 100644 --- a/apps/meteor/client/views/room/contextualBar/RoomMembers/RoomMembers.stories.tsx +++ b/apps/meteor/client/views/room/contextualBar/RoomMembers/RoomMembers.stories.tsx @@ -3,7 +3,7 @@ import { action } from '@storybook/addon-actions'; import type { ComponentMeta, ComponentStory } from '@storybook/react'; import React from 'react'; -import VerticalBar from '../../../../components/VerticalBar'; +import { Contextualbar } from '../../../../components/Contextualbar'; import RoomMembers from './RoomMembers'; export default { @@ -13,7 +13,7 @@ export default { layout: 'fullscreen', actions: { argTypesRegex: '^on.*' }, }, - decorators: [(fn) => {fn()}], + decorators: [(fn) => {fn()}], } as ComponentMeta; const Template: ComponentStory = (args) => ; diff --git a/apps/meteor/client/views/room/contextualBar/RoomMembers/RoomMembers.tsx b/apps/meteor/client/views/room/contextualBar/RoomMembers/RoomMembers.tsx index c09a5ec664c4..2db9e2eec21c 100644 --- a/apps/meteor/client/views/room/contextualBar/RoomMembers/RoomMembers.tsx +++ b/apps/meteor/client/views/room/contextualBar/RoomMembers/RoomMembers.tsx @@ -7,9 +7,17 @@ import type { ReactElement, FormEventHandler, ComponentProps, MouseEvent } from import React, { useMemo } from 'react'; import { Virtuoso } from 'react-virtuoso'; +import { + ContextualbarHeader, + ContextualbarIcon, + ContextualbarTitle, + ContextualbarClose, + ContextualbarContent, + ContextualbarFooter, + ContextualbarEmptyContent, +} from '../../../../components/Contextualbar'; import InfiniteListAnchor from '../../../../components/InfiniteListAnchor'; import ScrollableContentWrapper from '../../../../components/ScrollableContentWrapper'; -import VerticalBar from '../../../../components/VerticalBar'; import RoomMembersRow from './RoomMembersRow'; type RoomMemberUser = Pick; @@ -82,13 +90,12 @@ const RoomMembers = ({ return ( <> - - - {isTeam ? t('Teams_members') : t('Members')} - {onClickClose && } - - - + + + {isTeam ? t('Teams_members') : t('Members')} + {onClickClose && } + + @@ -122,43 +129,39 @@ const RoomMembers = ({ )} - {!loading && members.length <= 0 && ( - - {t('No_members_found')} - - )} + {!loading && members.length <= 0 && } + + {!loading && members && members.length > 0 && ( + <> + + + {t('Showing')}: {members.length} + - {!loading && members.length > 0 && ( - - - {t('Showing')}: {members.length} + + {t('Total')}: {total} + - - {t('Total')}: {total} + + }} + itemContent={(index, data): ReactElement => } + /> - + )} - - - {!loading && members && members.length > 0 && ( - }} - itemContent={(index, data): ReactElement => } - /> - )} - - + {!isDirect && (onClickInvite || onClickAdd) && ( - + {onClickInvite && ( )} - + )} ); diff --git a/apps/meteor/client/views/room/contextualBar/Threads/Thread.tsx b/apps/meteor/client/views/room/contextualBar/Threads/Thread.tsx index 32e5aecdd3c7..3c551603f65d 100644 --- a/apps/meteor/client/views/room/contextualBar/Threads/Thread.tsx +++ b/apps/meteor/client/views/room/contextualBar/Threads/Thread.tsx @@ -6,12 +6,15 @@ import { useLayoutContextualBarExpanded, useToastMessageDispatch, useTranslation import type { VFC } from 'react'; import React from 'react'; -import VerticalBar from '../../../../components/VerticalBar'; -import VerticalBarAction from '../../../../components/VerticalBar/VerticalBarAction'; -import VerticalBarActions from '../../../../components/VerticalBar/VerticalBarActions'; -import VerticalBarClose from '../../../../components/VerticalBar/VerticalBarClose'; -import VerticalBarHeader from '../../../../components/VerticalBar/VerticalBarHeader'; -import VerticalBarInnerContent from '../../../../components/VerticalBar/VerticalBarInnerContent'; +import { + Contextualbar, + ContextualbarHeader, + ContextualbarAction, + ContextualbarActions, + ContextualbarClose, + ContextualbarBack, + ContextualbarInnerContent, +} from '../../../../components/Contextualbar'; import { useTabBarClose } from '../../contexts/ToolboxContext'; import { useGoToThreadList } from '../../hooks/useGoToThreadList'; import ChatProvider from '../../providers/ChatProvider'; @@ -76,10 +79,10 @@ const Thread: VFC = ({ tmid }) => { }; return ( - + {canExpand && expanded && } - = ({ tmid }) => { insetBlock={0} border='none' > - - + + {(mainMessageQueryResult.isLoading && ) || (mainMessageQueryResult.isSuccess && ) || null} - + {canExpand && ( - )} - - - - + + + {(mainMessageQueryResult.isLoading && ) || (mainMessageQueryResult.isSuccess && ( @@ -130,9 +133,9 @@ const Thread: VFC = ({ tmid }) => { )) || null} - + - + ); }; diff --git a/apps/meteor/client/views/room/contextualBar/Threads/ThreadList.tsx b/apps/meteor/client/views/room/contextualBar/Threads/ThreadList.tsx index 9e9605756a2e..b589f45b8394 100644 --- a/apps/meteor/client/views/room/contextualBar/Threads/ThreadList.tsx +++ b/apps/meteor/client/views/room/contextualBar/Threads/ThreadList.tsx @@ -6,12 +6,15 @@ import type { FormEvent, ReactElement, VFC } from 'react'; import React, { useMemo, useState, useCallback } from 'react'; import { Virtuoso } from 'react-virtuoso'; +import { + ContextualbarClose, + ContextualbarContent, + ContextualbarHeader, + ContextualbarIcon, + ContextualbarTitle, + ContextualbarEmptyContent, +} from '../../../../components/Contextualbar'; import ScrollableContentWrapper from '../../../../components/ScrollableContentWrapper'; -import VerticalBarClose from '../../../../components/VerticalBar/VerticalBarClose'; -import VerticalBarContent from '../../../../components/VerticalBar/VerticalBarContent'; -import VerticalBarHeader from '../../../../components/VerticalBar/VerticalBarHeader'; -import VerticalBarIcon from '../../../../components/VerticalBar/VerticalBarIcon'; -import VerticalBarText from '../../../../components/VerticalBar/VerticalBarText'; import { useRecordList } from '../../../../hooks/lists/useRecordList'; import { AsyncStatePhase } from '../../../../lib/asyncState'; import type { ThreadsListOptions } from '../../../../lib/lists/ThreadsList'; @@ -114,13 +117,13 @@ const ThreadList: VFC = () => { return ( <> - - - {t('Threads')} - - + + + {t('Threads')} + + - + { )} - {phase !== AsyncStatePhase.LOADING && itemCount === 0 && ( - - {t('No_Threads')} - - )} + {phase !== AsyncStatePhase.LOADING && itemCount === 0 && } {!error && itemCount > 0 && items.length > 0 && ( @@ -192,7 +191,7 @@ const ThreadList: VFC = () => { /> )} - + ); }; diff --git a/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadChat.tsx b/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadChat.tsx index 752603e2f0e3..434db3aed0d9 100644 --- a/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadChat.tsx +++ b/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadChat.tsx @@ -6,7 +6,7 @@ import { useMethod, useTranslation, useUserPreference } from '@rocket.chat/ui-co import React, { useState, useEffect, useCallback } from 'react'; import { callbacks } from '../../../../../../lib/callbacks'; -import VerticalBarContent from '../../../../../components/VerticalBar/VerticalBarContent'; +import { ContextualbarContent } from '../../../../../components/Contextualbar'; import MessageListErrorBoundary from '../../../MessageList/MessageListErrorBoundary'; import DropTargetOverlay from '../../../components/body/DropTargetOverlay'; import ComposerContainer from '../../../components/body/composer/ComposerContainer'; @@ -90,7 +90,7 @@ const ThreadChat = ({ mainMessage }: ThreadChatProps) => { const t = useTranslation(); return ( - + @@ -123,7 +123,7 @@ const ThreadChat = ({ mainMessage }: ThreadChatProps) => { - + ); }; diff --git a/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadTitle.tsx b/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadTitle.tsx index c2044ba2afb6..6440a5e726a1 100644 --- a/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadTitle.tsx +++ b/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadTitle.tsx @@ -2,7 +2,7 @@ import type { IThreadMainMessage } from '@rocket.chat/core-typings'; import React, { useMemo } from 'react'; import { normalizeThreadTitle } from '../../../../../../app/threads/client/lib/normalizeThreadTitle'; -import VerticalBar from '../../../../../components/VerticalBar'; +import { ContextualbarTitle } from '../../../../../components/Contextualbar'; type ThreadTitleProps = { mainMessage: IThreadMainMessage; @@ -10,7 +10,7 @@ type ThreadTitleProps = { const ThreadTitle = ({ mainMessage }: ThreadTitleProps) => { const innerHTML = useMemo(() => ({ __html: normalizeThreadTitle(mainMessage) }), [mainMessage]); - return ; + return ; }; export default ThreadTitle; diff --git a/apps/meteor/client/views/room/contextualBar/UserInfo/UserInfoWithData.tsx b/apps/meteor/client/views/room/contextualBar/UserInfo/UserInfoWithData.tsx index a16d0cb00a8d..7c8257610900 100644 --- a/apps/meteor/client/views/room/contextualBar/UserInfo/UserInfoWithData.tsx +++ b/apps/meteor/client/views/room/contextualBar/UserInfo/UserInfoWithData.tsx @@ -5,11 +5,18 @@ import type { ReactElement } from 'react'; import React, { useMemo } from 'react'; import { getUserEmailAddress } from '../../../../../lib/getUserEmailAddress'; +import { + ContextualbarHeader, + ContextualbarBack, + ContextualbarIcon, + ContextualbarTitle, + ContextualbarClose, + ContextualbarContent, +} from '../../../../components/Contextualbar'; import { FormSkeleton } from '../../../../components/Skeleton'; import UserCard from '../../../../components/UserCard'; import UserInfo from '../../../../components/UserInfo'; import { ReactiveUserStatus } from '../../../../components/UserStatus'; -import VerticalBar from '../../../../components/VerticalBar'; import { AsyncStatePhase } from '../../../../hooks/useAsyncState'; import { useEndpointData } from '../../../../hooks/useEndpointData'; import { getUserEmailVerified } from '../../../../lib/utils/getUserEmailVerified'; @@ -78,23 +85,23 @@ const UserInfoWithData = ({ uid, username, rid, onClose, onClickBack }: UserInfo return ( <> - - {onClickBack && } - {!onClickBack && } - {t('User_Info')} - {onClose && } - + + {onClickBack && } + {!onClickBack && } + {t('User_Info')} + {onClose && } + {isLoading && ( - + - + )} {error && !user && ( - + {t('User_not_found')} - + )} {!isLoading && user && ( diff --git a/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfList/VideoConfList.tsx b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfList/VideoConfList.tsx index 8cdb21c8c4a6..132a3a0d5857 100644 --- a/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfList/VideoConfList.tsx +++ b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfList/VideoConfList.tsx @@ -6,8 +6,16 @@ import type { ReactElement } from 'react'; import React from 'react'; import { Virtuoso } from 'react-virtuoso'; +import { + ContextualbarSkeleton, + ContextualbarHeader, + ContextualbarIcon, + ContextualbarTitle, + ContextualbarClose, + ContextualbarContent, + ContextualbarEmptyContent, +} from '../../../../../components/Contextualbar'; import ScrollableContentWrapper from '../../../../../components/ScrollableContentWrapper'; -import VerticalBar from '../../../../../components/VerticalBar'; import { getErrorMessage } from '../../../../../lib/errorHandling'; import VideoConfListItem from './VideoConfListItem'; @@ -29,18 +37,18 @@ const VideoConfList = ({ onClose, total, videoConfs, loading, error, reload, loa }); if (loading) { - return ; + return ; } return ( <> - - - {t('Calls')} - - + + + {t('Calls')} + + - + {(total === 0 || error) && ( {error && ( @@ -51,11 +59,11 @@ const VideoConfList = ({ onClose, total, videoConfs, loading, error, reload, loa )} {!error && total === 0 && ( - - - {t('No_history')} - {t('There_is_no_video_conference_history_in_this_room')} - + )} )} @@ -75,7 +83,7 @@ const VideoConfList = ({ onClose, total, videoConfs, loading, error, reload, loa /> )} - + ); }; diff --git a/apps/meteor/client/views/room/layout/RoomLayout.tsx b/apps/meteor/client/views/room/layout/RoomLayout.tsx index 25c22f43c431..4e9f690a4617 100644 --- a/apps/meteor/client/views/room/layout/RoomLayout.tsx +++ b/apps/meteor/client/views/room/layout/RoomLayout.tsx @@ -2,7 +2,7 @@ import { Box } from '@rocket.chat/fuselage'; import type { ComponentProps, ReactElement, ReactNode } from 'react'; import React from 'react'; -import VerticalBar from '../../../components/VerticalBar/VerticalBar'; +import { Contextualbar } from '../../../components/Contextualbar'; type RoomLayoutProps = { header?: ReactNode; @@ -21,7 +21,7 @@ const RoomLayout = ({ header, body, footer, aside, ...props }: RoomLayoutProps): {footer && {footer}} - {aside && {aside}} + {aside && {aside}} ); diff --git a/apps/meteor/client/views/root/PageLoading.tsx b/apps/meteor/client/views/root/PageLoading.tsx index cb95a546929d..77cef560a1b8 100644 --- a/apps/meteor/client/views/root/PageLoading.tsx +++ b/apps/meteor/client/views/root/PageLoading.tsx @@ -1,13 +1,11 @@ import type { FC } from 'react'; import React from 'react'; +import LoadingIndicator from '../../components/LoadingIndicator'; + const PageLoading: FC = () => (
-
-
-
-
-
+
); diff --git a/apps/meteor/client/views/teams/contextualBar/channels/BaseTeamsChannels.tsx b/apps/meteor/client/views/teams/contextualBar/channels/BaseTeamsChannels.tsx index 4f0e5422d583..4201ee4793ef 100644 --- a/apps/meteor/client/views/teams/contextualBar/channels/BaseTeamsChannels.tsx +++ b/apps/meteor/client/views/teams/contextualBar/channels/BaseTeamsChannels.tsx @@ -7,9 +7,17 @@ import type { ChangeEvent, Dispatch, SetStateAction, SyntheticEvent } from 'reac import React, { useMemo } from 'react'; import { Virtuoso } from 'react-virtuoso'; +import { + ContextualbarHeader, + ContextualbarIcon, + ContextualbarTitle, + ContextualbarClose, + ContextualbarContent, + ContextualbarFooter, + ContextualbarEmptyContent, +} from '../../../../components/Contextualbar'; import InfiniteListAnchor from '../../../../components/InfiniteListAnchor'; import ScrollableContentWrapper from '../../../../components/ScrollableContentWrapper'; -import VerticalBar from '../../../../components/VerticalBar'; import Row from './Row'; type BaseTeamsChannelsProps = { @@ -70,13 +78,13 @@ const BaseTeamsChannels = ({ return ( <> - - - {t('Team_Channels')} - {onClickClose && } - + + + {t('Team_Channels')} + {onClickClose && } + - + @@ -97,39 +105,34 @@ const BaseTeamsChannels = ({ )} - {!loading && channels.length === 0 && ( - - {t('No_channels_in_team')} - - )} + {!loading && channels.length === 0 && } {!loading && channels.length > 0 && ( - - - {t('Showing')}: {channels.length} - + <> + + + {t('Showing')}: {channels.length} + - - {t('Total')}: {total} + + {t('Total')}: {total} + - - )} - - {!loading && ( - - }} - itemContent={(index, data) => } - /> - + + }} + itemContent={(index, data) => } + /> + + )} - + {(onClickAddExisting || onClickCreateNew) && ( - + {onClickAddExisting && ( )} - + )} ); diff --git a/apps/meteor/client/views/teams/contextualBar/info/TeamsInfo.stories.tsx b/apps/meteor/client/views/teams/contextualBar/info/TeamsInfo.stories.tsx index cd45b91c44a4..bfd7f6d8cd98 100644 --- a/apps/meteor/client/views/teams/contextualBar/info/TeamsInfo.stories.tsx +++ b/apps/meteor/client/views/teams/contextualBar/info/TeamsInfo.stories.tsx @@ -2,7 +2,7 @@ import type { IRoom } from '@rocket.chat/core-typings'; import type { ComponentMeta, ComponentStory } from '@storybook/react'; import React from 'react'; -import VerticalBar from '../../../../components/VerticalBar'; +import { Contextualbar } from '../../../../components/Contextualbar'; import TeamsInfo from './TeamsInfo'; const room = { @@ -23,7 +23,7 @@ export default { layout: 'fullscreen', actions: { argTypesRegex: '^on.*' }, }, - decorators: [(fn) => {fn()}], + decorators: [(fn) => {fn()}], args: { room, icon: 'lock', diff --git a/apps/meteor/client/views/teams/contextualBar/info/TeamsInfo.tsx b/apps/meteor/client/views/teams/contextualBar/info/TeamsInfo.tsx index 670ece8ba9e0..1863434fcf4d 100644 --- a/apps/meteor/client/views/teams/contextualBar/info/TeamsInfo.tsx +++ b/apps/meteor/client/views/teams/contextualBar/info/TeamsInfo.tsx @@ -5,10 +5,16 @@ import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement, ComponentProps } from 'react'; import React, { useMemo } from 'react'; +import { + ContextualbarHeader, + ContextualbarIcon, + ContextualbarTitle, + ContextualbarClose, + ContextualbarScrollableContent, +} from '../../../../components/Contextualbar'; import InfoPanel from '../../../../components/InfoPanel'; import RetentionPolicyCallout from '../../../../components/InfoPanel/RetentionPolicyCallout'; import MarkdownText from '../../../../components/MarkdownText'; -import VerticalBar from '../../../../components/VerticalBar'; import RoomAvatar from '../../../../components/avatar/RoomAvatar'; import type { Action } from '../../../hooks/useActionSpread'; import { useActionSpread } from '../../../hooks/useActionSpread'; @@ -120,13 +126,12 @@ const TeamsInfo = ({ return ( <> - - - {t('Teams_Info')} - {onClickClose && } - - - + + + {t('Teams_Info')} + {onClickClose && } + + @@ -196,7 +201,7 @@ const TeamsInfo = ({ )} - + ); }; diff --git a/apps/meteor/client/views/teams/contextualBar/info/TeamsInfoWithRooms.tsx b/apps/meteor/client/views/teams/contextualBar/info/TeamsInfoWithRooms.tsx index 5d9aa5afcc3b..6a2ad1f9e252 100644 --- a/apps/meteor/client/views/teams/contextualBar/info/TeamsInfoWithRooms.tsx +++ b/apps/meteor/client/views/teams/contextualBar/info/TeamsInfoWithRooms.tsx @@ -4,7 +4,15 @@ import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useTranslation } from '@rocket.chat/ui-contexts'; import React, { useState, useMemo } from 'react'; -import VerticalBar from '../../../../components/VerticalBar'; +import { + Contextualbar, + ContextualbarHeader, + ContextualbarSkeleton, + ContextualbarIcon, + ContextualbarTitle, + ContextualbarClose, + ContextualbarScrollableContent, +} from '../../../../components/Contextualbar'; import { AsyncStatePhase } from '../../../../hooks/useAsyncState'; import { useEndpointData } from '../../../../hooks/useEndpointData'; import EditChannelWithData from '../../../room/contextualBar/Info/EditRoomInfo'; @@ -23,21 +31,21 @@ const TeamsInfoWithRooms = ({ rid }: TeamsInfoWithRoomsProps) => { const { phase, value, error } = useEndpointData('/v1/rooms.info', { params }); if (phase === AsyncStatePhase.LOADING) { - return ; + return ; } if (error) { return ( - - - - {t('Team_Info')} - - - + + + + {t('Team_Info')} + + + {JSON.stringify(error)} - - + + ); } diff --git a/apps/meteor/definition/externals/meteor/meteor.d.ts b/apps/meteor/definition/externals/meteor/meteor.d.ts index 1b3ca251fe27..1bf3bf0ebc64 100644 --- a/apps/meteor/definition/externals/meteor/meteor.d.ts +++ b/apps/meteor/definition/externals/meteor/meteor.d.ts @@ -92,11 +92,12 @@ declare module 'meteor/meteor' { reconnect: () => void; }; subscribe( + id: string, name: string, ...args: [ ...unknown, callbacks?: { - onReady?: () => void; + onReady?: (...args: any[]) => void; onStop?: (error?: Error) => void; onError?: (error: Error) => void; }, diff --git a/apps/meteor/ee/app/license/server/license.ts b/apps/meteor/ee/app/license/server/license.ts index 0857ad6e5457..f8cd98dd3245 100644 --- a/apps/meteor/ee/app/license/server/license.ts +++ b/apps/meteor/ee/app/license/server/license.ts @@ -174,7 +174,7 @@ class LicenseClass { return item; } if (!this._validateURL(license.url, this.url)) { - item.valid = false; + this.invalidate(item); console.error(`#### License error: invalid url, licensed to ${license.url}, used on ${this.url}`); this._invalidModules(license.modules); return item; @@ -182,7 +182,7 @@ class LicenseClass { } if (license.expiry && this._validateExpiration(license.expiry)) { - item.valid = false; + this.invalidate(item); console.error(`#### License error: expired, valid until ${license.expiry}`); this._invalidModules(license.modules); return item; @@ -212,6 +212,12 @@ class LicenseClass { this.showLicenses(); } + invalidate(item: IValidLicense): void { + item.valid = false; + + EnterpriseLicenses.emit('invalidate'); + } + async canAddNewUser(): Promise { if (!maxActiveUsers) { return true; @@ -421,6 +427,10 @@ export function onValidateLicenses(cb: (...args: any[]) => void): void { EnterpriseLicenses.on('validate', cb); } +export function onInvalidateLicense(cb: (...args: any[]) => void): void { + EnterpriseLicenses.on('invalidate', cb); +} + export function flatModules(modulesAndBundles: string[]): string[] { const bundles = modulesAndBundles.filter(isBundle); const modules = modulesAndBundles.filter((x) => !isBundle(x)); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoCloseOnHoldScheduler.ts b/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoCloseOnHoldScheduler.ts index f58b6b4d4547..3447da4d491c 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoCloseOnHoldScheduler.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoCloseOnHoldScheduler.ts @@ -6,6 +6,8 @@ import { LivechatRooms, Users } from '@rocket.chat/models'; import type { IUser } from '@rocket.chat/core-typings'; import { Livechat } from '../../../../../app/livechat/server/lib/LivechatTyped'; +import { schedulerLogger } from './logger'; +import type { MainLogger } from '../../../../../server/lib/logger/getPino'; const SCHEDULER_NAME = 'omnichannel_auto_close_on_hold_scheduler'; @@ -16,8 +18,15 @@ class AutoCloseOnHoldSchedulerClass { running: boolean; + logger: MainLogger; + + constructor() { + this.logger = schedulerLogger.section('AutoCloseOnHoldScheduler'); + } + public async init(): Promise { if (this.running) { + this.logger.debug('Already running'); return; } @@ -29,9 +38,11 @@ class AutoCloseOnHoldSchedulerClass { await this.scheduler.start(); this.running = true; + this.logger.debug('Started'); } public async scheduleRoom(roomId: string, timeout: number, comment: string): Promise { + this.logger.debug(`Scheduling room ${roomId} to be closed in ${timeout} seconds`); await this.unscheduleRoom(roomId); const jobName = `${SCHEDULER_NAME}-${roomId}`; @@ -42,11 +53,13 @@ class AutoCloseOnHoldSchedulerClass { } public async unscheduleRoom(roomId: string): Promise { + this.logger.debug(`Unscheduling room ${roomId}`); const jobName = `${SCHEDULER_NAME}-${roomId}`; await this.scheduler.cancel({ name: jobName }); } private async executeJob({ attrs: { data } }: any = {}): Promise { + this.logger.debug(`Executing job for room ${data.roomId}`); const { roomId, comment } = data; const [room, user] = await Promise.all([LivechatRooms.findOneById(roomId), this.getSchedulerUser()]); @@ -62,6 +75,7 @@ class AutoCloseOnHoldSchedulerClass { comment, }; + this.logger.debug(`Closing room ${roomId}`); await Livechat.closeRoom(payload); } diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts b/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts index c81634f2ff3f..975519e16a87 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts @@ -8,6 +8,8 @@ import { Livechat } from '../../../../../app/livechat/server'; import { RoutingManager } from '../../../../../app/livechat/server/lib/RoutingManager'; import { forwardRoomToAgent } from '../../../../../app/livechat/server/lib/Helper'; import { settings } from '../../../../../app/settings/server'; +import { schedulerLogger } from './logger'; +import type { MainLogger } from '../../../../../server/lib/logger/getPino'; const SCHEDULER_NAME = 'omnichannel_scheduler'; @@ -18,8 +20,15 @@ class AutoTransferChatSchedulerClass { user: IUser; + logger: MainLogger; + + constructor() { + this.logger = schedulerLogger.section('AutoTransferChatScheduler'); + } + public async init(): Promise { if (this.running) { + this.logger.debug('Already running'); return; } @@ -31,6 +40,7 @@ class AutoTransferChatSchedulerClass { await this.scheduler.start(); this.running = true; + this.logger.debug('Started'); } private async getSchedulerUser(): Promise { @@ -38,6 +48,7 @@ class AutoTransferChatSchedulerClass { } public async scheduleRoom(roomId: string, timeout: number): Promise { + this.logger.debug(`Scheduling room ${roomId} to be transferred in ${timeout} seconds`); await this.unscheduleRoom(roomId); const jobName = `${SCHEDULER_NAME}-${roomId}`; @@ -47,9 +58,11 @@ class AutoTransferChatSchedulerClass { this.scheduler.define(jobName, this.executeJob.bind(this)); await this.scheduler.schedule(when, jobName, { roomId }); await LivechatRooms.setAutoTransferOngoingById(roomId); + this.logger.debug(`Scheduled room ${roomId} to be transferred in ${timeout} seconds`); } public async unscheduleRoom(roomId: string): Promise { + this.logger.debug(`Unscheduling room ${roomId}`); const jobName = `${SCHEDULER_NAME}-${roomId}`; await LivechatRooms.unsetAutoTransferOngoingById(roomId); @@ -57,6 +70,7 @@ class AutoTransferChatSchedulerClass { } private async transferRoom(roomId: string): Promise { + this.logger.debug(`Transferring room ${roomId}`); const room = await LivechatRooms.findOneById(roomId, { _id: 1, v: 1, @@ -76,6 +90,7 @@ class AutoTransferChatSchedulerClass { const timeoutDuration = settings.get('Livechat_auto_transfer_chat_timeout').toString(); if (!RoutingManager.getConfig().autoAssignAgent) { + this.logger.debug(`Auto-assign agent is disabled, returning room ${roomId} as inquiry`); return Livechat.returnRoomAsInquiry(room._id, departmentId, { scope: 'autoTransferUnansweredChatsToQueue', comment: timeoutDuration, @@ -85,6 +100,7 @@ class AutoTransferChatSchedulerClass { const agent = await RoutingManager.getNextAgent(departmentId, ignoreAgentId); if (agent) { + this.logger.debug(`Transferring room ${roomId} to agent ${agent.agentId}`); return forwardRoomToAgent(room, { userId: agent.agentId, transferredBy: await this.getSchedulerUser(), @@ -94,6 +110,7 @@ class AutoTransferChatSchedulerClass { }); } + this.logger.debug(`No agent found to transfer room ${roomId}`); return false; } diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/QueueInactivityMonitor.ts b/apps/meteor/ee/app/livechat-enterprise/server/lib/QueueInactivityMonitor.ts index 6da5898123c5..94a99cfb1431 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/QueueInactivityMonitor.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/QueueInactivityMonitor.ts @@ -6,9 +6,10 @@ import type { IUser, IOmnichannelRoom } from '@rocket.chat/core-typings'; import { LivechatRooms, LivechatInquiry as LivechatInquiryRaw, Users } from '@rocket.chat/models'; import { settings } from '../../../../../app/settings/server'; -import { Logger } from '../../../../../app/logger/server'; import { Livechat } from '../../../../../app/livechat/server/lib/LivechatTyped'; import { i18n } from '../../../../../server/lib/i18n'; +import { schedulerLogger } from './logger'; +import type { MainLogger } from '../../../../../server/lib/logger/getPino'; const SCHEDULER_NAME = 'omnichannel_queue_inactivity_monitor'; @@ -17,7 +18,7 @@ class OmnichannelQueueInactivityMonitorClass { running: boolean; - logger: Logger; + logger: MainLogger; _name: string; @@ -33,7 +34,7 @@ class OmnichannelQueueInactivityMonitorClass { this._db = MongoInternals.defaultRemoteCollectionDriver().mongo.db; this.running = false; this._name = 'Omnichannel-Queue-Inactivity-Monitor'; - this.logger = new Logger('QueueInactivityMonitor'); + this.logger = schedulerLogger.section(this._name); this.scheduler = new Agenda({ mongo: (MongoInternals.defaultRemoteCollectionDriver().mongo as any).client.db(), db: { collection: SCHEDULER_NAME }, diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.ts b/apps/meteor/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.ts index 6115e93b7f0f..2e5e31c14673 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.ts @@ -5,8 +5,9 @@ import { cronJobs } from '@rocket.chat/cron'; import { settings } from '../../../../../app/settings/server'; import { Livechat } from '../../../../../app/livechat/server/lib/LivechatTyped'; import { LivechatEnterprise } from './LivechatEnterprise'; -import { logger } from './logger'; import { i18n } from '../../../../../server/lib/i18n'; +import { schedulerLogger } from './logger'; +import type { MainLogger } from '../../../../../server/lib/logger/getPino'; const isPromiseRejectedResult = (result: any): result is PromiseRejectedResult => result && result.status === 'rejected'; @@ -19,15 +20,19 @@ export class VisitorInactivityMonitor { user: IUser; + logger: MainLogger; + private scheduler = cronJobs; constructor() { this._started = false; this._name = 'Omnichannel Visitor Inactivity Monitor'; this.messageCache = new Map(); + this.logger = schedulerLogger.section(this._name); } async start() { + this.logger.debug('Starting'); await this._startMonitoring(); this._initializeMessageCache(); const cat = await Users.findOneById('rocket.cat'); @@ -38,20 +43,24 @@ export class VisitorInactivityMonitor { private async _startMonitoring() { if (this.isRunning()) { + this.logger.debug('Already running'); return; } const everyMinute = '* * * * *'; await this.scheduler.add(this._name, everyMinute, async () => this.handleAbandonedRooms()); this._started = true; + this.logger.debug('Started'); } async stop() { if (!this.isRunning()) { + this.logger.debug('Not running'); return; } await this.scheduler.remove(this._name); this._started = false; + this.logger.debug('Stopped'); } isRunning() { @@ -64,18 +73,23 @@ export class VisitorInactivityMonitor { } async _getDepartmentAbandonedCustomMessage(departmentId: string) { + this.logger.debug(`Getting department abandoned custom message for department ${departmentId}`); if (this.messageCache.has('departmentId')) { + this.logger.debug(`Using cached department abandoned custom message for department ${departmentId}`); return this.messageCache.get('departmentId'); } const department = await LivechatDepartment.findOneById(departmentId); if (!department) { + this.logger.debug(`Department ${departmentId} not found`); return; } + this.logger.debug(`Setting department abandoned custom message for department ${departmentId}`); this.messageCache.set(department._id, department.abandonedRoomsCloseCustomMessage); return department.abandonedRoomsCloseCustomMessage; } async closeRooms(room: IOmnichannelRoom) { + this.logger.debug(`Closing room ${room._id}`); let comment = this.messageCache.get('default'); if (room.departmentId) { comment = (await this._getDepartmentAbandonedCustomMessage(room.departmentId)) || comment; @@ -85,18 +99,22 @@ export class VisitorInactivityMonitor { room, user: this.user, }); + this.logger.debug(`Room ${room._id} closed`); } async placeRoomOnHold(room: IOmnichannelRoom) { + this.logger.debug(`Placing room ${room._id} on hold`); const timeout = settings.get('Livechat_visitor_inactivity_timeout'); const { v: { _id: visitorId } = {} } = room; if (!visitorId) { + this.logger.debug(`Room ${room._id} does not have a visitor`); throw new Error('error-invalid_visitor'); } const visitor = await LivechatVisitors.findOneById(visitorId); if (!visitor) { + this.logger.debug(`Room ${room._id} does not have a visitor`); throw new Error('error-invalid_visitor'); } @@ -107,15 +125,17 @@ export class VisitorInactivityMonitor { LivechatEnterprise.placeRoomOnHold(room, comment, this.user), LivechatRooms.unsetPredictedVisitorAbandonmentByRoomId(room._id), ]); + this.logger.debug(`Room ${room._id} placed on hold`); const rejected = result.filter(isPromiseRejectedResult).map((r) => r.reason); if (rejected.length) { - logger.error({ msg: 'Error placing room on hold', error: rejected }); + this.logger.error({ msg: 'Error placing room on hold', error: rejected }); throw new Error('Error placing room on hold. Please check logs for more details.'); } } async handleAbandonedRooms() { + this.logger.debug('Handling abandoned rooms'); const action = settings.get('Livechat_abandoned_rooms_action'); if (!action || action === 'none') { return; @@ -125,10 +145,12 @@ export class VisitorInactivityMonitor { await LivechatRooms.findAbandonedOpenRooms(new Date()).forEach((room) => { switch (action) { case 'close': { + this.logger.debug(`Closing room ${room._id}`); promises.push(this.closeRooms(room)); break; } case 'on-hold': { + this.logger.debug(`Placing room ${room._id} on hold`); promises.push(this.placeRoomOnHold(room)); break; } @@ -140,8 +162,8 @@ export class VisitorInactivityMonitor { const errors = result.filter(isPromiseRejectedResult).map((r) => r.reason); if (errors.length) { - logger.error({ msg: `Error while removing priority from ${errors.length} rooms`, reason: errors[0] }); - logger.debug({ msg: 'Rejection results', errors }); + this.logger.error({ msg: `Error while removing priority from ${errors.length} rooms`, reason: errors[0] }); + this.logger.debug({ msg: 'Rejection results', errors }); } this._initializeMessageCache(); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/logger.ts b/apps/meteor/ee/app/livechat-enterprise/server/lib/logger.ts index 22d26754f321..9d31072d43a2 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/logger.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/logger.ts @@ -7,3 +7,5 @@ export const queueLogger = logger.section('Queue'); export const helperLogger = logger.section('Helper'); export const cbLogger = logger.section('Callbacks'); export const bhLogger = logger.section('Business-Hours'); + +export const schedulerLogger = new Logger('Scheduler'); diff --git a/apps/meteor/ee/client/apps/gameCenter/GameCenterContainer.tsx b/apps/meteor/ee/client/apps/gameCenter/GameCenterContainer.tsx index 7dbb50b95158..c79486604151 100644 --- a/apps/meteor/ee/client/apps/gameCenter/GameCenterContainer.tsx +++ b/apps/meteor/ee/client/apps/gameCenter/GameCenterContainer.tsx @@ -2,7 +2,13 @@ import { Avatar } from '@rocket.chat/fuselage'; import type { ReactElement } from 'react'; import React from 'react'; -import VerticalBar from '../../../../client/components/VerticalBar'; +import { + ContextualbarTitle, + ContextualbarHeader, + ContextualbarBack, + ContextualbarContent, + ContextualbarClose, +} from '../../../../client/components/Contextualbar'; import type { IGame } from './GameCenter'; interface IGameCenterContainerProps { @@ -14,17 +20,16 @@ interface IGameCenterContainerProps { const GameCenterContainer = ({ handleClose, handleBack, game }: IGameCenterContainerProps): ReactElement => { return ( <> - - {handleBack && } - + + {handleBack && } + {game.name} - - {handleClose && } - - - + + {handleClose && } + + - + ); }; diff --git a/apps/meteor/ee/client/apps/gameCenter/GameCenterList.tsx b/apps/meteor/ee/client/apps/gameCenter/GameCenterList.tsx index 419fcbc48979..f45ba934ba3b 100644 --- a/apps/meteor/ee/client/apps/gameCenter/GameCenterList.tsx +++ b/apps/meteor/ee/client/apps/gameCenter/GameCenterList.tsx @@ -3,8 +3,13 @@ import { useSetModal, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useCallback } from 'react'; +import { + ContextualbarHeader, + ContextualbarTitle, + ContextualbarClose, + ContextualbarContent, +} from '../../../../client/components/Contextualbar'; import { FormSkeleton } from '../../../../client/components/Skeleton'; -import VerticalBar from '../../../../client/components/VerticalBar'; import type { IGame } from './GameCenter'; import GameCenterInvitePlayersModal from './GameCenterInvitePlayersModal'; @@ -30,13 +35,12 @@ const GameCenterList = ({ handleClose, handleOpenGame, games, isLoading }: IGame return (
- - {t('Apps_Game_Center')} - {handleClose && } - - + + {t('Apps_Game_Center')} + {handleClose && } + {!isLoading && ( - + {games && (
@@ -74,13 +78,13 @@ const GameCenterList = ({ handleClose, handleOpenGame, games, isLoading }: IGame
)} -
+ )} {isLoading && ( - + - + )}
); diff --git a/apps/meteor/ee/client/omnichannel/additionalForms/DepartmentBusinessHours.js b/apps/meteor/ee/client/omnichannel/additionalForms/DepartmentBusinessHours.js deleted file mode 100644 index 2b3fabc73a8b..000000000000 --- a/apps/meteor/ee/client/omnichannel/additionalForms/DepartmentBusinessHours.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Field, TextInput } from '@rocket.chat/fuselage'; -import { useTranslation } from '@rocket.chat/ui-contexts'; -import React, { useMemo } from 'react'; - -import { useEndpointData } from '../../../../client/hooks/useEndpointData'; - -export const DepartmentBusinessHours = ({ bhId }) => { - const t = useTranslation(); - const { value: data } = useEndpointData('/v1/livechat/business-hour', { params: useMemo(() => ({ _id: bhId, type: 'custom' }), [bhId]) }); - - const name = data && data.businessHour && data.businessHour.name; - - return ( - - {t('Business_Hour')} - - - - - ); -}; - -export default DepartmentBusinessHours; diff --git a/apps/meteor/ee/client/omnichannel/additionalForms/DepartmentBusinessHours.tsx b/apps/meteor/ee/client/omnichannel/additionalForms/DepartmentBusinessHours.tsx new file mode 100644 index 000000000000..41674427cdd0 --- /dev/null +++ b/apps/meteor/ee/client/omnichannel/additionalForms/DepartmentBusinessHours.tsx @@ -0,0 +1,23 @@ +import { Field, TextInput } from '@rocket.chat/fuselage'; +import { useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; +import { useQuery } from '@tanstack/react-query'; +import React from 'react'; + +export const DepartmentBusinessHours = ({ bhId }: { bhId: string | undefined }) => { + const t = useTranslation(); + const getBusinessHour = useEndpoint('GET', '/v1/livechat/business-hour'); + const { data } = useQuery(['/v1/livechat/business-hour', bhId], () => getBusinessHour({ _id: bhId, type: 'custom' })); + + const name = data?.businessHour?.name; + + return ( + + {t('Business_Hour')} + + + + + ); +}; + +export default DepartmentBusinessHours; diff --git a/apps/meteor/ee/client/omnichannel/components/contextualBar/CannedResponse/CannedResponse.tsx b/apps/meteor/ee/client/omnichannel/components/contextualBar/CannedResponse/CannedResponse.tsx index 3c51bee8e759..05d6895d5dfc 100644 --- a/apps/meteor/ee/client/omnichannel/components/contextualBar/CannedResponse/CannedResponse.tsx +++ b/apps/meteor/ee/client/omnichannel/components/contextualBar/CannedResponse/CannedResponse.tsx @@ -4,7 +4,14 @@ import { useTranslation } from '@rocket.chat/ui-contexts'; import type { FC, MouseEventHandler } from 'react'; import React, { memo } from 'react'; -import VerticalBar from '../../../../../../client/components/VerticalBar'; +import { + Contextualbar, + ContextualbarHeader, + ContextualbarTitle, + ContextualbarAction, + ContextualbarContent, + ContextualbarFooter, +} from '../../../../../../client/components/Contextualbar'; import { useScopeDict } from '../../../hooks/useScopeDict'; const CannedResponse: FC<{ @@ -24,12 +31,12 @@ const CannedResponse: FC<{ const scope = useScopeDict(dataScope, departmentName); return ( - - - {onClickBack && } - !{shortcut} - - + + + {onClickBack && } + !{shortcut} + + @@ -74,16 +81,16 @@ const CannedResponse: FC<{ - - + + {canEdit && } - - + + ); }; diff --git a/apps/meteor/ee/client/omnichannel/components/contextualBar/CannedResponse/CannedResponseList.stories.tsx b/apps/meteor/ee/client/omnichannel/components/contextualBar/CannedResponse/CannedResponseList.stories.tsx index f69deac5bb38..a27dc589fd78 100644 --- a/apps/meteor/ee/client/omnichannel/components/contextualBar/CannedResponse/CannedResponseList.stories.tsx +++ b/apps/meteor/ee/client/omnichannel/components/contextualBar/CannedResponse/CannedResponseList.stories.tsx @@ -3,7 +3,7 @@ import { action } from '@storybook/addon-actions'; import type { ComponentMeta, ComponentStory } from '@storybook/react'; import React from 'react'; -import VerticalBar from '../../../../../../client/components/VerticalBar'; +import { Contextualbar } from '../../../../../../client/components/Contextualbar'; import CannedResponseList from './CannedResponseList'; export default { @@ -80,7 +80,7 @@ Default.args = { Default.decorators = [ (fn) => ( - {fn()} + {fn()} ), ]; diff --git a/apps/meteor/ee/client/omnichannel/components/contextualBar/CannedResponse/CannedResponseList.tsx b/apps/meteor/ee/client/omnichannel/components/contextualBar/CannedResponse/CannedResponseList.tsx index 084c77f87174..7885d9581c35 100644 --- a/apps/meteor/ee/client/omnichannel/components/contextualBar/CannedResponse/CannedResponseList.tsx +++ b/apps/meteor/ee/client/omnichannel/components/contextualBar/CannedResponse/CannedResponseList.tsx @@ -1,13 +1,20 @@ import type { ILivechatDepartment, IOmnichannelCannedResponse } from '@rocket.chat/core-typings'; -import { Box, Button, ButtonGroup, Icon, Margins, Select, TextInput } from '@rocket.chat/fuselage'; +import { Box, Button, ButtonGroup, ContextualbarEmptyContent, Icon, Margins, Select, TextInput } from '@rocket.chat/fuselage'; import { useAutoFocus, useResizeObserver } from '@rocket.chat/fuselage-hooks'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { Dispatch, FC, FormEventHandler, MouseEvent, ReactElement, SetStateAction } from 'react'; import React, { memo } from 'react'; import { Virtuoso } from 'react-virtuoso'; +import { + ContextualbarHeader, + ContextualbarTitle, + ContextualbarClose, + ContextualbarContent, + ContextualbarInnerContent, + ContextualbarFooter, +} from '../../../../../../client/components/Contextualbar'; import ScrollableContentWrapper from '../../../../../../client/components/ScrollableContentWrapper'; -import VerticalBar from '../../../../../../client/components/VerticalBar'; import { useTabContext } from '../../../../../../client/views/room/contexts/ToolboxContext'; import Item from './Item'; import WrapCannedResponse from './WrapCannedResponse'; @@ -54,12 +61,12 @@ const CannedResponseList: FC<{ return ( <> - - {t('Canned_Responses')} - - + + {t('Canned_Responses')} + + - + @@ -74,9 +81,9 @@ const CannedResponseList: FC<{ - - {itemCount === 0 && {t('No_Canned_Responses')}} - {itemCount > 0 && cannedItems.length > 0 && ( + {itemCount === 0 && } + {itemCount > 0 && cannedItems.length > 0 && ( + ( )} /> - )} - - + + )} + {cannedId && ( - + canned._id === (cannedId as unknown))} onClickBack={onClickItem} onClickUse={onClickUse} reload={reload} /> - + )} - + - + ); }; diff --git a/apps/meteor/ee/client/omnichannel/components/contextualBar/CannedResponse/Item.tsx b/apps/meteor/ee/client/omnichannel/components/contextualBar/CannedResponse/Item.tsx index 969bba748af2..e3ba7708735a 100644 --- a/apps/meteor/ee/client/omnichannel/components/contextualBar/CannedResponse/Item.tsx +++ b/apps/meteor/ee/client/omnichannel/components/contextualBar/CannedResponse/Item.tsx @@ -27,7 +27,8 @@ const Item: FC<{ pbs={16} pbe={12} pi={24} - borderBlockEndWidth='2px' + color='default' + borderBlockEndWidth={1} borderBlockEndColor='light' borderBlockEndStyle='solid' onClick={onClickItem} diff --git a/apps/meteor/ee/client/omnichannel/priorities/PrioritiesPage.tsx b/apps/meteor/ee/client/omnichannel/priorities/PrioritiesPage.tsx index b8c88f817cca..8a479324a8f0 100644 --- a/apps/meteor/ee/client/omnichannel/priorities/PrioritiesPage.tsx +++ b/apps/meteor/ee/client/omnichannel/priorities/PrioritiesPage.tsx @@ -10,7 +10,7 @@ import { useOmnichannelPriorities } from '../hooks/useOmnichannelPriorities'; import { PrioritiesResetModal } from './PrioritiesResetModal'; import { PrioritiesTable } from './PrioritiesTable'; import type { PriorityFormData } from './PriorityEditForm'; -import { PriorityVerticalBar } from './PriorityVerticalBar'; +import PriorityList from './PriorityList'; type PrioritiesPageProps = { priorityId: string; @@ -59,7 +59,7 @@ export const PrioritiesPage = ({ priorityId, context }: PrioritiesPageProps): Re prioritiesRoute.push({ context: 'edit', id }); }); - const onVerticalBarClose = (): void => { + const onContextualbarClose = (): void => { prioritiesRoute.push({}); }; @@ -88,7 +88,7 @@ export const PrioritiesPage = ({ priorityId, context }: PrioritiesPageProps): Re {context === 'edit' && ( - + )} ); diff --git a/apps/meteor/ee/client/omnichannel/priorities/PriorityVerticalBar.tsx b/apps/meteor/ee/client/omnichannel/priorities/PriorityList.tsx similarity index 50% rename from apps/meteor/ee/client/omnichannel/priorities/PriorityVerticalBar.tsx rename to apps/meteor/ee/client/omnichannel/priorities/PriorityList.tsx index e075e229d2c2..7fb940025035 100644 --- a/apps/meteor/ee/client/omnichannel/priorities/PriorityVerticalBar.tsx +++ b/apps/meteor/ee/client/omnichannel/priorities/PriorityList.tsx @@ -2,29 +2,36 @@ import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; -import VerticalBar from '../../../../client/components/VerticalBar'; +import { + Contextualbar, + ContextualbarHeader, + ContextualbarClose, + ContextualbarScrollableContent, +} from '../../../../client/components/Contextualbar'; import type { PriorityFormData } from './PriorityEditForm'; import PriorityEditFormWithData from './PriorityEditFormWithData'; -type PriorityVerticalBarProps = { +type PriorityListProps = { context: 'edit'; priorityId: string; onSave: (data: PriorityFormData) => Promise; onClose: () => void; }; -export const PriorityVerticalBar = ({ priorityId, onClose, onSave }: PriorityVerticalBarProps): ReactElement | null => { +const PriorityList = ({ priorityId, onClose, onSave }: PriorityListProps): ReactElement | null => { const t = useTranslation(); return ( - - + + {t('Edit_Priority')} - - - + + + - - + + ); }; + +export default PriorityList; diff --git a/apps/meteor/ee/client/omnichannel/slaPolicies/SlaEdit.tsx b/apps/meteor/ee/client/omnichannel/slaPolicies/SlaEdit.tsx index bc0e49836a5f..d872fb1b50c8 100644 --- a/apps/meteor/ee/client/omnichannel/slaPolicies/SlaEdit.tsx +++ b/apps/meteor/ee/client/omnichannel/slaPolicies/SlaEdit.tsx @@ -6,7 +6,7 @@ import type { ReactElement } from 'react'; import React from 'react'; import { useController, useForm } from 'react-hook-form'; -import VerticalBar from '../../../../client/components/VerticalBar'; +import { ContextualbarScrollableContent } from '../../../../client/components/Contextualbar'; type SlaEditProps = { isNew?: boolean; @@ -76,7 +76,7 @@ function SlaEdit({ data, isNew, slaId, reload, ...props }: SlaEditProps): ReactE }); return ( - + {t('Name')}* @@ -102,7 +102,6 @@ function SlaEdit({ data, isNew, slaId, reload, ...props }: SlaEditProps): ReactE {errors.dueTimeInMinutes?.message} - @@ -117,7 +116,7 @@ function SlaEdit({ data, isNew, slaId, reload, ...props }: SlaEditProps): ReactE - + ); } diff --git a/apps/meteor/ee/client/omnichannel/slaPolicies/SlasRoute.tsx b/apps/meteor/ee/client/omnichannel/slaPolicies/SlasRoute.tsx index 9ee8dad90e2a..e45f0cacd9ae 100644 --- a/apps/meteor/ee/client/omnichannel/slaPolicies/SlasRoute.tsx +++ b/apps/meteor/ee/client/omnichannel/slaPolicies/SlasRoute.tsx @@ -5,9 +5,9 @@ import { useQuery } from '@tanstack/react-query'; import type { ReactElement } from 'react'; import React, { useMemo, useCallback, useState } from 'react'; +import { Contextualbar, ContextualbarHeader, ContextualbarClose } from '../../../../client/components/Contextualbar'; import GenericTable from '../../../../client/components/GenericTable'; import type { GenericTableParams } from '../../../../client/components/GenericTable/GenericTable'; -import VerticalBar from '../../../../client/components/VerticalBar'; import NotAuthorizedPage from '../../../../client/views/notAuthorized/NotAuthorizedPage'; import RemoveSlaButton from './RemoveSlaButton'; import SlaEditWithData from './SlaEditWithData'; @@ -123,21 +123,20 @@ function SlasRoute(): ReactElement { return null; } - const handleVerticalBarCloseButtonClick = (): void => { + const handleContextualbarCloseButtonClick = (): void => { SlasRoute.push({}); }; return ( - - + + {context === 'edit' && t('Edit_SLA_Policy')} {context === 'new' && t('New_SLA_Policy')} - - - + + {context === 'edit' && } {context === 'new' && } - + ); }, [t, context, id, SlasRoute, refetch]); diff --git a/apps/meteor/ee/client/views/admin/deviceManagement/DeviceManagementInfo/DeviceManagementInfo.tsx b/apps/meteor/ee/client/views/admin/deviceManagement/DeviceManagementInfo/DeviceManagementInfo.tsx index e5a773f0793f..39e15fff52b0 100644 --- a/apps/meteor/ee/client/views/admin/deviceManagement/DeviceManagementInfo/DeviceManagementInfo.tsx +++ b/apps/meteor/ee/client/views/admin/deviceManagement/DeviceManagementInfo/DeviceManagementInfo.tsx @@ -4,8 +4,14 @@ import { useRoute, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useCallback } from 'react'; +import { + Contextualbar, + ContextualbarHeader, + ContextualbarClose, + ContextualbarScrollableContent, + ContextualbarFooter, +} from '../../../../../../client/components/Contextualbar'; import InfoPanel from '../../../../../../client/components/InfoPanel'; -import VerticalBar from '../../../../../../client/components/VerticalBar'; import UserAvatar from '../../../../../../client/components/avatar/UserAvatar'; import { useFormatDateAndTime } from '../../../../../../client/hooks/useFormatDateAndTime'; import { usePresence } from '../../../../../../client/hooks/usePresence'; @@ -29,12 +35,12 @@ const DeviceManagementInfo = ({ device, sessionId, loginAt, ip, userId, _user, o const handleCloseContextualBar = useCallback((): void => deviceManagementRouter.push({}), [deviceManagementRouter]); return ( - - + + {t('Device_Info')} - - - + + + {t('Client')} @@ -77,15 +83,15 @@ const DeviceManagementInfo = ({ device, sessionId, loginAt, ip, userId, _user, o {ip} - - + + - - + + ); }; diff --git a/apps/meteor/ee/client/views/admin/deviceManagement/DeviceManagementInfo/DeviceManagementInfoWithData.tsx b/apps/meteor/ee/client/views/admin/deviceManagement/DeviceManagementInfo/DeviceManagementInfoWithData.tsx index 645de7126228..7e3609f85420 100644 --- a/apps/meteor/ee/client/views/admin/deviceManagement/DeviceManagementInfo/DeviceManagementInfoWithData.tsx +++ b/apps/meteor/ee/client/views/admin/deviceManagement/DeviceManagementInfo/DeviceManagementInfoWithData.tsx @@ -4,7 +4,13 @@ import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useMemo } from 'react'; -import VerticalBar from '../../../../../../client/components/VerticalBar'; +import { + Contextualbar, + ContextualbarSkeleton, + ContextualbarHeader, + ContextualbarClose, + ContextualbarContent, +} from '../../../../../../client/components/Contextualbar'; import { useEndpointData } from '../../../../../../client/hooks/useEndpointData'; import { AsyncStatePhase } from '../../../../../../client/lib/asyncState'; import DeviceManagementInfo from './DeviceManagementInfo'; @@ -30,20 +36,20 @@ const DeviceInfoWithData = ({ deviceId, onReload }: { deviceId: string; onReload if (phase === AsyncStatePhase.LOADING) { return ( - - - + + + ); } if (error || !data) { return ( - - + + {t('Device_Info')} - - - + + + @@ -52,8 +58,8 @@ const DeviceInfoWithData = ({ deviceId, onReload }: { deviceId: string; onReload {error?.message} - - + + ); } diff --git a/apps/meteor/ee/client/views/admin/users/SeatsCapUsage/SeatsCapUsage.tsx b/apps/meteor/ee/client/views/admin/users/SeatsCapUsage/SeatsCapUsage.tsx index 6ee02b11c319..d87799a9ff98 100644 --- a/apps/meteor/ee/client/views/admin/users/SeatsCapUsage/SeatsCapUsage.tsx +++ b/apps/meteor/ee/client/views/admin/users/SeatsCapUsage/SeatsCapUsage.tsx @@ -1,8 +1,9 @@ -import { ProgressBar, Box } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; +import { GenericResourceUsage } from '../../../../../../client/components/GenericResourceUsage'; + type SeatsCapUsageProps = { limit: number; members: number; @@ -11,26 +12,9 @@ type SeatsCapUsageProps = { const SeatsCapUsage = ({ limit, members }: SeatsCapUsageProps): ReactElement => { const t = useTranslation(); const percentage = Math.max(0, Math.min((100 / limit) * members, 100)); - const closeToLimit = percentage >= 80; - const reachedLimit = percentage >= 100; const seatsLeft = Math.max(0, limit - members); - return ( - - -
{t('Seats_Available', { seatsLeft })}
- {`${members}/${limit}`} -
- -
- ); + return ; }; export default SeatsCapUsage; diff --git a/apps/meteor/ee/client/views/admin/users/useSeatsCap.ts b/apps/meteor/ee/client/views/admin/users/useSeatsCap.ts index 99290f70d034..e0b7b4eb43f7 100644 --- a/apps/meteor/ee/client/views/admin/users/useSeatsCap.ts +++ b/apps/meteor/ee/client/views/admin/users/useSeatsCap.ts @@ -1,4 +1,5 @@ -import { useEndpointData } from '../../../../../client/hooks/useEndpointData'; +import { useEndpoint } from '@rocket.chat/ui-contexts'; +import { useQuery } from '@tanstack/react-query'; export const useSeatsCap = (): | { @@ -7,15 +8,17 @@ export const useSeatsCap = (): reload: () => void; } | undefined => { - const { value, reload } = useEndpointData('/v1/licenses.maxActiveUsers'); + const fetch = useEndpoint('GET', '/v1/licenses.maxActiveUsers'); - if (!value) { + const result = useQuery(['/v1/licenses.maxActiveUsers'], () => fetch()); + + if (!result.isSuccess) { return undefined; } return { - activeUsers: value.activeUsers, - maxActiveUsers: value.maxActiveUsers ?? Number.POSITIVE_INFINITY, - reload, + activeUsers: result.data.activeUsers, + maxActiveUsers: result.data.maxActiveUsers ?? Number.POSITIVE_INFINITY, + reload: () => result.refetch(), }; }; diff --git a/apps/meteor/ee/server/apps/orchestrator.js b/apps/meteor/ee/server/apps/orchestrator.js index 14d077d0cdd0..abf98f088283 100644 --- a/apps/meteor/ee/server/apps/orchestrator.js +++ b/apps/meteor/ee/server/apps/orchestrator.js @@ -184,6 +184,14 @@ export class AppServerOrchestrator { this._rocketchatLogger.info(`Loaded the Apps Framework and loaded a total of ${this.getManager().get({ enabled: true }).length} Apps!`); } + async disableApps() { + await this.getManager() + .get() + .forEach((app) => { + this.getManager().disable(app.getID()); + }); + } + async unload() { // Don't try to unload it if it's already been // unlaoded or wasn't unloaded to start with diff --git a/apps/meteor/ee/server/startup/apps/index.ts b/apps/meteor/ee/server/startup/apps/index.ts new file mode 100644 index 000000000000..389658ee535c --- /dev/null +++ b/apps/meteor/ee/server/startup/apps/index.ts @@ -0,0 +1 @@ +import './trialExpiration'; diff --git a/apps/meteor/ee/server/startup/apps/trialExpiration.ts b/apps/meteor/ee/server/startup/apps/trialExpiration.ts new file mode 100644 index 000000000000..a96630be5c04 --- /dev/null +++ b/apps/meteor/ee/server/startup/apps/trialExpiration.ts @@ -0,0 +1,10 @@ +import { Meteor } from 'meteor/meteor'; + +import { Apps } from '../../apps'; +import { onInvalidateLicense } from '../../../app/license/server/license'; + +Meteor.startup(() => { + onInvalidateLicense(() => { + void Apps.disableApps(); + }); +}); diff --git a/apps/meteor/ee/server/startup/index.ts b/apps/meteor/ee/server/startup/index.ts index 605838552d75..409bc1bba597 100644 --- a/apps/meteor/ee/server/startup/index.ts +++ b/apps/meteor/ee/server/startup/index.ts @@ -1,4 +1,5 @@ import '../apps/startup'; +import './apps'; import './audit'; import './deviceManagement'; import './engagementDashboard'; diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ar.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ar.i18n.json index 6ed23b581720..1c0d59314d5a 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ar.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ar.i18n.json @@ -3776,7 +3776,6 @@ "Save_Mobile_Bandwidth": "حفظ النطاق الترددي للهاتف المحمول", "Save_to_enable_this_action": "حفظ لتمكين هذا الإجراء", "Save_To_Webdav": "حفظ إلى WebDAV", - "Save_Your_Encryption_Password": "حفظ كلمة مرور التشفير الخاصة بك", "save-others-livechat-room-info": "حفظ معلومات Room القناة متعددة الاتجاهات الأخرى", "save-others-livechat-room-info_description": "إذن لحفظ المعلومات من غرف القناة متعددة الاتجاهات الأخرى", "Saved": "تم الحفظ", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ca.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ca.i18n.json index 4c79c5225a80..562151c3cb3d 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ca.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ca.i18n.json @@ -1487,7 +1487,7 @@ "E2E_Encryption_Password_Explanation": "Ara podeu crear grups privats xifrats i missatges directes. També podeu canviar els grups privats o DM existents a xifrats.

Aquest és un xifratge d'extrem a extrem, de manera que la clau per codificar / descodificar els seus missatges no es desarà al servidor. Per això, heu de desar la contrasenya en un lloc segur. Se us demanarà que l'introduïu en altres dispositius on vulgueu utilitzar el xifratge e2e.", "E2E_key_reset_email": "Notificació de reinici de clau E2E", "E2E_password_request_text": "Per accedir als seus grups privats xifrats i als missatges directes, introdueixi la contrasenya de xifrat.
Necessites introduir aquesta contrasenya per xifrar / desxifrar els teus missatges en cada client que utilitzis, ja que la clau no s'emmagatzema en el servidor.", - "E2E_password_reveal_text": "Ara podeu crear grups privats xifrats i missatges directes. També podeu canviar els grups privats o DM existents a xifrats.

Aquest és un xifratge d'extrem a extrem, de manera que la clau per codificar / descodificar els seus missatges no es desarà al servidor. Per això, heu de desar aquesta contrasenya en un lloc segur. Se us demanarà que l'introduïu en altres dispositius on vulgueu utilitzar el xifratge e2e. Obtingueu més informació aquí!

La vostra contrasenya és:% s

Aquesta és una contrasenya generada automàticament, podeu configurar una nova contrasenya per a la vostra clau de xifrat en qualsevol moment des de qualsevol navegador que hagi introduït la contrasenya existent.
Aquesta contrasenya només s'emmagatzema en aquest navegador fins que la deseu i descarteu aquest missatge.", + "E2E_password_reveal_text": "Ara podeu crear grups privats xifrats i missatges directes. També podeu canviar els grups privats o DM existents a xifrats.

Aquest és un xifratge d'extrem a extrem, de manera que la clau per codificar / descodificar els seus missatges no es desarà al servidor. Per això, heu de desar aquesta contrasenya en un lloc segur. Se us demanarà que l'introduïu en altres dispositius on vulgueu utilitzar el xifratge e2e. Obtingueu més informació aquí!

La vostra contrasenya és:%s

Aquesta és una contrasenya generada automàticament, podeu configurar una nova contrasenya per a la vostra clau de xifrat en qualsevol moment des de qualsevol navegador que hagi introduït la contrasenya existent.
Aquesta contrasenya només s'emmagatzema en aquest navegador fins que la deseu i descarteu aquest missatge.", "E2E_Reset_Email_Content": "S'ha desconnectat automàticament. Quan torneu a iniciar sessió, Rocket.Chat generarà una nova clau i restaurarà el vostre accés a qualsevol sala xifrada que tingui un o més membres en línia. A causa de la naturalesa del xifratge E2E, Rocket.Chat no podrà restaurar l'accés a cap sala xifrada que no tingui membres en línia.", "E2E_Reset_Key_Explanation": "Aquesta opció eliminarà la clau E2E actual i tancarà la sessió.
Quan torneu a iniciar sessió, Rocket.Chat us generarà una nova clau i restaurarà l'accés a qualsevol sala xifrada que tingui un o més membres en línia.
A causa de la naturalesa del xifratge E2E, Rocket.Chat no podrà restaurar l'accés a cap sala xifrada que no tingui membres en línia.", "E2E_Reset_Other_Key_Warning": "Restablir la clau E2E actual tancarà la sessió de l'usuari. Quan l'usuari torna a iniciar sessió, Rocket.Chat generarà una nova clau i restaurarà l'accés de l'usuari a qualsevol sala xifrada que tingui un o més membres en línia. A causa de la naturalesa de l'xifrat E2E, Rocket.Chat no podrà restaurar l'accés a cap sala xifrada que no tingui membres en línia.", @@ -2093,11 +2093,11 @@ "Hide_counter": "Amaga comptador", "Hide_flextab": "Amaga la barra lateral dreta amb un clic", "Hide_Group_Warning": "Segur que voleu ocultar el grup \"%s\"?", - "Hide_Livechat_Warning": "Estàs segur que vols amagar al xat amb \"% s\"?", + "Hide_Livechat_Warning": "Estàs segur que vols amagar al xat amb \"%s\"?", "Hide_Private_Warning": "Segur que voleu ocultar la discussió amb \"%s\"?", "Hide_roles": "Amaga rols", "Hide_room": "Amagar", - "Hide_Room_Warning": "Segur que vols amagar la sala amb \"% s\"?", + "Hide_Room_Warning": "Segur que vols amagar la sala amb \"%s\"?", "Hide_System_Messages": "Ocultar els missatges del sistema", "Hide_Unread_Room_Status": "Amaga l'estat de sales no llegides", "Hide_usernames": "Oculta els noms d'usuari", @@ -2588,7 +2588,7 @@ "Leave": "Sortir ", "Leave_a_comment": "Deixar un comentari", "Leave_Group_Warning": "Segur que vols deixar el grup \"%s\"?", - "Leave_Livechat_Warning": "Segur que vols sortir de l'LiveChat amb \"% s\"?", + "Leave_Livechat_Warning": "Segur que vols sortir de l'LiveChat amb \"%s\"?", "Leave_Private_Warning": "Segur que vols sortir de la conversa amb \"%s\"?", "Leave_room": "Sortir ", "Leave_Room_Warning": "Segur que vols sortir de la sala \"%s\"?", @@ -2993,7 +2993,7 @@ "Mongo_version": "Versió Mongo", "MongoDB": "MongoDB", "MongoDB_Deprecated": "MongoDB obsolet", - "MongoDB_version_s_is_deprecated_please_upgrade_your_installation": "La versió% s de MongoDB està obsoleta, actualitzeu la vostra instal·lació.", + "MongoDB_version_s_is_deprecated_please_upgrade_your_installation": "La versió %s de MongoDB està obsoleta, actualitzeu la vostra instal·lació.", "Monitor_added": "Monitor afegit", "Monitor_history_for_changes_on": "Monitoritza l'historial per canvis a ", "Monitor_removed": "Monitor eliminat", @@ -3716,7 +3716,7 @@ "Save_Mobile_Bandwidth": "Estalvia ample de banda mòbil", "Save_to_enable_this_action": "Desa per activar els canvis", "Save_To_Webdav": "Desar a WebDAV", - "Save_Your_Encryption_Password": "Deseu la contrasenya de xifrat", + "Save_your_encryption_password": "Deseu la contrasenya de xifrat", "save-others-livechat-room-info": "Desar informació d'altres sales de LiveChat", "save-others-livechat-room-info_description": "Permís per guardar informació d'altres sales de LiveChat", "Saved": "Desat", @@ -4115,7 +4115,7 @@ "The_selected_user_is_not_an_agent": "L'usuari seleccionat no és un agent", "The_server_will_restart_in_s_seconds": "El servidor es reiniciarà d'aquí %s segons", "The_setting_s_is_configured_to_s_and_you_are_accessing_from_s": "L'opció %s està configurada com %s i s'està accedint des de %s!", - "The_user_s_will_be_removed_from_role_s": "L'usuari% s serà eliminat de el rol% s", + "The_user_s_will_be_removed_from_role_s": "L'usuari %s serà eliminat de el rol %s", "The_user_will_be_removed_from_s": "L'usuari s'eliminarà de %s", "The_user_wont_be_able_to_type_in_s": "L'usuari no podrà escriure a %s", "Theme": "Tema", @@ -4746,4 +4746,4 @@ "registration.component.form.sendConfirmationEmail": "Envia correu-e de confirmació", "RegisterWorkspace_Features_Marketplace_Title": "Mercat", "RegisterWorkspace_Features_Omnichannel_Title": "LiveChat" -} \ No newline at end of file +} diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/cs.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/cs.i18n.json index 83707b22b1f3..a7f0fd25e2ad 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/cs.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/cs.i18n.json @@ -3123,6 +3123,7 @@ "Save_Mobile_Bandwidth": "Šetřit mobilní data", "Save_to_enable_this_action": "Uložte pro povolení akce", "Save_To_Webdav": "Uložit do WebDAV", + "Save_your_encryption_password": "Uložit své šifrovací heslo", "save-others-livechat-room-info": "Upravit informace jiné místnosti Omnichannel", "save-others-livechat-room-info_description": "Právo uložit informace z jiných Omnichannel místností", "Saved": "Uloženo", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/da.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/da.i18n.json index 0ade0dd74af2..9597f3db4a33 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/da.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/da.i18n.json @@ -3140,6 +3140,7 @@ "Save_Mobile_Bandwidth": "Gem mobil båndbredde", "Save_to_enable_this_action": "Gem for at aktivere denne handling", "Save_To_Webdav": "Gem til WebDAV", + "Save_your_encryption_password": "Gem din krypteringsadgangskode", "save-others-livechat-room-info": "Gem andre Livechat Room Info", "save-others-livechat-room-info_description": "Tilladelse til at gemme information fra andre livechat kanaler", "Saved": "Gemt", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json index 264b495dc9b5..ec32b11a7e91 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json @@ -276,9 +276,11 @@ "Active_users": "Aktive Benutzer", "Activity": "Aktivität", "Add": "Hinzufügen", + "Add_a_Message": "Eine Nachricht hinzufügen", "Add_agent": "Berater/in hinzufügen", "Add_custom_oauth": "Benutzerdefiniertes OAuth-Konto hinzufügen", "Add_Domain": "Domain hinzufügen", + "Add_emoji": "Emoji hinzufügen", "Add_files_from": "Dateien hinzufügen von", "Add_manager": "Manager hinzufügen", "Add_monitor": "Monitor hinzufügen", @@ -297,6 +299,8 @@ "add-livechat-department-agents_description": "Berechtigung zum Hinzufügen von Omnichannel-Agenten zu Abteilungen", "add-oauth-service": "OAuth-Dienst hinzufügen", "add-oauth-service_description": "Berechtigung, einen neuen OAuth-Dienst hinzuzufügen", + "bypass-time-limit-edit-and-delete": "Zeitlimit umgehen", + "bypass-time-limit-edit-and-delete_description": "Erlaubnis, das Zeitlimit für das Bearbeiten und Löschen von Nachrichten zu umgehen", "add-team-channel": "Team Channel hinzufügen", "add-team-channel_description": "Erlaubnis zum Hinzufügen eines Kanals zu einem Team", "add-team-member": "Teammitglied hinzufügen", @@ -488,8 +492,10 @@ "Apply": "Anwenden", "Apply_and_refresh_all_clients": "Anwenden und alle Clients aktualisieren", "Apps": "Anwendungen", + "Apps_context_explore": "Erkunden", "Apps_context_enterprise": "Unternehmen", "Apps_context_installed": "Installiert", + "Apps_context_requested": "Angefordert", "Apps_Engine_Version": "Version der Anwendungs-Engine", "Apps_Essential_Alert": "Diese App ist für die folgenden Ereignisse unerlässlich:", "Apps_Essential_Disclaimer": "Die oben aufgeführten Ereignisse werden gestört, wenn diese App deaktiviert wird. Wenn Sie Rocket.Chat ohne die Funktionalität dieser App ausführen möchten, müssen Sie sie deinstallieren", @@ -610,11 +616,18 @@ "is_uploading": "lädt hoch", "is_recording": "nimmt auf", "Are_you_sure": "Sind Sie sicher?", + "Moderation_Are_you_sure_dismiss_and_delete_reports": "Möchten Sie wirklich alle Berichte für die Nachrichten dieses Benutzers verwerfen und löschen? Diese Aktion kann nicht rückgängig gemacht werden.", + "Are_you_sure_delete_department": "Möchten Sie diese Abteilung wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden. Bitte geben Sie zur Bestätigung den Namen der Abteilung ein.", + "Moderation_Are_you_sure_you_want_to_deactivate_this_user": "Möchten Sie diesen Benutzer wirklich deaktivieren und alle gemeldeten Nachrichten löschen? Alle Nachrichten werden dauerhaft gelöscht und der Benutzer kann sich nicht anmelden. Diese Aktion kann nicht rückgängig gemacht werden.", "Are_you_sure_you_want_to_clear_all_unread_messages": "Sind Sie sicher, dass Sie alle ungelesenen Nachrichten löschen möchten?", + "Moderation_Are_you_sure_you_want_to_delete_this_message": "Möchten Sie diese Nachricht wirklich löschen und alle Berichte zu dieser Nachricht verwerfen? Die Nachricht wird aus dem Nachrichtenverlauf gelöscht und niemand kann sie sehen. Diese Aktion kann nicht rückgängig gemacht werden.", "Are_you_sure_you_want_to_close_this_chat": "Sind Sie sicher, dass Sie diesen Chat schließen möchten?", "Are_you_sure_you_want_to_delete_this_record": "Möchten Sie diesen Eintrag wirklich löschen?", "Are_you_sure_you_want_to_delete_your_account": "Sind Sie sich sicher, dass Sie Ihr Konto löschen möchten?", + "Moderation_Are_you_sure_you_want_to_delete_all_reported_messages_from_this_user": "Möchten Sie wirklich alle gemeldeten Nachrichten von diesem Benutzer löschen? Die Nachrichten werden aus dem Nachrichtenverlauf gelöscht und niemand kann sie sehen. Diese Aktion kann nicht rückgängig gemacht werden.", "Are_you_sure_you_want_to_disable_Facebook_integration": "Sind Sie sich sicher, dass Sie die Facebook-Integration deaktivieren möchten?", + "Moderation_Are_you_sure_you_want_to_reset_the_avatar": "Sind Sie sicher, dass Sie den Avatar dieses Benutzers zurücksetzen möchten? Diese Aktion kann nicht rückgängig gemacht werden.", + "Are_you_sure_you_want_to_reset_the_name_of_all_priorities": "Sind Sie sicher, dass Sie den Namen aller Prioritäten zurücksetzen möchten?", "Assets": "Assets", "Assets_Description": "Ändern Sie das Logo, das Symbol, das Favicon und vieles mehr für Ihren Arbeitsbereich.", "Asset_preview": "Asset-Vorschau", @@ -651,6 +664,7 @@ "Author_Site": "Seite des Autors", "Authorization_URL": "Autorisierungs-URL", "Authorize": "Berechtigen", + "Authorize_access_to_your_account": "Autorisieren Sie den Zugriff auf Ihr Konto", "Auto_Load_Images": "Automatisches Laden der Bilder", "Auto_Selection": "Automatische Auswahl", "Auto_Translate": "Automatische Übersetzung", @@ -673,6 +687,7 @@ "Avatar": "Profilbild", "Avatars": "Avatare", "Avatar_changed_successfully": "Das Profilbild wurde erfolgreich geändert.", + "Moderation_Avatar_reset_successfully": "Avatar wurde erfolgreich zurückgesetzt", "Avatar_URL": "URL des Profilbilds", "Avatar_format_invalid": "Ungültiges Format. Nur Bilddateien sind erlaubt", "Avatar_url_invalid_or_error": "Die angegebene Internetadresse ist ungültig oder nicht verfügbar. Bitte versuchen Sie es mit einer anderen Internetadresse erneut.", @@ -739,6 +754,7 @@ "Blockstack_ButtonLabelText": "Text des Button-Labels", "Blockstack_Generate_Username": "Benutzername generieren", "Body": "Textkörper", + "Bold": "Fett", "bot_request": "Bot-Anfrage", "BotHelpers_userFields": "Benutzerfelder", "BotHelpers_userFields_Description": "CSV-Datei mit Benutzerfeldern die von Bot-Methoden genutzt werden dürfen.", @@ -802,6 +818,7 @@ "Call_was_not_answered": "Anruf wurde nicht beantwortet", "Caller": "Anrufer", "Caller_Id": "Anrufer-ID", + "Camera_access_not_allowed": "Der Zugriff auf die Kamera wurde nicht erlaubt. Bitte überprüfen Sie Ihre Browsereinstellungen.", "Cam_on": "Kamera an", "Cam_off": "Kamera aus", "can-audit": "Kann auditieren", @@ -825,6 +842,7 @@ "Cannot_open_conversation_with_yourself": "Ein Selbstgespräch kann nicht gestartet werden", "Cannot_share_your_location": "Standort teilen nicht möglich...", "Cannot_disable_while_on_call": "Status kann während eines Anrufs nicht geändert werden ", + "Cant_join": "Kann nicht teilnehmen", "CAS": "CAS", "CAS_Description": "Der zentrale Authentifizierungsdienst ermöglicht es den Mitgliedern, sich mit einem Satz von Anmeldedaten über mehrere Protokolle bei mehreren Websites anzumelden.", "CAS_autoclose": "Anmelde-Popup automatisch schließen", @@ -977,7 +995,7 @@ "Cloud_registration_pending_html": "Push-Benachrichtigungen funktionieren nicht, bis die Registrierung abgeschlossen ist. Mehr erfahren", "Cloud_registration_pending_title": "Die Cloud-Registrierung steht noch aus", "Cloud_registration_required": "Registrierung erforderlich", - "Cloud_registration_required_description": "Sieht aus, als hätten Sie sich während des Setups nicht für die Registrierung Ihres Arbeitsbereichs entschieden.", + "Cloud_registration_required_description": "Es sieht aus, als hätten Sie sich während des Setups nicht für die Registrierung Ihres Arbeitsbereichs entschieden.", "Cloud_registration_required_link_text": "Klicken Sie hier, um Ihren Arbeitsbereich zu registrieren.", "Cloud_resend_email": "E-Mail erneut versenden", "Cloud_Service_Agree_PrivacyTerms": "Cloud-Dienst-Datenschutzbestimmungen zustimmen", @@ -988,6 +1006,8 @@ "Cloud_troubleshooting": "Troubleshooting", "Cloud_update_email": "E-Mail aktualisieren", "Cloud_what_is_it": "Was ist das?", + "Copy_Link": "Link kopieren", + "Copy_password": "Passwort kopieren", "Cloud_what_is_it_additional": "Zusätzlich ermöglicht die Rocket.Chat Cloud Console die Verwaltung von Lizenzen, Support-Anfragen und Rechnungen.", "Cloud_what_is_it_description": "Mit Rocket.Chat Cloud Connect können Sie Ihren selbst gehosteten Rocket.Chat Arbeitsbereich mit unserer Cloud verbinden. Auf diese Weise können Sie Ihre Lizenzen, Abrechnung und Support in der Rocket.Chat Cloud verwalten. ", "Cloud_what_is_it_services_like": "Dienste wie:", @@ -1362,6 +1382,7 @@ "Current_File": "Aktuelle Datei", "Current_Import_Operation": "Aktueller Importvorgang", "Current_Status": "Aktueller Status", + "Currently_we_dont_support_joining_servers_with_this_many_people": "Derzeit unterstützen wir es nicht, Servern mit so vielen Leuten beizutreten", "Custom": "Benutzerdefiniert", "Custom CSS": "Benutzerdefiniertes CSS", "Custom_agent": "Benutzerdefinierter Agent", @@ -1384,6 +1405,9 @@ "Custom_OAuth_has_been_removed": "Benutzerdefiniertes OAuth wurde entfernt", "Custom_oauth_helper": "Bei der Einrichtung Ihres OAuth-Providers muss eine Rückruf-URL angegeben werden. Benutzen Sie dafür folgende URL:
%s
", "Custom_oauth_unique_name": "Name des OAuth-Kontos", + "Custom_roles": "Benutzerdefinierte Rollen", + "Custom_roles_upsell_add_custom_roles_workspace": "Fügen Sie benutzerdefinierte Rollen hinzu, die zu Ihrem Arbeitsbereich passen", + "Custom_roles_upsell_add_custom_roles_workspace_description": "Mit benutzerdefinierten Rollen können Sie Berechtigungen für die Personen in Ihrem Workspace festlegen. Legen Sie alle Rollen fest, die Sie benötigen, um sicherzustellen, dass die Mitarbeiter ein sicheres Arbeitsumfeld haben.", "Custom_Script_Logged_In": "Benutzerdefiniertes Skript für angemeldete Benutzer", "Custom_Script_Logged_In_Description": "Benutzerdefiniertes Skript, das IMMER und für JEDEN angemeldeten Benutzer ausgeführt wird (bspw. beim Öffnen eines Raums).", "Custom_Script_Logged_Out": "Benutzerdefiniertes Skript für abgemeldete Benutzer", @@ -1415,6 +1439,7 @@ "Custom_User_Status_Updated_Successfully": "Benutzerdefinierter Benutzerstatus erfolgreich aktualisiert", "Customer_without_registered_email": "Der Kunde hat keine registrierte E-Mail Adresse", "Customize": "Anpassen", + "Customize_Content": "Inhalt anpassen", "CustomSoundsFilesystem": "Dateisystem für benutzerdefinierte Töne", "CustomSoundsFilesystem_Description": "Legen Sie fest, wie benutzerdefinierte Sounds gespeichert werden.", "Daily_Active_Users": "Täglich aktive Benutzer", @@ -1447,18 +1472,25 @@ "DDP_Rate_Limit_User_Interval_Time": "Beschränkung durch Benutzer: Intervallzeit", "DDP_Rate_Limit_User_Requests_Allowed": "Beschränkung durch Benutzer: Anforderungen zulässig", "Deactivate": "Deaktivieren", + "Moderation_Deactivate_User": "Nutzer deaktivieren", "Decline": "Ablehnen", "Decode_Key": "Entschlüsseln", "default": "Standard", "Default": "Voreinstellung", + "Default_provider": "Standardanbieter", "Default_value": "Standardwert", "Delete": "Löschen", "Deleting": "Wird gelöscht", + "Delete_account": "Konto löschen", + "Delete_account?": "Konto löschen?", "Delete_all_closed_chats": "Alle geschlossenen Chats löschen", + "Delete_Department?": "Abteilung löschen?", "Delete_File_Warning": "Wenn Sie eine Datei löschen, wird diese für immer gelöscht. Dies kann nicht rückgängig gemacht werden.", "Delete_message": "Nachricht löschen", + "Moderation_Delete_all_messages": "Alle Nachrichten löschen", "Delete_my_account": "Mein Konto löschen", "Delete_Role_Warning": "Wenn Sie eine Rolle löschen, wird sie für immer gelöscht. Dies kann nicht rückgängig gemacht werden.", + "Delete_Role_Warning_Community_Edition": "Dies kann nicht rückgängig gemacht werden. Beachten Sie, dass es in der Community Edition nicht möglich ist, neue benutzerdefinierte Rollen zu erstellen.", "Delete_Room_Warning": "Beim Löschen eines Raumes werden alle Nachrichten in diesem Raum unwiderruflich gelöscht.", "Delete_User_Warning": "Beim Löschen eines Benutzers werden alle Nachrichten des Benutzers unwiderruflich gelöscht.", "Delete_User_Warning_Delete": "Beim Löschen eines Benutzers werden alle Nachrichten des Benutzers unwiderruflich gelöscht.", @@ -1482,9 +1514,12 @@ "Deleted__roomName__": "#{{roomName}} gelöscht", "Deleted__roomName__room": "#{{roomName}} gelöscht", "Department": "Abteilung", + "Department_archived": "Abteilung archiviert", "Department_name": "Name der Abteilung", "Department_not_found": "Abteilung konnte nicht gefunden werden.", "Department_removed": "Die Abteilung wurde gelöscht.", + "Department_Removal_Disabled": "Lösch-Option vom Admin gesperrt", + "Department_unarchived": "Abteilung nicht mehr archiviert", "Departments": "Abteilungen", "Deployment_ID": "Deployment-ID", "Deployment": "Implementierung", @@ -1582,6 +1617,7 @@ "Discussions_unavailable_for_federation": "Diskussionen sind für Verbundräume nicht verfügbar", "discussion-created": "{{message}}", "Discussions": "Diskussionen", + "Moderation_Dismiss_reports": "Berichte abweisen", "Display": "Anzeige", "Display_avatars": "Avatare anzeigen", "Display_Avatars_Sidebar": "Avatare in der Seitenleiste anzeigen", @@ -1630,7 +1666,8 @@ "Markdown_Marked_SmartLists": "Markierte intelligente Listen aktivieren", "Duplicate_private_group_name": "Eine private Gruppe mit dem Namen '%s' existiert bereits.", "Markdown_Marked_Smartypants": "Mit intelligenter Punktsetzung (\"Smartypants\") formatieren", - "Duplicated_Email_address_will_be_ignored": "Doppelte E-Mail_Adressen werden ignoriert.", + "Moderation_Duplicate_messages": "Doppelte Nachrichten", + "Duplicated_Email_address_will_be_ignored": "Doppelte E-Mail-Adressen werden ignoriert.", "Markdown_Marked_Tables": "Markierte Tabellen aktivieren", "duplicated-account": "Doppeltes Konto", "E2E Encryption": "Ende-zu-Ende-Verschlüsselung", @@ -1746,6 +1783,7 @@ "Email_verified": "Die E-Mail-Adresse wurde bestätigt.", "Email_sent": "E-Mail gesendet", "Emoji": "Emoji", + "Emoji_picker": "Emoji-Picker", "EmojiCustomFilesystem": "Dateisystem für eigene Emojis", "EmojiCustomFilesystem_Description": "Legen Sie fest, wie Emojis gespeichert werden.", "Empty_no_agent_selected": "Leer, kein Agent ausgewählt", @@ -2128,6 +2166,7 @@ "File_Type": "Dateityp", "File_type_is_not_accepted": "Dateityp wird nicht akzeptiert.", "File_uploaded": "Datei hochgeladen", + "File_Upload_Disabled": "Datei-Upload deaktiviert", "File_uploaded_successfully": "Datei erfolgreich hochgeladen", "File_URL": "Datei-URL", "FileType": "Dateityp", @@ -2143,6 +2182,8 @@ "FileUpload_Disabled": "Datei-Uploads sind deaktiviert.", "FileUpload_Enable_json_web_token_for_files": "Json Web Tokens-Schutz zum Hochladen von Dateien aktivieren", "FileUpload_Enable_json_web_token_for_files_description": "Hängt eine JWT an die URLs der hochgeladenen Dateien an", + "FileUpload_Restrict_to_room_members": "Dateien auf Mitglieder des Raumes beschränken", + "FileUpload_Restrict_to_room_members_Description": "Beschränken Sie den Zugriff auf Dateien, die in Räumen hochgeladen wurden auf die Mitglieder der jeweiligen Räume", "FileUpload_Enabled": "Hochladen von Dateien aktivieren", "FileUpload_Enabled_Direct": "Dateiaustausch in Direktnachrichten aktiviert", "FileUpload_Error": "Datei-Upload-Fehler", @@ -2324,6 +2365,7 @@ "Hide_flextab": "Rechte Seitenleiste mit Klick ausblenden", "Hide_Group_Warning": "Sind Sie sicher, dass Sie die Gruppe \"%s\" ausblenden wollen?", "Hide_Livechat_Warning": "Sind Sie sich sicher, dass Sie den Livechat mit \"%s\" ausblenden wollen?", + "Hide_On_Workspace": "Im Arbeitsbereich verstecken", "Hide_Private_Warning": "Sind Sie sicher, dass Sie das Gespräch mit \"%s\" ausblenden wollen?", "Hide_roles": "Rollen ausblenden", "Hide_room": "Raum verstecken", @@ -2453,6 +2495,7 @@ "Industry": "Branche", "Info": "Info", "initials_avatar": "Avatar aus Initialen", + "Inline_code": "Inline-Code", "Install": "Installieren", "Install_Extension": "Erweiterung installieren", "Install_FxOs": "Installieren Sie Rocket.Chat auf Ihrem Firefox", @@ -2584,6 +2627,8 @@ "IssueLinks_Incompatible": "Warnung: Aktivieren Sie diese und die 'Hex-Farbvorschau' Einstellung nicht gleichzeitig.", "IssueLinks_LinkTemplate": "Vorlage für Issue-Verknüpfungen", "IssueLinks_LinkTemplate_Description": "Vorlage für Issue-Verknüpfungen; %s wird mit der Issue-Nummer ersetzt werden.", + "It_Will_Hide_All_Other_Content_Blocks_In_The_Homepage": "Dadurch werden alle anderen Inhaltsblöcke auf der Homepage ausgeblendet", + "It_Will_Show_All_Other_Content_Blocks_In_The_Homepage": "Es werden alle anderen Inhaltsblöcke auf der Homepage angezeigt", "It_works": "Es funktioniert", "It_Security": "IT-Sicherheit", "Italic": "Kursiv", @@ -2688,8 +2733,12 @@ "Layout_Login_Hide_Powered_By_Description": "Blenden Sie das \"Powered by\" auf der Anmeldeseite aus.", "Layout_Login_Template": "Login-Vorlage", "Layout_Login_Template_Description": "Passen Sie das Aussehen der Anmeldeseite an.", + "Layout_Login_Template_Vertical": "Vertikal", + "Layout_Login_Template_Horizontal": "Horizontal", "Layout_Description": "Passen Sie das Aussehen Ihres Arbeitsbereichs an.", "Layout_Home_Body": "Inhalt der Startseite", + "Layout_Home_Page_Content": "Layout/Inhalt der Startseite", + "Layout_Home_Page_Content_Title": "Inhalt der Startseite", "Layout_Home_Title": "Titel der Startseite", "Layout_Legal_Notice": "Impressum", "Layout_Login_Terms": "Anmeldebedingungen", @@ -3094,6 +3143,8 @@ "Manager_added": "Manager wurde hinzugefügt", "Manager_removed": "Manager wurde gelöscht", "Managers": "Manager", + "Manage_server_list": "Serverliste verwalten", + "Manage_servers": "Server verwalten", "Management_Server": "Management-Server", "Managing_assets": "Asset-Verwaltung", "Managing_integrations": "Integrationsverwaltung", @@ -3178,6 +3229,7 @@ "Message_Audio_bitRate": "Audionachrichten-Bitrate", "Message_AudioRecorderEnabled": "Audioaufnahme aktivieren", "Message_AudioRecorderEnabled_Description": "Benötigt \"Audio / MP3\" -Dateien als akzeptierter Medientyp innerhalb der \"Datei-Upload\" -Einstellungen.", + "Message_Audio_Recording_Disabled": "Audioaufnahme in Nachrichten deaktiviert", "Message_auditing": "Nachrichtenüberprüfung", "Message_auditing_log": "Protokoll der Nachrichtenüberprüfung", "Message_BadWordsFilterList": "Wörter zur Blacklist hinzufügen", @@ -3206,6 +3258,7 @@ "Message_has_been_edited_by": "Die Nachricht wurde editiert von {{username}}", "Message_has_been_edited_by_at": "Die Nachricht wurde bearbeitet von __Benutzername__ am __datum__", "Message_has_been_pinned": "Nachricht wurde angeheftet", + "Message_has_been_shared": "Nachricht wurde geteilt", "Message_has_been_starred": "Nachricht wurde als Favorit gekennzeichnet", "Message_has_been_unpinned": "Nachricht wurde entpinnt", "Message_has_been_unstarred": "Nachricht nicht mehr favorisiert", @@ -3271,6 +3324,7 @@ "Message_UserId": "Benutze-ID", "Message_view_mode_info": "Dadurch ändert sich der Platzbedarf für Nachrichten auf dem Bildschirm", "Message_VideoRecorderEnabled": "Videoaufnahme eingeschaltet", + "Message_Video_Recording_Disabled": "Videoaufzeichnung in Nachrichten deaktiviert", "MessageBox_view_mode": "MessageBox-Ansichtsmodus", "Message_VideoRecorderEnabledDescription": "Erfordert, dass der Medientyp 'video/webm' in den \"Datei-Upload\"-Einstellungen als Medientyp akzeptiert wird", "messages": "Nachrichten", @@ -3298,6 +3352,7 @@ "Method": "Methode", "Mic_on": "Mikrofon an", "Microphone": "Mikrofon", + "Microphone_access_not_allowed": "Der Mikrofonzugriff wurde nicht erlaubt. Bitte überprüfen Sie Ihre Browsereinstellungen.", "Mic_off": "Mikrofon aus", "Min_length_is": "Die minimale Länge beträgt %s", "Minimum": "Minimum", @@ -3321,13 +3376,20 @@ "mobile-upload-file": "Datei-Upload von Mobilgeräten erlauben", "mobile-upload-file_description": "Berechtigung zum Hochladen von Dateien auf mobilen Geräten", "Mobile_Push_Notifications_Default_Alert": "Push-Benachrichtigungen bei", + "Moderation_Console": "Moderationskonsole", + "Moderation_View_reports": "Meldungen ansehen", + "Moderation_Go_to_message": "Zur Nachricht", "Moderation_Delete_message": "Nachricht löschen", + "Moderation_Dismiss_and_delete": "Ablehnen und löschen", + "Moderation_Delete_this_message": "Diese Nachricht löschen", + "Moderation_Message_context_header": "Nachricht(en) von __displayName__", + "Moderation_Action_View_reports": "Gemeldete Nachrichten anzeigen", "Monday": "Montag", "Mongo_storageEngine": "Mongo Storage-Engine", "Mongo_version": "Mongo Version", "MongoDB": "MongoDB", "MongoDB_Deprecated": "MongoDB veraltet", - "MongoDB_version_s_is_deprecated_please_upgrade_your_installation": "MongoDB-Version % s ist veraltet. Bitte aktualisieren Sie Ihre Installation.", + "MongoDB_version_s_is_deprecated_please_upgrade_your_installation": "MongoDB-Version %s ist veraltet. Bitte aktualisieren Sie Ihre Installation.", "Monitor_added": "Monitor hinzugefügt", "Monitor_new_and_suspicious_logins": "Neue und verdächtige Anmeldungen überwachen", "Monitor_history_for_changes_on": "Verlaufsänderungen beobachten für", @@ -3342,11 +3404,14 @@ "More_options": "Mehr Optionen", "Most_popular_channels_top_5": "Beliebteste Kanäle (Top 5)", "Most_recent_updated": "Zuletzt aktualisiert", + "Most_recent_requested": "Zuletzt angefragt", "Move_beginning_message": "`%s` - Zum Anfang der Nachricht springen", "Move_end_message": "`%s` - Zum Ende der Nachricht springen", "Move_queue": "In Warteschlange verschieben", "Msgs": "Nachrichten", "multi": "mehrere", + "Multi_line": "Mehrzeilig", + "Multiple_monolith_instances_alert": "Sie betreiben mehrere Instanzen ohne eine aktive Unternehmenslizenz — einige Funktionen verhalten sich möglicherweise nicht wie vorgesehen", "Mute": "Stummschalten", "Mute_and_dismiss": "Stummschalten und Abweisen", "Mute_all_notifications": "Alle Benachrichtigungen stummschalten", @@ -3926,6 +3991,7 @@ "Report_Number": "Berichtnummer", "Report_this_message_question_mark": "Diese Nachricht melden?", "Reporting": "Berichtswesen", + "Moderation_Message_already_deleted": "Nachricht ist bereits gelöscht", "Request": "Anfordern", "Request_seats": "Plätze anfordern", "Request_more_seats": "Mehr Plätze anfordern", @@ -4041,6 +4107,7 @@ "room_disallowed_reactions": "unzulässige Reaktionen", "Room_Edit": "Raum bearbeiten", "Room_has_been_archived": "Der Room wurde archiviert", + "Room_has_been_converted": "Room wurde konvertiert", "Room_has_been_created": "Room wurde erstellt", "Room_has_been_deleted": "Der Room wurde gelöscht", "Room_has_been_removed": "Raum wurde entfernt", @@ -4160,7 +4227,7 @@ "Save_Mobile_Bandwidth": "Mobiles Datenvolumen sparen", "Save_to_enable_this_action": "Speichern, um diese Aktion zu aktivieren", "Save_To_Webdav": "In WebDAV speichern", - "Save_Your_Encryption_Password": "Ihr Verschlüsselungspasswort speichern", + "Save_your_encryption_password": "Speichern Sie Ihr Verschlüsselungs-Passwort", "save-all-canned-responses": "Alle Antwortvorlagen speichern", "save-all-canned-responses_description": "Berechtigung alle Antwortvorlagen zu speichern", "save-canned-responses": "Antwortvorlagen speichern", @@ -4182,6 +4249,7 @@ "Search": "Suche", "Searchable": "Durchsuchbar", "Search_Apps": "Apps suchen", + "Search_Installed_Apps": "Installierte Apps durchsuchen", "Search_by_file_name": "Suche nach Dateiname", "Search_by_username": "Anhand des Nutzernamens suchen", "Search_by_category": "Nach Kategorie suchen", @@ -4220,6 +4288,7 @@ "Select_an_option": "Eine Option auswählen", "Select_at_least_one_user": "Mindestens einen Benutzer auswählen", "Select_at_least_two_users": "Mindestens zwei Benutzer auswählen", + "Share_Message": "Nachricht teilen", "Select_department": "Eine Abteilung auswählen", "Select_file": "Datei auswählen", "Select_role": "Eine Rolle auswählen", @@ -4279,10 +4348,12 @@ "Separate_multiple_words_with_commas": "Trennen Sie mehrere Wörter durch Kommas", "Served_By": "Bedient von", "Server": "Server", + "Servers": "Server", "Server_Configuration": "Serverkonfiguration", "Server_File_Path": "Server-Dateipfad", "Server_Folder_Path": "Server-Ordnerpfad", "Server_Info": "Serverinformationen", + "Server_name": "Servername", "Server_Type": "Server Typ", "Service": "Service", "Service_account_key": "Service-Konto-Schlüssel", @@ -4304,6 +4375,7 @@ "set-readonly_description": "Berechtigung, einen Raum schreibgeschützt zu machen", "Settings": "Einstellungen", "Settings_updated": "Die Einstellungen wurden aktualisiert", + "Setup_SMTP": "SMTP einrichten", "Setup_Wizard": "Setup-Assistent", "Setup_Wizard_Description": "Grundlegende Informationen über Ihren Arbeitsbereich wie Name der Organisation und Land.", "Setup_Wizard_Info": "Wir führen Sie durch die Einrichtung Ihres ersten Admin-Benutzers, die Konfiguration Ihrer Organisation und die Registrierung Ihres Servers, um kostenlose Push-Benachrichtigungen und mehr zu erhalten.", @@ -4353,6 +4425,7 @@ "Site_Url": "Website-URL", "Site_Url_Description": "Beispiel: https://chat.domain.com/", "Size": "Größe", + "Skin_tone": "Hautton", "Skip": "Überspringen", "Slack_Users": "Benutzer-CSV von Slack", "SlackBridge_APIToken": "API-Tokens", @@ -4489,6 +4562,7 @@ "Stream_Cast": "Stream Cast", "Stream_Cast_Address": "Stream Cast-Adresse", "Stream_Cast_Address_Description": "IP oder Host Ihres zentralen Stream Cast-Servers inkl. Port, bspw. `192.168.1.1:3000` oder `localhost:4000`", + "Strike": "Durchgestrichen", "Style": "Stil", "Subject": "Betreff", "Submit": "Abssenden", @@ -4670,7 +4744,7 @@ "theme-custom-css": "Benutzerdefiniertes CSS", "theme-font-body-font-family": "Schriftart-Familie für den Textkörper", "There_are_no_agents_added_to_this_department_yet": "Es wurden bisher keine Agenten zu dieser Abteilung hinzugefügt", - "There_are_no_applications": "Bisher wurden keine oAuth Anwendungen hinzugefügt.", + "There_are_no_applications": "Bisher wurden keine oAuth-Anwendungen hinzugefügt.", "There_are_no_applications_installed": "Zur Zeit sind keine Rocket.Chat-Anwendungen installiert.", "There_are_no_available_monitors": "Keine verfügbaren Monitore", "There_are_no_departments_added_to_this_tag_yet": "Dem Tag wurden noch keine Abteilungen hinzugefügt", @@ -5473,10 +5547,20 @@ "Create_an_account": "Ein Konto erstellen", "Undo_request": "Anfrage rückgängig machen", "No_permission": "Keine Berechtigung", + "New_custom_status": "Neuer benutzerdefinierter Status", + "Service_disabled": "Der Dienst ist jetzt deaktiviert", + "Service_disabled_description": "Sie können es erst wieder aktivieren, wenn weniger als 200 aktive Verbindungen gleichzeitig bestehen", + "User_status_disabled": "Der Benutzerstatus wurde vorübergehend deaktiviert, um die Leistung aufrechtzuerhalten.", + "User_status_disabled_learn_more": "Benutzerstatus deaktiviert", + "User_status_disabled_learn_more_description": "Aufgrund des hohen Volumens an aktiven Verbindungen ist der Dienst, der den Benutzerstatus verwaltet, vorübergehend deaktiviert. Administratoren können dies manuell in den Workspace-Einstellungen erneut aktivieren.", + "User_status_temporarily_disabled": "Benutzerstatus vorübergehend deaktiviert", "Awaiting_confirmation": "Warten auf Bestätigung", + "Security_code": "Sicherheitscode", "RegisterWorkspace_Features_MobileNotifications_Title": "Mobile Push-Benachrichtigungen", "RegisterWorkspace_Features_Marketplace_Title": "Marktplatz", "RegisterWorkspace_Features_Omnichannel_Title": "Omnichannel", "RegisterWorkspace_Setup_Label": "Cloud-Account-E-Mail", - "cloud.RegisterWorkspace_Setup_Terms_Privacy": "Ich bin mit den Nutzungsvereinbarung und den Datenschutzbestimmungen einverstanden" -} \ No newline at end of file + "RegisterWorkspace_Setup_Email_Verification": "Bitte überprüfen Sie, ob der unten stehende Sicherheitscode mit dem in der E-Mail übereinstimmt.", + "cloud.RegisterWorkspace_Setup_Terms_Privacy": "Ich bin mit den Nutzungsvereinbarung und den Datenschutzbestimmungen einverstanden", + "Uninstall_grandfathered_app": "{{appName}} deinstallieren?" +} diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 3873e3448fc3..4185cc208dbe 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -17,7 +17,7 @@ "@username": "@username", "@username_message": "@username ", "#channel": "#channel", - "%_of_conversations": "% of Conversations", + "%_of_conversations": "%% of Conversations", "0_Errors_Only": "0 - Errors Only", "1_Errors_and_Information": "1 - Errors and Information", "2_Erros_Information_and_Debug": "2 - Errors, Information and Debug", @@ -5881,4 +5881,4 @@ "Uninstall_grandfathered_app": "Uninstall {{appName}}?", "App_will_lose_grandfathered_status": "**This {{context}} app will lose its grandfathered status.**\n\nWorkspaces on Community Edition can have up to {{limit}} {{context}} apps enabled. Grandfathered apps count towards the limit but the limit is not applied to them.", "Theme_Appearence": "Theme Appearence" -} \ No newline at end of file +} diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/es.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/es.i18n.json index cc33d9d6713c..511961271a37 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/es.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/es.i18n.json @@ -2591,7 +2591,7 @@ "Learn_how_to_unlock_the_myriad_possibilities_of_rocket_chat": "Aprenda a desbloquear las innumerables posibilidades de Rocket.Chat.", "Leave": "Salir", "Leave_a_comment": "Dejar un comentario", - "Leave_Group_Warning": "¿Seguro que quieres salir del grupo \"% s\"?", + "Leave_Group_Warning": "¿Seguro que quieres salir del grupo \"%s\"?", "Leave_Livechat_Warning": "¿Seguro que quieres salir de la sala de Omnichannel con \"%s\"?", "Leave_Private_Warning": "¿Seguro que quieres salir de la discusión con \"%s\"?", "Leave_room": "Salir ", @@ -3746,7 +3746,6 @@ "Save_Mobile_Bandwidth": "Ahorrar ancho de banda móvil", "Save_to_enable_this_action": "Guarda para habilitar esta acción", "Save_To_Webdav": "Guardar en WebDAV", - "Save_Your_Encryption_Password": "Guardar contraseña de cifrado", "save-others-livechat-room-info": "Guardar información de otra Room de Omnichannel", "save-others-livechat-room-info_description": "Permiso para guardar información de otras salas de Omnichannel", "Saved": "Guardado", @@ -4898,4 +4897,4 @@ "RegisterWorkspace_Features_Omnichannel_Title": "Omnichannel", "RegisterWorkspace_Setup_Label": "Cuenta de correo electrónico en la nube", "cloud.RegisterWorkspace_Setup_Terms_Privacy": "Acepto los <1>términos y condiciones y la <3>política de privacidad" -} \ No newline at end of file +} diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/fi.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/fi.i18n.json index e6126af406a0..e36d0f890df1 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/fi.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/fi.i18n.json @@ -4315,7 +4315,6 @@ "Save_Mobile_Bandwidth": "Säästä mobiilikaistaa", "Save_to_enable_this_action": "Ota toiminto käyttöön tallentamalla", "Save_To_Webdav": "Tallenna WebDAV:iin", - "Save_Your_Encryption_Password": "Tallenna salauksen salasana", "save-all-canned-responses": "Tallenna kaikki esivalmistetut vastaukset", "save-all-canned-responses_description": "Lupa tallentaa kaikki esivalmistetut vastaukset", "save-canned-responses": "Tallenna esivalmistetut vastaukset", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/fr.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/fr.i18n.json index c1673ea39362..3ca9fb17fc32 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/fr.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/fr.i18n.json @@ -3770,7 +3770,7 @@ "Save_Mobile_Bandwidth": "Préserver la bande passante sur mobile", "Save_to_enable_this_action": "Sauvegarder pour activer cette action", "Save_To_Webdav": "Enregistrer sur WebDAV", - "Save_Your_Encryption_Password": "Enregistrer votre mot de passe de chiffrement", + "Save_your_encryption_password": "Enregistrez votre mot de passe de cryptage", "save-others-livechat-room-info": "Enregistrer des informations sur les autres salons omnicanaux", "save-others-livechat-room-info_description": "Autorisation d'enregistrer des informations sur d'autres salons omnicanaux", "Saved": "Enregistré", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/hu.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/hu.i18n.json index 133c006a649f..a5a057f31f3f 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/hu.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/hu.i18n.json @@ -4159,7 +4159,7 @@ "Save_Mobile_Bandwidth": "Mobil sávszélesség kímélés", "Save_to_enable_this_action": "Mentés ezen művelet engedélyezéséhez", "Save_To_Webdav": "Mentés WebDAV-ba", - "Save_Your_Encryption_Password": "Az Ön titkosítási jelszavának mentése", + "Save_your_encryption_password": "Titkosítási jelszó mentése", "save-all-canned-responses": "Összes sablonválasz mentése", "save-all-canned-responses_description": "Jogosultság az összes sablonválasz mentéséhez", "save-canned-responses": "Sablonválaszok mentése", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ja.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ja.i18n.json index 7336e833ace8..de08b478813f 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ja.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ja.i18n.json @@ -3738,7 +3738,7 @@ "Save_Mobile_Bandwidth": "モバイル帯域幅の節約", "Save_to_enable_this_action": "保存してこのアクションを有効にする", "Save_To_Webdav": "WebDAVに保存", - "Save_Your_Encryption_Password": "暗号化パスワードの保存", + "Save_your_encryption_password": "暗号化パスワードを保存する", "save-others-livechat-room-info": "その他のオムニチャネルRoom情報", "save-others-livechat-room-info_description": "他のオムニチャネルルームから情報を保存する権限", "Saved": "保存しました", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ka-GE.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ka-GE.i18n.json index eedfa4db7cf2..9001f2344bf7 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ka-GE.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ka-GE.i18n.json @@ -2891,6 +2891,7 @@ "Save_Mobile_Bandwidth": "შეინახეთ მობილური ბენდვიზი", "Save_to_enable_this_action": "შეინახეთ ამ ქმედების გასააქტიურებლად", "Save_To_Webdav": "შეინახეთ WebDAV-ში", + "Save_your_encryption_password": "შეინახეთ თქვენი დაშიფვრის პაროლი", "save-others-livechat-room-info": "შეინახეთ სხვა Omnichannel ოთახების ინფორმაცია", "save-others-livechat-room-info_description": "სხვა omnichannel ოთახებიდან ინფორმაციის შენახვის უფლება", "Saved": "შენახულია", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/km.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/km.i18n.json index 5eccdbca2a85..0e1839f15fb9 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/km.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/km.i18n.json @@ -2464,6 +2464,7 @@ "Save_Mobile_Bandwidth": "រក្សាទុកកម្រិតបញ្ជូនតាមទូរស័ព្ទដៃ", "Save_to_enable_this_action": "រក្សាទុកដើម្បីបើកសកម្មភាពនេះ", "Save_To_Webdav": "រក្សាទុកទៅ WebDAV", + "Save_your_encryption_password": "រក្សាទុកពាក្យសម្ងាត់សំរាប់បំលែងកូដថ្មីរបស់អ្នក", "save-others-livechat-room-info": "រក្សាទុកព័ត៌មានផ្សេងទៀតរបស់ Livechat បន្ទប់", "save-others-livechat-room-info_description": "សិទ្ធិរក្សាទុកព័ត៌មានពីឆានែល livechat ផ្សេងទៀត", "Saved": "ដែលបានរក្សាទុក", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ko.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ko.i18n.json index 190ddf135cec..288b097a37ba 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ko.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ko.i18n.json @@ -3182,6 +3182,7 @@ "Save_Mobile_Bandwidth": "모바일 대역폭 저장", "Save_to_enable_this_action": "이 작업을 활성화 합니다.", "Save_To_Webdav": "WebDAV에 저장", + "Save_your_encryption_password": "암호화 비밀번호 저장", "save-others-livechat-room-info": "다른 사용자의 실시간상담 대화방 정보 저장", "save-others-livechat-room-info_description": "다른 실시간상담 대화방의 정보를 저장할 수있는 권한", "Saved": "저장됨", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/nl.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/nl.i18n.json index 17811493570e..5795bc6b6615 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/nl.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/nl.i18n.json @@ -3763,7 +3763,7 @@ "Save_Mobile_Bandwidth": "Bespaar mobiele bandbreedte", "Save_to_enable_this_action": "Opslaan om deze actie mogelijk te maken", "Save_To_Webdav": "Opslaan in WebDAV", - "Save_Your_Encryption_Password": "Bewaar uw versleutelingswachtwoord", + "Save_your_encryption_password": "Bewaar uw versleutelingswachtwoord", "save-others-livechat-room-info": "Bewaar andere omnichannel kamerinformatie", "save-others-livechat-room-info_description": "Toestemming om informatie uit andere omnichannel-kamers op te slaan", "Saved": "Opgeslagen", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json index 8567a993a5ac..3128a540b790 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json @@ -3708,7 +3708,7 @@ "Public_Community": "Społeczność publiczna", "Public_URL": "Publiczny URL", "Purchase_for_free": "Za darmo", - "Purchase_for_price": "Kup za $% s", + "Purchase_for_price": "Kup za $%s", "Purchased": "Zakupione", "Push": "Powiadomienia", "Push_Description": "Włącz i skonfiguruj powiadomienia push dla członków obszaru roboczego korzystających z urządzeń mobilnych.", @@ -4093,7 +4093,7 @@ "Save_Mobile_Bandwidth": "Oszczędzaj przepustowość", "Save_to_enable_this_action": "Zapisz, aby włączyć tą akcję", "Save_To_Webdav": "Zapisz w WebDAV", - "Save_Your_Encryption_Password": "Zapisz swoje hasło szyfrowania", + "Save_your_encryption_password": "Zapisz swoje hasło szyfrowania", "save-all-canned-responses": "Zapisz wszystkie predefiniowane odpowiedzi", "save-all-canned-responses_description": "Pozwolenie na zapisanie wszystkich predefiniowanych odpowiedzi", "save-canned-responses": "Zapisz predefiniowane odpowiedzi", @@ -5407,4 +5407,4 @@ "RegisterWorkspace_Setup_Label": "E-mail konta w chmurze", "RegisterWorkspace_Syncing_Complete": "Synchronizacja zakończona", "cloud.RegisterWorkspace_Setup_Terms_Privacy": "Zgadzam się z <1>zasadami i warunkami i <3>Polityką prywatności." -} \ No newline at end of file +} diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json index 4ce5a0e68241..a50f1612727a 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json @@ -3801,7 +3801,7 @@ "Save_Mobile_Bandwidth": "Economizar largura de banda móvel", "Save_to_enable_this_action": "Salvar para habilitar esta ação", "Save_To_Webdav": "Salvar para WebDAV", - "Save_Your_Encryption_Password": "Salvar sua senha de criptografia", + "Save_your_encryption_password": "Salve sua senha de criptografia", "save-others-livechat-room-info": "Salvar outras informações de sala do omnichannel", "save-others-livechat-room-info_description": "Permissão para salvar informações de outras salas do omnichannel", "Saved": "Salvo", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/pt.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/pt.i18n.json index 73cdbe486edd..24b6e5aae11f 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/pt.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/pt.i18n.json @@ -2493,6 +2493,7 @@ "Save_Mobile_Bandwidth": "Economizar Rede móvel", "Save_to_enable_this_action": "Salvar para habilitar esta acção", "Save_To_Webdav": "Salvar para WebDAV", + "Save_your_encryption_password": "Salve sua senha de criptografia", "save-others-livechat-room-info": "Salve outros serviços Livechat Room", "save-others-livechat-room-info_description": "Permissão para salvar informações de outros canais de Livechat", "Saved": "Guardado", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ru.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ru.i18n.json index ced3de5e8598..76d2fba017c6 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ru.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ru.i18n.json @@ -1,9 +1,10 @@ { "500": "Внутренняя ошибка сервера", - "__count__empty_rooms_will_be_removed_automatically": "{{count}} пустых чатов будет удалено автоматически.", + "__count__empty_rooms_will_be_removed_automatically": "{{count}}пустые комнаты будут удалены автоматически.", "__count__empty_rooms_will_be_removed_automatically__rooms__": "{{count}} пустых чатов будет удалено автоматически:
{{rooms}}.", "__count__message_pruned": "{{count}} сообщение удалено", "__count__message_pruned_plural": "{{count}} сообщений удалено", + "__usersCount__member_joined": "+ {{usersCount}} участников присоединилось", "__usersCount__member_joined_plural": "+ {{usersCount}} участников присоединилось", "__usersCount__people_will_be_invited": "{{usersCount}} человек будет приглашено", "__username__is_no_longer__role__defined_by__user_by_": "{{username}} больше не {{role}} по решению {{user_by}}", @@ -11,6 +12,7 @@ "This_room_encryption_has_been_enabled_by__username_": "Шифрование этой комнаты было включено {{username}}", "This_room_encryption_has_been_disabled_by__username_": "Шифрование этой комнаты было отключено {{username}}", "Enabled_E2E_Encryption_for_this_room": "включено шифрование E2E для этой комнаты", + "disabled": "Отключено", "Disabled_E2E_Encryption_for_this_room": "отключено шифрование E2E для этой комнаты", "@username": "@логин", "@username_message": "@логин ", @@ -21,9 +23,12 @@ "2_Erros_Information_and_Debug": "2 - Ошибки, информация и отладка", "12_Hour": "12-часовой формат времени", "24_Hour": "24-часовой формат времени", + "A_cloud-based_platform_for_those_needing_a_plug-and-play_app": "Облачная платформа для тех, кому нужно приложение plug-and-play.", "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_workspace_admin_needs_to_install_and_configure_a_conference_call_app": "Администратору рабочего пространства необходимо установить и настроить приложение для конференц-связи.", + "An_app_needs_to_be_installed_and_configured": "Необходимо установить и настроить приложение.", "Accept_Call": "Принять вызов", "Accept": "Принять", "Accept_incoming_livechat_requests_even_if_there_are_no_online_agents": "Принимать входящие запросы с livechat, даже если нет подключенных сотрудников", @@ -32,6 +37,7 @@ "Access_not_authorized": "Неавторизованный доступ", "Access_Token_URL": "Access Token URL", "Access_Your_Account": "Доступ к вашей учетной записи", + "access_your_basic_information": "доступ к вашей основной информации", "access-mailer": "Доступ к странице мейлера", "access-mailer_description": "Разрешение рассылку email сообщений всем пользователям", "access-permissions": "Доступ к странице разрешений", @@ -174,7 +180,7 @@ "Accounts_OAuth_Nextcloud_URL": "URL сервера Nextcloud", "Accounts_OAuth_Proxy_host": "Прокси хост", "Accounts_OAuth_Proxy_services": "Прокси сервисы", - "Accounts_OAuth_Tokenpass": "Tokenpass Login", + "Accounts_OAuth_Tokenpass": "Tokenpass Логин", "Accounts_OAuth_Tokenpass_callback_url": "Tokenpass Callback URL", "Accounts_OAuth_Tokenpass_id": "Tokenpass Id", "Accounts_OAuth_Tokenpass_secret": "Tokenpass Secret", @@ -187,7 +193,7 @@ "Accounts_OAuth_Wordpress_callback_url": "WordPress Callback URL", "Accounts_OAuth_Wordpress_id": "Идентификатор WordPress", "Accounts_OAuth_Wordpress_identity_path": "Identity Path", - "Accounts_OAuth_Wordpress_identity_token_sent_via": "Identity Token Sent Via", + "Accounts_OAuth_Wordpress_identity_token_sent_via": "Токен идентификации, отправленный через", "Accounts_OAuth_Wordpress_scope": "Область", "Accounts_OAuth_Wordpress_secret": "WordPress Secret", "Accounts_OAuth_Wordpress_server_type_custom": "Пользовательские ", @@ -258,28 +264,33 @@ "Accounts_TwoFactorAuthentication_RememberFor_Description": "Не запрашивать двухфакторный код авторизации, если он уже был предоставлен ранее в данное время.", "Accounts_UseDefaultBlockedDomainsList": "Использовать список запрещённых доменов по умолчанию", "Accounts_UseDNSDomainCheck": "Использовать DNS проверку доменов", - "API_EmbedDisabledFor": "Запретить встраивание для пользователей", + "API_EmbedDisabledFor": "Отключить вставку для пользователей", "Accounts_UserAddedEmail_Default": "

Добро пожаловать в [Site_Name]

Посетите [Site_URL] и попробуйте лучшее решение для чатов с открытым исходным кодом на сегодняшний день!

Вы можете войти в систему, используя адрес электронной почты: [email] и пароль: [password]. Возможно, вам потребуется сменить его после первого входа в систему.", - "Accounts_UserAddedEmail_Description": "Вы можете использовать следующие подстановки:

  • [name], [fname], [lname] для полного имени пользователя, только имени или только фамилии соответственно).
  • [email] - для email адреса пользователя.
  • [password] - для пароля пользователя.
  • [Site_Name] и [Site_URL] - название вашего приложения и его URL.
", + "Accounts_UserAddedEmail_Description": "Вы можете использовать следующие обозначения:
  • [name], [fname], [lname] для полного имени пользователя, только имени или только фамилии соответственно).
  • [email] - для email адреса пользователя.
  • [password] - для пароля пользователя.
  • [Site_Name] и [Site_URL] - название вашего приложения и его URL.
", "API_EmbedDisabledFor_Description": "Список логинов, разделенных запятыми, для отключения предварительного просмотра ссылок.", "Accounts_UserAddedEmailSubject_Default": "Вы были добавлены в [Site_Name]", "Accounts_Verify_Email_For_External_Accounts": "Отметить электронную почту для внешних учетных записей проверенной", "Action": "Действие", "Action_required": "Требуется действие", + "Action_Available_After_Custom_Content_Added": "Это действие станет доступно после добавления пользовательского содержимого", + "Action_Available_After_Custom_Content_Added_And_Visible": "Это действие станет доступным после того, как пользовательское содержимое будет добавлено и станет видимым для всех", "Activate": "Активировать", "Active": "В сети", "Active_users": "Активные пользователи", "Activity": "Активность", "Add": "Добавить", + "Add_a_Message": "Добавить сообщение", "Add_agent": "Добавить представителя", "Add_custom_oauth": "Добавить собственный OAuth", "Add_Domain": "Добавить домен", + "Add_emoji": "Добавить эмодзи", "Add_files_from": "Добавить файлы из", "Add_manager": "Добавить менеджера", "Add_monitor": "Добавить монитор", "Add_Reaction": "Добавить реакцию", "Add_Role": "Добавить роль", "Add_Sender_To_ReplyTo": "Добавить отправителя в ответ", + "Add_Server": "Добавить Сервер", "Add_URL": "Добавить URL", "Add_user": "Добавить пользователя", "Add_User": "Добавить Пользователя", @@ -291,6 +302,8 @@ "add-livechat-department-agents_description": "Разрешение на добавление омниканальных агентов в отделы", "add-oauth-service": "Добавить сервис Oauth", "add-oauth-service_description": "Разрешение на добавление новых сервисов Oauth", + "bypass-time-limit-edit-and-delete": "Обход ограничения по времени", + "bypass-time-limit-edit-and-delete_description": "Разрешение на обход ограничения по времени для редактирования и удаления сообщений", "add-team-channel": "Добавить Channel Команды", "add-team-channel_description": "Разрешение на добавление канала в Команду", "add-team-member": "Добавить участника Команды", @@ -299,7 +312,7 @@ "add-user_description": "Разрешение на добавление новых пользователей на сервер на странице пользователей", "add-user-to-any-c-room": "Добавить пользователя к любому публичному каналу", "add-user-to-any-c-room_description": "Разрешение на добавление пользователя к любому публичному каналу", - "add-user-to-any-p-room": "Добавить пользователя к любому приватному каналу", + "add-user-to-any-p-room": "Добавить пользователя к любому приватному каналуChannel", "add-user-to-any-p-room_description": "Разрешение на добавление пользователя к любому приватному каналу", "add-user-to-joined-room": "Добавление пользователя к любому доступному каналу", "add-user-to-joined-room_description": "Разрешение на добавление пользователя к каналу, к которому имеет доступ текущий пользователь", @@ -397,6 +410,8 @@ "API_Allow_Infinite_Count_Description": "Могут ли вызовы к REST API возвращать всё за один вызов?", "API_Analytics": "Аналитика", "API_CORS_Origin": "Заголовок CORS Origin", + "API_Apply_permission_view-outside-room_on_users-list": "Примените разрешение `view-outside-room` к api `users.list`", + "API_Apply_permission_view-outside-room_on_users-list_Description": "Временная настройка для принудительного разрешения. Будет удалена в следующем крупном обновлении в рамках изменения для постоянного применения разрешения", "API_Default_Count": "Количество по-умолчанию", "API_Default_Count_Description": "Количество результатов REST API для использования по-умолчанию, если потребитель не указал его.", "API_Drupal_URL": "URL сервера Drupal", @@ -457,6 +472,7 @@ "App_Info": "Информация о приложении", "App_Information": "Информация о приложении", "App_Installation": "Установка приложения", + "App_not_enabled": "Приложение не включено", "App_not_found": "Приложение не найдено", "App_status_auto_enabled": "Включено", "App_status_constructed": "построенный", @@ -483,6 +499,14 @@ "Apps": "Приложения", "Apps_context_enterprise": "Организация", "Apps_context_installed": "Установлен", + "Apps_context_requested": "Запрошено", + "Apps_context_private": "Приватные приложения", + "Apps_Count_Enabled": "{{count}} приложение включено", + "Apps_Count_Enabled_plural": "{{count}} приложений(-я) включено", + "Private_Apps_Count_Enabled": "{{count}} приватное приложение включено", + "Private_Apps_Count_Enabled_plural": "{{count}} приватных приложений включено", + "Apps_Count_Enabled_tooltip": "В рабочих пространствах Community Edition можно использовать до {{number}} {{context}} приложений", + "Apps_disabled_when_Enterprise_trial_ended": "Приложения отключены по окончании пробной версии Enterprise", "Apps_Engine_Version": "Версия движка приложений", "Apps_Essential_Alert": "Это приложение необходимо для следующих событий:", "Apps_Essential_Disclaimer": "Перечисленные выше события будут прерваны, если это приложение будет отключено. Если вы хотите, чтобы Rocket.Chat работал без функциональности этого приложения, вам необходимо его удалить", @@ -495,7 +519,7 @@ "Apps_Game_Center": "Игровой центр", "Apps_Game_Center_Back": "Назад в игровой центр", "Apps_Game_Center_Invite_Friends": "Пригласить своих друзей присоединиться", - "Apps_Game_Center_Play_Game_Together": "@here Давай поиграем в {{name}} вметсе!", + "Apps_Game_Center_Play_Game_Together": "@here Давай поиграем в {{name}} вместе!", "Apps_Interface_IPostExternalComponentClosed": "Событие, происходящее после закрытия внешнего компонента", "Apps_Interface_IPostExternalComponentOpened": "Событие, происходящее после открытия внешнего компонента", "Apps_Interface_IPostMessageDeleted": "Событие, происходящее после удаления сообщения", @@ -3883,7 +3907,7 @@ "Save_Mobile_Bandwidth": "Включить режим экономии трафика для мобильных устройств", "Save_to_enable_this_action": "Сохраните, чтобы активировать это действие", "Save_To_Webdav": "Сохранить в WebDAV", - "Save_Your_Encryption_Password": "Сохраните Ваш пароль шифрования", + "Save_your_encryption_password": "Сохраните пароль для шифрования", "save-others-livechat-room-info": "Сохранить информацию о других комнатах Livechat", "save-others-livechat-room-info_description": "Разрешение сохранять информацию из других чатов livechat", "Saved": "Сохранено", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/sv.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/sv.i18n.json index 5709594340ed..10104181fa15 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/sv.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/sv.i18n.json @@ -4320,7 +4320,6 @@ "Save_Mobile_Bandwidth": "Spara bandbredd i mobilen", "Save_to_enable_this_action": "Spara för att aktivera denna åtgärd", "Save_To_Webdav": "Spara till WebDAV", - "Save_Your_Encryption_Password": "Spara krypteringslösenordet", "save-all-canned-responses": "Spara alla standardsvar", "save-all-canned-responses_description": "Behörighet att spara alla standardsvar", "save-canned-responses": "Spara standardsvar", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/tr.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/tr.i18n.json index 7b3fed3775f3..871990341c04 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/tr.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/tr.i18n.json @@ -2558,6 +2558,7 @@ "Save_Mobile_Bandwidth": "Mobil Kota Koruma Etkin", "Save_to_enable_this_action": "Bu eylemi etkinleştirmek için kaydet", "Save_To_Webdav": "WebDAV'a kaydet", + "Save_your_encryption_password": "Şifreleme parolanızı kaydedin", "save-others-livechat-room-info": "Diğerlerini Kaydedin Livechat Oda Bilgileri", "save-others-livechat-room-info_description": "Diğer livechat kanallarından bilgi kaydetme izni", "Saved": "Kaydedildi", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/uk.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/uk.i18n.json index cb365bd07a3f..39f92e06c57b 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/uk.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/uk.i18n.json @@ -1194,7 +1194,7 @@ "E2E_Encryption_Password_Change": "Змінити пароль шифрування", "E2E_Encryption_Password_Explanation": "Тепер ви можете створювати зашифровані приватні групи та особистіі повідомлення. Також Ви можете змінити існуючі приватні групи або DM-файли на зашифровані.

З цієї причини Вам потрібно зберігати цей пароль десь у безпеці. Вам потрібно буде ввести його на інших пристроях, на яких ви хочете використовувати шифрування e2e.", "E2E_password_request_text": "Щоб отримати доступ до своїх зашифрованих приватних груп та особистих повідомлень, введіть пароль шифрування.
Вам потрібно ввести цей пароль, щоб кодувати / декодувати Ваші повідомлення для кожного клієнта, який Ви використовуєте, оскільки ключ не зберігається на сервері.", - "E2E_password_reveal_text": "Тепер ви можете створювати зашифровані приватні групи та прямі повідомлення. Ви також можете змінити існуючі приватні групи або DM-файли на зашифровані.

З цієї причини вам потрібно зберігати цей пароль десь у безпеці. Вам потрібно буде ввести його на інших пристроях, на яких ви хочете використовувати шифрування e2e. Детальніше тут

Ваш пароль: % s

Це автоматичний згенерований пароль, Ви можете встановити новий пароль для шифрування введіть будь-який час у будь-якому веб-переглядачі, який ви ввели існуючий пароль.
Цей пароль зберігається в цьому веб-переглядачі, поки ви не збережете пароль і не відхилите це повідомлення.", + "E2E_password_reveal_text": "Тепер ви можете створювати зашифровані приватні групи та прямі повідомлення. Ви також можете змінити існуючі приватні групи або DM-файли на зашифровані.

З цієї причини вам потрібно зберігати цей пароль десь у безпеці. Вам потрібно буде ввести його на інших пристроях, на яких ви хочете використовувати шифрування e2e. Детальніше тут

Ваш пароль: %s

Це автоматичний згенерований пароль, Ви можете встановити новий пароль для шифрування введіть будь-який час у будь-якому веб-переглядачі, який ви ввели існуючий пароль.
Цей пароль зберігається в цьому веб-переглядачі, поки ви не збережете пароль і не відхилите це повідомлення.", "E2E_Reset_Key_Explanation": "Ця опція видалить ваш поточний ключ E2E і вийде з системи.
Коли ви знову ввійдете в систему, Rocket.Chat згенерує для вас новий ключ і відновить ваш доступ до усіх зашифрованих кімнати, в яких є хоча б один учасник у стані онлайн.
Rocket.Chat не зможе відновити доступ до кімнат, в яких немає онлайн жодного учасника, бо цього не дозволяє природа шифрування E2E.", "Edit": "Редагувати", "Edit_Business_Hour": "Редагування час роботи", @@ -3272,7 +3272,7 @@ "WebRTC_Enable_Channel": "Включити для загальнодоступних каналів", "WebRTC_Enable_Direct": "Включити для прямих повідомлень", "WebRTC_Enable_Private": "Включити для приватних каналів", - "WebRTC_group_audio_call_from_%s": "Груповий аудіо дзвінок від% s", + "WebRTC_group_audio_call_from_%s": "Груповий аудіо дзвінок від%s", "WebRTC_group_video_call_from_%s": "Груповий відео дзвінок від %s", "WebRTC_monitor_call_from_%s": "Відстежує виклик від %s", "WebRTC_Servers": "STUN / TURN Сервери", @@ -3360,4 +3360,4 @@ "registration.component.form.invalidConfirmPass": "Підтвердження пароля не збігаються пароль", "registration.component.form.confirmPassword": "Підтвердити новий пароль", "registration.component.form.sendConfirmationEmail": "Надіслати електронною поштою підтвердження" -} \ No newline at end of file +} diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/zh-TW.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/zh-TW.i18n.json index e20bdc4cdbbc..4116aed17bc6 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/zh-TW.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/zh-TW.i18n.json @@ -3598,6 +3598,7 @@ "Save_Mobile_Bandwidth": "節省手機流量", "Save_to_enable_this_action": "保存啟用此動作", "Save_To_Webdav": "儲存到 WebDAV", + "Save_your_encryption_password": "儲存您的加密密碼", "save-others-livechat-room-info": "保存其他即時聊天 Room 資訊", "save-others-livechat-room-info_description": "允許保存來自其他即時聊天頻道的訊息", "Saved": "保存", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/zh.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/zh.i18n.json index 201a42259608..9a0cc6680c6f 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/zh.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/zh.i18n.json @@ -3261,6 +3261,7 @@ "Save_Mobile_Bandwidth": "节约移动网络带宽", "Save_to_enable_this_action": "保存以启用该操作", "Save_To_Webdav": "保存到 WebDAV", + "Save_your_encryption_password": "保存您的加密密码", "save-others-livechat-room-info": "保存其他 Omnichannel 聊天室信息", "save-others-livechat-room-info_description": "保存其他 Omnichannel 聊天室信息的权限", "Saved": "已保存", diff --git a/apps/meteor/server/models/raw/Messages.ts b/apps/meteor/server/models/raw/Messages.ts index d6d838726b22..f1ee7590df51 100644 --- a/apps/meteor/server/models/raw/Messages.ts +++ b/apps/meteor/server/models/raw/Messages.ts @@ -1354,8 +1354,10 @@ export class MessagesRaw extends BaseRaw implements IMessagesModel { if (!limit) { const count = (await this.deleteMany(query)).deletedCount; - // decrease message count - await Rooms.decreaseMessageCountById(rid, count); + if (count) { + // decrease message count + await Rooms.decreaseMessageCountById(rid, count); + } return count; } @@ -1377,8 +1379,10 @@ export class MessagesRaw extends BaseRaw implements IMessagesModel { }) ).deletedCount; - // decrease message count - await Rooms.decreaseMessageCountById(rid, count); + if (count) { + // decrease message count + await Rooms.decreaseMessageCountById(rid, count); + } return count; } diff --git a/apps/meteor/server/models/raw/Users.js b/apps/meteor/server/models/raw/Users.js index 066fad73fc9d..2e31696eda52 100644 --- a/apps/meteor/server/models/raw/Users.js +++ b/apps/meteor/server/models/raw/Users.js @@ -2819,7 +2819,11 @@ export class UsersRaw extends BaseRaw { // here getActiveLocalUserCount() { - return this.col.countDocuments({ active: true, federated: false, isRemote: false }); + return Promise.all([ + this.col.countDocuments({ active: true }), + this.col.countDocuments({ federated: true }), + this.col.countDocuments({ isRemote: true }), + ]).then((results) => results.reduce((a, b) => a - b)); } getActiveLocalGuestCount(idExceptions = []) { diff --git a/apps/meteor/server/modules/notifications/notifications.module.ts b/apps/meteor/server/modules/notifications/notifications.module.ts index a9fd5fadd013..0b82792ee819 100644 --- a/apps/meteor/server/modules/notifications/notifications.module.ts +++ b/apps/meteor/server/modules/notifications/notifications.module.ts @@ -531,11 +531,8 @@ export class NotificationsModule { } sendPresence(uid: string, ...args: [username: string, statusChanged: 0 | 1 | 2 | 3, statusText: string | undefined]): void { - // if (this.debug === true) { - // console.log('notifyUserAndBroadcast', [userId, eventName, ...args]); - // } - emit(uid, args as any); - return this.streamPresence.emitWithoutBroadcast(uid, ...args); + emit(uid, [args]); + return this.streamPresence.emitWithoutBroadcast(uid, args); } progressUpdated(progress: { diff --git a/apps/meteor/tests/data/livechat/rooms.ts b/apps/meteor/tests/data/livechat/rooms.ts index 073fe711eecb..429246d0189a 100644 --- a/apps/meteor/tests/data/livechat/rooms.ts +++ b/apps/meteor/tests/data/livechat/rooms.ts @@ -137,13 +137,13 @@ export const createAgent = (overrideUsername?: string): Promise }); }); -export const createManager = (): Promise => +export const createManager = (overrideUsername?: string): Promise => new Promise((resolve, reject) => { request .post(api('livechat/users/manager')) .set(credentials) .send({ - username: adminUsername, + username: overrideUsername || adminUsername, }) .end((err: Error, res: DummyResponse) => { if (err) { diff --git a/apps/meteor/tests/data/livechat/visitor.ts b/apps/meteor/tests/data/livechat/visitor.ts new file mode 100644 index 000000000000..86c1043fb05d --- /dev/null +++ b/apps/meteor/tests/data/livechat/visitor.ts @@ -0,0 +1,9 @@ +import { ILivechatVisitor } from '@rocket.chat/core-typings'; +import { expect } from 'chai'; +import { api, credentials, request } from '../api-data'; + +export const getLivechatVisitorByToken = async (token: string): Promise => { + const response = await request.get(api(`livechat/visitor/${token}`)).set(credentials).expect(200); + expect(response.body).to.have.property('visitor'); + return response.body.visitor; +} diff --git a/apps/meteor/tests/data/permissions.helper.ts b/apps/meteor/tests/data/permissions.helper.ts index d57bb4112718..263ae919f9cb 100644 --- a/apps/meteor/tests/data/permissions.helper.ts +++ b/apps/meteor/tests/data/permissions.helper.ts @@ -1,6 +1,7 @@ import type { ISetting } from '@rocket.chat/core-typings'; import { IS_EE } from '../e2e/config/constants'; import { api, credentials, request } from './api-data'; +import { permissions } from '../../app/authorization/server/constant/permissions'; export const updatePermission = (permission:string, roles:string[]):Promise => new Promise((resolve,reject) => { @@ -63,3 +64,30 @@ export const removePermissions = async (perms: string[]) => { export const addPermissions = async (perms: { [key: string]: string[] }) => { await updateManyPermissions(perms); }; + +type Permission = typeof permissions[number]['_id'] + +export const removePermissionFromAllRoles = async (permission: Permission) => { + await updatePermission(permission, []); +}; + +export const restorePermissionToRoles = async (permission: Permission) => { + const defaultPermission = permissions.find((p) => p._id === permission); + if (!defaultPermission) { + throw new Error(`No default roles found for permission ${permission}`); + } + + const mutableDefaultRoles: string[] = defaultPermission.roles.map((r) => r); + + if (!IS_EE) { + const eeOnlyRoles = ['livechat-monitor']; + eeOnlyRoles.forEach((role) => { + const index = mutableDefaultRoles.indexOf(role); + if (index !== -1) { + mutableDefaultRoles.splice(index, 1); + } + }); + } + + await updatePermission(permission, mutableDefaultRoles); +} diff --git a/apps/meteor/tests/e2e/administration-menu.spec.ts b/apps/meteor/tests/e2e/administration-menu.spec.ts index 3e1e8b493583..755862a3ef80 100644 --- a/apps/meteor/tests/e2e/administration-menu.spec.ts +++ b/apps/meteor/tests/e2e/administration-menu.spec.ts @@ -22,6 +22,7 @@ test.describe.serial('administration-menu', () => { }); test('expect open info page', async ({ page }) => { + test.skip(!IS_EE, 'Enterprise only'); await poHomeDiscussion.sidenav.openAdministrationByLabel('Workspace'); await expect(page).toHaveURL('admin/info'); diff --git a/apps/meteor/tests/e2e/apps.spec.ts b/apps/meteor/tests/e2e/apps.spec.ts index b392f736d879..bc41cf7aed06 100644 --- a/apps/meteor/tests/e2e/apps.spec.ts +++ b/apps/meteor/tests/e2e/apps.spec.ts @@ -16,12 +16,12 @@ test.describe.serial('Apps', () => { test('expect allow user open app contextualbar', async () => { await poHomeChannel.content.dispatchSlashCommand('/contextualbar'); - await expect(poHomeChannel.btnVerticalBarClose).toBeVisible(); + await expect(poHomeChannel.btnContextualbarClose).toBeVisible(); }); test('expect app contextualbar to be closed', async () => { await poHomeChannel.content.dispatchSlashCommand('/contextualbar'); - await poHomeChannel.btnVerticalBarClose.click(); - await expect(poHomeChannel.btnVerticalBarClose).toBeHidden(); + await poHomeChannel.btnContextualbarClose.click(); + await expect(poHomeChannel.btnContextualbarClose).toBeHidden(); }); }); diff --git a/apps/meteor/tests/e2e/federation/page-objects/account-profile.ts b/apps/meteor/tests/e2e/federation/page-objects/account-profile.ts index 2b422757fb35..229e970b0a10 100644 --- a/apps/meteor/tests/e2e/federation/page-objects/account-profile.ts +++ b/apps/meteor/tests/e2e/federation/page-objects/account-profile.ts @@ -49,7 +49,7 @@ export class FederationAccountProfile { } get tokensTableEmpty(): Locator { - return this.page.locator('//div[contains(text(), "No results found")]'); + return this.page.locator('//h3[contains(text(), "No results found")]'); } get btnTokensAdd(): Locator { diff --git a/apps/meteor/tests/e2e/federation/page-objects/channel.ts b/apps/meteor/tests/e2e/federation/page-objects/channel.ts index c05ddf06f043..50ac5f04fc27 100644 --- a/apps/meteor/tests/e2e/federation/page-objects/channel.ts +++ b/apps/meteor/tests/e2e/federation/page-objects/channel.ts @@ -28,8 +28,8 @@ export class FederationChannel { return this.page.locator('.rcx-toastbar.rcx-toastbar--error'); } - get btnVerticalBarClose(): Locator { - return this.page.locator('[data-qa="VerticalBarActionClose"]'); + get btnContextualbarClose(): Locator { + return this.page.locator('[data-qa="ContextualbarActionClose"]'); } async getFederationServerName(): Promise { diff --git a/apps/meteor/tests/e2e/omnichannel-contact-center.spec.ts b/apps/meteor/tests/e2e/omnichannel-contact-center.spec.ts index 143806c4256a..0037fbfd720d 100644 --- a/apps/meteor/tests/e2e/omnichannel-contact-center.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel-contact-center.spec.ts @@ -102,22 +102,26 @@ test.describe('Omnichannel Contact Center', () => { await expect(poContacts.newContact.errorMessage(ERROR.invalidEmail)).toBeVisible(); }); - await test.step('validate existing email', async () => { + await test.step('input existing email', async () => { await poContacts.newContact.inputEmail.selectText(); await poContacts.newContact.inputEmail.type(EXISTING_CONTACT.email); - await expect(poContacts.newContact.errorMessage(ERROR.existingEmail)).toBeVisible(); - await expect(poContacts.newContact.btnSave).toBeDisabled(); - }); - - await test.step('input email', async () => { - await poContacts.newContact.inputEmail.selectText(); - await poContacts.newContact.inputEmail.type(NEW_CONTACT.email); await expect(poContacts.newContact.errorMessage(ERROR.invalidEmail)).not.toBeVisible(); await expect(poContacts.newContact.errorMessage(ERROR.existingEmail)).not.toBeVisible(); }); - await test.step('validate existing phone ', async () => { + await test.step('input existing phone ', async () => { + await poContacts.newContact.inputPhone.selectText(); await poContacts.newContact.inputPhone.type(EXISTING_CONTACT.phone); + await expect(poContacts.newContact.errorMessage(ERROR.existingPhone)).not.toBeVisible(); + }); + + await test.step('run async validations ', async () => { + await expect(poContacts.newContact.btnSave).toBeEnabled(); + await poContacts.newContact.btnSave.click(); + + await expect(poContacts.newContact.errorMessage(ERROR.existingEmail)).toBeVisible(); + await expect(poContacts.newContact.btnSave).toBeDisabled(); + await expect(poContacts.newContact.errorMessage(ERROR.existingPhone)).toBeVisible(); await expect(poContacts.newContact.btnSave).toBeDisabled(); }); @@ -128,6 +132,13 @@ test.describe('Omnichannel Contact Center', () => { await expect(poContacts.newContact.errorMessage(ERROR.existingPhone)).not.toBeVisible(); }); + await test.step('input email', async () => { + await poContacts.newContact.inputEmail.selectText(); + await poContacts.newContact.inputEmail.type(NEW_CONTACT.email); + await expect(poContacts.newContact.errorMessage(ERROR.invalidEmail)).not.toBeVisible(); + await expect(poContacts.newContact.errorMessage(ERROR.existingEmail)).not.toBeVisible(); + }); + await test.step('save new contact ', async () => { await expect(poContacts.newContact.btnSave).toBeEnabled(); await poContacts.newContact.btnSave.click(); @@ -172,31 +183,17 @@ test.describe('Omnichannel Contact Center', () => { await expect(poContacts.contactInfo.errorMessage(ERROR.invalidEmail)).toBeVisible(); }); - await test.step('validate existing email', async () => { + await test.step('input existing email', async () => { await poContacts.contactInfo.inputEmail.selectText(); await poContacts.contactInfo.inputEmail.type(EXISTING_CONTACT.email); - await expect(poContacts.contactInfo.errorMessage(ERROR.existingEmail)).toBeVisible(); - await expect(poContacts.contactInfo.btnSave).toBeDisabled(); - }); - - await test.step('input email', async () => { - await poContacts.contactInfo.inputEmail.selectText(); - await poContacts.contactInfo.inputEmail.type(EDIT_CONTACT.email); await expect(poContacts.contactInfo.errorMessage(ERROR.invalidEmail)).not.toBeVisible(); await expect(poContacts.contactInfo.errorMessage(ERROR.existingEmail)).not.toBeVisible(); await expect(poContacts.contactInfo.btnSave).toBeEnabled(); }); - await test.step('validate existing phone ', async () => { + await test.step('input existing phone ', async () => { await poContacts.contactInfo.inputPhone.selectText(); await poContacts.contactInfo.inputPhone.type(EXISTING_CONTACT.phone); - await expect(poContacts.contactInfo.errorMessage(ERROR.existingPhone)).toBeVisible(); - await expect(poContacts.contactInfo.btnSave).toBeDisabled(); - }); - - await test.step('input phone ', async () => { - await poContacts.contactInfo.inputPhone.selectText(); - await poContacts.contactInfo.inputPhone.type(EDIT_CONTACT.phone); await expect(poContacts.contactInfo.errorMessage(ERROR.existingPhone)).not.toBeVisible(); await expect(poContacts.contactInfo.btnSave).toBeEnabled(); }); @@ -204,10 +201,9 @@ test.describe('Omnichannel Contact Center', () => { await test.step('validate name is required', async () => { await poContacts.contactInfo.inputName.selectText(); await poContacts.contactInfo.inputName.type(' '); - - await expect(poContacts.contactInfo.btnSave).toBeEnabled(); - await poContacts.contactInfo.btnSave.click(); await expect(poContacts.contactInfo.errorMessage(ERROR.nameRequired)).toBeVisible(); + + await expect(poContacts.contactInfo.btnSave).not.toBeEnabled(); }); await test.step('edit name', async () => { @@ -215,6 +211,30 @@ test.describe('Omnichannel Contact Center', () => { await poContacts.contactInfo.inputName.type(EDIT_CONTACT.name); }); + await test.step('run async validations ', async () => { + await expect(poContacts.newContact.btnSave).toBeEnabled(); + await poContacts.newContact.btnSave.click(); + + await expect(poContacts.newContact.errorMessage(ERROR.existingEmail)).toBeVisible(); + await expect(poContacts.newContact.btnSave).toBeDisabled(); + + await expect(poContacts.newContact.errorMessage(ERROR.existingPhone)).toBeVisible(); + await expect(poContacts.newContact.btnSave).toBeDisabled(); + }); + + await test.step('input phone ', async () => { + await poContacts.newContact.inputPhone.selectText(); + await poContacts.newContact.inputPhone.type(EDIT_CONTACT.phone); + await expect(poContacts.newContact.errorMessage(ERROR.existingPhone)).not.toBeVisible(); + }); + + await test.step('input email', async () => { + await poContacts.newContact.inputEmail.selectText(); + await poContacts.newContact.inputEmail.type(EDIT_CONTACT.email); + await expect(poContacts.newContact.errorMessage(ERROR.invalidEmail)).not.toBeVisible(); + await expect(poContacts.newContact.errorMessage(ERROR.existingEmail)).not.toBeVisible(); + }); + await test.step('save new contact ', async () => { await poContacts.contactInfo.btnSave.click(); await expect(poContacts.toastSuccess).toBeVisible(); diff --git a/apps/meteor/tests/e2e/omnichannel-departaments.spec.ts b/apps/meteor/tests/e2e/omnichannel-departaments.spec.ts index 9286082863f5..2623d2e51c7f 100644 --- a/apps/meteor/tests/e2e/omnichannel-departaments.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel-departaments.spec.ts @@ -6,6 +6,12 @@ import { Users } from './fixtures/userStates'; import { OmnichannelDepartments } from './page-objects'; import { test, expect } from './utils/test'; +const ERROR = { + requiredName: 'The field name is required.', + requiredEmail: 'The field email is required.', + invalidEmail: 'Invalid email address', +}; + test.use({ storageState: Users.admin.state }); test.describe.serial('omnichannel-departments', () => { @@ -35,8 +41,32 @@ test.describe.serial('omnichannel-departments', () => { await expect(poOmnichannelDepartments.btnEnabled).not.toBeVisible(); await page.goBack(); }); - await test.step('expect create new department', async () => { + + await test.step('expect name and email to be required', async () => { await poOmnichannelDepartments.btnNew.click(); + await expect(poOmnichannelDepartments.invalidInputEmail).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 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 create new department', async () => { await poOmnichannelDepartments.btnEnabled.click(); await poOmnichannelDepartments.inputName.fill(departmentName); await poOmnichannelDepartments.inputEmail.fill(faker.internet.email()); @@ -154,6 +184,8 @@ test.describe.serial('omnichannel-departments', () => { }); await test.step('Enabled tags state', async () => { + const tagName = faker.datatype.string(5); + await poOmnichannelDepartments.inputSearch.fill(tagsDepartmentName); await poOmnichannelDepartments.firstRowInTableMenu.click(); await poOmnichannelDepartments.menuEditOption.click(); @@ -167,29 +199,26 @@ test.describe.serial('omnichannel-departments', () => { await expect(poOmnichannelDepartments.inputTags).toBeVisible(); await expect(poOmnichannelDepartments.btnTagsAdd).toBeVisible(); }); - await test.step('expect to be invalid if there is no tag added', async () => { - await expect(poOmnichannelDepartments.btnSave).toBeDisabled(); - await expect(poOmnichannelDepartments.invalidInputTags).toBeVisible(); - }); - - await test.step('expect to be not possible adding empty tags', async () => { - await poOmnichannelDepartments.inputTags.fill(''); - await expect(poOmnichannelDepartments.btnTagsAdd).toBeDisabled(); - }); await test.step('expect to have add and remove one tag properly tags', async () => { - const tagName = faker.datatype.string(5); await poOmnichannelDepartments.inputTags.fill(tagName); await poOmnichannelDepartments.btnTagsAdd.click(); await expect(poOmnichannelDepartments.btnTag(tagName)).toBeVisible(); - await expect(poOmnichannelDepartments.btnSave).toBeEnabled(); + }); + await test.step('expect to be invalid if there is no tag added', async () => { await poOmnichannelDepartments.btnTag(tagName).click(); await expect(poOmnichannelDepartments.invalidInputTags).toBeVisible(); await expect(poOmnichannelDepartments.btnSave).toBeDisabled(); }); + + await test.step('expect to be not possible adding empty tags', async () => { + await poOmnichannelDepartments.inputTags.fill(''); + await expect(poOmnichannelDepartments.btnTagsAdd).toBeDisabled(); + }); + await test.step('expect to not be possible adding same tag twice', async () => { const tagName = faker.datatype.string(5); await poOmnichannelDepartments.inputTags.fill(tagName); diff --git a/apps/meteor/tests/e2e/page-objects/account-profile.ts b/apps/meteor/tests/e2e/page-objects/account-profile.ts index 180ba560dcd6..55297ad1fd16 100644 --- a/apps/meteor/tests/e2e/page-objects/account-profile.ts +++ b/apps/meteor/tests/e2e/page-objects/account-profile.ts @@ -49,7 +49,7 @@ export class AccountProfile { } get tokensTableEmpty(): Locator { - return this.page.locator('//div[contains(text(), "No results found")]'); + return this.page.locator('//h3[contains(text(), "No results found")]'); } get btnTokensAdd(): Locator { diff --git a/apps/meteor/tests/e2e/page-objects/home-channel.ts b/apps/meteor/tests/e2e/page-objects/home-channel.ts index b632df31ddeb..1d6e450d9764 100644 --- a/apps/meteor/tests/e2e/page-objects/home-channel.ts +++ b/apps/meteor/tests/e2e/page-objects/home-channel.ts @@ -22,7 +22,7 @@ export class HomeChannel { return this.page.locator('.rcx-toastbar.rcx-toastbar--success'); } - get btnVerticalBarClose(): Locator { - return this.page.locator('[data-qa="VerticalBarActionClose"]'); + get btnContextualbarClose(): Locator { + return this.page.locator('[data-qa="ContextualbarActionClose"]'); } } diff --git a/apps/meteor/tests/e2e/page-objects/home-omnichannel.ts b/apps/meteor/tests/e2e/page-objects/home-omnichannel.ts index 456c1700c782..ce7f33244570 100644 --- a/apps/meteor/tests/e2e/page-objects/home-omnichannel.ts +++ b/apps/meteor/tests/e2e/page-objects/home-omnichannel.ts @@ -33,8 +33,8 @@ export class HomeOmnichannel { return this.page.locator('.rcx-toastbar.rcx-toastbar--success'); } - get btnVerticalBarClose(): Locator { - return this.page.locator('[data-qa="VerticalBarActionClose"]'); + get btnContextualbarClose(): Locator { + return this.page.locator('[data-qa="ContextualbarActionClose"]'); } get btnCurrentChats(): Locator { diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-departments.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-departments.ts index f4c20bd078e8..59611bc77f89 100644 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-departments.ts +++ b/apps/meteor/tests/e2e/page-objects/omnichannel-departments.ts @@ -44,6 +44,14 @@ export class OmnichannelDepartments { return this.page.locator('[data-qa="DepartmentEditTextInput-ConversationClosingTags"]:invalid'); } + 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"]'); } @@ -131,4 +139,8 @@ export class OmnichannelDepartments { btnTag(tagName: string) { return this.page.locator('button', { hasText: tagName }); } + + errorMessage(message: string): Locator { + return this.page.locator(`.rcx-field__error >> text="${message}"`); + } } diff --git a/apps/meteor/tests/end-to-end/api/24-methods.js b/apps/meteor/tests/end-to-end/api/24-methods.js index a1c5c2e1e21b..8ec3b2754d65 100644 --- a/apps/meteor/tests/end-to-end/api/24-methods.js +++ b/apps/meteor/tests/end-to-end/api/24-methods.js @@ -282,6 +282,127 @@ describe('Meteor.methods', function () { }); }); + describe('[@cleanRoomHistory]', () => { + let rid = false; + + let channelName = false; + + before('create room', (done) => { + channelName = `methods-test-channel-${Date.now()}`; + request + .post(api('groups.create')) + .set(credentials) + .send({ + name: channelName, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.nested.property('group._id'); + expect(res.body).to.have.nested.property('group.name', channelName); + expect(res.body).to.have.nested.property('group.t', 'p'); + expect(res.body).to.have.nested.property('group.msgs', 0); + rid = res.body.group._id; + }) + .end(done); + }); + + before('send sample message', (done) => { + request + .post(api('chat.sendMessage')) + .set(credentials) + .send({ + message: { + text: 'Sample message', + rid, + }, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + }) + .end(done); + }); + + before('send another sample message', (done) => { + request + .post(api('chat.sendMessage')) + .set(credentials) + .send({ + message: { + text: 'Second Sample message', + rid, + }, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + }) + .end(done); + }); + + it('should not change the _updatedAt value when nothing is changed on the room', async () => { + const roomBefore = await request.get(api('groups.info')).set(credentials).query({ + roomId: rid, + }); + + await request + .post(api('rooms.cleanHistory')) + .set(credentials) + .send({ + roomId: rid, + latest: '2016-12-09T13:42:25.304Z', + oldest: '2016-08-30T13:42:25.304Z', + excludePinned: false, + filesOnly: false, + ignoreThreads: false, + ignoreDiscussion: false, + }) + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('success', true); + expect(res.body).to.have.a.property('count', 0); + }); + + const roomAfter = await request.get(api('groups.info')).set(credentials).query({ + roomId: rid, + }); + expect(roomBefore.body.group._updatedAt).to.be.equal(roomAfter.body.group._updatedAt); + }); + + it('should change the _updatedAt value when room is cleaned', async () => { + const roomBefore = await request.get(api('groups.info')).set(credentials).query({ + roomId: rid, + }); + + await request + .post(api('rooms.cleanHistory')) + .set(credentials) + .send({ + roomId: rid, + latest: '9999-12-31T23:59:59.000Z', + oldest: '0001-01-01T00:00:00.000Z', + excludePinned: false, + filesOnly: false, + ignoreThreads: false, + ignoreDiscussion: false, + }) + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('success', true); + expect(res.body).to.have.a.property('count', 2); + }); + + const roomAfter = await request.get(api('groups.info')).set(credentials).query({ + roomId: rid, + }); + expect(roomBefore.body.group._updatedAt).to.not.be.equal(roomAfter.body.group._updatedAt); + }); + }); + describe('[@loadHistory]', () => { let rid = false; let postMessageDate = false; diff --git a/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts b/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts index 59283d0b9788..87a2a3a008d5 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts @@ -16,7 +16,7 @@ import { LivechatPriorityWeight } from '@rocket.chat/core-typings'; import type { Response } from 'supertest'; import faker from '@faker-js/faker'; -import { getCredentials, api, request, credentials } from '../../../data/api-data'; +import { getCredentials, api, request, credentials, methodCall } from '../../../data/api-data'; import { createVisitor, createLivechatRoom, @@ -25,9 +25,17 @@ import { getLivechatRoomInfo, sendMessage, startANewLivechatRoomAndTakeIt, + createManager, closeOmnichannelRoom, } from '../../../data/livechat/rooms'; -import { addPermissions, updateEEPermission, updatePermission, updateSetting } from '../../../data/permissions.helper'; +import { + restorePermissionToRoles, + addPermissions, + removePermissionFromAllRoles, + updateEEPermission, + updatePermission, + updateSetting, +} from '../../../data/permissions.helper'; import { createUser, login } from '../../../data/users.helper.js'; import { adminUsername, password } from '../../../data/user'; import { createDepartmentWithAnOnlineAgent } from '../../../data/livechat/department'; @@ -119,7 +127,7 @@ describe('LIVECHAT - rooms', function () { describe('livechat/rooms', () => { it('should return an "unauthorized error" when the user does not have the necessary permission', async () => { - await updatePermission('view-livechat-rooms', []); + await removePermissionFromAllRoles('view-livechat-rooms'); await request .get(api('livechat/rooms')) .set(credentials) @@ -129,9 +137,10 @@ describe('LIVECHAT - rooms', function () { expect(res.body).to.have.property('success', false); expect(res.body.error).to.be.equal('unauthorized'); }); + + await restorePermissionToRoles('view-livechat-rooms'); }); it('should return an error when the "agents" query parameter is not valid', async () => { - await updatePermission('view-livechat-rooms', ['admin']); await request .get(api('livechat/rooms?agents=invalid')) .set(credentials) @@ -428,11 +437,12 @@ describe('LIVECHAT - rooms', function () { describe('livechat/room.join', () => { it('should fail if user doesnt have view-l-room permission', async () => { - await updatePermission('view-l-room', []); + await removePermissionFromAllRoles('view-l-room'); await request.get(api('livechat/room.join')).set(credentials).query({ roomId: '123' }).send().expect(403); + + await restorePermissionToRoles('view-l-room'); }); it('should fail if no roomId is present on query params', async () => { - await updatePermission('view-l-room', ['admin', 'livechat-agent']); await request.get(api('livechat/room.join')).set(credentials).expect(400); }); it('should fail if room is present but invalid', async () => { @@ -448,11 +458,13 @@ describe('LIVECHAT - rooms', function () { describe('livechat/room.join', () => { it('should fail if user doesnt have view-l-room permission', async () => { - await updatePermission('view-l-room', []); + await removePermissionFromAllRoles('view-l-room'); + await request.get(api('livechat/room.join')).set(credentials).query({ roomId: '123' }).send().expect(403); + + await restorePermissionToRoles('view-l-room'); }); it('should fail if no roomId is present on query params', async () => { - await updatePermission('view-l-room', ['admin', 'livechat-agent']); await request.get(api('livechat/room.join')).set(credentials).expect(400); }); it('should fail if room is present but invalid', async () => { @@ -464,6 +476,23 @@ describe('LIVECHAT - rooms', function () { await request.get(api('livechat/room.join')).set(credentials).query({ roomId: room._id }).send().expect(200); }); + it('should allow managers to join a room which is already being served by an agent', async () => { + await updateSetting('Livechat_Routing_Method', 'Manual_Selection'); + // delay for 1 second to make sure the routing queue gets stopped + await sleep(1000); + + const { + room: { _id: roomId }, + } = await startANewLivechatRoomAndTakeIt(); + + const manager: IUser = await createUser(); + const managerCredentials = await login(manager.username, password); + await createManager(manager.username); + + await request.get(api('livechat/room.join')).set(managerCredentials).query({ roomId }).send().expect(200); + + await updateSetting('Livechat_Routing_Method', 'Auto_Selection'); + }); }); describe('livechat/room.close', () => { @@ -572,7 +601,7 @@ describe('LIVECHAT - rooms', function () { describe('livechat/room.forward', () => { it('should return an "unauthorized error" when the user does not have "view-l-room" permission', async () => { await updatePermission('transfer-livechat-guest', ['admin']); - await updatePermission('view-l-room', []); + await removePermissionFromAllRoles('view-l-room'); await request .post(api('livechat/room.forward')) @@ -589,7 +618,7 @@ describe('LIVECHAT - rooms', function () { }); it('should return an "unauthorized error" when the user does not have "transfer-livechat-guest" permission', async () => { - await updatePermission('transfer-livechat-guest', []); + await removePermissionFromAllRoles('transfer-livechat-guest'); await updatePermission('view-l-room', ['admin']); await request @@ -604,12 +633,12 @@ describe('LIVECHAT - rooms', function () { expect(res.body).to.have.property('success', false); expect(res.body.error).to.have.string('unauthorized'); }); + + await restorePermissionToRoles('transfer-livechat-guest'); + await restorePermissionToRoles('view-l-room'); }); it('should not be successful when no target (userId or departmentId) was specified', async () => { - await updatePermission('transfer-livechat-guest', ['admin']); - await updatePermission('view-l-room', ['admin', 'livechat-manager', 'livechat-agent']); - await request .post(api('livechat/room.forward')) .set(credentials) @@ -832,12 +861,13 @@ describe('LIVECHAT - rooms', function () { await request.get(api('livechat/test/messages')).set(credentials).expect('Content-Type', 'application/json').expect(400); }); it('should throw an error if user doesnt have permission view-l-room', async () => { - await updatePermission('view-l-room', []); + await removePermissionFromAllRoles('view-l-room'); await request.get(api('livechat/test/messages')).set(credentials).expect('Content-Type', 'application/json').expect(403); + + await restorePermissionToRoles('view-l-room'); }); it('should return the messages of the room', async () => { - await updatePermission('view-l-room', ['admin']); const visitor = await createVisitor(); const room = await createLivechatRoom(visitor.token); await sendMessage(room._id, 'Hello', visitor.token); @@ -1246,16 +1276,17 @@ describe('LIVECHAT - rooms', function () { describe('livechat/transfer.history/:rid', () => { it('should fail if user doesnt have "view-livechat-rooms" permission', async () => { - await updatePermission('view-livechat-rooms', []); + await removePermissionFromAllRoles('view-livechat-rooms'); const { body } = await request .get(api(`livechat/transfer.history/test`)) .set(credentials) .expect('Content-Type', 'application/json') .expect(403); expect(body).to.have.property('success', false); + + await restorePermissionToRoles('view-livechat-rooms'); }); it('should fail if room is not a valid room id', async () => { - await updatePermission('view-livechat-rooms', ['admin', 'livechat-manager']); const { body } = await request .get(api(`livechat/transfer.history/test`)) .set(credentials) @@ -1569,6 +1600,75 @@ describe('LIVECHAT - rooms', function () { .expect('Content-Type', 'application/json') .expect(400); }); + (IS_EE ? it : it.skip)('should throw an error if a valid custom field fails the check', async () => { + await request + .post(methodCall('livechat:saveCustomField')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'livechat:saveCustomField', + params: [ + null, + { + field: 'intfield', + label: 'intfield', + scope: 'room', + visibility: 'visible', + regexp: '\\d+', + searchable: true, + type: 'input', + required: false, + defaultValue: '0', + options: '', + public: false, + }, + ], + id: 'id', + msg: 'method', + }), + }) + .expect(200); + const newVisitor = await createVisitor(); + const newRoom = await createLivechatRoom(newVisitor.token); + + const response = await request + .post(api('livechat/room.saveInfo')) + .set(credentials) + .send({ + roomData: { + _id: newRoom._id, + livechatData: { intfield: 'asdasd' }, + }, + guestData: { + _id: newVisitor._id, + }, + }) + .expect('Content-Type', 'application/json') + .expect(400); + expect(response.body).to.have.property('success', false); + expect(response.body).to.have.property('error', 'Invalid value for intfield field'); + }); + (IS_EE ? it : it.skip)('should not throw an error if a valid custom field passes the check', async () => { + const newVisitor = await createVisitor(); + const newRoom = await createLivechatRoom(newVisitor.token); + + const response2 = await request + .post(api('livechat/room.saveInfo')) + .set(credentials) + .send({ + roomData: { + _id: newRoom._id, + livechatData: { intfield: '1' }, + }, + guestData: { + _id: newVisitor._id, + }, + }) + .expect('Content-Type', 'application/json') + .expect(200); + expect(response2.body).to.have.property('success', true); + }); + (IS_EE ? it : it.skip)('should update room priority', async () => { await addPermissions({ 'save-others-livechat-room-info': ['admin', 'livechat-manager'], diff --git a/apps/meteor/tests/end-to-end/api/livechat/06-integrations.ts b/apps/meteor/tests/end-to-end/api/livechat/06-integrations.ts index f5a88fd69ca2..8a90822406da 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/06-integrations.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/06-integrations.ts @@ -123,6 +123,7 @@ describe('LIVECHAT - Integrations', function () { describe('Livechat - Webhooks', () => { const webhookUrl = process.env.WEBHOOK_TEST_URL || 'https://httpbin.org'; + describe('livechat/webhook.test', () => { it('should fail when user doesnt have view-livechat-webhooks permission', async () => { await updatePermission('view-livechat-webhooks', []); diff --git a/apps/meteor/tests/end-to-end/api/livechat/09-visitors.ts b/apps/meteor/tests/end-to-end/api/livechat/09-visitors.ts index de8cdae6723b..ba0cd8b083d2 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/09-visitors.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/09-visitors.ts @@ -3,9 +3,10 @@ import { expect } from 'chai'; import type { ILivechatVisitor } from '@rocket.chat/core-typings'; import type { Response } from 'supertest'; +import faker from '@faker-js/faker'; import { getCredentials, api, request, credentials } from '../../../data/api-data'; -import { updatePermission, updateSetting } from '../../../data/permissions.helper'; +import { updatePermission, updateSetting, removePermissionFromAllRoles, restorePermissionToRoles } from '../../../data/permissions.helper'; import { makeAgentAvailable, createAgent, @@ -14,6 +15,10 @@ import { startANewLivechatRoomAndTakeIt, } from '../../../data/livechat/rooms'; import { createCustomField, deleteCustomField } from '../../../data/livechat/custom-fields'; +import { getRandomVisitorToken } from '../../../data/livechat/users'; +import { getLivechatVisitorByToken } from '../../../data/livechat/visitor'; +import { adminUsername } from '../../../data/user'; +import { IS_EE } from '../../../e2e/config/constants'; describe('LIVECHAT - visitors', function () { this.retries(0); @@ -783,4 +788,125 @@ describe('LIVECHAT - visitors', function () { expect(res.body.visitors[0]).to.have.property('visitorEmails'); }); }); + describe('omnichannel/contact', () => { + let contact: ILivechatVisitor; + it('should fail if user doesnt have view-l-room permission', async () => { + await removePermissionFromAllRoles('view-l-room'); + const res = await request.get(api(`omnichannel/contact?text=nel`)).set(credentials).send(); + expect(res.body).to.have.property('success', false); + + await restorePermissionToRoles('view-l-room'); + }); + it('should create a new contact', async () => { + const token = getRandomVisitorToken(); + const res = await request.post(api('omnichannel/contact')).set(credentials).send({ + name: faker.name.findName(), + token, + }); + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('contact'); + expect(res.body.contact).to.be.an('string'); + const contactId: string = res.body.contact; + + contact = await getLivechatVisitorByToken(token); + expect(contact._id).to.equal(contactId); + }); + it('should update an existing contact', async () => { + const name = faker.name.findName(); + const res = await request.post(api('omnichannel/contact')).set(credentials).send({ + name, + token: contact.token, + }); + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('contact'); + expect(res.body.contact).to.be.an('string'); + expect(res.body.contact).to.equal(contact._id); + + contact = await getLivechatVisitorByToken(contact.token); + expect(contact.name).to.equal(name); + }); + it('should change the contact name, email and phone', async () => { + const name = faker.name.findName(); + const email = faker.internet.email().toLowerCase(); + const phone = faker.phone.phoneNumber(); + const res = await request.post(api('omnichannel/contact')).set(credentials).send({ + name, + email, + phone, + token: contact.token, + }); + + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('contact'); + expect(res.body.contact).to.be.an('string'); + expect(res.body.contact).to.equal(contact._id); + + contact = await getLivechatVisitorByToken(contact.token); + expect(contact.name).to.equal(name); + expect(contact.visitorEmails).to.be.an('array'); + expect(contact.visitorEmails).to.have.lengthOf(1); + if (contact.visitorEmails?.[0]) { + expect(contact.visitorEmails[0].address).to.equal(email); + } + expect(contact.phone).to.be.an('array'); + expect(contact.phone).to.have.lengthOf(1); + if (contact.phone?.[0]) { + expect(contact.phone[0].phoneNumber).to.equal(phone); + } + }); + (IS_EE ? it : it.skip)('should change the contact manager', async () => { + const managerUsername = adminUsername; + + const res = await request + .post(api('omnichannel/contact')) + .set(credentials) + .send({ + contactManager: { + username: managerUsername, + }, + token: contact.token, + name: contact.name, + }); + + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('contact'); + expect(res.body.contact).to.be.an('string'); + expect(res.body.contact).to.equal(contact._id); + + contact = await getLivechatVisitorByToken(contact.token); + expect(contact.contactManager).to.be.an('object'); + expect(contact.contactManager).to.have.property('username', managerUsername); + }); + it('should change custom fields', async () => { + const cfName = faker.lorem.word(); + await createCustomField({ + searchable: true, + field: cfName, + label: cfName, + scope: 'visitor', + visibility: 'visible', + regexp: '', + }); + + const res = await request + .post(api('omnichannel/contact')) + .set(credentials) + .send({ + token: contact.token, + name: contact.name, + customFields: { + [cfName]: 'test', + }, + }); + + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('contact'); + expect(res.body.contact).to.be.an('string'); + expect(res.body.contact).to.equal(contact._id); + + contact = await getLivechatVisitorByToken(contact.token); + expect(contact).to.have.property('livechatData'); + expect(contact.livechatData).to.have.property(cfName, 'test'); + }); + }); }); diff --git a/docker-compose-ci.yml b/docker-compose-ci.yml index a47ca7db662c..ab54ed2d57d2 100644 --- a/docker-compose-ci.yml +++ b/docker-compose-ci.yml @@ -15,7 +15,6 @@ services: - 'TRANSPORTER=${TRANSPORTER}' - MOLECULER_LOG_LEVEL=info - 'ROCKETCHAT_LICENSE=${ENTERPRISE_LICENSE}' - - 'WEBHOOK_TEST_URL=host.docker.internal:10000' extra_hosts: - 'host.docker.internal:host-gateway' depends_on: diff --git a/ee/packages/ui-theming/package.json b/ee/packages/ui-theming/package.json index ea11288f04c1..e9208d3351f7 100644 --- a/ee/packages/ui-theming/package.json +++ b/ee/packages/ui-theming/package.json @@ -8,14 +8,14 @@ "@rocket.chat/fuselage-hooks": "next", "@rocket.chat/icons": "next", "@rocket.chat/ui-contexts": "workspace:~", - "@storybook/addon-actions": "~6.5.15", + "@storybook/addon-actions": "~6.5.16", "@storybook/addon-docs": "~6.5.15", "@storybook/addon-essentials": "~6.5.15", "@storybook/addon-interactions": "~6.5.15", "@storybook/addon-links": "~6.5.15", "@storybook/addon-postcss": "~2.0.0", "@storybook/builder-webpack4": "~6.5.15", - "@storybook/manager-webpack4": "~6.5.15", + "@storybook/manager-webpack4": "~6.5.16", "@storybook/react": "~6.5.15", "@storybook/testing-library": "~0.0.13", "@types/jest": "~29.5.0", diff --git a/ee/packages/ui-theming/src/palette.ts b/ee/packages/ui-theming/src/palette.ts index 9623db5ea604..4abcefa65240 100644 --- a/ee/packages/ui-theming/src/palette.ts +++ b/ee/packages/ui-theming/src/palette.ts @@ -64,7 +64,7 @@ export const palette = [ { name: 'status-background-success', token: 'S500', color: '#C0F6E4' }, { name: 'status-background-danger', token: 'D200', color: '#FFC1C9' }, { name: 'status-background-warning', token: 'W200', color: '#FFECAD' }, - { name: 'status-background-warning-2', token: 'W100', color: '#FFF6D6' }, + { name: 'status-background-warning-2', token: 'W100', color: '#FFF8E0' }, { name: 'status-background-service-1', token: 'S1-200', color: '#FAD1B0' }, { name: 'status-background-service-2', token: 'S2-200', color: '#EDD0F7' }, { name: 'status-background-service-3', token: 'S2-700', color: '#5F1477' }, @@ -87,6 +87,7 @@ export const palette = [ category: 'Badge', description: 'Badge Background', list: [ + { name: 'badge-background-level-0', token: '', color: '#E4E7EA' }, { name: 'badge-background-level-1', token: 'N700', color: '#6C727A' }, { name: 'badge-background-level-2', token: '', color: '#1D74F5' }, { name: 'badge-background-level-3', token: '', color: '#F38C39' }, diff --git a/ee/packages/ui-theming/src/paletteDark.ts b/ee/packages/ui-theming/src/paletteDark.ts index 9e5ef724b1c0..de8616ffdb64 100644 --- a/ee/packages/ui-theming/src/paletteDark.ts +++ b/ee/packages/ui-theming/src/paletteDark.ts @@ -63,7 +63,7 @@ export const palette = [ { name: 'status-background-info', token: '', color: '#A8C3EB' }, { name: 'status-background-success', token: '', color: '#C1EBDD' }, { name: 'status-background-warning', token: '', color: '#FEEFBE' }, - { name: 'status-background-warning-2', token: '', color: '#4E4731' }, + { name: 'status-background-warning-2', token: '', color: '#3C3625' }, { name: 'status-background-danger', token: '', color: '#FFBDC5' }, { name: 'status-background-service-1', token: '', color: '#FCE3CF' }, { name: 'status-background-service-2', token: '', color: '#EDD0F7' }, @@ -87,6 +87,7 @@ export const palette = [ category: 'Badge', description: 'Badge Background', list: [ + { name: 'badge-background-level-0', token: '', color: '#2F343D' }, { name: 'badge-background-level-1', token: '', color: '#484C51' }, { name: 'badge-background-level-2', token: '', color: '#3070CF' }, { name: 'badge-background-level-3', token: '', color: '#A9642D' }, @@ -98,11 +99,11 @@ export const palette = [ description: 'Used to show user status', list: [ { name: 'status-bullet-online', token: '', color: '#1CBF89' }, - { name: 'status-bullet-away', token: '', color: '#AC892F' }, - { name: 'status-bullet-busy', token: '', color: '#C14454' }, - { name: 'status-bullet-disabled', token: '', color: '#955828' }, - { name: 'status-bullet-offline', token: '', color: '#6C727A' }, - { name: 'status-bullet-loading', token: '', color: '#9ea2a8' }, + { name: 'status-bullet-away', token: '', color: '#B08C30' }, + { name: 'status-bullet-busy', token: '', color: '#C75765' }, + { name: 'status-bullet-disabled', token: '', color: '#CC7F42' }, + { name: 'status-bullet-offline', token: '', color: '#8B9098' }, + { name: 'status-bullet-loading', token: '', color: '#8B9098' }, ], }, { diff --git a/ee/packages/ui-theming/src/sidebarPalette.ts b/ee/packages/ui-theming/src/sidebarPalette.ts index aa0d90ca2213..a50dba41a36b 100644 --- a/ee/packages/ui-theming/src/sidebarPalette.ts +++ b/ee/packages/ui-theming/src/sidebarPalette.ts @@ -21,11 +21,11 @@ export const palette = [ description: 'Used to show user status', list: [ { name: 'status-bullet-online', token: '', color: '#1CBF89' }, - { name: 'status-bullet-away', token: '', color: '#AC892F' }, - { name: 'status-bullet-busy', token: '', color: '#C14454' }, - { name: 'status-bullet-disabled', token: '', color: '#955828' }, - { name: 'status-bullet-offline', token: '', color: '#6C727A' }, - { name: 'status-bullet-loading', token: '', color: '#9ea2a8' }, + { name: 'status-bullet-away', token: '', color: '#B08C30' }, + { name: 'status-bullet-busy', token: '', color: '#C75765' }, + { name: 'status-bullet-disabled', token: '', color: '#CC7F42' }, + { name: 'status-bullet-offline', token: '', color: '#8B9098' }, + { name: 'status-bullet-loading', token: '', color: '#8B9098' }, ], }, { diff --git a/ee/packages/ui-theming/src/sidebarPaletteDark.ts b/ee/packages/ui-theming/src/sidebarPaletteDark.ts index 0a64e0ea5adc..26f683102e7e 100644 --- a/ee/packages/ui-theming/src/sidebarPaletteDark.ts +++ b/ee/packages/ui-theming/src/sidebarPaletteDark.ts @@ -23,11 +23,11 @@ export const palette = [ description: 'Used to show user status', list: [ { name: 'status-bullet-online', token: '', color: '#1CBF89' }, - { name: 'status-bullet-away', token: '', color: '#AC892F' }, - { name: 'status-bullet-busy', token: '', color: '#C14454' }, - { name: 'status-bullet-disabled', token: '', color: '#955828' }, - { name: 'status-bullet-offline', token: '', color: '#6C727A' }, - { name: 'status-bullet-loading', token: '', color: '#6C727A' }, + { name: 'status-bullet-away', token: '', color: '#B08C30' }, + { name: 'status-bullet-busy', token: '', color: '#C75765' }, + { name: 'status-bullet-disabled', token: '', color: '#CC7F42' }, + { name: 'status-bullet-offline', token: '', color: '#8B9098' }, + { name: 'status-bullet-loading', token: '', color: '#8B9098' }, ], }, { diff --git a/packages/api-client/__tests__/2fahandling.spec.ts b/packages/api-client/__tests__/2fahandling.spec.ts index f4cffc105d55..84ae8ea24dc0 100644 --- a/packages/api-client/__tests__/2fahandling.spec.ts +++ b/packages/api-client/__tests__/2fahandling.spec.ts @@ -71,7 +71,7 @@ test('if the 2fa handler is not provided, it should throw an error', async () => expect(error.status).toBe(400); - const body = error.body && (await JSON.parse(error.body.toString())); + const body = await error.json(); expect(body).toMatchObject({ errorType: 'totp-required', @@ -128,7 +128,7 @@ test('if the 2fa handler is provided it should resolves', async () => { expect(fn).toHaveBeenCalledTimes(1); }); -test.only('should be ask for 2fa code again if the code is wrong', async () => { +test('should be ask for 2fa code again if the code is wrong', async () => { const fn = jest.fn(); const client = new RestClient({ diff --git a/packages/api-client/jest.config.ts b/packages/api-client/jest.config.ts index 4f4e7697b108..2e2575dc1209 100644 --- a/packages/api-client/jest.config.ts +++ b/packages/api-client/jest.config.ts @@ -3,6 +3,9 @@ export default { errorOnDeprecated: true, testEnvironment: 'jsdom', modulePathIgnorePatterns: ['/dist/'], + transform: { + '^.+\\.(t|j)sx?$': '@swc/jest', + }, moduleNameMapper: { '\\.css$': 'identity-obj-proxy', }, diff --git a/packages/api-client/package.json b/packages/api-client/package.json index b0e515f67ad6..dd907f4db973 100644 --- a/packages/api-client/package.json +++ b/packages/api-client/package.json @@ -3,12 +3,13 @@ "version": "0.0.1", "private": true, "devDependencies": { + "@swc/core": "^1.3.60", + "@swc/jest": "^0.2.26", "@types/jest": "~29.5.0", "@types/strict-uri-encode": "^2.0.0", "eslint": "^8.29.0", - "jest": "~29.5.0", + "jest": "^29.5.0", "jest-fetch-mock": "^3.0.3", - "ts-jest": "~29.0.5", "typescript": "~5.0.2" }, "scripts": { diff --git a/packages/api-client/src/index.ts b/packages/api-client/src/index.ts index fbad9807f451..a93cb5942730 100644 --- a/packages/api-client/src/index.ts +++ b/packages/api-client/src/index.ts @@ -234,7 +234,9 @@ export class RestClient implements RestClientInterface { return Promise.reject(response); } - const error = await response.json(); + const clone = response.clone(); + + const error = await clone.json(); if ((isTotpRequiredError(error) || isTotpInvalidError(error)) && hasRequiredTwoFactorMethod(error) && this.twoFactorHandler) { const method2fa = 'details' in error ? error.details.method : 'password'; diff --git a/packages/api-client/tsconfig.json b/packages/api-client/tsconfig.json index e2be47cf5499..9d8ef0c3a373 100644 --- a/packages/api-client/tsconfig.json +++ b/packages/api-client/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../../tsconfig.base.client.json", "compilerOptions": { + "module": "commonjs", "rootDir": "./src", "outDir": "./dist" }, diff --git a/packages/ddp-client/__tests__/ClientStream.spec.ts b/packages/ddp-client/__tests__/ClientStream.spec.ts index 93f1eddebc46..6f0407b8fa97 100644 --- a/packages/ddp-client/__tests__/ClientStream.spec.ts +++ b/packages/ddp-client/__tests__/ClientStream.spec.ts @@ -21,7 +21,7 @@ describe('call procedures', () => { expect(callback).toBeCalledWith(null, ['arg1', 'arg2']); }); - it('should be able to handle errors thrown by the method call', async () => { + it('should be able to handle errors thrown by the method call', async () => { const callback = jest.fn(); const ws = new MinimalDDPClient(() => undefined); @@ -151,24 +151,21 @@ describe('subscribe procedures', () => { it('should be able to subscribe to a collection and receive a result', async () => { const ws = new MinimalDDPClient(() => undefined); const client = new ClientStreamImpl(ws); - const promise = client.subscribe('test'); + const subscription = client.subscribe('test'); ws.handleMessage( JSON.stringify({ msg: 'ready', - subs: [promise.id], + subs: [subscription.id], }), ); - await expect(promise).resolves.toEqual({ - msg: 'ready', - subs: [promise.id], - }); + await expect(subscription.ready()).resolves.toBeUndefined(); }); it('should be able to subscribe to a collection and receive an error', async () => { const ws = new MinimalDDPClient(() => undefined); const client = new ClientStreamImpl(ws); - const promise = client.subscribe('test'); + const subscription = client.subscribe('test'); ws.handleMessage( JSON.stringify({ msg: 'nosub', @@ -178,11 +175,11 @@ describe('subscribe procedures', () => { message: 'Bad Request [400]', errorType: 'Meteor.Error', }, - id: promise.id, + id: subscription.id, }), ); - await expect(promise).rejects.toEqual({ + await expect(subscription.ready()).rejects.toEqual({ error: 400, reason: 'Bad Request', message: 'Bad Request [400]', @@ -193,29 +190,26 @@ describe('subscribe procedures', () => { it('should be able to unsubscribe from a collection', async () => { const ws = new MinimalDDPClient(() => undefined); const client = new ClientStreamImpl(ws); - const promise = client.subscribe('test'); + const subscription = client.subscribe('test'); ws.handleMessage( JSON.stringify({ msg: 'ready', - subs: [promise.id], + subs: [subscription.id], }), ); - await expect(promise).resolves.toEqual({ - msg: 'ready', - subs: [promise.id], - }); - const unsubPromise = client.unsubscribe(promise.id); + await expect(subscription.ready()).resolves.toBeUndefined(); + const unsubPromise = client.unsubscribe(subscription.id); ws.handleMessage( JSON.stringify({ msg: 'nosub', - id: promise.id, + id: subscription.id, }), ); expect(unsubPromise).resolves.toEqual({ msg: 'nosub', - id: promise.id, + id: subscription.id, }); }); @@ -231,10 +225,7 @@ describe('subscribe procedures', () => { }), ); - await expect(promise).resolves.toEqual({ - msg: 'ready', - subs: [promise.id], - }); + await expect(promise.ready()).resolves.toBeUndefined(); const observer = jest.fn(); diff --git a/packages/ddp-client/__tests__/Connection.spec.ts b/packages/ddp-client/__tests__/Connection.spec.ts index 772491b41161..ef06c56437c9 100644 --- a/packages/ddp-client/__tests__/Connection.spec.ts +++ b/packages/ddp-client/__tests__/Connection.spec.ts @@ -2,6 +2,7 @@ import WS from 'jest-websocket-mock'; import { MinimalDDPClient } from '../src/MinimalDDPClient'; import { ConnectionImpl } from '../src/Connection'; +import { handleConnection, handleConnectionAndRejects, handleMethod } from './helpers'; let server: WS; beforeEach(() => { @@ -17,17 +18,11 @@ it('should connect', async () => { const client = new MinimalDDPClient(); const connection = new ConnectionImpl('ws://localhost:1234', WebSocket as any, client, { retryCount: 0, retryTime: 0 }); - server.nextMessage.then((message) => { - expect(message).toBe('{"msg":"connect","version":"1","support":["1","pre2","pre1"]}'); - return server.send('{"msg":"connected","session":"123"}'); - }); - expect(connection.status).toBe('idle'); expect(connection.session).toBeUndefined(); + await handleConnection(server, connection.connect()); - await expect(connection.connect()).resolves.toBe(true); - - expect(connection.session).toBe('123'); + expect(connection.session).toBe('session'); expect(connection.status).toBe('connected'); }); @@ -35,38 +30,26 @@ it('should handle a failing connection', async () => { const client = new MinimalDDPClient(); const connection = new ConnectionImpl('ws://localhost:1234', WebSocket as any, client, { retryCount: 0, retryTime: 0 }); - const suggestedVersion = '1'; - - const message = server.nextMessage.then((message) => { - expect(message).toBe('{"msg":"connect","version":"1","support":["1","pre2","pre1"]}'); - return server.send(`{"msg":"failed","version":"${suggestedVersion}"}`); - }); - expect(connection.status).toBe('idle'); expect(connection.session).toBeUndefined(); - await expect(connection.connect()).rejects.toBe(suggestedVersion); + await expect(handleConnectionAndRejects(server, connection.connect())).rejects.toBe('1'); expect(connection.session).toBeUndefined(); expect(connection.status).toBe('failed'); - await message; }); it('should trigger a disconnect callback', async () => { const client = new MinimalDDPClient(); const connection = ConnectionImpl.create('ws://localhost:1234', globalThis.WebSocket, client, { retryCount: 0, retryTime: 0 }); - const suggestedVersion = '1'; - const s = server.nextMessage.then((message) => { - expect(message).toBe(`{"msg":"connect","version":"${suggestedVersion}","support":["1","pre2","pre1"]}`); - return server.send('{"msg":"connected","session":"123"}'); - }); + expect(connection.status).toBe('idle'); expect(connection.session).toBeUndefined(); const disconnectCallback = jest.fn(); connection.on('connection', disconnectCallback); - const connectionPromise = connection.connect(); - await s; - await expect(connectionPromise).resolves.toBe(true); + + await handleConnection(server, connection.connect()); + expect(disconnectCallback).toHaveBeenNthCalledWith(1, 'connecting'); expect(disconnectCallback).toHaveBeenNthCalledWith(2, 'connected'); expect(disconnectCallback).toBeCalledTimes(2); @@ -106,35 +89,69 @@ it('should handle reconnecting', async () => { const client = new MinimalDDPClient(); const connection = ConnectionImpl.create('ws://localhost:1234', WebSocket, client, { retryCount: 1, retryTime: 100 }); - server.nextMessage.then((message) => { - expect(message).toBe('{"msg":"connect","version":"1","support":["1","pre2","pre1"]}'); - return server.send('{"msg":"connected","session":"123"}'); - }); - expect(connection.status).toBe('idle'); expect(connection.session).toBeUndefined(); - await expect(connection.connect()).resolves.toBe(true); + await handleConnection(server, connection.connect()); - expect(connection.session).toBe('123'); + expect(connection.session).toBe('session'); expect(connection.status).toBe('connected'); + // Fake timers are used to avoid waiting for the reconnect timeout + jest.useFakeTimers(); + server.close(); WS.clean(); server = new WS('ws://localhost:1234'); - server.nextMessage.then((message) => { - expect(message).toBe('{"msg":"connect","version":"1","support":["1","pre2","pre1"]}'); - return server.send('{"msg":"connected","session":"123"}'); - }); - expect(connection.status).toBe('disconnected'); - await expect(new Promise((resolve) => connection.once('reconnecting', () => resolve(undefined)))).resolves.toBeUndefined(); + await handleConnection( + server, + jest.advanceTimersByTimeAsync(200), + new Promise((resolve) => connection.once('reconnecting', () => resolve(undefined))), + new Promise((resolve) => connection.once('connection', (data) => resolve(data))), + ); - expect(connection.status).toBe('connecting'); + expect(connection.status).toBe('connected'); + jest.useRealTimers(); +}); - await expect(new Promise((resolve) => connection.once('connection', (data) => resolve(data)))).resolves.toBe('connected'); +it('should queue messages if the connection is not ready', async () => { + const client = new MinimalDDPClient(); + const connection = ConnectionImpl.create('ws://localhost:1234', globalThis.WebSocket, client, { retryCount: 0, retryTime: 0 }); - expect(connection.status).toBe('connected'); + await handleConnection(server, connection.connect()); + + connection.close(); + + expect(connection.status).toBe('closed'); + + client.emit('send', { msg: 'method', method: 'method', params: ['arg1', 'arg2'], id: '1' }); + + expect(connection.queue.size).toBe(1); + + await handleConnection(server, connection.reconnect()); + + expect(connection.queue.size).toBe(0); + + await handleMethod(server, 'method', ['arg1', 'arg2']); +}); + +it('should throw an error if a reconnect is called while a connection is in progress', async () => { + const client = new MinimalDDPClient(); + const connection = ConnectionImpl.create('ws://localhost:1234', globalThis.WebSocket, client, { retryCount: 0, retryTime: 0 }); + + await handleConnection(server, connection.connect()); + + await expect(connection.reconnect()).rejects.toThrow('Connection in progress'); +}); + +it('should throw an error if a connect is called while a connection is in progress', async () => { + const client = new MinimalDDPClient(); + const connection = ConnectionImpl.create('ws://localhost:1234', globalThis.WebSocket, client, { retryCount: 0, retryTime: 0 }); + + await handleConnection(server, connection.connect()); + + await expect(connection.connect()).rejects.toThrow('Connection in progress'); }); diff --git a/packages/ddp-client/__tests__/DDPSDK.spec.ts b/packages/ddp-client/__tests__/DDPSDK.spec.ts index 3f264c0b4759..83c7f213957e 100644 --- a/packages/ddp-client/__tests__/DDPSDK.spec.ts +++ b/packages/ddp-client/__tests__/DDPSDK.spec.ts @@ -1,17 +1,35 @@ +/* eslint-disable no-debugger */ +import util from 'util'; + import WS from 'jest-websocket-mock'; import { WebSocket } from 'ws'; import { DDPSDK } from '../src/DDPSDK'; +import { fireStreamChange, fireStreamAdded, fireStreamRemove, handleConnection, handleSubscription, handleMethod } from './helpers'; (global as any).WebSocket = (global as any).WebSocket || WebSocket; -let server: WS; +export let server: WS; + +const callXTimes = any>(fn: F, times: number): F => { + return (async (...args) => { + const methods = [].concat(...Array(times)); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + for (const _ of methods) { + // eslint-disable-next-line no-await-in-loop + await fn(...args); + } + }) as F; +}; + beforeEach(async () => { server = new WS('ws://localhost:1234'); }); afterEach(() => { + server.close(); WS.clean(); + jest.useRealTimers(); }); it('should handle a stream of messages', async () => { @@ -20,30 +38,23 @@ it('should handle a stream of messages', async () => { const streamName = 'stream'; const streamParams = '123'; - const create = DDPSDK.create('ws://localhost:1234'); + const sdk = DDPSDK.create('ws://localhost:1234'); - await server.nextMessage.then((message) => { - expect(message).toBe('{"msg":"connect","version":"1","support":["1","pre2","pre1"]}'); - server.send(`{"msg":"connected","session":"${streamParams}"}`); - }); - - const sdk = await create; + await handleConnection(server, sdk.connection.connect()); const stream = sdk.stream(streamName, streamParams, cb); - const [id] = [...sdk.client.subscriptions.keys()]; - await server.nextMessage.then((message) => { - expect(message).toBe(`{"msg":"sub","id":"${id}","name":"stream-${streamName}","params":["${streamParams}"]}`); - return server.send(`{"msg":"ready","subs":["${id}"]}`); - }); + await handleSubscription(server, stream.id, streamName, streamParams); - await stream; + await stream.ready(); - await server.send(`{"msg":"changed","collection":"stream-${streamName}","id":"id","fields":{"eventName":"${streamParams}", "args":[1]}}`); - await server.send(`{"msg":"changed","collection":"stream-${streamName}","id":"id","fields":{"eventName":"${streamParams}", "args":[1]}}`); - await server.send(`{"msg":"changed","collection":"stream-${streamName}","id":"id","fields":{"eventName":"${streamParams}", "args":[1]}}`); + fireStreamChange(server, streamName, streamParams); + fireStreamChange(server, streamName, streamParams); + fireStreamChange(server, streamName, streamParams); - await server.send(`{"msg":"changed","collection":"stream-${streamName}","id":"id","fields":{"eventName":"${streamParams}-another"}}`); + expect(cb).toBeCalledTimes(3); + + fireStreamChange(server, streamName, `${streamParams}-another`); expect(cb).toBeCalledTimes(3); @@ -56,27 +67,19 @@ it('should ignore messages other from changed', async () => { const streamName = 'stream'; const streamParams = '123'; - const create = DDPSDK.create('ws://localhost:1234'); - - await server.nextMessage.then((message) => { - expect(message).toBe('{"msg":"connect","version":"1","support":["1","pre2","pre1"]}'); - server.send(`{"msg":"connected","session":"${streamParams}"}`); - }); + const sdk = DDPSDK.create('ws://localhost:1234'); - const sdk = await create; + await handleConnection(server, sdk.connection.connect()); const stream = sdk.stream(streamName, streamParams, cb); - const [id] = [...sdk.client.subscriptions.keys()]; - await server.nextMessage.then((message) => { - expect(message).toBe(`{"msg":"sub","id":"${id}","name":"stream-${streamName}","params":["${streamParams}"]}`); - return server.send(`{"msg":"ready","subs":["${id}"]}`); - }); + await handleSubscription(server, stream.id, streamName, streamParams); - await stream; + await stream.ready(); - await server.send(`{"msg":"added","collection":"stream-${streamName}","id":"id","fields":{"eventName":"${streamParams}", "args":[1]}}`); - await server.send(`{"msg":"removed","collection":"stream-${streamName}","id":"id","fields":{"eventName":"${streamParams}", "args":[1]}}`); + fireStreamAdded(server, streamName, streamParams); + + fireStreamRemove(server, streamName, streamParams); expect(cb).toBeCalledTimes(0); }); @@ -87,61 +90,46 @@ it('should handle streams after reconnect', async () => { const streamName = 'stream'; const streamParams = '123'; - const create = DDPSDK.create('ws://localhost:1234'); + const sdk = DDPSDK.create('ws://localhost:1234'); - await server.nextMessage.then((message) => { - expect(message).toBe('{"msg":"connect","version":"1","support":["1","pre2","pre1"]}'); - return server.send(`{"msg":"connected","session":"${streamParams}"}`); - }); + await handleConnection(server, sdk.connection.connect()); - const sdk = await create; + const result = sdk.stream(streamName, streamParams, cb); - const stream = sdk.stream(streamName, streamParams, cb); + expect(result.isReady).toBe(false); - const [id] = [...sdk.client.subscriptions.keys()]; - await server.nextMessage.then((message) => { - expect(message).toBe(`{"msg":"sub","id":"${id}","name":"stream-${streamName}","params":["${streamParams}"]}`); - return server.send(`{"msg":"ready","subs":["${id}"]}`); - }); + expect(sdk.client.subscriptions.size).toBe(1); + + await handleSubscription(server, result.id, streamName, streamParams); - await stream; + await result.ready(); - await server.send(`{"msg":"changed","collection":"stream-${streamName}","id":"id","fields":{"eventName":"${streamParams}", "args":[1]}}`); - await server.send(`{"msg":"changed","collection":"stream-${streamName}","id":"id","fields":{"eventName":"${streamParams}", "args":[1]}}`); - await server.send(`{"msg":"changed","collection":"stream-${streamName}","id":"id","fields":{"eventName":"${streamParams}", "args":[1]}}`); + const fire = callXTimes(fireStreamChange, 3); + + await fire(server, streamName, streamParams); expect(cb).toBeCalledTimes(3); - jest.useFakeTimers(); + // Fake timers are used to avoid waiting for the reconnect timeout + jest.useFakeTimers(); server.close(); WS.clean(); - server = new WS('ws://localhost:1234'); - server.nextMessage.then((message) => { - expect(message).toBe('{"msg":"connect","version":"1","support":["1","pre2","pre1"]}'); - return server.send(`{"msg":"connected","session":"${streamParams}"}`); - }); + server = new WS('ws://localhost:1234'); const reconnect = new Promise((resolve) => sdk.connection.once('reconnecting', () => resolve(undefined))); const connecting = new Promise((resolve) => sdk.connection.once('connecting', () => resolve(undefined))); const connected = new Promise((resolve) => sdk.connection.once('connected', () => resolve(undefined))); + await handleConnection(server, jest.advanceTimersByTimeAsync(1000), reconnect, connecting, connected); - jest.runAllTimers(); + // the client should reconnect and resubscribe + await handleSubscription(server, result.id, streamName, streamParams); - await reconnect; - await connecting; - await connected; - - server.nextMessage.then((message) => { - expect(message).toBe(`{"msg":"sub","id":"${id}","name":"stream-${streamName}","params":["${streamParams}"]}`); - return server.send(`{"msg":"ready","subs":["${id}"]}`); - }); - - await server.send(`{"msg":"changed","collection":"stream-${streamName}","id":"id","fields":{"eventName":"${streamParams}", "args":[1]}}`); - await server.send(`{"msg":"changed","collection":"stream-${streamName}","id":"id","fields":{"eventName":"${streamParams}", "args":[1]}}`); - await server.send(`{"msg":"changed","collection":"stream-${streamName}","id":"id","fields":{"eventName":"${streamParams}", "args":[1]}}`); + fire(server, streamName, streamParams); + await jest.advanceTimersByTimeAsync(1000); expect(cb).toBeCalledTimes(6); + jest.useRealTimers(); }); @@ -151,66 +139,130 @@ it('should handle an unsubscribe stream after reconnect', async () => { const streamName = 'stream'; const streamParams = '123'; - const create = DDPSDK.create('ws://localhost:1234'); + const sdk = DDPSDK.create('ws://localhost:1234'); - await server.nextMessage.then((message) => { - expect(message).toBe('{"msg":"connect","version":"1","support":["1","pre2","pre1"]}'); - return server.send(`{"msg":"connected","session":"${streamParams}"}`); - }); + await handleConnection(server, sdk.connection.connect()); - const sdk = await create; + const subscription = sdk.stream(streamName, streamParams, cb); - const stopSubscription = sdk.stream(streamName, streamParams, cb); + expect(subscription.isReady).toBe(false); expect(sdk.client.subscriptions.size).toBe(1); - const [id] = [...sdk.client.subscriptions.keys()]; + await handleSubscription(server, subscription.id, streamName, streamParams); - await server.nextMessage.then((message) => { - expect(message).toBe(`{"msg":"sub","id":"${id}","name":"stream-${streamName}","params":["${streamParams}"]}`); - return server.send(`{"msg":"ready","subs":["${id}"]}`); - }); + await expect(subscription.ready()).resolves.toBe(undefined); - await server.send(`{"msg":"changed","collection":"stream-${streamName}","id":"id","fields":{"eventName":"${streamParams}", "args":[1]}}`); - await server.send(`{"msg":"changed","collection":"stream-${streamName}","id":"id","fields":{"eventName":"${streamParams}", "args":[1]}}`); - await server.send(`{"msg":"changed","collection":"stream-${streamName}","id":"id","fields":{"eventName":"${streamParams}", "args":[1]}}`); + expect(subscription.isReady).toBe(true); + + fireStreamChange(server, streamName, streamParams); + fireStreamChange(server, streamName, streamParams); + fireStreamChange(server, streamName, streamParams); expect(cb).toBeCalledTimes(3); + + // Fake timers are used to avoid waiting for the reconnect timeout jest.useFakeTimers(); server.close(); WS.clean(); server = new WS('ws://localhost:1234'); - server.nextMessage.then((message) => { - expect(message).toBe('{"msg":"connect","version":"1","support":["1","pre2","pre1"]}'); - return server.send(`{"msg":"connected","session":"${streamParams}"}`); - }); - const reconnect = new Promise((resolve) => sdk.connection.once('reconnecting', () => resolve(undefined))); const connecting = new Promise((resolve) => sdk.connection.once('connecting', () => resolve(undefined))); const connected = new Promise((resolve) => sdk.connection.once('connected', () => resolve(undefined))); + await handleConnection(server, jest.advanceTimersByTimeAsync(1000), reconnect, connecting, connected); - jest.runAllTimers(); + await handleSubscription(server, subscription.id, streamName, streamParams); - await reconnect; - await connecting; - await connected; + expect(subscription.isReady).toBe(true); - server.nextMessage.then((message) => { - expect(message).toBe(`{"msg":"sub","id":"${id}","name":"stream-${streamName}","params":["${streamParams}"]}`); - return server.send(`{"msg":"ready","subs":["${id}"]}`); - }); + fireStreamChange(server, streamName, streamParams); - await server.send(`{"msg":"changed","collection":"stream-${streamName}","id":"id","fields":{"eventName":"${streamParams}", "args":[1]}}`); + subscription.stop(); - stopSubscription(); + expect(sdk.client.subscriptions.size).toBe(0); - await server.send(`{"msg":"changed","collection":"stream-${streamName}","id":"id","fields":{"eventName":"${streamParams}", "args":[1]}}`); - await server.send(`{"msg":"changed","collection":"stream-${streamName}","id":"id","fields":{"eventName":"${streamParams}", "args":[1]}}`); + fireStreamChange(server, streamName, streamParams); + fireStreamChange(server, streamName, streamParams); + jest.advanceTimersByTimeAsync(1000); expect(cb).toBeCalledTimes(4); expect(sdk.client.subscriptions.size).toBe(0); jest.useRealTimers(); + sdk.connection.close(); +}); + +it('should create and connect to a stream', async () => { + const promise = DDPSDK.createAndConnect('ws://localhost:1234'); + await handleConnection(server, promise); + const sdk = await promise; + sdk.connection.close(); +}); + +describe('Method call and Disconnection cases', () => { + it('should handle properly if the message was sent after disconnection', async () => { + const sdk = DDPSDK.create('ws://localhost:1234'); + + await handleConnection(server, sdk.connection.connect()); + + const [result] = await handleMethod(server, 'method', ['args1'], sdk.client.callAsync('method', 'args1')); + + expect(result).toBe(1); + // Fake timers are used to avoid waiting for the reconnect timeout + jest.useFakeTimers(); + + server.close(); + WS.clean(); + server = new WS('ws://localhost:1234'); + + const reconnect = new Promise((resolve) => sdk.connection.once('reconnecting', () => resolve(undefined))); + const connecting = new Promise((resolve) => sdk.connection.once('connecting', () => resolve(undefined))); + const connected = new Promise((resolve) => sdk.connection.once('connected', () => resolve(undefined))); + + const callResult = sdk.client.callAsync('method', 'args2'); + + expect(util.inspect(callResult).includes('pending')).toBe(true); + + await handleConnection(server, jest.advanceTimersByTimeAsync(1000), reconnect, connecting, connected); + + const [result2] = await handleMethod(server, 'method', ['args2'], callResult); + + expect(util.inspect(callResult).includes('pending')).toBe(false); + expect(result2).toBe(1); + sdk.connection.close(); + jest.useRealTimers(); + }); + + it.skip('should handle properly if the message was sent before disconnection but got disconnected before receiving the response', async () => { + const sdk = DDPSDK.create('ws://localhost:1234'); + + await handleConnection(server, sdk.connection.connect()); + + const callResult = sdk.client.callAsync('method', 'args'); + + expect(util.inspect(callResult).includes('pending')).toBe(true); + + // Fake timers are used to avoid waiting for the reconnect timeout + jest.useFakeTimers(); + + server.close(); + WS.clean(); + server = new WS('ws://localhost:1234'); + + const reconnect = new Promise((resolve) => sdk.connection.once('reconnecting', () => resolve(undefined))); + const connecting = new Promise((resolve) => sdk.connection.once('connecting', () => resolve(undefined))); + const connected = new Promise((resolve) => sdk.connection.once('connected', () => resolve(undefined))); + + await handleConnection(server, jest.advanceTimersByTimeAsync(1000), reconnect, connecting, connected); + + expect(util.inspect(callResult).includes('pending')).toBe(true); + + const [result] = await handleMethod(server, 'method', ['args2'], callResult); + + expect(result).toBe(1); + + jest.useRealTimers(); + }); }); diff --git a/packages/ddp-client/__tests__/Timeout.spec.ts b/packages/ddp-client/__tests__/Timeout.spec.ts index aea52782baa5..c7826672b20a 100644 --- a/packages/ddp-client/__tests__/Timeout.spec.ts +++ b/packages/ddp-client/__tests__/Timeout.spec.ts @@ -8,6 +8,7 @@ it('should call the heartbeat and timeout callbacks respecting the informed time const timeoutCallback = jest.fn(); const timeout = new TimeoutControl(100); + timeout.reset(); expect(setTimeout).toHaveBeenCalledTimes(2); @@ -34,6 +35,7 @@ it('should never call the timeout callback if the reset method is called', async const timeoutCallback = jest.fn(); const timeout = new TimeoutControl(100); + timeout.reset(); expect(setTimeout).toHaveBeenCalledTimes(2); diff --git a/packages/ddp-client/__tests__/helpers/index.ts b/packages/ddp-client/__tests__/helpers/index.ts new file mode 100644 index 000000000000..ce264e430290 --- /dev/null +++ b/packages/ddp-client/__tests__/helpers/index.ts @@ -0,0 +1,62 @@ +import type WS from 'jest-websocket-mock'; + +const acceptConnection = async (server: WS) => { + await server.nextMessage.then(async (message) => { + await expect(message).toBe('{"msg":"connect","version":"1","support":["1","pre2","pre1"]}'); + server.send(`{"msg":"connected","session":"session"}`); + }); +}; +export const handleConnection = async (server: WS, ...client: Promise[]) => { + await Promise.all([acceptConnection(server), ...client]); +}; + +export const handleConnectionAndRejects = async (server: WS, ...client: Promise[]) => { + const suggestedVersion = '1'; + + return Promise.all([ + server.nextMessage.then((message) => { + expect(message).toBe('{"msg":"connect","version":"1","support":["1","pre2","pre1"]}'); + return server.send(`{"msg":"failed","version":"${suggestedVersion}"}`); + }), + ...client, + ]); +}; + +const handleConnectionButNoResponse = async (server: WS, method: string, params: string[]) => { + return server.nextMessage.then(async (msg) => { + if (typeof msg !== 'string') throw new Error('Expected message to be a string'); + const message = JSON.parse(msg); + await expect(message).toMatchObject({ + msg: 'method', + method, + params, + }); + return message; + }); +}; + +export const handleMethod = async (server: WS, method: string, params: string[], ...client: Promise[]) => { + const result = await handleConnectionButNoResponse(server, method, params); + return Promise.all([server.send(`{"msg":"result","id":"${result.id}","result":1}`), ...client]).then((result) => { + result.shift(); + return result; + }); +}; + +export const handleSubscription = async (server: WS, id: string, streamName: string, streamParams: string) => { + await server.nextMessage.then(async (message) => { + await expect(message).toBe(`{"msg":"sub","id":"${id}","name":"stream-${streamName}","params":["${streamParams}"]}`); + server.send(`{"msg":"ready","subs":["${id}"]}`); + }); +}; +export const fireStream = (action: 'changed' | 'removed' | 'added') => { + return (server: WS, streamName: string, streamParams: string) => { + return server.send( + `{"msg":"${action}","collection":"stream-${streamName}","id":"id","fields":{"eventName":"${streamParams}", "args":[1]}}`, + ); + }; +}; + +export const fireStreamChange = fireStream('changed'); +export const fireStreamRemove = fireStream('removed'); +export const fireStreamAdded = fireStream('added'); diff --git a/packages/ddp-client/jest.config.ts b/packages/ddp-client/jest.config.ts index 4f4e7697b108..0739147f43fe 100644 --- a/packages/ddp-client/jest.config.ts +++ b/packages/ddp-client/jest.config.ts @@ -3,6 +3,10 @@ export default { errorOnDeprecated: true, testEnvironment: 'jsdom', modulePathIgnorePatterns: ['/dist/'], + testMatch: ['**/**.spec.ts'], + transform: { + '^.+\\.(t|j)sx?$': '@swc/jest', + }, moduleNameMapper: { '\\.css$': 'identity-obj-proxy', }, diff --git a/packages/ddp-client/package.json b/packages/ddp-client/package.json index d016485127ba..fc6ae7638c61 100644 --- a/packages/ddp-client/package.json +++ b/packages/ddp-client/package.json @@ -3,14 +3,15 @@ "version": "0.0.1", "private": true, "devDependencies": { + "@swc/core": "^1.3.60", + "@swc/jest": "^0.2.26", "@types/jest": "^29.5.0", "@types/ws": "^8", "eslint": "^8.12.0", - "jest": "~29.5.0", + "jest": "^29.5.0", "jest-environment-jsdom": "~29.5.0", "jest-websocket-mock": "^2.4.0", - "ts-jest": "~29.0.5", - "typescript": "~5.0.2", + "typescript": "~5.0.4", "ws": "^8.13.0" }, "peerDependencies": { @@ -20,6 +21,7 @@ "lint": "eslint --ext .js,.jsx,.ts,.tsx .", "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", "unit": "jest", + "testunit": "jest", "build": "rm -rf dist && tsc -p tsconfig.json", "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput" }, diff --git a/packages/ddp-client/src/ClientStream.ts b/packages/ddp-client/src/ClientStream.ts index edaf56df6aea..d0941640d32a 100644 --- a/packages/ddp-client/src/ClientStream.ts +++ b/packages/ddp-client/src/ClientStream.ts @@ -5,148 +5,11 @@ import type { PublicationPayloads } from './types/publicationPayloads'; import type { DDPDispatchOptions } from './MinimalDDPClient'; import { DDPDispatcher } from './DDPDispatcher'; import type { MethodPayload } from './types/methodsPayloads'; - -export interface ClientStream extends Emitter { - /** - * Calls a method on the server. - * @param method - The name of the method to be called. - * @param params - The parameters to be passed to the method. - * @param params.lastArgument - The last argument can be a callback function, the first argument of the callback function is the error, the second argument is the result. - * - * @returns {string} - The id of the method call. - */ - call(method: string, ...params: any[]): string; - - /** - * Calls a method on the server. The same as `call` but with options. - * This options are passed to the dispatcher. The dispatcher is responsible for handling the method call. - * So depending on the dispatcher, the options may or may not be used. By default, the dispatcher only uses the `wait` option. - * One reason to use options is to make the call wait for the result to dispatch other calls. - * Mainly for authentication purposes. - * - * @param method - The name of the method to be called. - * @param options - The options to be passed to the method. - * @param options.wait - If true, the call will wait for the result to dispatch other calls. - * @param params - The parameters to be passed to the method. - * @param params.lastArgument - The last argument can be a callback function, the first argument of the callback function is the error, the second argument is the result. - * @returns {string} - The id of the method call. - **/ - callWithOptions(method: string, options: DDPDispatchOptions, ...params: any[]): string; - - /** - * Calls a method on the server. The same as `call` but returns a promise. - * @param method - The name of the method to be called. - * @param params - The parameters to be passed to the method. - * @returns {Promise} - A promise that resolves when the server returns the result. - * @example - * ```ts - * const result = await ddp.callAsync('login', { - * user: { - * username: 'my-username', - * password: 'my-password', - * }, - * }); - * ``` - */ - - callAsync( - method: string, - ...params: any[] - ): Promise & { - id: string; - }; - - /** - * Calls a method on the server. The same as `callWithOptions` but returns a promise. - * @param method - The name of the method to be called. - * @param options - The options to be passed to the method. - * @param options.wait - If true, the call will wait for the result to dispatch other calls. - * @param params - The parameters to be passed to the method. - * @returns {Promise} - A promise that resolves when the server returns the result. - */ - callAsyncWithOptions( - method: string, - options: DDPDispatchOptions, - ...params: any[] - ): Promise & { - id: string; - }; - - /** - * Subscribes to a publication on the server. - * @param name - The name of the publication to subscribe to. - * @param params - The parameters to be passed to the publication. - * @returns {Promise} - A promise that resolves when the server returns the subscription id. - */ - subscribe(name: string, ...params: any[]): Promise & { id: string }; - /** - * Unsubscribes from a publication on the server. - * @param id - The id of the subscription to unsubscribe from. - * @returns {Promise} - A promise that resolves when the server unsubscribes the subscription. - */ - unsubscribe(id: string): Promise; - /** - * Connects to the server. - * @returns {Promise} - A promise that resolves when the server connects. - * @example - * ```ts - * await ddp.connect(); - * ``` - */ - connect(): Promise; - /** - * Fired when the server send any collection update. - * usually this used after subscribing to a publication. - * @param id - The id/name of the collection. - * @param callback - The callback function to be called when the server sends any collection update. - * @returns {Function} - A function to stop listening for collection updates. - * @example - * ```ts - * const stop = ddp.onCollection('users', (data) => { - * console.log(data); - * }); - * ``` - */ - onCollection(id: string, callback: (data: PublicationPayloads) => void): () => void; - - /** - * The list of subscriptions. - * @type {Map} - * @example - * ```ts - * const subscription = ddp.subscribe('my-subscription'); - * console.log(ddp.subscriptions.get(subscription.id)); - * // prints: - * // { - * // id: 'my-subscription', - * // status: 'loading', - * // name: 'my-subscription', - * // params: [], - * // } - * ``` - */ - - subscriptions: Map< - string, - { - id: string; - status: 'ready' | 'loading'; - name: string; - params: any[]; - } - >; -} +import type { Subscription } from './types/Subscription'; +import type { ClientStream } from './types/ClientStream'; export class ClientStreamImpl extends Emitter implements ClientStream { - subscriptions = new Map< - string, - { - id: string; - status: 'ready' | 'loading'; - name: string; - params: any[]; - } - >(); + subscriptions = new Map(); constructor(private ddp: DDPClient, readonly dispatcher: DDPDispatcher = new DDPDispatcher()) { super(); @@ -229,31 +92,65 @@ export class ClientStreamImpl extends Emitter implements ClientStream { ); } - subscribe(name: string, ...params: any[]): Promise & { id: string } { + subscribe(name: string, ...params: any[]) { const id = this.ddp.subscribe(name, params); - this.subscriptions.set(id, { + // eslint-disable-next-line @typescript-eslint/no-this-alias + const self = this; + + const s = self.ddp.onPublish(id, (payload) => { + if ('error' in payload) { + result.error = payload.error; + this.subscriptions.delete(id); + return; + } + result.isReady = true; + this.subscriptions.set(id, { + ...result, + isReady: true, + }); + }); + + const stop = () => { + s(); + self.unsubscribe(id); + }; + + const result: Subscription = { id, - status: 'loading', name, params, - }); - const result = new Promise((resolve, reject) => { - this.ddp.onPublish(id, (payload) => { - if ('error' in payload) { - this.subscriptions.delete(id); - return reject(payload.error); + async ready() { + const subscription = self.subscriptions.get(id); + if (!subscription) { + return Promise.reject(result.error); + } + + if (subscription.isReady) { + return Promise.resolve(); } - this.subscriptions.set(id, { - id, - status: 'ready', - name, - params, + + return new Promise((resolve, reject) => { + this.onChange((payload) => { + if ('error' in payload) { + reject(payload.error); + return; + } + resolve(); + }); }); - resolve(payload); - }); - }); - return Object.assign(result, { id }); + }, + isReady: false, + onChange: (cb) => { + self.ddp.onPublish(id, cb); + }, + + stop, + }; + + this.subscriptions.set(id, result); + + return result; } unsubscribe(id: string): Promise { diff --git a/packages/ddp-client/src/Connection.ts b/packages/ddp-client/src/Connection.ts index 375387a36f84..da5227baac00 100644 --- a/packages/ddp-client/src/Connection.ts +++ b/packages/ddp-client/src/Connection.ts @@ -22,7 +22,7 @@ type RetryOptions = { retryTime: number; }; -type ConnectionStatus = 'idle' | 'connecting' | 'connected' | 'failed' | 'closed' | 'disconnected'; +type ConnectionStatus = 'idle' | 'connecting' | 'connected' | 'failed' | 'closed' | 'disconnected' | 'reconnecting'; export interface Connection extends Emitter<{ @@ -69,6 +69,8 @@ export class ConnectionImpl retryCount = 0; + public queue = new Set(); + constructor( readonly url: string, private WS: WebSocketConstructor, @@ -76,6 +78,23 @@ export class ConnectionImpl readonly retryOptions: RetryOptions = { retryCount: 0, retryTime: 1000 }, ) { super(); + + this.client.onDispatchMessage((message: string) => { + if (this.ws && this.ws.readyState === this.ws.OPEN) { + this.ws.send(message); + return; + } + + this.queue.add(message); + }); + + this.on('connected', () => { + this.queue.forEach((message) => { + this.ws?.send(message); + }); + + this.queue.clear(); + }); } private emitStatus() { @@ -84,63 +103,51 @@ export class ConnectionImpl reconnect(): Promise { if (this.status === 'connecting' || this.status === 'connected') { - return Promise.resolve(true); + return Promise.reject(new Error('Connection in progress')); } clearTimeout(this.retryOptions.retryTimer); this.emit('reconnecting'); + this.emit('connection', 'reconnecting'); + return this.connect(); } connect() { + if (this.status === 'connecting' || this.status === 'connected') { + return Promise.reject(new Error('Connection in progress')); + } + this.status = 'connecting'; this.emit('connecting'); this.emitStatus(); const ws = new this.WS(this.url); - let stop: () => void | undefined; - return new Promise((resolve, reject) => { - const queue = new Set(); - - stop = this.client.onDispatchMessage((message: string) => { - queue.add(message); - }); + this.ws = ws; + return new Promise((resolve, reject) => { ws.onopen = () => { ws.onmessage = (event) => { this.client.handleMessage(String(event.data)); }; - stop?.(); - - queue.forEach((message) => { - ws.send(message); - }); - - queue.clear(); - - stop = this.client.onDispatchMessage((message: string) => { - ws.send(message); - }); - - this.retryCount = 0; // The server may send an initial message which is a JSON object lacking a msg key. If so, the client should ignore it. The client does not have to wait for this message. // (The message was once used to help implement Meteor's hot code reload feature; it is now only included to force old clients to update). - this.client.onceMessage((data) => { - if (data.msg === undefined) { - return; - } - if (data.msg === 'failed') { - return; - } - if (data.msg === 'connected') { - return; - } - this.close(); - }); + // this.client.onceMessage((data) => { + // if (data.msg === undefined) { + // return; + // } + // if (data.msg === 'failed') { + // return; + // } + // if (data.msg === 'connected') { + // return; + // } + // this.close(); + // }); // The client sends a connect message. @@ -151,9 +158,9 @@ export class ConnectionImpl this.client.onConnection((payload) => { if (payload.msg === 'connected') { - this.emit('connected', payload.session); this.status = 'connected'; this.emitStatus(); + this.emit('connected', payload.session); this.session = payload.session; return resolve(true); } @@ -163,13 +170,13 @@ export class ConnectionImpl this.emit('disconnected'); return reject(payload.version); } + /* istanbul ignore next */ reject(new Error('Unknown message type')); }); }; ws.onclose = () => { clearTimeout(this.retryOptions.retryTimer); - stop?.(); if (this.status === 'closed') { return; } @@ -202,7 +209,7 @@ export class ConnectionImpl WebSocketImpl: WebSocketConstructor, client: DDPClient, retryOptions: RetryOptions = { retryCount: 0, retryTime: 1000 }, - ): Connection { + ): ConnectionImpl { return new ConnectionImpl(url, WebSocketImpl, client, retryOptions); } } diff --git a/packages/ddp-client/src/DDPSDK.ts b/packages/ddp-client/src/DDPSDK.ts index 23576d3092f2..1dacfd81c576 100644 --- a/packages/ddp-client/src/DDPSDK.ts +++ b/packages/ddp-client/src/DDPSDK.ts @@ -1,48 +1,14 @@ import { RestClient } from '@rocket.chat/api-client'; import { ClientStreamImpl } from './ClientStream'; -import type { ClientStream } from './ClientStream'; +import type { ClientStream } from './types/ClientStream'; import type { Connection } from './Connection'; import { ConnectionImpl } from './Connection'; import { DDPDispatcher } from './DDPDispatcher'; import { TimeoutControl } from './TimeoutControl'; import type { Account } from './types/Account'; import { AccountImpl } from './types/Account'; - -/* -* The following procedure is used for streaming data: -* In the original Meteor DDP, Collections were used and publications and subscriptions were used to synchronize data between the client and server. -* However, controlling the `mergebox` can be expensive and doesn't scale well for many clients. -* To address this issue, we are using a specific part of the original implementation of the DDP protocol to send the data directly to the client without using the mergebox. -* This allows the client to receive more data directly from the server, even if the data is the same as before. - -* To maintain compatibility with the original Meteor DDP, we use virtual collections. -* These collections are not real collections, but rather a way to send data to the client. -* They are named with the prefix stream- followed by the name of the stream. -* Instead of storing the data, they simply call the changed method. -* It's up to the application to handle the changed method and use the data it contains. - -* In order for the server to function properly, it is important that it is aware of the 'agreement' and uses the same assumptions. -*/ -export interface SDK { - stream( - name: string, - params: unknown[], - cb: (...data: unknown[]) => void, - ): { - stop: () => void; - ready: () => Promise; - isReady: boolean; - onReady: (cb: () => void) => void; - }; - - connection: Connection; - account: Account; - client: ClientStream; - - timeoutControl: TimeoutControl; - rest: RestClient; -} +import type { SDK } from './types/SDK'; interface PublicationPayloads { collection: string; @@ -71,10 +37,11 @@ export class DDPSDK implements SDK { ) {} stream(name: string, key: unknown, cb: (...data: PublicationPayloads['fields']['args']) => void) { - const { id } = this.client.subscribe(`stream-${name}`, key); + const subscription = this.client.subscribe(`stream-${name}`, key); + const stop = subscription.stop.bind(subscription); const cancel = [ - () => this.client.unsubscribe(id), + () => stop(), this.client.onCollection(`stream-${name}`, (data) => { if (!isValidPayload(data)) { return; @@ -92,14 +59,11 @@ export class DDPSDK implements SDK { }), ]; - return { + return Object.assign(subscription, { stop: () => { cancel.forEach((fn) => fn()); }, - ready: async () => undefined, - isReady: false, - onReady: (_cb: () => void) => void 0, - }; + }); } /** @@ -144,7 +108,7 @@ export class DDPSDK implements SDK { const sdk = new DDPSDK(connection, stream, account, timeoutControl, rest); connection.on('connected', () => { - Object.entries(stream.subscriptions).forEach(([, sub]) => { + [...stream.subscriptions.entries()].forEach(([, sub]) => { ddp.subscribeWithId(sub.id, sub.name, sub.params); }); }); diff --git a/packages/ddp-client/src/README.md b/packages/ddp-client/src/README.md new file mode 100644 index 000000000000..99e313f0dba5 --- /dev/null +++ b/packages/ddp-client/src/README.md @@ -0,0 +1,97 @@ +[rest]: https://rocket.chat/docs/developer-guides/rest-api/ +[api-rest]: ../../api-client/ + + + +# Rocket.Chat Javascript/Typescript SDK + +Library for building Rocket.Chat clients in Javascript/Typescript. + +## Quick Start + +``` +npm install @rocket.chat/sdk --save +``` + +or + +``` +yarn add @rocket.chat/sdk +``` + +This is pretty straightforward, but covers all the basics you need to do! + +```ts +import { DDPSDK } from '@rocket.chat/sdk'; + +const sdk = DDPSDK.create('http://localhost:3000'); + +await sdk.connection.connect(); + +await sdk.accounts.login({ + user: { + username: 'username', + }, + password: 'password', +}); + +await sdk.stream('room-messages', 'GENERAL', (data) => { + console.log('RECEIVED->>', data); +}); + +await sdk.rest.post('chat.postMessage', { + rid: 'GENERAL', + msg: 'Hello World', +}); +``` + +This works out of the box for browsers. if you want to use it on NodeJS, you need to offer a `WebSocket` implementation and a `fetch` implementation. + +We decided to not include any of those dependencies on the SDK, keeping it as lightweight as possible. + +If you are coding in Typescript, which we recommend, you are going to inherit all the types from the Rocket.Chat server, so everything is going to be type safe. + +All types used on the server and original clients are going to be available for you to use. + +if you don't want to use realtime communication, you can use the REST API client directly: `@rocket.chat/api-rest` + +## Overview + +The sdk is implemented based on the following interface definition: + +```ts +export interface SDK { + stream(name: string, params: unknown[], cb: (...data: unknown[]) => void): Publication; + + connection: Connection; + account: Account; + client: ClientStream; + + timeoutControl: TimeoutControl; + rest: RestClient; +} +``` + +Which means that in case of any new feature, bug fix or improvement, you can implement your own SDK variant and use it instead of the default one. + +Each peace contains a set of methods and responsibilities: + +### Connection + +Responsible for the connection to the server, status and connection states. + +### Account + +Responsible for the account management, login, logout, handle credentials, get user information, etc. + +### ClientStream + +Responsible for the DDP communication, method calls, subscriptions, etc. + +### TimeoutControl + +Responsible for the Reconnection control + +### RestClient + +Responsible for the REST API communication for more info [see here][api-rest] diff --git a/packages/ddp-client/src/TimeoutControl.ts b/packages/ddp-client/src/TimeoutControl.ts index 432c9ff1d20d..cda4568f2150 100644 --- a/packages/ddp-client/src/TimeoutControl.ts +++ b/packages/ddp-client/src/TimeoutControl.ts @@ -27,7 +27,7 @@ export class TimeoutControl constructor(readonly timeout: number = 60_000, readonly heartbeat: number = timeout / 2) { super(); - this.reset(); + /* istanbul ignore next */ if (this.heartbeat >= this.timeout) { throw new Error('Heartbeat must be less than timeout'); } @@ -66,6 +66,18 @@ export class TimeoutControl ddp.onMessage(() => timeoutControl.reset()); + connection.on('close', () => { + timeoutControl.stop(); + }); + + connection.on('disconnected', () => { + timeoutControl.stop(); + }); + + connection.on('connected', () => { + timeoutControl.reset(); + }); + return timeoutControl; } } diff --git a/packages/ddp-client/src/legacy/RocketchatSDKLegacy.ts b/packages/ddp-client/src/legacy/RocketchatSDKLegacy.ts index b0ca0922fbb9..fb307b2a83f7 100644 --- a/packages/ddp-client/src/legacy/RocketchatSDKLegacy.ts +++ b/packages/ddp-client/src/legacy/RocketchatSDKLegacy.ts @@ -20,6 +20,7 @@ import { ConnectionImpl } from '../Connection'; import { ClientStreamImpl } from '../ClientStream'; import { AccountImpl } from '../types/Account'; import { TimeoutControl } from '../TimeoutControl'; +import type { ClientStream } from '../types/ClientStream'; declare module '../ClientStream' { interface ClientStream { @@ -35,18 +36,13 @@ declare module '../ClientStream' { } } -declare module '../DDPSDK' { - interface DDPSDK { +declare module '../types/SDK' { + interface SDK { stream>( streamName: N, key: K, callback: (...args: StreamerCallbackArgs) => void, - ): { - stop: () => void; - ready: () => Promise; - isReady: boolean; - onReady: (cb: () => void) => void; - }; + ): ReturnType; } } @@ -190,8 +186,8 @@ export class RocketchatSdkLegacyImpl extends DDPSDK implements RocketchatSDKLega return this.client.callAsync(method, ...args); } - subscribe(topic: string, ...args: any[]): Promise { - return this.client.subscribe(topic, ...args); + subscribe(topic: string, ...args: any[]) { + return this.client.subscribe(topic, ...args).ready(); } subscribeRoom(rid: string): Promise { diff --git a/packages/ddp-client/src/livechat/LivechatClientImpl.ts b/packages/ddp-client/src/livechat/LivechatClientImpl.ts index c8c8b75ea701..91adee9dd4ca 100644 --- a/packages/ddp-client/src/livechat/LivechatClientImpl.ts +++ b/packages/ddp-client/src/livechat/LivechatClientImpl.ts @@ -5,6 +5,7 @@ import { Emitter } from '@rocket.chat/emitter'; import type { DDPDispatchOptions } from '../types/DDPClient'; import type { LivechatClient, LivechatRoomEvents } from './types/LivechatSDK'; import { DDPSDK } from '../DDPSDK'; +import type { ClientStream } from '../types/ClientStream'; declare module '../ClientStream' { interface ClientStream { @@ -26,12 +27,7 @@ declare module '../DDPSDK' { streamName: N, key: K, callback: (...args: StreamerCallbackArgs) => void, - ): { - stop: () => void; - ready: () => Promise; - isReady: boolean; - onReady: (cb: () => void) => void; - }; + ): ReturnType; } } diff --git a/packages/ddp-client/src/types/Account.ts b/packages/ddp-client/src/types/Account.ts index 6bccf8c96dfe..b065e03ad023 100644 --- a/packages/ddp-client/src/types/Account.ts +++ b/packages/ddp-client/src/types/Account.ts @@ -1,6 +1,6 @@ import { Emitter } from '@rocket.chat/emitter'; -import type { ClientStream } from '../ClientStream'; +import type { ClientStream } from './ClientStream'; export interface Account extends Emitter<{ diff --git a/packages/ddp-client/src/types/ClientStream.ts b/packages/ddp-client/src/types/ClientStream.ts new file mode 100644 index 000000000000..a91801afe3f6 --- /dev/null +++ b/packages/ddp-client/src/types/ClientStream.ts @@ -0,0 +1,126 @@ +import type { Emitter } from '@rocket.chat/emitter'; + +import type { PublicationPayloads } from './publicationPayloads'; +import type { DDPDispatchOptions } from '../MinimalDDPClient'; +import type { Subscription } from './Subscription'; + +export interface ClientStream extends Emitter { + /** + * Calls a method on the server. + * @param method - The name of the method to be called. + * @param params - The parameters to be passed to the method. + * @param params.lastArgument - The last argument can be a callback function, the first argument of the callback function is the error, the second argument is the result. + * + * @returns {string} - The id of the method call. + */ + call(method: string, ...params: any[]): string; + + /** + * Calls a method on the server. The same as `call` but with options. + * This options are passed to the dispatcher. The dispatcher is responsible for handling the method call. + * So depending on the dispatcher, the options may or may not be used. By default, the dispatcher only uses the `wait` option. + * One reason to use options is to make the call wait for the result to dispatch other calls. + * Mainly for authentication purposes. + * + * @param method - The name of the method to be called. + * @param options - The options to be passed to the method. + * @param options.wait - If true, the call will wait for the result to dispatch other calls. + * @param params - The parameters to be passed to the method. + * @param params.lastArgument - The last argument can be a callback function, the first argument of the callback function is the error, the second argument is the result. + * @returns {string} - The id of the method call. + **/ + callWithOptions(method: string, options: DDPDispatchOptions, ...params: any[]): string; + + /** + * Calls a method on the server. The same as `call` but returns a promise. + * @param method - The name of the method to be called. + * @param params - The parameters to be passed to the method. + * @returns {Promise} - A promise that resolves when the server returns the result. + * @example + * ```ts + * const result = await ddp.callAsync('login', { + * user: { + * username: 'my-username', + * password: 'my-password', + * }, + * }); + * ``` + */ + callAsync( + method: string, + ...params: any[] + ): Promise & { + id: string; + }; + + /** + * Calls a method on the server. The same as `callWithOptions` but returns a promise. + * @param method - The name of the method to be called. + * @param options - The options to be passed to the method. + * @param options.wait - If true, the call will wait for the result to dispatch other calls. + * @param params - The parameters to be passed to the method. + * @returns {Promise} - A promise that resolves when the server returns the result. + */ + callAsyncWithOptions( + method: string, + options: DDPDispatchOptions, + ...params: any[] + ): Promise & { + id: string; + }; + + /** + * Subscribes to a publication on the server. + * @param name - The name of the publication to subscribe to. + * @param params - The parameters to be passed to the publication. + * @returns {Promise} - A promise that resolves when the server returns the subscription id. + */ + subscribe(name: string, ...params: any[]): Subscription; + /** + * Unsubscribes from a publication on the server. + * @param id - The id of the subscription to unsubscribe from. + * @returns {Promise} - A promise that resolves when the server unsubscribes the subscription. + */ + unsubscribe(id: string): Promise; + /** + * Connects to the server. + * @returns {Promise} - A promise that resolves when the server connects. + * @example + * ```ts + * await ddp.connect(); + * ``` + */ + connect(): Promise; + /** + * Fired when the server send any collection update. + * usually this used after subscribing to a publication. + * @param id - The id/name of the collection. + * @param callback - The callback function to be called when the server sends any collection update. + * @returns {Function} - A function to stop listening for collection updates. + * @example + * ```ts + * const stop = ddp.onCollection('users', (data) => { + * console.log(data); + * }); + * ``` + */ + onCollection(id: string, callback: (data: PublicationPayloads) => void): () => void; + + /** + * The list of subscriptions. + * @type {Map} + * @example + * ```ts + * const subscription = ddp.subscribe('my-subscription'); + * console.log(ddp.subscriptions.get(subscription.id)); + * // prints: + * // { + * // id: 'my-subscription', + * // status: 'loading', + * // name: 'my-subscription', + * // params: [], + * // } + * ``` + */ + subscriptions: Map; +} diff --git a/packages/ddp-client/src/types/SDK.ts b/packages/ddp-client/src/types/SDK.ts new file mode 100644 index 000000000000..32cae49e9856 --- /dev/null +++ b/packages/ddp-client/src/types/SDK.ts @@ -0,0 +1,33 @@ +import type { RestClient } from '@rocket.chat/api-client'; + +import type { ClientStream } from './ClientStream'; +import type { Connection } from '../Connection'; +import type { TimeoutControl } from '../TimeoutControl'; +import type { Account } from './Account'; + +/* +* The following procedure is used for streaming data: +* In the original Meteor DDP, Collections were used and publications and subscriptions were used to synchronize data between the client and server. +* However, controlling the `mergebox` can be expensive and doesn't scale well for many clients. +* To address this issue, we are using a specific part of the original implementation of the DDP protocol to send the data directly to the client without using the mergebox. +* This allows the client to receive more data directly from the server, even if the data is the same as before. + +* To maintain compatibility with the original Meteor DDP, we use virtual collections. +* These collections are not real collections, but rather a way to send data to the client. +* They are named with the prefix stream- followed by the name of the stream. +* Instead of storing the data, they simply call the changed method. +* It's up to the application to handle the changed method and use the data it contains. + +* In order for the server to function properly, it is important that it is aware of the 'agreement' and uses the same assumptions. +*/ + +export interface SDK { + stream(name: string, params: unknown[], cb: (...data: unknown[]) => void): ReturnType; + + connection: Connection; + account: Account; + client: ClientStream; + + timeoutControl: TimeoutControl; + rest: RestClient; +} diff --git a/packages/ddp-client/src/types/Subscription.ts b/packages/ddp-client/src/types/Subscription.ts new file mode 100644 index 000000000000..2dd207e8cb43 --- /dev/null +++ b/packages/ddp-client/src/types/Subscription.ts @@ -0,0 +1,12 @@ +import type { ServerPublicationPayloads } from './publicationPayloads'; + +export interface Subscription { + stop: () => void; + ready: () => Promise; + isReady: boolean; + onChange: (cb: (arg: ServerPublicationPayloads) => void) => void; + id: string; + params: any[]; + name: string; + error?: unknown; +} diff --git a/packages/fuselage-ui-kit/package.json b/packages/fuselage-ui-kit/package.json index 17de7037114f..2e801ea67489 100644 --- a/packages/fuselage-ui-kit/package.json +++ b/packages/fuselage-ui-kit/package.json @@ -70,7 +70,7 @@ "@storybook/builder-webpack5": "~6.5.15", "@storybook/manager-webpack5": "~6.5.15", "@storybook/react": "~6.5.15", - "@storybook/source-loader": "~6.5.15", + "@storybook/source-loader": "~6.5.16", "@storybook/theming": "~6.5.15", "@tanstack/react-query": "^4.16.1", "@types/react": "~17.0.57", diff --git a/packages/gazzodown/jest.config.ts b/packages/gazzodown/jest.config.ts index 4f4e7697b108..48bf45c0003e 100644 --- a/packages/gazzodown/jest.config.ts +++ b/packages/gazzodown/jest.config.ts @@ -3,6 +3,27 @@ export default { errorOnDeprecated: true, testEnvironment: 'jsdom', modulePathIgnorePatterns: ['/dist/'], + transform: { + '^.+\\.(ts|tsx)$': [ + '@swc/jest', + { + sourceMaps: true, + jsc: { + parser: { + syntax: 'typescript', + tsx: true, + decorators: false, + dynamicImport: false, + }, + transform: { + react: { + runtime: 'automatic', + }, + }, + }, + }, + ], + }, moduleNameMapper: { '\\.css$': 'identity-obj-proxy', }, diff --git a/packages/gazzodown/package.json b/packages/gazzodown/package.json index 040eb0ade531..82d8f4d29358 100644 --- a/packages/gazzodown/package.json +++ b/packages/gazzodown/package.json @@ -13,15 +13,17 @@ "@rocket.chat/styled": "next", "@rocket.chat/ui-client": "workspace:^", "@rocket.chat/ui-contexts": "workspace:^", - "@storybook/addon-actions": "~6.5.15", + "@storybook/addon-actions": "~6.5.16", "@storybook/addon-docs": "~6.5.15", "@storybook/addon-essentials": "~6.5.15", "@storybook/addon-interactions": "~6.5.15", "@storybook/addon-links": "~6.5.15", "@storybook/builder-webpack4": "~6.5.15", - "@storybook/manager-webpack4": "~6.5.15", + "@storybook/manager-webpack4": "~6.5.16", "@storybook/react": "~6.5.15", "@storybook/testing-library": "~0.0.13", + "@swc/core": "^1.3.60", + "@swc/jest": "^0.2.26", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "~12.1.5", "@types/babel__core": "^7.1.20", @@ -49,6 +51,7 @@ "scripts": { "lint": "eslint --ext .js,.jsx,.ts,.tsx .", "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", + "testunit": "jest", "test": "jest", "build": "rm -rf dist && tsc -p tsconfig.json", "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput", diff --git a/packages/gazzodown/src/mentions/ChannelMentionElement.tsx b/packages/gazzodown/src/mentions/ChannelMentionElement.tsx index 4dc61a43f731..ad74a2b33463 100644 --- a/packages/gazzodown/src/mentions/ChannelMentionElement.tsx +++ b/packages/gazzodown/src/mentions/ChannelMentionElement.tsx @@ -1,3 +1,4 @@ +import { Message } from '@rocket.chat/fuselage'; import { memo, ReactElement, useContext, useMemo } from 'react'; import { MarkupInteractionContext } from '../MarkupInteractionContext'; @@ -17,9 +18,9 @@ const ChannelMentionElement = ({ mention }: ChannelMentionElementProps): ReactEl } return ( - - #{resolved.name ?? mention} - + + {resolved.name ?? mention} + ); }; diff --git a/packages/gazzodown/src/mentions/UserMentionElement.tsx b/packages/gazzodown/src/mentions/UserMentionElement.tsx index 2f6f0ef2996e..fec37b85973c 100644 --- a/packages/gazzodown/src/mentions/UserMentionElement.tsx +++ b/packages/gazzodown/src/mentions/UserMentionElement.tsx @@ -1,3 +1,4 @@ +import { Message } from '@rocket.chat/fuselage'; import { useLayout, useSetting, useUserId } from '@rocket.chat/ui-contexts'; import { memo, ReactElement, useContext, useMemo } from 'react'; @@ -18,11 +19,19 @@ const UserMentionElement = ({ mention }: UserMentionElementProps): ReactElement const showRealName = useSetting('UI_Use_Real_Name') && !isMobile; if (mention === 'all') { - return all; + return ( + + all + + ); } if (mention === 'here') { - return here; + return ( + + here + + ); } if (!resolved) { @@ -30,14 +39,16 @@ const UserMentionElement = ({ mention }: UserMentionElementProps): ReactElement } return ( - {(showRealName ? resolved.name : resolved.username) ?? mention} - + ); }; diff --git a/packages/livechat/package.json b/packages/livechat/package.json index 72dcecb3e139..453584fae0c5 100644 --- a/packages/livechat/package.json +++ b/packages/livechat/package.json @@ -29,12 +29,12 @@ "@rocket.chat/eslint-config": "workspace:^", "@rocket.chat/fuselage-tokens": "next", "@rocket.chat/logo": "next", - "@storybook/addon-actions": "~6.5.15", + "@storybook/addon-actions": "~6.5.16", "@storybook/addon-backgrounds": "~6.5.15", "@storybook/addon-essentials": "~6.5.15", "@storybook/addon-knobs": "~6.4.0", "@storybook/addon-postcss": "~2.0.0", - "@storybook/addon-viewport": "~6.5.15", + "@storybook/addon-viewport": "~6.5.16", "@storybook/react": "~6.5.15", "@storybook/theming": "~6.5.15", "@typescript-eslint/eslint-plugin": "^5.30.7", diff --git a/packages/rest-typings/src/v1/omnichannel.ts b/packages/rest-typings/src/v1/omnichannel.ts index a15e87ae7d7f..82a9f23ba225 100644 --- a/packages/rest-typings/src/v1/omnichannel.ts +++ b/packages/rest-typings/src/v1/omnichannel.ts @@ -138,8 +138,8 @@ export const isLivechatDepartmentDepartmentIdAgentsGETProps = ajv.compile = forwardRef(function ToolBoxAction( - { id, icon, color, action, className, index, title, 'data-tooltip': tooltip, ...props }, +const ToolBoxAction = forwardRef(function ToolBoxAction( + { id, icon, action, index, title, 'data-tooltip': tooltip, ...props }, ref, ) { return ( action(id)} data-toolbox={index} key={id} icon={icon} - position='relative' tiny - overflow='visible' - ref={ref} - color={!!color && color} - {...{ ...props, ...(tooltip ? { 'data-tooltip': tooltip, 'title': '' } : { title }) }} + {...(tooltip ? { 'data-tooltip': tooltip, 'title': '' } : { title })} + {...props} /> ); }); diff --git a/packages/ui-composer/package.json b/packages/ui-composer/package.json index 45b29a2cd1e3..7845b2ea2699 100644 --- a/packages/ui-composer/package.json +++ b/packages/ui-composer/package.json @@ -7,11 +7,11 @@ "@rocket.chat/eslint-config": "workspace:^", "@rocket.chat/fuselage": "next", "@rocket.chat/icons": "next", - "@storybook/addon-actions": "~6.5.15", + "@storybook/addon-actions": "~6.5.16", "@storybook/addon-docs": "~6.5.15", "@storybook/addon-essentials": "~6.5.15", "@storybook/builder-webpack4": "~6.5.15", - "@storybook/manager-webpack4": "~6.5.15", + "@storybook/manager-webpack4": "~6.5.16", "@storybook/react": "~6.5.15", "@storybook/testing-library": "~0.0.13", "@types/babel__core": "^7.1.20", diff --git a/packages/ui-contexts/src/ServerContext/ServerContext.ts b/packages/ui-contexts/src/ServerContext/ServerContext.ts index cc0a96f368cb..7c8ad7ff486e 100644 --- a/packages/ui-contexts/src/ServerContext/ServerContext.ts +++ b/packages/ui-contexts/src/ServerContext/ServerContext.ts @@ -2,6 +2,7 @@ import type { IServerInfo, Serialized } from '@rocket.chat/core-typings'; import type { Method, OperationParams, OperationResult, PathFor, PathPattern, UrlParams } from '@rocket.chat/rest-typings'; import { createContext } from 'react'; +import type { StreamKeys, StreamNames, StreamerCallbackArgs } from './streams'; import type { ServerMethodName, ServerMethodParameters, ServerMethodReturn } from './methods'; export type UploadResult = { @@ -31,20 +32,20 @@ export type ServerContextValue = { | { promise: Promise; }; - getStream: ( - streamName: string, - options?: { + getStream: >( + streamName: N, + _options?: { retransmit?: boolean | undefined; retransmitToSelf?: boolean | undefined; }, - ) => (eventName: string, callback: (...event: TEvent) => void) => () => void; - getSingleStream: ( - streamName: string, - options?: { + ) => (eventName: K, callback: (...args: StreamerCallbackArgs) => void) => () => void; + getSingleStream: >( + streamName: N, + _options?: { retransmit?: boolean | undefined; retransmitToSelf?: boolean | undefined; }, - ) => (eventName: string, callback: (...event: TEvent) => void) => () => void; + ) => (eventName: K, callback: (...args: StreamerCallbackArgs) => void) => () => void; }; export const ServerContext = createContext({ diff --git a/packages/ui-contexts/src/ServerContext/streams.ts b/packages/ui-contexts/src/ServerContext/streams.ts index 036941a983d5..6165e15708d6 100644 --- a/packages/ui-contexts/src/ServerContext/streams.ts +++ b/packages/ui-contexts/src/ServerContext/streams.ts @@ -297,7 +297,7 @@ export interface StreamerEvents { }, ]; - 'user-presence': [{ key: string; args: [username: string, statusChanged?: 0 | 1 | 2 | 3, statusText?: string] }]; + 'user-presence': [{ key: string; args: [[username: string, statusChanged?: 0 | 1 | 2 | 3, statusText?: string]] }]; // TODO: rename to 'integration-history' 'integrationHistory': [ diff --git a/packages/ui-contexts/src/hooks/useStream.ts b/packages/ui-contexts/src/hooks/useStream.ts index 32d403fb0a29..6aef7d9a0191 100644 --- a/packages/ui-contexts/src/hooks/useStream.ts +++ b/packages/ui-contexts/src/hooks/useStream.ts @@ -24,15 +24,7 @@ export function useStream( retransmit?: boolean; retransmitToSelf?: boolean; }, -): StreamerCallback; - -export function useStream( - streamName: string, - options?: { - retransmit?: boolean | undefined; - retransmitToSelf?: boolean | undefined; - }, -): (eventName: string, callback: (...event: unknown[]) => void) => () => void { +): StreamerCallback { const { getStream } = useContext(ServerContext); return useMemo(() => getStream(streamName, options), [getStream, streamName, options]); } @@ -50,14 +42,7 @@ export function useSingleStream( retransmit?: boolean; retransmitToSelf?: boolean; }, -): StreamerCallback; -export function useSingleStream( - streamName: N, - options?: { - retransmit?: boolean | undefined; - retransmitToSelf?: boolean | undefined; - }, -): (eventName: string, callback: (...event: unknown[]) => void) => () => void { +): StreamerCallback { const { getSingleStream } = useContext(ServerContext); return useMemo(() => getSingleStream(streamName, options), [getSingleStream, streamName, options]); } diff --git a/packages/ui-video-conf/package.json b/packages/ui-video-conf/package.json index e132706e54be..4ab2ae0ee4a9 100644 --- a/packages/ui-video-conf/package.json +++ b/packages/ui-video-conf/package.json @@ -11,11 +11,11 @@ "@rocket.chat/icons": "next", "@rocket.chat/styled": "next", "@rocket.chat/ui-contexts": "workspace:^", - "@storybook/addon-actions": "~6.5.15", + "@storybook/addon-actions": "~6.5.16", "@storybook/addon-docs": "~6.5.15", "@storybook/addon-essentials": "~6.5.15", "@storybook/builder-webpack4": "~6.5.15", - "@storybook/manager-webpack4": "~6.5.15", + "@storybook/manager-webpack4": "~6.5.16", "@storybook/react": "~6.5.15", "@storybook/testing-library": "~0.0.13", "@types/babel__core": "^7.1.20", diff --git a/packages/uikit-playground/.eslintignore b/packages/uikit-playground/.eslintignore new file mode 100644 index 000000000000..8a42e7e798be --- /dev/null +++ b/packages/uikit-playground/.eslintignore @@ -0,0 +1,18 @@ +/dist +/build +/node_modules +/storybook-static +!/.jest +!/.storybook +/.storybook/jest-results.json + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/packages/uikit-playground/.eslintrc.cjs b/packages/uikit-playground/.eslintrc.cjs new file mode 100644 index 000000000000..4020bcbf409d --- /dev/null +++ b/packages/uikit-playground/.eslintrc.cjs @@ -0,0 +1,14 @@ +module.exports = { + env: { browser: true, es2020: true }, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:react-hooks/recommended', + ], + parser: '@typescript-eslint/parser', + parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, + plugins: ['react-refresh'], + rules: { + 'react-refresh/only-export-components': 'warn', + }, +} diff --git a/packages/uikit-playground/.gitignore b/packages/uikit-playground/.gitignore new file mode 100644 index 000000000000..a547bf36d8d1 --- /dev/null +++ b/packages/uikit-playground/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/packages/uikit-playground/index.html b/packages/uikit-playground/index.html new file mode 100644 index 000000000000..7b1100bf1387 --- /dev/null +++ b/packages/uikit-playground/index.html @@ -0,0 +1,13 @@ + + + + + + + UiKit-Playground + + +
+ + + diff --git a/packages/uikit-playground/package.json b/packages/uikit-playground/package.json new file mode 100644 index 000000000000..d07effed2a73 --- /dev/null +++ b/packages/uikit-playground/package.json @@ -0,0 +1,54 @@ +{ + "name": "@rocket.chat/uikit-playground", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview" + }, + "dependencies": { + "@codemirror/lang-javascript": "^6.1.8", + "@codemirror/lang-json": "^6.0.1", + "@codemirror/tooltip": "^0.19.16", + "@lezer/highlight": "^1.1.4", + "@rocket.chat/css-in-js": "^0.31.12", + "@rocket.chat/fuselage": "next", + "@rocket.chat/fuselage-hooks": "next", + "@rocket.chat/fuselage-polyfills": "next", + "@rocket.chat/fuselage-tokens": "next", + "@rocket.chat/fuselage-ui-kit": "workspace:~", + "@rocket.chat/icons": "next", + "@rocket.chat/logo": "next", + "@rocket.chat/styled": "next", + "@rocket.chat/ui-contexts": "workspace:~", + "codemirror": "^6.0.1", + "eslint4b-prebuilt": "^6.7.2", + "react": "^17.0.2", + "react-beautiful-dnd": "^13.1.1", + "react-dom": "^17.0.2", + "react-router-dom": "^6.11.2", + "react-split-pane": "^0.1.92", + "react-virtuoso": "^4.3.7", + "use-subscription": "^1.8.0" + }, + "devDependencies": { + "@types/react": "^17.0.57", + "@types/react-beautiful-dnd": "^13", + "@types/react-dom": "^17.0.19", + "@types/use-subscription": "^1", + "@typescript-eslint/eslint-plugin": "^5.57.1", + "@typescript-eslint/parser": "^5.57.1", + "@vitejs/plugin-react": "^4.0.0", + "eslint": "^8.38.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.3.4", + "typescript": "^5.0.2", + "vite": "^4.3.2" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/packages/uikit-playground/public/vite.svg b/packages/uikit-playground/public/vite.svg new file mode 100644 index 000000000000..e7b8dfb1b2a6 --- /dev/null +++ b/packages/uikit-playground/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/uikit-playground/src/App.css b/packages/uikit-playground/src/App.css new file mode 100644 index 000000000000..10e4996c0f9a --- /dev/null +++ b/packages/uikit-playground/src/App.css @@ -0,0 +1,39 @@ +.App { + text-align: center; +} + +.App-logo { + height: 40vmin; + pointer-events: none; +} + +@media (prefers-reduced-motion: no-preference) { + .App-logo { + animation: App-logo-spin infinite 20s linear; + } +} + +.App-header { + background-color: #282c34; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: calc(10px + 2vmin); + color: white; +} + +.App-link { + color: #61dafb; +} + +@keyframes App-logo-spin { + from { + transform: rotate(0deg); + } + + to { + transform: rotate(360deg); + } +} diff --git a/packages/uikit-playground/src/App.tsx b/packages/uikit-playground/src/App.tsx new file mode 100644 index 000000000000..750c4df4795f --- /dev/null +++ b/packages/uikit-playground/src/App.tsx @@ -0,0 +1,18 @@ +import './App.css'; +import './cssVariables.css'; +import { BrowserRouter, Routes, Route } from 'react-router-dom'; + +import Playground from './Pages/Playground'; + +function App() { + return ( + + + } /> + } /> + + + ); +} + +export default App; diff --git a/packages/uikit-playground/src/Components/CodeEditor/Extensions/HighlightStyle.ts b/packages/uikit-playground/src/Components/CodeEditor/Extensions/HighlightStyle.ts new file mode 100644 index 000000000000..3329c3c6a859 --- /dev/null +++ b/packages/uikit-playground/src/Components/CodeEditor/Extensions/HighlightStyle.ts @@ -0,0 +1,14 @@ +import { HighlightStyle, syntaxHighlighting } from '@codemirror/language'; +import { tags as t } from '@lezer/highlight'; + +const highLightStyle = () => { + const style = HighlightStyle.define([ + { tag: t.literal, color: 'var(--RCPG-primary-color)' }, + { tag: t.bool, color: 'var(--RCPG-tertary-color)' }, + { tag: t.number, color: 'var(--RCPG-secondary-color)' }, + ]); + + return syntaxHighlighting(style); +}; + +export default highLightStyle(); diff --git a/packages/uikit-playground/src/Components/CodeEditor/Extensions/basicSetup.ts b/packages/uikit-playground/src/Components/CodeEditor/Extensions/basicSetup.ts new file mode 100644 index 000000000000..e5973a792217 --- /dev/null +++ b/packages/uikit-playground/src/Components/CodeEditor/Extensions/basicSetup.ts @@ -0,0 +1,59 @@ +import { + completionKeymap, + closeBrackets, + closeBracketsKeymap, +} from '@codemirror/autocomplete'; +import { + defaultKeymap, + history, + historyKeymap, + indentWithTab, +} from '@codemirror/commands'; +import { + defaultHighlightStyle, + syntaxHighlighting, + indentOnInput, + bracketMatching, + foldGutter, + foldKeymap, +} from '@codemirror/language'; +import { lintKeymap } from '@codemirror/lint'; +import { searchKeymap, highlightSelectionMatches } from '@codemirror/search'; +import type { Extension } from '@codemirror/state'; +import { + keymap, + drawSelection, + dropCursor, + rectangularSelection, + crosshairCursor, + lineNumbers, + EditorView, +} from '@codemirror/view'; + +const basicSetup: Extension = (() => [ + lineNumbers(), + history(), + foldGutter(), + drawSelection(), + dropCursor(), + indentOnInput(), + EditorView.lineWrapping, + syntaxHighlighting(defaultHighlightStyle, { fallback: true }), + bracketMatching(), + closeBrackets(), + rectangularSelection(), + crosshairCursor(), + highlightSelectionMatches(), + keymap.of([ + ...closeBracketsKeymap, + ...defaultKeymap, + ...searchKeymap, + ...historyKeymap, + ...foldKeymap, + ...completionKeymap, + ...lintKeymap, + indentWithTab, + ]), +])(); + +export default basicSetup; diff --git a/packages/uikit-playground/src/Components/CodeEditor/Extensions/index.ts b/packages/uikit-playground/src/Components/CodeEditor/Extensions/index.ts new file mode 100644 index 000000000000..9dda5b45b58d --- /dev/null +++ b/packages/uikit-playground/src/Components/CodeEditor/Extensions/index.ts @@ -0,0 +1,10 @@ +import { javascript } from '@codemirror/lang-javascript'; + +import highlightStyle from './HighlightStyle'; +import basicSetup from './basicSetup'; +import lint from './lint'; +import theme from './theme'; + +const extensions = [highlightStyle, javascript(), lint, basicSetup, ...theme]; + +export default extensions; diff --git a/packages/uikit-playground/src/Components/CodeEditor/Extensions/lint.ts b/packages/uikit-playground/src/Components/CodeEditor/Extensions/lint.ts new file mode 100644 index 000000000000..d8eb7870f0ec --- /dev/null +++ b/packages/uikit-playground/src/Components/CodeEditor/Extensions/lint.ts @@ -0,0 +1,5 @@ +import { esLint } from '@codemirror/lang-javascript'; +import { lintGutter, linter } from '@codemirror/lint'; +import Linter from 'eslint4b-prebuilt'; + +export default [lintGutter(), linter(esLint(new Linter()))]; diff --git a/packages/uikit-playground/src/Components/CodeEditor/Extensions/theme.ts b/packages/uikit-playground/src/Components/CodeEditor/Extensions/theme.ts new file mode 100644 index 000000000000..71938add617f --- /dev/null +++ b/packages/uikit-playground/src/Components/CodeEditor/Extensions/theme.ts @@ -0,0 +1,41 @@ +import type { Extension } from '@codemirror/state'; +import { EditorView } from '@codemirror/view'; + +const gutters: Extension = EditorView.theme({ + '.cm-gutters': { + backgroundColor: 'transparent', + border: 'none', + userSelect: 'none', + minWidth: '32px', + display: 'flex', + justifyContent: 'flex-end', + }, + + '.cm-activeLineGutter': { + backgroundColor: 'transparent', + }, +}); + +const selection: Extension = EditorView.theme({ + '.cm-selectionBackground': { + backgroundColor: 'var(--RCPG-secondary-color) !important', + opacity: 0.3, + }, + + '.cm-selectionMatch': { + backgroundColor: '#74808930 !important', + }, + + '.cm-matchingBracket': { + backgroundColor: 'transparent !important', + border: '1px solid #1d74f580', + }, +}); + +const line: Extension = EditorView.theme({ + '.cm-activeLine': { + backgroundColor: 'transparent !important', + }, +}); + +export default [gutters, selection, line] as const; diff --git a/packages/uikit-playground/src/Components/CodeEditor/index.tsx b/packages/uikit-playground/src/Components/CodeEditor/index.tsx new file mode 100644 index 000000000000..781c00c4821a --- /dev/null +++ b/packages/uikit-playground/src/Components/CodeEditor/index.tsx @@ -0,0 +1,75 @@ +import type { Extension } from '@codemirror/state'; +import { Box } from '@rocket.chat/fuselage'; +import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; +import json5 from 'json5'; +import { useEffect, useContext } from 'react'; + +import { context } from '../../Context'; +import { docAction } from '../../Context/action'; +import useCodeMirror from '../../hooks/useCodeMirror'; +import codePrettier from '../../utils/codePrettier'; + +type CodeMirrorProps = { + extensions?: Extension[]; +}; + +const CodeEditor = ({ extensions }: CodeMirrorProps) => { + const { state, dispatch } = useContext(context); + const { editor, changes, setValue } = useCodeMirror( + extensions, + json5.stringify(state.doc.payload, undefined, 4) + ); + const debounceValue = useDebouncedValue(changes?.value, 500); + + useEffect(() => { + console.log('a'); + if (!changes?.isDispatch) { + try { + const parsedCode = json5.parse(changes.value); + dispatch( + docAction({ + payload: parsedCode, + changedByEditor: false, + }) + ); + + dispatch(docAction({ payload: parsedCode })); + } catch (e) { + console.log(e); + // do nothing + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [changes?.value]); + + useEffect(() => { + console.log('b'); + if (!changes?.isDispatch) { + try { + const prettierCode = codePrettier(changes.value, changes.cursor); + setValue(prettierCode.formatted, { + cursor: prettierCode.cursorOffset, + }); + } catch (e) { + // do nothing + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [debounceValue]); + + useEffect(() => { + console.log('c'); + if (!state.doc.changedByEditor) { + setValue(JSON.stringify(state.doc.payload, undefined, 4), {}); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [state.doc.payload]); + + return ( + <> + + + ); +}; + +export default CodeEditor; diff --git a/packages/uikit-playground/src/Components/ComponentSideBar/ScrollableSideBar.tsx b/packages/uikit-playground/src/Components/ComponentSideBar/ScrollableSideBar.tsx new file mode 100644 index 000000000000..1c75c93cbf6a --- /dev/null +++ b/packages/uikit-playground/src/Components/ComponentSideBar/ScrollableSideBar.tsx @@ -0,0 +1,24 @@ +import { css } from '@rocket.chat/css-in-js'; +import { Scrollable, Box } from '@rocket.chat/fuselage'; +import type { FC } from 'react'; + +import BlocksTree from '../../Payload/BlocksTree'; +import DropDown from '../DropDown'; + +const ScrollableSideBar: FC = () => ( + + + + + +); + +export default ScrollableSideBar; diff --git a/packages/uikit-playground/src/Components/ComponentSideBar/SideBar.tsx b/packages/uikit-playground/src/Components/ComponentSideBar/SideBar.tsx new file mode 100644 index 000000000000..0cf71a6a159b --- /dev/null +++ b/packages/uikit-playground/src/Components/ComponentSideBar/SideBar.tsx @@ -0,0 +1,46 @@ +import { css } from '@rocket.chat/css-in-js'; +import { Box } from '@rocket.chat/fuselage'; +import type { FC } from 'react'; +import { useEffect, useContext } from 'react'; + +import { context } from '../../Context'; +import { sidebarToggleAction } from '../../Context/action'; +import ScrollableSideBar from './ScrollableSideBar'; +import SliderBtn from './SliderBtn'; + +const SideBar: FC = () => { + const { state, dispatch } = useContext(context); + + useEffect(() => { + dispatch(sidebarToggleAction(false)); + }, [state?.isMobile, dispatch]); + + const slide = state?.isMobile + ? css` + width: 100%; + user-select: none; + transform: translateX(${state?.sideBarToggle ? '0' : '-100%'}); + transition: var(--animation-default); + ` + : css` + width: var(--sidebar-width); + user-select: none; + transition: var(--animation-default); + `; + + return ( + + + + + ); +}; + +export default SideBar; diff --git a/packages/uikit-playground/src/Components/ComponentSideBar/SliderBtn.tsx b/packages/uikit-playground/src/Components/ComponentSideBar/SliderBtn.tsx new file mode 100644 index 000000000000..a328ee395074 --- /dev/null +++ b/packages/uikit-playground/src/Components/ComponentSideBar/SliderBtn.tsx @@ -0,0 +1,116 @@ +import { css } from '@rocket.chat/css-in-js'; +import { Box, Label } from '@rocket.chat/fuselage'; +import type { FC } from 'react'; +import { useContext } from 'react'; + +import { context } from '../../Context'; +import { sidebarToggleAction } from '../../Context/action'; + +const SliderBtn: FC = () => { + const { + state: { sideBarToggle, isMobile }, + dispatch, + } = useContext(context); + const slideBtnAnimation = sideBarToggle + ? css` + clip-path: polygon( + 10% 0, + 50% 40%, + 90% 0, + 100% 10%, + 60% 50%, + 100% 90%, + 90% 100%, + 50% 60%, + 10% 100%, + 0 90%, + 40% 50%, + 0 10% + ); + cursor: pointer; + transition: var(--animation-default); + ` + : css` + clip-path: polygon( + 32% 35%, + 32% 35%, + 79% 0, + 87% 10%, + 32% 50%, + 87% 90%, + 79% 100%, + 32% 64%, + 32% 65%, + 13% 50%, + 13% 50%, + 13% 50% + ); + transform: rotate(180deg); + transition: var(--animation-default); + `; + + const toggleStyle = !isMobile + ? css` + left: 0px; + ` + : sideBarToggle + ? css` + right: 0; + transition: var(--animation-default); + ` + : css` + right: 0; + transform: translateX(100%); + cursor: pointer; + transition: var(--animation-default); + `; + + return ( + + !sideBarToggle && dispatch(sidebarToggleAction(!sideBarToggle)) + } + zIndex={1} + className={toggleStyle} + > + + {isMobile && ( + + sideBarToggle && dispatch(sidebarToggleAction(!sideBarToggle)) + } + className={css` + cursor: pointer; + `} + > + + + )} + + ); +}; + +export default SliderBtn; diff --git a/packages/uikit-playground/src/Components/ComponentSideBar/index.tsx b/packages/uikit-playground/src/Components/ComponentSideBar/index.tsx new file mode 100644 index 000000000000..c90236635b47 --- /dev/null +++ b/packages/uikit-playground/src/Components/ComponentSideBar/index.tsx @@ -0,0 +1 @@ +export { default } from './SideBar'; diff --git a/packages/uikit-playground/src/Components/Draggable/DraggableList.tsx b/packages/uikit-playground/src/Components/Draggable/DraggableList.tsx new file mode 100644 index 000000000000..76e7af15538d --- /dev/null +++ b/packages/uikit-playground/src/Components/Draggable/DraggableList.tsx @@ -0,0 +1,51 @@ +import type { LayoutBlock } from '@rocket.chat/ui-kit'; +import * as React from 'react'; +import type { OnDragEndResponder } from 'react-beautiful-dnd'; +import { DragDropContext, Droppable } from 'react-beautiful-dnd'; + +import DraggableListItem from './DraggableListItem'; + +export type Block = { + id: string; + payload: LayoutBlock; +}; + +export type DraggableListProps = { + blocks: Block[]; + surface?: number; + onDragEnd: OnDragEndResponder; +}; + +const DraggableList = React.memo( + ({ blocks, surface, onDragEnd }: DraggableListProps) => ( + <> + + <> + + {(provided) => ( +
+ <> + {blocks.map((block, index) => ( + + ))} + {provided.placeholder} + +
+ )} +
+ +
+ + ) +); + +export default DraggableList; diff --git a/packages/uikit-playground/src/Components/Draggable/DraggableListItem.tsx b/packages/uikit-playground/src/Components/Draggable/DraggableListItem.tsx new file mode 100644 index 000000000000..d06d2769f15e --- /dev/null +++ b/packages/uikit-playground/src/Components/Draggable/DraggableListItem.tsx @@ -0,0 +1,34 @@ +import { Draggable } from 'react-beautiful-dnd'; + +import RenderPayload from '../Preview/Display/RenderPayload/RenderPayload'; +import type { Block } from './DraggableList'; + +export type DraggableListItemProps = { + block: Block; + surface: number; + index: number; +}; + +const DraggableListItem = ({ + block, + surface, + index, +}: DraggableListItemProps) => ( + + {(provided) => ( +
+ +
+ )} +
+); + +export default DraggableListItem; diff --git a/packages/uikit-playground/src/Components/DropDown/DropDown.tsx b/packages/uikit-playground/src/Components/DropDown/DropDown.tsx new file mode 100644 index 000000000000..dd6470a4d632 --- /dev/null +++ b/packages/uikit-playground/src/Components/DropDown/DropDown.tsx @@ -0,0 +1,34 @@ +import { Box } from '@rocket.chat/fuselage'; +import { Fragment } from 'react'; + +import Items from './Items'; +import type { Item, ItemBranch } from './types'; + +interface DropDownProps { + readonly BlocksTree: Item; +} + +const DropDown = ({ BlocksTree }: DropDownProps) => { + const layer = 1; + + const recursiveComponentTree = (branch: ItemBranch, layer: number) => ( + + {branch.branches && + branch.branches.map((branch: ItemBranch, index: number) => ( + + {recursiveComponentTree(branch, layer + 1)} + + ))} + + ); + + return ( + + {BlocksTree.map((branch: ItemBranch, i: number) => ( + {recursiveComponentTree(branch, layer)} + ))} + + ); +}; + +export default DropDown; diff --git a/packages/uikit-playground/src/Components/DropDown/Items.tsx b/packages/uikit-playground/src/Components/DropDown/Items.tsx new file mode 100644 index 000000000000..f37822c564bc --- /dev/null +++ b/packages/uikit-playground/src/Components/DropDown/Items.tsx @@ -0,0 +1,66 @@ +import { css } from '@rocket.chat/css-in-js'; +import { Box, Label, Chevron } from '@rocket.chat/fuselage'; +import { useState, useContext } from 'react'; + +import { context } from '../../Context'; +import ItemsIcon from './ItemsIcon'; +import { itemStyle, labelStyle } from './itemsStyle'; +import type { ItemProps } from './types'; +import { docAction } from '../../Context/action'; + +const Items = ({ label, children, layer, payload }: ItemProps) => { + const [isOpen, toggleItemOpen] = useState(layer === 1); + const [hover, setHover] = useState(false); + const { state, dispatch } = useContext(context); + + const itemClickHandler = () => { + toggleItemOpen(!isOpen); + payload && + dispatch( + docAction({ + payload: [...state.doc.payload, payload[0]], + changedByEditor: false, + }) + ); + }; + + return ( + + setHover(true)} + onMouseLeave={() => setHover(false)} + onClick={itemClickHandler} + > + + {children && children.length > 0 && ( + + + + )} + + + + + + + {isOpen && children} + + ); +}; + +export default Items; diff --git a/packages/uikit-playground/src/Components/DropDown/ItemsIcon.tsx b/packages/uikit-playground/src/Components/DropDown/ItemsIcon.tsx new file mode 100644 index 000000000000..9a1d74f3f1f1 --- /dev/null +++ b/packages/uikit-playground/src/Components/DropDown/ItemsIcon.tsx @@ -0,0 +1,28 @@ +import { Icon } from '@rocket.chat/fuselage'; + +const ItemsIcon = ({ + layer, + lastNode, + hover, +}: { + layer: number; + lastNode: boolean; + hover: boolean; +}) => { + const selectIcon = (layer: number, hover: boolean) => { + if (layer === 1) { + return ( + + ); + } + if (lastNode) { + return ; + } + return ( + + ); + }; + return <>{selectIcon(layer, hover)}; +}; + +export default ItemsIcon; diff --git a/packages/uikit-playground/src/Components/DropDown/index.tsx b/packages/uikit-playground/src/Components/DropDown/index.tsx new file mode 100644 index 000000000000..d76df1b0ff42 --- /dev/null +++ b/packages/uikit-playground/src/Components/DropDown/index.tsx @@ -0,0 +1 @@ +export { default } from './DropDown'; diff --git a/packages/uikit-playground/src/Components/DropDown/itemsStyle.ts b/packages/uikit-playground/src/Components/DropDown/itemsStyle.ts new file mode 100644 index 000000000000..da9c9ff0b2c5 --- /dev/null +++ b/packages/uikit-playground/src/Components/DropDown/itemsStyle.ts @@ -0,0 +1,45 @@ +import { css } from '@rocket.chat/css-in-js'; + +export const itemStyle = (layer: number, hover: boolean) => { + const style = css` + cursor: pointer; + padding-left: ${10 + (layer - 1) * 16}px; + background-color: ${hover ? 'var(--RCPG-primary-color)' : 'transparent'}; + `; + return style; +}; + +export const labelStyle = (layer: number, hover: boolean) => { + let customStyle; + const basicStyle = css` + cursor: pointer !important; + padding-left: 4px !important; + `; + switch (layer) { + case 1: + customStyle = css` + font-weight: 700; + font-size: 14px; + letter-spacing: 0.3px; + color: ${hover ? '#fff' : '#999'}; + text-transform: uppercase; + `; + break; + case 2: + customStyle = css` + letter-spacing: 0.1px; + font-size: 12px; + color: ${hover ? '#fff' : '#555'}; + text-transform: capitalize; + `; + break; + default: + customStyle = css` + font-size: 12px; + color: ${hover ? '#fff' : '#555'}; + text-transform: capitalize; + `; + break; + } + return [customStyle, basicStyle]; +}; diff --git a/packages/uikit-playground/src/Components/DropDown/types.ts b/packages/uikit-playground/src/Components/DropDown/types.ts new file mode 100644 index 000000000000..3b0c505d38f9 --- /dev/null +++ b/packages/uikit-playground/src/Components/DropDown/types.ts @@ -0,0 +1,16 @@ +import type { LayoutBlock } from '@rocket.chat/ui-kit'; + +export type ItemProps = { + label: string; + layer: number; + payload?: readonly LayoutBlock[]; + children?: ReadonlyArray; +}; + +export type ItemBranch = { + label: string; + branches?: Item; + payload?: readonly LayoutBlock[]; +}; + +export type Item = ItemBranch[]; diff --git a/packages/uikit-playground/src/Components/NavBar/BurgerIcon/BurgerIcon.tsx b/packages/uikit-playground/src/Components/NavBar/BurgerIcon/BurgerIcon.tsx new file mode 100644 index 000000000000..af02130ca4e0 --- /dev/null +++ b/packages/uikit-playground/src/Components/NavBar/BurgerIcon/BurgerIcon.tsx @@ -0,0 +1,25 @@ +import { usePrefersReducedMotion } from '@rocket.chat/fuselage-hooks'; +import type { ReactElement, ReactNode } from 'react'; +import { useContext } from 'react'; + +import { context } from '../../../Context'; +import Line from './Line'; +import Wrapper from './Wrapper'; + +const BurgerIcon = ({ children }: { children?: ReactNode }): ReactElement => { + const isReducedMotionPreferred = usePrefersReducedMotion(); + const { + state: { navMenuToggle }, + } = useContext(context); + + return ( + + + + + {children} + + ); +}; + +export default BurgerIcon; diff --git a/packages/uikit-playground/src/Components/NavBar/BurgerIcon/Line.tsx b/packages/uikit-playground/src/Components/NavBar/BurgerIcon/Line.tsx new file mode 100644 index 000000000000..e1ddfed3ac0b --- /dev/null +++ b/packages/uikit-playground/src/Components/NavBar/BurgerIcon/Line.tsx @@ -0,0 +1,52 @@ +import { css } from '@rocket.chat/css-in-js'; +import { Box } from '@rocket.chat/fuselage'; +import type { ReactElement } from 'react'; + +const Line = ({ + animated, + moved, +}: { + animated: boolean; + moved?: boolean; +}): ReactElement => { + const animatedStyle = animated + ? css` + will-change: transform; + transition: transform 0.1s ease-out; + ` + : ''; + + const movedStyle = moved + ? css` + &:nth-child(1), + &:nth-child(3) { + transform-origin: 50%, 50%, 0; + } + &:nth-child(1) { + transform: translate(-25%, 3px) rotate(-45deg) scale(0.5, 1); + } + [dir='rtl'] &:nth-child(1) { + transform: translate(25%, 3px) rotate(45deg) scale(0.5, 1); + } + &:nth-child(3) { + transform: translate(-25%, -3px) rotate(45deg) scale(0.5, 1); + } + [dir='rtl'] &:nth-child(3) { + transform: translate(25%, -3px) rotate(-45deg) scale(0.5, 1); + } + ` + : ''; + + return ( +