diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index db3be22d5464..df5438eb29c5 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -485,7 +485,9 @@ jobs: RC_DOCKER_TAG: '${{ needs.release-versions.outputs.gh-docker-tag }}.official' DOCKER_TAG: ${{ needs.release-versions.outputs.gh-docker-tag }} TRANSPORTER: nats://nats:4222 - ENTERPRISE_LICENSE: ${{ secrets.ENTERPRISE_LICENSE }} + # this is 100% intentional, secrets are not available at forks, so ee-tests will always fail + # to avoid this, we are using a dummy license, expiring on 2022-12-31 + ENTERPRISE_LICENSE: GWHMQe3C3lu3if4ropk1iQ09sCKjIaQogwMymgl7pbuotLpHPhTzxvuSqJYmEtHGKX4MQsrYA+8witFF3JMOc3AswwAkxQz+TZz9+XfiBo49p7OL0xfO6f9ElhLHclptcEPFDSilQlmDI1Jjr7k+MdqXxE4XbZ4Pdeg9vdEDtroKvSvd0ngtFMkIXa6829Cg0nzTCCcWo3Jc80JPub6VMgpdZ2KnsswQt8dBz+psvztcwrqF7X5731HkPX0lTttjF6SSIs+WAIdwAY4b2VRtD3PbMWxbe0ac+2x5+ItIRH0PFRh/gVWdtiKfskQpcucU739CX/d4pQGK/OWxKifH1XH824doeyYuvjXwvCW5anPlRXKAqIn2gh6LcdGfFNIbjX9wG4/U162vDr3AtEpRYdjzfvvBW1gveYMGidIiEhQZu7h5Ie9b3tNBkNBgBcqibL9JO6K3/2pxX7gymjT+9VRZgB5+ME2pziD54SjSWCwkLcf/RByxArdDSqI3llKjyDnRXjhqzsLhmsc6UdJCJd1IiqjQ4v5QXH2C57cJlej93ZLuNRFcnb0cE0nAFHIfLSZP5CSJA/gY7mtBL0b0osUisNL/os7a74JFMoa4DDfTD2CarYV5E4+fpX3NqINzpYwFzZi9pH2wIL6oe1yuZPcn9vjRhK2jvG81tHSR/4s= run: | docker compose -f docker-compose-ci.yml up -d --build diff --git a/apps/meteor/app/api/server/v1/users.ts b/apps/meteor/app/api/server/v1/users.ts index 80775f4ca0b2..379608a210b9 100644 --- a/apps/meteor/app/api/server/v1/users.ts +++ b/apps/meteor/app/api/server/v1/users.ts @@ -501,8 +501,12 @@ API.v1.addRoute( return API.v1.failure('Username is already in use'); } + const { secret: secretURL, ...params } = this.bodyParams; // Register the user - const userId = Meteor.call('registerUser', this.bodyParams); + const userId = Meteor.call('registerUser', { + ...params, + ...(secretURL && { secretURL }), + }); // Now set their username Meteor.runAsUser(userId, () => Meteor.call('setUsername', this.bodyParams.username)); diff --git a/apps/meteor/app/authentication/server/startup/index.js b/apps/meteor/app/authentication/server/startup/index.js index 536117e232d0..019c259d0815 100644 --- a/apps/meteor/app/authentication/server/startup/index.js +++ b/apps/meteor/app/authentication/server/startup/index.js @@ -205,7 +205,11 @@ Accounts.onCreateUser(function (options, user = {}) { to: destinations, from: settings.get('From_Email'), subject: Accounts.emailTemplates.userToActivate.subject(), - html: Accounts.emailTemplates.userToActivate.html(options), + html: Accounts.emailTemplates.userToActivate.html({ + ...options, + name: options.name || options.profile?.name, + email: options.email || user.emails[0].address, + }), }; Mailer.send(email); diff --git a/apps/meteor/app/lib/server/startup/settings.ts b/apps/meteor/app/lib/server/startup/settings.ts index 2362ef501984..fb97e085b020 100644 --- a/apps/meteor/app/lib/server/startup/settings.ts +++ b/apps/meteor/app/lib/server/startup/settings.ts @@ -431,15 +431,15 @@ settingsRegistry.addGroup('Accounts', function () { values: [ { key: 'default', - i18nLabel: 'Default', + i18nLabel: 'Selected_first_reply_unselected_following_replies', }, { key: 'always', - i18nLabel: 'Always', + i18nLabel: 'Selected_by_default', }, { key: 'never', - i18nLabel: 'Never', + i18nLabel: 'Unselected_by_default', }, ], public: true, diff --git a/apps/meteor/app/livechat/lib/messageTypes.ts b/apps/meteor/app/livechat/lib/messageTypes.ts index 65872433a25f..c30916d4b4c9 100644 --- a/apps/meteor/app/livechat/lib/messageTypes.ts +++ b/apps/meteor/app/livechat/lib/messageTypes.ts @@ -48,8 +48,20 @@ MessageTypes.registerType({ ...(comment && { comment }), }), queue: (): string => - TAPi18n.__('Livechat_transfer_return_to_the_queue', { + TAPi18n.__(`Livechat_transfer_return_to_the_queue${commentLabel}`, { from, + ...(comment && { comment }), + }), + autoTransferUnansweredChatsToAgent: (): string => + TAPi18n.__(`Livechat_transfer_to_agent_auto_transfer_unanswered_chat`, { + from, + to: message?.transferData?.transferredTo?.name || message?.transferData?.transferredTo?.username || '', + duration: comment, + }), + autoTransferUnansweredChatsToQueue: (): string => + TAPi18n.__(`Livechat_transfer_return_to_the_queue_auto_transfer_unanswered_chat`, { + from, + duration: comment, }), }; return { diff --git a/apps/meteor/app/livechat/server/hooks/saveAnalyticsData.js b/apps/meteor/app/livechat/server/hooks/saveAnalyticsData.js index 1a82c6152b3e..692040a67771 100644 --- a/apps/meteor/app/livechat/server/hooks/saveAnalyticsData.js +++ b/apps/meteor/app/livechat/server/hooks/saveAnalyticsData.js @@ -3,10 +3,12 @@ import { isOmnichannelRoom } from '@rocket.chat/core-typings'; import { callbacks } from '../../../../lib/callbacks'; import { LivechatRooms } from '../../../models/server'; import { normalizeMessageFileUpload } from '../../../utils/server/functions/normalizeMessageFileUpload'; +import { callbackLogger } from '../lib/callbackLogger'; callbacks.add( 'afterSaveMessage', function (message, room) { + callbackLogger.debug(`Calculating Omnichannel metrics for room ${room._id}`); // check if room is livechat if (!isOmnichannelRoom(room)) { return message; @@ -19,6 +21,10 @@ callbacks.add( // if the message has a token, it was sent by the visitor if (message.token) { + // When visitor sends a mesage, most metrics wont be calculated/served. + // But, v.lq (last query) will be updated to the message time. This has to be done + // As not doing it will cause the metrics to be crazy and not have real values. + LivechatRooms.saveAnalyticsDataByRoomId(room, message); return message; } @@ -37,6 +43,7 @@ callbacks.add( const isResponseTotal = room.metrics && room.metrics.response && room.metrics.response.total; if (agentLastReply === room.ts) { + callbackLogger.debug('Calculating: first message from agent'); // first response const firstResponseDate = now; const firstResponseTime = (now.getTime() - visitorLastQuery) / 1000; @@ -58,6 +65,7 @@ callbacks.add( reactionTime, }; } else if (visitorLastQuery > agentLastReply) { + callbackLogger.debug('Calculating: visitor sent a message after agent'); // response, not first const responseTime = (now.getTime() - visitorLastQuery) / 1000; const avgResponseTime = diff --git a/apps/meteor/app/livechat/server/lib/Livechat.js b/apps/meteor/app/livechat/server/lib/Livechat.js index 9505f15c1af8..af5f779a214c 100644 --- a/apps/meteor/app/livechat/server/lib/Livechat.js +++ b/apps/meteor/app/livechat/server/lib/Livechat.js @@ -751,7 +751,7 @@ export const Livechat = { return RoutingManager.transferRoom(room, guest, transferData); }, - returnRoomAsInquiry(rid, departmentId) { + returnRoomAsInquiry(rid, departmentId, overrideTransferData = {}) { Livechat.logger.debug(`Transfering room ${rid} to ${departmentId ? 'department' : ''} queue`); const room = LivechatRooms.findOneById(rid); if (!room) { @@ -791,7 +791,7 @@ export const Livechat = { const transferredBy = normalizeTransferredByData(user, room); Livechat.logger.debug(`Transfering room ${room._id} by user ${transferredBy._id}`); - const transferData = { roomId: rid, scope: 'queue', departmentId, transferredBy }; + const transferData = { roomId: rid, scope: 'queue', departmentId, transferredBy, ...overrideTransferData }; try { this.saveTransferHistory(room, transferData); RoutingManager.unassignAgent(inquiry, departmentId); diff --git a/apps/meteor/app/mail-messages/server/functions/sendMail.ts b/apps/meteor/app/mail-messages/server/functions/sendMail.ts index cec6b8c6d1c2..36dd1ad5c7a9 100644 --- a/apps/meteor/app/mail-messages/server/functions/sendMail.ts +++ b/apps/meteor/app/mail-messages/server/functions/sendMail.ts @@ -9,7 +9,19 @@ import { placeholders } from '../../../utils/server'; import { SystemLogger } from '../../../../server/lib/logger/system'; import * as Mailer from '../../../mailer'; -export const sendMail = function (from: string, subject: string, body: string, dryrun: boolean, query: string): void { +export const sendMail = function ({ + from, + subject, + body, + dryrun, + query, +}: { + from: string; + subject: string; + body: string; + dryrun?: boolean; + query?: string; +}): void { Mailer.checkAddressFormatAndThrow(from, 'Mailer.sendMail'); if (body.indexOf('[unsubscribe]') === -1) { diff --git a/apps/meteor/app/mail-messages/server/methods/sendMail.ts b/apps/meteor/app/mail-messages/server/methods/sendMail.ts index 80f3df8a4073..ba376e32161d 100644 --- a/apps/meteor/app/mail-messages/server/methods/sendMail.ts +++ b/apps/meteor/app/mail-messages/server/methods/sendMail.ts @@ -13,7 +13,7 @@ Meteor.methods({ }); } - return Mailer.sendMail(from, subject, body, dryrun, query); + return Mailer.sendMail({ from, subject, body, dryrun, query }); }, }); diff --git a/apps/meteor/app/models/server/models/LivechatRooms.js b/apps/meteor/app/models/server/models/LivechatRooms.js index cef4529b21b0..9759b85b1488 100644 --- a/apps/meteor/app/models/server/models/LivechatRooms.js +++ b/apps/meteor/app/models/server/models/LivechatRooms.js @@ -835,45 +835,6 @@ export class LivechatRooms extends Base { return this.update(query, update); } - setAutoTransferredAtById(roomId) { - const query = { - _id: roomId, - }; - const update = { - $set: { - autoTransferredAt: new Date(), - }, - }; - - return this.update(query, update); - } - - setAutoTransferOngoingById(roomId) { - const query = { - _id: roomId, - }; - const update = { - $set: { - autoTransferOngoing: true, - }, - }; - - return this.update(query, update); - } - - unsetAutoTransferOngoingById(roomId) { - const query = { - _id: roomId, - }; - const update = { - $unset: { - autoTransferOngoing: 1, - }, - }; - - return this.update(query, update); - } - changeVisitorByRoomId(roomId, { _id, username, token }) { const query = { _id: roomId, diff --git a/apps/meteor/app/search/client/provider/result.js b/apps/meteor/app/search/client/provider/result.js index 03c00c69fda2..4749b5d0f684 100644 --- a/apps/meteor/app/search/client/provider/result.js +++ b/apps/meteor/app/search/client/provider/result.js @@ -79,7 +79,8 @@ Template.DefaultSearchResultTemplate.onCreated(function () { // global search this.globalSearchEnabled = this.data.settings.GlobalSearchEnabled; - this.data.parentPayload.searchAll = this.globalSearchEnabled; + // default value for global search + this.data.parentPayload.searchAll = false; this.hasMore = new ReactiveVar(true); diff --git a/apps/meteor/app/theme/client/imports/general/base.css b/apps/meteor/app/theme/client/imports/general/base.css index eb6eecd06db1..8ca927ef3378 100644 --- a/apps/meteor/app/theme/client/imports/general/base.css +++ b/apps/meteor/app/theme/client/imports/general/base.css @@ -35,12 +35,6 @@ body { -moz-osx-font-smoothing: grayscale; } -@media (width <= 500px) { - body { - position: fixed; - } -} - :focus { outline: 0 !important; outline-style: none; @@ -54,7 +48,7 @@ body { display: table; clear: both; - content: ""; + content: ''; } } @@ -136,7 +130,7 @@ button { width: 100%; height: 1px; - content: ""; + content: ''; animation-name: unread; animation-duration: 0.2s; 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 64a75e9a58ef..f1295db53604 100644 --- a/apps/meteor/app/theme/client/imports/general/base_old.css +++ b/apps/meteor/app/theme/client/imports/general/base_old.css @@ -280,40 +280,6 @@ & input[type='text'] { display: block; } - - &.double-col { - & > label { - width: 30%; - margin-bottom: 0; - padding: 10px 20px 10px 0; - - text-align: right; - - line-height: 15px; - } - - & > div { - width: 60%; - min-height: 2.5rem; - - & label { - display: inline-block; - - margin-right: 4px; - padding: 10px 20px 10px 0; - - line-height: 15px; - - &:nth-last-child(1) { - margin-right: 0; - } - - & input { - margin-right: 4px; - } - } - } - } } .rc-old form.inline { @@ -365,22 +331,6 @@ content: ' *'; } -.rc-old .btn-loading { - cursor: not-allowed; - pointer-events: none; - - border-width: 0; - box-shadow: none !important; - - & i { - display: block; - } - - & div { - display: none; - } -} - /* new layout buttons */ .rc-old .button { @@ -552,37 +502,6 @@ } } -/* -.rc-old #rocket-chat { - position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; - - display: block; - visibility: visible; - - width: auto; - - transition: opacity 0.2s ease-out; - - opacity: 1; - - &.animated-hidden { - visibility: hidden; - - opacity: 0; - } - - & > .alert { - position: absolute; - - margin-bottom: 0; - padding: 5px; - } -} */ - /* rooms-box */ .rc-old.flex-nav { @@ -797,34 +716,6 @@ animation: highlight 6s infinite; } -.cms-page { - display: flex; - flex-direction: column; - - max-width: 800px; - max-height: 100%; - margin: auto; - - padding: 2rem; - - border-radius: var(--border-radius); - - &__content { - overflow: auto; - - margin: -1rem; - padding: 1rem; - } - - & .cms-page-close { - display: flex; - - margin-bottom: 10px; - - justify-content: flex-end; - } -} - /* MAIN CONTENT + MAIN PAGES */ .rc-old .page-settings { @@ -2723,21 +2614,6 @@ } } -@media (width <= 500px) { - .cms-page { - padding: 0; - - &__content { - margin: 0; - padding: 0 1rem 1rem; - } - - & .cms-page-close { - margin-top: 10px; - } - } -} - @media (height <= 480px) { .rc-old #login-card { margin: 10px auto; diff --git a/apps/meteor/app/theme/client/main.css b/apps/meteor/app/theme/client/main.css index 7d71e8a6e0f0..87a1ade5d42b 100644 --- a/apps/meteor/app/theme/client/main.css +++ b/apps/meteor/app/theme/client/main.css @@ -36,7 +36,6 @@ @import 'imports/components/read-receipts.css'; @import 'imports/components/contextual-bar.css'; @import 'imports/components/emojiPicker.css'; -@import 'imports/components/table.css'; /* User Info */ @import 'imports/components/userInfo.css'; diff --git a/apps/meteor/app/ui-login/README.md b/apps/meteor/app/ui-login/README.md deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/apps/meteor/app/ui-login/client/index.js b/apps/meteor/app/ui-login/client/index.js deleted file mode 100644 index f78815730e02..000000000000 --- a/apps/meteor/app/ui-login/client/index.js +++ /dev/null @@ -1,9 +0,0 @@ -import './login/form.html'; -import './login/layout.html'; -import './login/layout'; -import './login/services.html'; -import './username/username.html'; -import './login/form'; -import './login/services'; -import './username/username'; -import './login/startup'; diff --git a/apps/meteor/app/ui-login/client/index.ts b/apps/meteor/app/ui-login/client/index.ts new file mode 100644 index 000000000000..6197fdfb1a3f --- /dev/null +++ b/apps/meteor/app/ui-login/client/index.ts @@ -0,0 +1,2 @@ +import './username/username.html'; +import './username/username'; diff --git a/apps/meteor/app/ui-login/client/login/form.html b/apps/meteor/app/ui-login/client/login/form.html deleted file mode 100644 index 4c4a253104d9..000000000000 --- a/apps/meteor/app/ui-login/client/login/form.html +++ /dev/null @@ -1,156 +0,0 @@ - diff --git a/apps/meteor/app/ui-login/client/login/form.js b/apps/meteor/app/ui-login/client/login/form.js deleted file mode 100644 index 840b5f1264da..000000000000 --- a/apps/meteor/app/ui-login/client/login/form.js +++ /dev/null @@ -1,282 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { ReactiveVar } from 'meteor/reactive-var'; -import { Tracker } from 'meteor/tracker'; -import { FlowRouter } from 'meteor/kadira:flow-router'; -import { Session } from 'meteor/session'; -import { Template } from 'meteor/templating'; -import _ from 'underscore'; - -import { settings } from '../../../settings'; -import { callbacks } from '../../../../lib/callbacks'; -import { t } from '../../../utils'; -import { dispatchToastMessage } from '../../../../client/lib/toast'; - -Template.loginForm.helpers({ - userName() { - const user = Meteor.user(); - return user && user.username; - }, - namePlaceholder() { - if (settings.get('Accounts_RequireNameForSignUp')) { - return t('Name'); - } - return t('Name_optional'); - }, - showFormLogin() { - return settings.get('Accounts_ShowFormLogin'); - }, - state(...state) { - return state.indexOf(Template.instance().state.get()) > -1; - }, - btnLoginSave() { - if (Template.instance().loading.get()) { - return `${t('Please_wait')}...`; - } - switch (Template.instance().state.get()) { - case 'register': - return t('Register_new_account'); - case 'login': - return t('Login'); - case 'email-verification': - return t('Send_confirmation_email'); - case 'forgot-password': - return t('Reset_password'); - } - }, - loginTerms() { - return settings.get('Layout_Login_Terms'); - }, - registrationAllowed() { - const validSecretUrl = Template.instance().validSecretURL; - return settings.get('Accounts_RegistrationForm') === 'Public' || (validSecretUrl && validSecretUrl.get()); - }, - linkReplacementText() { - return settings.get('Accounts_RegistrationForm_LinkReplacementText'); - }, - passwordResetAllowed() { - return settings.get('Accounts_PasswordReset'); - }, - requirePasswordConfirmation() { - return settings.get('Accounts_RequirePasswordConfirmation'); - }, - emailOrUsernamePlaceholder() { - return settings.get('Accounts_EmailOrUsernamePlaceholder') || t('Email_or_username'); - }, - passwordPlaceholder() { - return settings.get('Accounts_PasswordPlaceholder') || t('Password'); - }, - confirmPasswordPlaceholder() { - return settings.get('Accounts_ConfirmPasswordPlaceholder') || t('Confirm_password'); - }, - manuallyApproveNewUsers() { - return settings.get('Accounts_ManuallyApproveNewUsers'); - }, - typedEmail() { - return Template.instance().typedEmail?.trim(); - }, -}); - -Template.loginForm.events({ - 'submit #login-card'(event, instance) { - event.preventDefault(); - $(event.target).find('button.login').focus(); - instance.loading.set(true); - const formData = instance.validate(); - const state = instance.state.get(); - if (formData) { - if (state === 'email-verification') { - Meteor.call('sendConfirmationEmail', formData.email?.trim(), () => { - instance.loading.set(false); - callbacks.run('userConfirmationEmailRequested'); - dispatchToastMessage({ type: 'success', message: t('We_have_sent_registration_email') }); - return instance.state.set('login'); - }); - return; - } - if (state === 'forgot-password') { - Meteor.call('sendForgotPasswordEmail', formData.email?.trim(), (error) => { - if (error) { - dispatchToastMessage({ type: 'error', message: error }); - return instance.state.set('login'); - } - instance.loading.set(false); - callbacks.run('userForgotPasswordEmailRequested'); - dispatchToastMessage({ type: 'success', message: t('If_this_email_is_registered') }); - return instance.state.set('login'); - }); - return; - } - if (state === 'register') { - formData.secretURL = FlowRouter.getParam('hash'); - return Meteor.call('registerUser', formData, function (error) { - instance.loading.set(false); - if (error != null) { - if (error.reason === 'Email already exists.') { - dispatchToastMessage({ type: 'error', message: t('Email_already_exists') }); - } else { - dispatchToastMessage({ type: 'error', message: error }); - } - return; - } - callbacks.run('userRegistered'); - return Meteor.loginWithPassword(formData.email?.trim(), formData.pass, function (error) { - if (error && error.error === 'error-invalid-email') { - return instance.state.set('wait-email-activation'); - } - if (error && error.error === 'error-user-is-not-activated') { - return instance.state.set('wait-activation'); - } - Session.set('forceLogin', false); - if (formData.secretURL) { - FlowRouter.go('home'); - } - }); - }); - } - let loginMethod = 'loginWithPassword'; - if (settings.get('LDAP_Enable')) { - loginMethod = 'loginWithLDAP'; - } - if (settings.get('CROWD_Enable')) { - loginMethod = 'loginWithCrowd'; - } - return Meteor[loginMethod](formData.emailOrUsername?.trim(), formData.pass, function (error) { - instance.loading.set(false); - if (error != null) { - switch (error.error) { - case 'error-user-is-not-activated': - return dispatchToastMessage({ type: 'error', message: t('Wait_activation_warning') }); - case 'error-invalid-email': - instance.typedEmail = formData.emailOrUsername; - return instance.state.set('email-verification'); - case 'error-app-user-is-not-allowed-to-login': - dispatchToastMessage({ type: 'error', message: t('App_user_not_allowed_to_login') }); - break; - case 'error-login-blocked-for-ip': - dispatchToastMessage({ type: 'error', message: t('Error_login_blocked_for_ip') }); - break; - case 'error-login-blocked-for-user': - dispatchToastMessage({ type: 'error', message: t('Error_login_blocked_for_user') }); - break; - case 'error-license-user-limit-reached': - dispatchToastMessage({ - type: 'error', - message: t('error-license-user-limit-reached'), - }); - break; - default: - return dispatchToastMessage({ - type: 'error', - message: t('User_not_found_or_incorrect_password'), - }); - } - } - Session.set('forceLogin', false); - }); - } - }, - 'click .register'() { - Template.instance().state.set('register'); - return callbacks.run('loginPageStateChange', Template.instance().state.get()); - }, - 'click .back-to-login'() { - Template.instance().state.set('login'); - return callbacks.run('loginPageStateChange', Template.instance().state.get()); - }, - 'click .forgot-password'() { - Template.instance().state.set('forgot-password'); - return callbacks.run('loginPageStateChange', Template.instance().state.get()); - }, -}); - -Template.loginForm.onCreated(function () { - const instance = this; - this.loading = new ReactiveVar(false); - - if (Session.get('loginDefaultState')) { - this.state = new ReactiveVar(Session.get('loginDefaultState')); - } else { - this.state = new ReactiveVar('login'); - } - - Tracker.autorun(() => { - const registrationForm = settings.get('Accounts_RegistrationForm'); - if (registrationForm === 'Disabled' && this.state.get() === 'register') { - this.state.set('login'); - } - }); - - this.validSecretURL = new ReactiveVar(false); - this.validate = function () { - const formData = $('#login-card').serializeArray(); - const formObj = {}; - const validationObj = {}; - formData.forEach((field) => { - formObj[field.name] = field.value; - }); - const state = instance.state.get(); - if (state !== 'login') { - if (!(formObj.email && /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]+\b/i.test(formObj.email))) { - validationObj.email = t('Invalid_email'); - } - } - if (state === 'login') { - if (!formObj.emailOrUsername) { - validationObj.emailOrUsername = t('Invalid_email'); - } - } - if (state !== 'forgot-password' && state !== 'email-verification') { - if (!formObj.pass) { - validationObj.pass = t('Invalid_pass'); - } - } - if (state === 'register') { - if (settings.get('Accounts_RequireNameForSignUp') && !formObj.name) { - validationObj.name = t('Invalid_name'); - } - if (settings.get('Accounts_RequirePasswordConfirmation') && formObj['confirm-pass'] !== formObj.pass) { - validationObj['confirm-pass'] = t('Invalid_confirm_pass'); - } - if (settings.get('Accounts_ManuallyApproveNewUsers') && !formObj.reason) { - validationObj.reason = t('Invalid_reason'); - } - } - $('#login-card h2').removeClass('error'); - $('#login-card input.error, #login-card select.error').removeClass('error'); - $('#login-card .input-error').text(''); - if (!_.isEmpty(validationObj)) { - $('#login-card h2').addClass('error'); - - Object.keys(validationObj).forEach((key) => { - const value = validationObj[key]; - $(`#login-card input[name=${key}], #login-card select[name=${key}]`).addClass('error'); - $(`#login-card input[name=${key}]~.input-error, #login-card select[name=${key}]~.input-error`).text(value); - }); - instance.loading.set(false); - return false; - } - return formObj; - }; - if (FlowRouter.getParam('hash')) { - return Meteor.call('checkRegistrationSecretURL', FlowRouter.getParam('hash'), () => this.validSecretURL.set(true)); - } -}); - -Template.loginForm.onRendered(function () { - Session.set('loginDefaultState'); - return Tracker.autorun(() => { - callbacks.run('loginPageStateChange', this.state.get()); - switch (this.state.get()) { - case 'login': - case 'forgot-password': - case 'email-verification': - return Meteor.defer(function () { - return $('input[name=email]').select().focus(); - }); - case 'register': - return Meteor.defer(function () { - return $('input[name=name]').select().focus(); - }); - } - }); -}); diff --git a/apps/meteor/app/ui-login/client/login/layout.html b/apps/meteor/app/ui-login/client/login/layout.html deleted file mode 100644 index 42ad59b33a82..000000000000 --- a/apps/meteor/app/ui-login/client/login/layout.html +++ /dev/null @@ -1,12 +0,0 @@ - diff --git a/apps/meteor/app/ui-login/client/login/layout.js b/apps/meteor/app/ui-login/client/login/layout.js deleted file mode 100644 index d9386ae42a1a..000000000000 --- a/apps/meteor/app/ui-login/client/login/layout.js +++ /dev/null @@ -1,17 +0,0 @@ -import { Session } from 'meteor/session'; -import { Template } from 'meteor/templating'; - -import { settings } from '../../../settings'; - -Template.loginLayout.helpers({ - backgroundUrl() { - const asset = settings.get('Assets_background'); - const prefix = __meteor_runtime_config__.ROOT_URL_PATH_PREFIX || ''; - if (asset && (asset.url || asset.defaultUrl)) { - return `${prefix}/${asset.url || asset.defaultUrl}`; - } - }, - showForcedLogoutBanner() { - return Session.get('force_logout'); - }, -}); diff --git a/apps/meteor/app/ui-login/client/login/services.html b/apps/meteor/app/ui-login/client/login/services.html deleted file mode 100644 index bd5650bb4978..000000000000 --- a/apps/meteor/app/ui-login/client/login/services.html +++ /dev/null @@ -1,18 +0,0 @@ - diff --git a/apps/meteor/app/ui-login/client/login/services.js b/apps/meteor/app/ui-login/client/login/services.js deleted file mode 100644 index 58758f0badee..000000000000 --- a/apps/meteor/app/ui-login/client/login/services.js +++ /dev/null @@ -1,93 +0,0 @@ -import { capitalize } from '@rocket.chat/string-helpers'; -import { Meteor } from 'meteor/meteor'; -import { Template } from 'meteor/templating'; -import { ServiceConfiguration } from 'meteor/service-configuration'; - -import { dispatchToastMessage } from '../../../../client/lib/toast'; - -Template.loginServices.helpers({ - loginService() { - return ServiceConfiguration.configurations - .find( - { - showButton: { $ne: false }, - }, - { - sort: { - service: 1, - }, - }, - ) - .fetch() - .map((service) => { - switch (service.service) { - case 'meteor-developer': - return { - service, - displayName: 'Meteor', - icon: 'meteor', - }; - - case 'github': - return { - service, - displayName: 'GitHub', - icon: 'github-circled', - }; - - case 'gitlab': - return { - service, - displayName: 'GitLab', - icon: service.service, - }; - - case 'wordpress': - return { - service, - displayName: 'WordPress', - icon: service.service, - }; - - default: - return { - service, - displayName: capitalize(String(service.service || '')), - icon: service.service, - }; - } - }); - }, -}); - -const loginMethods = { - 'meteor-developer': 'MeteorDeveloperAccount', - 'linkedin': 'Linkedin', -}; - -Template.loginServices.events({ - 'click .external-login'(e) { - if (this.service == null || this.service.service == null) { - return; - } - - const loadingIcon = $(e.currentTarget).find('.loading-icon'); - const serviceIcon = $(e.currentTarget).find('.service-icon'); - loadingIcon.removeClass('hidden'); - serviceIcon.addClass('hidden'); - - const loginWithService = `loginWith${loginMethods[this.service.service] || capitalize(String(this.service.service || ''))}`; - const serviceConfig = this.service.clientConfig || {}; - - Meteor[loginWithService](serviceConfig, (error) => { - loadingIcon.addClass('hidden'); - serviceIcon.removeClass('hidden'); - - if (!error) { - return; - } - - dispatchToastMessage({ type: 'error', message: error }); - }); - }, -}); diff --git a/apps/meteor/app/ui-login/client/username/username.html b/apps/meteor/app/ui-login/client/username/username.html index 9e51b2a34d14..247777adde72 100644 --- a/apps/meteor/app/ui-login/client/username/username.html +++ b/apps/meteor/app/ui-login/client/username/username.html @@ -39,7 +39,8 @@

{{_ "Username_title"}}

- {{>customFieldsForm}} + {{> customFieldsForm}} + {{#if username.ready}}
diff --git a/apps/meteor/app/ui-login/client/username/username.js b/apps/meteor/app/ui-login/client/username/username.ts similarity index 59% rename from apps/meteor/app/ui-login/client/username/username.js rename to apps/meteor/app/ui-login/client/username/username.ts index 82e433ce8825..164ed0170279 100644 --- a/apps/meteor/app/ui-login/client/username/username.js +++ b/apps/meteor/app/ui-login/client/username/username.ts @@ -2,7 +2,8 @@ import { Meteor } from 'meteor/meteor'; import { ReactiveVar } from 'meteor/reactive-var'; import { Template } from 'meteor/templating'; import { Tracker } from 'meteor/tracker'; -import _ from 'underscore'; +import type { Blaze } from 'meteor/blaze'; +import { escapeHTML } from '@rocket.chat/string-helpers'; import { settings } from '../../../settings/client'; import { Button } from '../../../ui/client'; @@ -10,14 +11,38 @@ import { t } from '../../../utils/client'; import { callbacks } from '../../../../lib/callbacks'; import { dispatchToastMessage } from '../../../../client/lib/toast'; -Template.username.onCreated(function () { - const self = this; - self.customFields = new ReactiveVar(); - self.username = new ReactiveVar(); +type UsernameTemplateInstance = Blaze.TemplateInstance> & { + customFields: ReactiveVar | null>; + username: ReactiveVar<{ + ready: boolean; + username: string; + empty?: boolean; + error?: boolean; + invalid?: boolean; + escaped?: string; + blocked?: boolean; + unavailable?: boolean; + }>; + validate: () => unknown; +}; + +Template.username.onCreated(function (this: UsernameTemplateInstance) { + this.customFields = new ReactiveVar(null); + this.username = new ReactiveVar({ + ready: false, + username: '', + }); Tracker.autorun(() => { - const Accounts_CustomFields = settings.get('Accounts_CustomFields'); - if (typeof Accounts_CustomFields === 'string' && Accounts_CustomFields.trim() !== '') { + const accountsCustomFields = settings.get('Accounts_CustomFields'); + if (typeof accountsCustomFields === 'string' && accountsCustomFields.trim() !== '') { try { return this.customFields.set(JSON.parse(settings.get('Accounts_CustomFields'))); } catch (error1) { @@ -28,8 +53,8 @@ Template.username.onCreated(function () { } }); - const validateCustomFields = function (formObj, validationObj) { - const customFields = self.customFields.get(); + const validateCustomFields = (formObj: Record, validationObj: Record) => { + const customFields = this.customFields.get(); if (!customFields) { return; } @@ -37,7 +62,7 @@ Template.username.onCreated(function () { for (const field in formObj) { if (formObj.hasOwnProperty(field)) { const value = formObj[field]; - if (customFields[field] == null) { + if (!customFields[field]) { continue; } const customField = customFields[field]; @@ -45,11 +70,11 @@ Template.username.onCreated(function () { validationObj[field] = t('Field_required'); return validationObj[field]; } - if (customField.maxLength != null && value.length > customField.maxLength) { + if (customField.maxLength && value.length > customField.maxLength) { validationObj[field] = t('Max_length_is', customField.maxLength); return validationObj[field]; } - if (customField.minLength != null && value.length < customField.minLength) { + if (customField.minLength && value.length < customField.minLength) { validationObj[field] = t('Min_length_is', customField.minLength); return validationObj[field]; } @@ -57,19 +82,19 @@ Template.username.onCreated(function () { } }; - this.validate = function () { + this.validate = () => { const formData = $('#login-card').serializeArray(); - const formObj = {}; - const validationObj = {}; - formData.forEach((field) => { - formObj[field.name] = field.value; - }); + const formObj = formData.reduce((formObj, { name, value }) => { + formObj[name] = value; + return formObj; + }, {} as Record); + const validationObj = {} as Record; $('#login-card h2').removeClass('error'); $('#login-card input.error, #login-card select.error').removeClass('error'); $('#login-card .input-error').text(''); validateCustomFields(formObj, validationObj); - if (!_.isEmpty(validationObj)) { + if (Object.keys(validationObj).length > 0) { $('#login-card h2').addClass('error'); Object.keys(validationObj).forEach((key) => { @@ -83,23 +108,23 @@ Template.username.onCreated(function () { return formObj; }; - return Meteor.call('getUsernameSuggestion', function (error, username) { - self.username.set({ + Meteor.call('getUsernameSuggestion', (_error: Error, username: string) => { + this.username.set({ ready: true, username, }); - return Meteor.defer(() => self.find('input').focus()); + return Meteor.defer(() => this.find('input').focus()); }); }); Template.username.helpers({ username() { - return Template.instance().username.get(); + return (Template.instance() as UsernameTemplateInstance).username.get(); }, backgroundUrl() { const asset = settings.get('Assets_background'); - const prefix = __meteor_runtime_config__.ROOT_URL_PATH_PREFIX || ''; + const prefix = window.__meteor_runtime_config__.ROOT_URL_PATH_PREFIX || ''; if (asset && (asset.url || asset.defaultUrl)) { return `${prefix}/${asset.url || asset.defaultUrl}`; } @@ -107,11 +132,11 @@ Template.username.helpers({ }); Template.username.events({ - 'focus .input-text input'(event) { + 'focus .input-text input'(event: JQuery.FocusEvent) { return $(event.currentTarget).parents('.input-text').addClass('focus'); }, - 'blur .input-text input'(event) { + 'blur .input-text input'(event: JQuery.BlurEvent) { if (event.currentTarget.value === '') { return $(event.currentTarget).parents('.input-text').removeClass('focus'); } @@ -119,7 +144,7 @@ Template.username.events({ 'reset #login-card'() { Meteor.logout(); }, - 'submit #login-card'(event, instance) { + 'submit #login-card'(event: JQuery.SubmitEvent, instance: UsernameTemplateInstance) { event.preventDefault(); const formData = instance.validate(); @@ -138,7 +163,7 @@ Template.username.events({ return; } - const usernameValue = $('#username').val().trim(); + const usernameValue = ($('#username').val() as string | undefined)?.trim(); if (usernameValue === '') { username.empty = true; instance.username.set(username); @@ -146,14 +171,14 @@ Template.username.events({ return; } - Meteor.call('saveCustomFields', formData, function (error) { + Meteor.call('saveCustomFields', formData, (error: Meteor.Error) => { if (error) { dispatchToastMessage({ type: 'error', message: error }); } }); - Meteor.call('setUsername', usernameValue, function (err) { - if (err != null) { + Meteor.call('setUsername', usernameValue, (err: Meteor.Error) => { + if (err) { if (err.error === 'username-invalid') { username.invalid = true; } else if (err.error === 'error-blocked-username') { @@ -161,8 +186,8 @@ Template.username.events({ } else { username.unavailable = true; } - username.username = usernameValue; - username.escaped = _.escape(usernameValue); + username.username = usernameValue ?? ''; + username.escaped = escapeHTML(usernameValue ?? ''); } Button.reset(button); diff --git a/apps/meteor/app/ui-login/index.js b/apps/meteor/app/ui-login/index.ts similarity index 100% rename from apps/meteor/app/ui-login/index.js rename to apps/meteor/app/ui-login/index.ts diff --git a/apps/meteor/app/ui-sidenav/client/index.js b/apps/meteor/app/ui-sidenav/client/index.js index 151b65767581..692e5db26760 100644 --- a/apps/meteor/app/ui-sidenav/client/index.js +++ b/apps/meteor/app/ui-sidenav/client/index.js @@ -1,5 +1,3 @@ import './sideNav.html'; -import './roomList.html'; import './sideNav'; -import './roomList'; import './userPresence'; diff --git a/apps/meteor/app/ui-sidenav/client/roomList.html b/apps/meteor/app/ui-sidenav/client/roomList.html deleted file mode 100644 index aa9759b8a8e7..000000000000 --- a/apps/meteor/app/ui-sidenav/client/roomList.html +++ /dev/null @@ -1,20 +0,0 @@ - diff --git a/apps/meteor/app/ui-sidenav/client/roomList.js b/apps/meteor/app/ui-sidenav/client/roomList.js deleted file mode 100644 index 08d7dbdd3c0b..000000000000 --- a/apps/meteor/app/ui-sidenav/client/roomList.js +++ /dev/null @@ -1,118 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Template } from 'meteor/templating'; - -import { UiTextContext } from '../../../definition/IRoomTypeConfig'; -import { ChatSubscription, Rooms, Users } from '../../models/client'; -import { getUserPreference } from '../../utils'; -import { settings } from '../../settings'; -import { roomCoordinator } from '../../../client/lib/rooms/roomCoordinator'; - -Template.roomList.helpers({ - rooms() { - /* - modes: - sortby activity/alphabetical - merge channels into one list - show favorites - show unread - */ - if (this.anonymous) { - return Rooms.find({ t: 'c' }, { sort: { name: 1 } }); - } - - const user = Users.findOne(Meteor.userId(), { - fields: { - 'settings.preferences.sidebarSortby': 1, - 'settings.preferences.sidebarShowFavorites': 1, - 'settings.preferences.sidebarShowUnread': 1, - 'messageViewMode': 1, - }, - }); - - const sortBy = getUserPreference(user, 'sidebarSortby') || 'activity'; - const query = { - open: true, - }; - - const sort = {}; - - if (sortBy === 'activity') { - sort.lm = -1; - } else { - // alphabetical - sort[this.identifier === 'd' && settings.get('UI_Use_Real_Name') ? 'lowerCaseFName' : 'lowerCaseName'] = /descending/.test(sortBy) - ? -1 - : 1; - } - - if (this.identifier === 'unread') { - query.alert = true; - query.$or = [{ hideUnreadStatus: { $ne: true } }, { unread: { $gt: 0 } }]; - - return ChatSubscription.find(query, { sort }); - } - - const favoritesEnabled = !!(settings.get('Favorite_Rooms') && getUserPreference(user, 'sidebarShowFavorites')); - - if (this.identifier === 'f') { - query.f = favoritesEnabled; - } else { - let types = [this.identifier]; - - if (this.identifier === 'merged') { - types = ['c', 'p', 'd']; - } - - if (this.identifier === 'discussion') { - types = ['c', 'p', 'd']; - query.prid = { $exists: true }; - } - - if (this.identifier === 'tokens') { - types = ['c', 'p']; - } - - if (getUserPreference(user, 'sidebarShowUnread')) { - query.$or = [ - { alert: { $ne: true } }, - { - $and: [{ hideUnreadStatus: true }, { unread: 0 }], - }, - ]; - } - query.t = { $in: types }; - if (favoritesEnabled) { - query.f = { $ne: favoritesEnabled }; - } - } - return ChatSubscription.find(query, { sort }); - }, - - isLivechat() { - return this.identifier === 'l'; - }, - - shouldAppear(group, rooms) { - /* - if is a normal group ('channel' 'private' 'direct') - or is favorite and has one room - or is unread and has one room - */ - - return !['unread', 'f'].includes(group.identifier) || rooms.length || (rooms.count && rooms.count()); - }, - - roomType(room) { - if (room.header || room.identifier) { - return `type-${room.header || room.identifier}`; - } - }, - - noSubscriptionText() { - const instance = Template.instance(); - if (instance.data.anonymous) { - return 'No_channels_yet'; - } - return roomCoordinator.getRoomDirectives(instance.data.identifier)?.getUiText(UiTextContext.NO_ROOMS_SUBSCRIBED) || 'No_channels_yet'; - }, -}); diff --git a/apps/meteor/app/ui/client/components/table.html b/apps/meteor/app/ui/client/components/table.html deleted file mode 100644 index a3a18f66acc7..000000000000 --- a/apps/meteor/app/ui/client/components/table.html +++ /dev/null @@ -1,13 +0,0 @@ - diff --git a/apps/meteor/app/ui/client/components/table.js b/apps/meteor/app/ui/client/components/table.js deleted file mode 100644 index df26ef8937bc..000000000000 --- a/apps/meteor/app/ui/client/components/table.js +++ /dev/null @@ -1,27 +0,0 @@ -import { Template } from 'meteor/templating'; -import _ from 'underscore'; - -Template.table.onRendered(function () { - const dummyTr = ''; - this.$('tbody').prepend(dummyTr).append(dummyTr); - - this.onResize = this.data.onResize; - if (this.onResize) { - this.onResize(); - $(window).on('resize', this.onResize); - } -}); - -Template.table.onDestroyed(function () { - $(window).on('off', this.onResize); -}); - -Template.table.events({ - 'click tbody tr:not(.table-no-click)'(e, t) { - t.data.onItemClick && t.data.onItemClick(this); - }, - 'scroll .table-scroll': _.debounce((e, t) => t.data.onScroll && t.data.onScroll(e.currentTarget), 300), - 'click .js-sort'(e, t) { - t.data.onSort(e.currentTarget.dataset.sort); - }, -}); diff --git a/apps/meteor/app/ui/client/index.ts b/apps/meteor/app/ui/client/index.ts index 77364b9fdf03..e97720042ca8 100644 --- a/apps/meteor/app/ui/client/index.ts +++ b/apps/meteor/app/ui/client/index.ts @@ -9,8 +9,7 @@ import './views/app/userSearch.html'; import './views/app/roomSearch'; import './views/app/photoswipeContent.ts'; // without the *.ts extension, *.html gets loaded first import './components/icon'; -import './components/table.html'; -import './components/table'; + import './components/popupList.html'; import './components/popupList'; import './components/selectDropdown.html'; diff --git a/apps/meteor/client/components/AdministrationList/AppsModelList.tsx b/apps/meteor/client/components/AdministrationList/AppsModelList.tsx index 3f9291fe2cc7..cb6ec49e3d36 100644 --- a/apps/meteor/client/components/AdministrationList/AppsModelList.tsx +++ b/apps/meteor/client/components/AdministrationList/AppsModelList.tsx @@ -1,6 +1,6 @@ import { OptionTitle } from '@rocket.chat/fuselage'; import { useTranslation, useRoute } from '@rocket.chat/ui-contexts'; -import React, { FC } from 'react'; +import React, { ReactElement } from 'react'; import { triggerActionButtonAction } from '../../../app/ui-message/client/ActionManager'; import { IAppAccountBoxItem } from '../../../app/ui-utils/client/lib/AccountBox'; @@ -12,9 +12,10 @@ type AppsModelListProps = { closeList: () => void; }; -const AppsModelList: FC = ({ appBoxItems, showManageApps, closeList }) => { +const AppsModelList = ({ appBoxItems, showManageApps, closeList }: AppsModelListProps): ReactElement => { const t = useTranslation(); const marketplaceRoute = useRoute('admin-marketplace'); + const page = 'list'; return ( <> @@ -26,7 +27,7 @@ const AppsModelList: FC = ({ appBoxItems, showManageApps, cl icon='store' text={t('Marketplace')} action={(): void => { - marketplaceRoute.push(); + marketplaceRoute.push({ context: 'all', page }); closeList(); }} /> @@ -34,7 +35,7 @@ const AppsModelList: FC = ({ appBoxItems, showManageApps, cl icon='cube' text={t('Installed')} action={(): void => { - marketplaceRoute.push({ context: 'installed' }); + marketplaceRoute.push({ context: 'installed', page }); closeList(); }} /> diff --git a/apps/meteor/client/components/AutoCompleteDepartment.js b/apps/meteor/client/components/AutoCompleteDepartment.js deleted file mode 100644 index 29759443c7b8..000000000000 --- a/apps/meteor/client/components/AutoCompleteDepartment.js +++ /dev/null @@ -1,73 +0,0 @@ -// Cannot convert this file to ts because PaginatedSelectFiltered is not typed yet -// Next release we'll add required types and convert this file, since a new -// fuselage release is OoS of this regression -import { PaginatedSelectFiltered } from '@rocket.chat/fuselage'; -import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; -import { useTranslation } from '@rocket.chat/ui-contexts'; -import React, { memo, useMemo, useState } from 'react'; - -import { useRecordList } from '../hooks/lists/useRecordList'; -import { AsyncStatePhase } from '../hooks/useAsyncState'; -import { useDepartmentsList } from './Omnichannel/hooks/useDepartmentsList'; - -const AutoCompleteDepartment = (props) => { - const { value, excludeDepartmentId, onlyMyDepartments = false, onChange = () => {}, haveAll = false, haveNone = false } = props; - - const t = useTranslation(); - const [departmentsFilter, setDepartmentsFilter] = useState(''); - - const debouncedDepartmentsFilter = useDebouncedValue(departmentsFilter, 500); - - const { itemsList: departmentsList, loadMoreItems: loadMoreDepartments } = useDepartmentsList( - useMemo( - () => ({ - filter: debouncedDepartmentsFilter, - onlyMyDepartments, - haveAll, - haveNone, - excludeDepartmentId, - }), - [debouncedDepartmentsFilter, onlyMyDepartments, haveAll, haveNone, excludeDepartmentId], - ), - ); - - const { phase: departmentsPhase, items: departmentsItems, itemCount: departmentsTotal } = useRecordList(departmentsList); - - const sortedByName = departmentsItems.sort((a, b) => { - if (a.value.value === 'all') { - return -1; - } - - if (a.name > b.name) { - return 1; - } - if (a.name < b.name) { - return -1; - } - - return 0; - }); - - const findValue = value !== undefined && value !== null ? value : ''; - const department = sortedByName.find( - (dep) => dep._id === (typeof findValue !== 'object' && findValue ? findValue : findValue.value), - )?.value; - - return ( - {} : (start) => loadMoreDepartments(start, Math.min(50, departmentsTotal)) - } - /> - ); -}; - -export default memo(AutoCompleteDepartment); diff --git a/apps/meteor/client/components/AutoCompleteDepartment.tsx b/apps/meteor/client/components/AutoCompleteDepartment.tsx new file mode 100644 index 000000000000..78cf107f13b0 --- /dev/null +++ b/apps/meteor/client/components/AutoCompleteDepartment.tsx @@ -0,0 +1,93 @@ +import { PaginatedSelectFiltered } from '@rocket.chat/fuselage'; +import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import React, { memo, ReactElement, useMemo, useState } from 'react'; + +import { useRecordList } from '../hooks/lists/useRecordList'; +import { AsyncStatePhase } from '../hooks/useAsyncState'; +import { useDepartmentsList } from './Omnichannel/hooks/useDepartmentsList'; + +type AutoCompleteDepartmentProps = { + value?: { value: string; label: string } | string; + onChange: (value: string) => void; + excludeDepartmentId?: string; + onlyMyDepartments?: boolean; + haveAll?: boolean; + haveNone?: boolean; +}; + +const AutoCompleteDepartment = ({ + value, + excludeDepartmentId, + onlyMyDepartments, + onChange, + haveAll, + haveNone, +}: AutoCompleteDepartmentProps): ReactElement | null => { + const t = useTranslation(); + const [departmentsFilter, setDepartmentsFilter] = useState(''); + + const debouncedDepartmentsFilter = useDebouncedValue(departmentsFilter, 500); + + const { itemsList: departmentsList, loadMoreItems: loadMoreDepartments } = useDepartmentsList( + useMemo( + () => ({ + filter: debouncedDepartmentsFilter, + onlyMyDepartments, + haveAll, + haveNone, + excludeDepartmentId, + }), + [debouncedDepartmentsFilter, onlyMyDepartments, haveAll, haveNone, excludeDepartmentId], + ), + ); + + const { phase: departmentsPhase, items: departmentsItems, itemCount: departmentsTotal } = useRecordList(departmentsList); + + const sortedByName = useMemo( + () => + departmentsItems.sort((a, b) => { + if (a.value.value === 'all') { + return -1; + } + + if (a.name > b.name) { + return 1; + } + if (a.name < b.name) { + return -1; + } + + return 0; + }), + [departmentsItems], + ); + + const department = useMemo(() => { + const valueFound = typeof value === 'string' ? value : value?.value || ''; + return sortedByName.find((dep) => dep.value.value === valueFound)?.value; + }, [sortedByName, value]); + + return ( + void} + // TODO: Fix typing on fuselage + // Workaround for options wrong typing + options={sortedByName as any} + placeholder={t('Select_an_option')} + data-qa='autocomplete-department' + endReached={ + departmentsPhase === AsyncStatePhase.LOADING + ? (): void => undefined + : (start): void => loadMoreDepartments(start, Math.min(50, departmentsTotal)) + } + /> + ); +}; + +export default memo(AutoCompleteDepartment); diff --git a/apps/meteor/client/components/CustomFieldsForm.js b/apps/meteor/client/components/CustomFieldsForm.js index b4c5d131716e..1e23b8ce9ccf 100644 --- a/apps/meteor/client/components/CustomFieldsForm.js +++ b/apps/meteor/client/components/CustomFieldsForm.js @@ -112,13 +112,7 @@ const CustomFieldsAssembler = ({ formValues, formHandlers, customFields, ...prop return null; }); -export default function CustomFieldsForm({ - jsonCustomFields = {}, - customFieldsData, - setCustomFieldsData, - onLoadFields = () => {}, - ...props -}) { +export default function CustomFieldsForm({ jsonCustomFields, customFieldsData, setCustomFieldsData, onLoadFields = () => {}, ...props }) { const accountsCustomFieldsJson = useSetting('Accounts_CustomFields'); const [customFields] = useState(() => { diff --git a/apps/meteor/client/components/Omnichannel/modals/CloseChatModal.tsx b/apps/meteor/client/components/Omnichannel/modals/CloseChatModal.tsx index a02feeaa7e98..2348f901d051 100644 --- a/apps/meteor/client/components/Omnichannel/modals/CloseChatModal.tsx +++ b/apps/meteor/client/components/Omnichannel/modals/CloseChatModal.tsx @@ -58,7 +58,7 @@ const CloseChatModal = ({ const cannotSubmit = useMemo(() => { const cannotSendTag = (tagRequired && !tags?.length) || errors.tags; const cannotSendComment = (commentRequired && !comment) || errors.comment; - return cannotSendTag || cannotSendComment; + return Boolean(cannotSendTag || cannotSendComment); }, [comment, commentRequired, errors, tagRequired, tags]); useEffect(() => { @@ -91,7 +91,17 @@ const CloseChatModal = ({ {t('Comment')} - + {errors.comment?.message} diff --git a/apps/meteor/client/components/Page/Page.tsx b/apps/meteor/client/components/Page/Page.tsx index 57a4c294ad48..4fc848a649d3 100644 --- a/apps/meteor/client/components/Page/Page.tsx +++ b/apps/meteor/client/components/Page/Page.tsx @@ -26,6 +26,7 @@ const Page = ({ background = 'light', ...props }: PageProps): ReactElement => { flexShrink={1} height='full' overflow='hidden' + aria-labelledby='PageHeader-title' {...props} backgroundColor={`var(--rcx-color-surface-${background}, ${surfaceMap[background]})`} /> diff --git a/apps/meteor/client/components/Page/PageHeader.tsx b/apps/meteor/client/components/Page/PageHeader.tsx index a99cc13ff2e7..5f3268df9104 100644 --- a/apps/meteor/client/components/Page/PageHeader.tsx +++ b/apps/meteor/client/components/Page/PageHeader.tsx @@ -36,7 +36,7 @@ const PageHeader: FC = ({ children = undefined, title, onClickB )} {onClickBack && } - + {title} {children} diff --git a/apps/meteor/client/components/UserAutoCompleteMultiple/UserAutoCompleteMultipleFederated.tsx b/apps/meteor/client/components/UserAutoCompleteMultiple/UserAutoCompleteMultipleFederated.tsx index 677f4dffda2e..24380550ded3 100644 --- a/apps/meteor/client/components/UserAutoCompleteMultiple/UserAutoCompleteMultipleFederated.tsx +++ b/apps/meteor/client/components/UserAutoCompleteMultiple/UserAutoCompleteMultipleFederated.tsx @@ -1,9 +1,8 @@ import { MultiSelectFiltered, Icon, Box, Chip } from '@rocket.chat/fuselage'; -import type { Options } from '@rocket.chat/fuselage'; import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; import { useEndpoint } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; -import React, { memo, ReactElement, useState, ComponentProps } from 'react'; +import React, { memo, ReactElement, useState, useCallback, useMemo } from 'react'; import UserAvatar from '../avatar/UserAvatar'; import AutocompleteOptions, { OptionsContext } from './UserAutoCompleteMultipleOptions'; @@ -55,33 +54,58 @@ const UserAutoCompleteMultipleFederated = ({ { keepPreviousData: true }, ); - const options = data || []; + const options = useMemo(() => data || [], [data]); - const onAddSelected: ComponentProps['onSelect'] = ([value]) => { - setFilter(''); - const cachedOption = options.find(([curVal]) => curVal === value)?.[1]; - if (!cachedOption) { - throw new Error('UserAutoCompleteMultiple - onAddSelected - failed to cache option'); - } - setSelectedCache({ ...selectedCache, [value]: cachedOption }); - }; + const onAddUser = useCallback( + (username: string): void => { + const user = options.find(([val]) => val === username)?.[1]; + if (!user) { + throw new Error('UserAutoCompleteMultiple - onAddSelected - failed to cache option'); + } + setSelectedCache((selectedCache) => ({ ...selectedCache, [username]: user })); + }, + [setSelectedCache, options], + ); + + const onRemoveUser = useCallback( + (username: string): void => + setSelectedCache((selectedCache) => { + const users = { ...selectedCache }; + delete users[username]; + return users; + }), + [setSelectedCache], + ); + + const handleOnChange = useCallback( + (usernames: string[]) => { + onChange(usernames); + const newAddedUsername = usernames.filter((username) => !value.includes(username))[0]; + const removedUsername = value.filter((username) => !usernames.includes(username))[0]; + setFilter(''); + newAddedUsername && onAddUser(newAddedUsername); + removedUsername && onRemoveUser(removedUsername); + }, + [onChange, setFilter, onAddUser, onRemoveUser, value], + ); return ( - + void }): ReactElement => { - const currentCachedOption = selectedCache[value]; + const currentCachedOption = selectedCache[value] || {}; return ( {currentCachedOption._federated ? : } - {currentCachedOption.name || currentCachedOption.username} + {currentCachedOption.name || currentCachedOption.username || value} ); diff --git a/apps/meteor/client/components/UserAutoCompleteMultiple/UserAutoCompleteMultipleOptions.tsx b/apps/meteor/client/components/UserAutoCompleteMultiple/UserAutoCompleteMultipleOptions.tsx index 79b92c3bf31a..69a25a5381e0 100644 --- a/apps/meteor/client/components/UserAutoCompleteMultiple/UserAutoCompleteMultipleOptions.tsx +++ b/apps/meteor/client/components/UserAutoCompleteMultiple/UserAutoCompleteMultipleOptions.tsx @@ -12,27 +12,22 @@ type Options = Array<[UserAutoCompleteOptionType['username'], UserAutoCompleteOp type OptionsContextValue = { options: ComponentProps['options']; - onSelect: ComponentProps['onSelect']; }; export const OptionsContext = createContext({ options: [], - onSelect: () => undefined, }); const UserAutoCompleteMultipleOptions = forwardRef(function UserAutoCompleteMultipleOptions( - { onSelect: _onSelect, ...props }: ComponentProps, + { onSelect, ...props }: ComponentProps, ref: Ref, ): ReactElement { - const { options, onSelect } = useContext(OptionsContext); + const { options } = useContext(OptionsContext); return ( { - onSelect(val); - _onSelect(val); - }} + onSelect={onSelect} ref={ref} renderItem={UserAutoCompleteMultipleOption} /> diff --git a/apps/meteor/client/lib/lists/RecordList.ts b/apps/meteor/client/lib/lists/RecordList.ts index e48cd83bd36d..c710dc48ee5e 100644 --- a/apps/meteor/client/lib/lists/RecordList.ts +++ b/apps/meteor/client/lib/lists/RecordList.ts @@ -24,7 +24,7 @@ export class RecordList extends Emitter { } protected compare(a: T, b: T): number { - return a._updatedAt.getTime() - b._updatedAt.getTime(); + return b._updatedAt.getTime() - a._updatedAt.getTime(); } public get phase(): AsyncStatePhase { diff --git a/apps/meteor/client/providers/TranslationProvider.js b/apps/meteor/client/providers/TranslationProvider.js deleted file mode 100644 index 3e4ef59b46d6..000000000000 --- a/apps/meteor/client/providers/TranslationProvider.js +++ /dev/null @@ -1,72 +0,0 @@ -import { TranslationContext } from '@rocket.chat/ui-contexts'; -import { TAPi18n, TAPi18next } from 'meteor/rocketchat:tap-i18n'; -import React, { useMemo } from 'react'; - -import { useReactiveValue } from '../hooks/useReactiveValue'; - -const createTranslateFunction = (language) => { - const translate = (key, ...replaces) => { - if (typeof replaces[0] === 'object') { - const [options, lang_tag = language] = replaces; - return TAPi18next.t(key, { - ns: 'project', - lng: lang_tag, - ...options, - }); - } - - if (replaces.length === 0) { - return TAPi18next.t(key, { ns: 'project', lng: language }); - } - - return TAPi18next.t(key, { - postProcess: 'sprintf', - sprintf: replaces, - ns: 'project', - lng: language, - }); - }; - - translate.has = (key, { lng = language, ...options } = {}) => !!key && TAPi18next.exists(key, { ns: 'project', lng, ...options }); - - return translate; -}; - -const getLanguages = () => { - const result = Object.entries(TAPi18n.getLanguages()) - .map(([key, language]) => ({ ...language, key: key.toLowerCase() })) - .sort((a, b) => a.key - b.key); - - result.unshift({ - name: 'Default', - en: 'Default', - key: '', - }); - - return result; -}; - -const getLanguage = () => TAPi18n.getLanguage(); - -const loadLanguage = (language) => TAPi18n._loadLanguage(language); - -function TranslationProvider({ children }) { - const languages = useReactiveValue(getLanguages); - const language = useReactiveValue(getLanguage); - - const translate = useMemo(() => createTranslateFunction(language), [language]); - - const value = useMemo( - () => ({ - languages, - language, - loadLanguage, - translate, - }), - [languages, language, translate], - ); - - return ; -} - -export default TranslationProvider; diff --git a/apps/meteor/client/providers/TranslationProvider.tsx b/apps/meteor/client/providers/TranslationProvider.tsx new file mode 100644 index 000000000000..1aac1f0aafb3 --- /dev/null +++ b/apps/meteor/client/providers/TranslationProvider.tsx @@ -0,0 +1,148 @@ +import { TranslationContext, TranslationKey, useAbsoluteUrl } from '@rocket.chat/ui-contexts'; +import i18next from 'i18next'; +import I18NextHttpBackend from 'i18next-http-backend'; +import { TAPi18n, TAPi18next } from 'meteor/rocketchat:tap-i18n'; +import { Tracker } from 'meteor/tracker'; +import React, { ReactElement, ReactNode, useEffect, useMemo, useState } from 'react'; +import { I18nextProvider, initReactI18next } from 'react-i18next'; + +import { useReactiveValue } from '../hooks/useReactiveValue'; + +type TranslationNamespace = Extract extends `${infer T}.${string}` + ? T extends Lowercase + ? T + : never + : never; + +const namespaces = ['onboarding', 'registration'] as TranslationNamespace[]; + +const useI18next = (lng: string): typeof i18next => { + const basePath = useAbsoluteUrl()('/i18n'); + + const i18n = useState(() => { + const i18n = i18next.createInstance().use(I18NextHttpBackend).use(initReactI18next); + + i18n.init({ + lng, + fallbackLng: 'en', + ns: namespaces, + nsSeparator: '.', + debug: false, + backend: { + loadPath: `${basePath}/{{lng}}.json`, + parse: (data: string, _languages?: string | string[], namespaces: string | string[] = []): { [key: string]: any } => { + const source = JSON.parse(data); + const result: { [key: string]: any } = {}; + + for (const key of Object.keys(source)) { + const prefix = (Array.isArray(namespaces) ? namespaces : [namespaces]).find((namespace) => key.startsWith(`${namespace}.`)); + + if (prefix) { + result[key.slice(prefix.length + 1)] = source[key]; + } + } + + return result; + }, + }, + }); + + return i18n; + })[0]; + + useEffect(() => { + i18n.changeLanguage(lng); + }, [i18n, lng]); + + return i18n; +}; + +const createTranslateFunction = ( + language: string, +): { + (key: TranslationKey, ...replaces: unknown[]): string; + has: (key: string | undefined, options?: { lng?: string }) => key is TranslationKey; +} => + Tracker.nonreactive(() => { + const translate = (key: TranslationKey, ...replaces: unknown[]): string => { + if (typeof replaces[0] === 'object') { + const [options, lang_tag = language] = replaces; + return TAPi18next.t(key, { + ns: 'project', + lng: String(lang_tag), + ...options, + }); + } + + if (replaces.length === 0) { + return TAPi18next.t(key, { ns: 'project', lng: language }); + } + + return TAPi18next.t(key, { + postProcess: 'sprintf', + sprintf: replaces, + ns: 'project', + lng: language, + }); + }; + + translate.has = ( + key: string | undefined, + { + lng = language, + }: { + lng?: string; + } = {}, + ): key is TranslationKey => !!key && TAPi18next.exists(key, { ns: 'project', lng }); + + return translate; + }); + +const getLanguages = (): { name: string; en: string; key: string }[] => { + const result = Object.entries(TAPi18n.getLanguages()) + .map(([key, language]) => ({ ...language, key: key.toLowerCase() })) + .sort((a, b) => a.key.localeCompare(b.key)); + + result.unshift({ + name: 'Default', + en: 'Default', + key: '', + }); + + return result; +}; + +const getLanguage = (): string => TAPi18n.getLanguage(); + +const loadLanguage = async (language: string): Promise => { + TAPi18n.setLanguage(language); +}; + +type TranslationProviderProps = { + children: ReactNode; +}; + +const TranslationProvider = ({ children }: TranslationProviderProps): ReactElement => { + const languages = useReactiveValue(getLanguages); + const language = useReactiveValue(getLanguage); + + const i18nextInstance = useI18next(language); + + const value = useMemo( + () => ({ + languages, + language, + loadLanguage, + translate: createTranslateFunction(language), + }), + [languages, language], + ); + + return ( + + + + ); +}; + +export default TranslationProvider; diff --git a/apps/meteor/client/providers/UserProvider.tsx b/apps/meteor/client/providers/UserProvider.tsx index 9f9812d29a01..a1458342fe9e 100644 --- a/apps/meteor/client/providers/UserProvider.tsx +++ b/apps/meteor/client/providers/UserProvider.tsx @@ -1,7 +1,7 @@ import type { IRoom, ISubscription, IUser } from '@rocket.chat/core-typings'; -import { UserContext } from '@rocket.chat/ui-contexts'; +import { UserContext, LoginService, useSetting } from '@rocket.chat/ui-contexts'; import { Meteor } from 'meteor/meteor'; -import React, { useMemo, FC } from 'react'; +import React, { FC, useEffect, useMemo } from 'react'; import { Subscriptions, Rooms } from '../../app/models/client'; import { getUserPreference } from '../../app/utils/client'; @@ -14,17 +14,20 @@ const getUserId = (): string | null => Meteor.userId(); const getUser = (): IUser | null => Meteor.user() as IUser | null; -const loginWithPassword = (user: string | object, password: string): Promise => - new Promise((resolve, reject) => { - Meteor.loginWithPassword(user, password, (error: Error | Meteor.Error | Meteor.TypedError | undefined) => { - if (error) { - reject(error); - return; - } +const capitalize = (str: string): string => str.charAt(0).toUpperCase() + str.slice(1); - resolve(); - }); - }); +const config: Record> = { + 'facebook': { buttonColor: '#325c99' }, + 'twitter': { buttonColor: '#02acec' }, + 'google': { buttonColor: '#dd4b39' }, + 'github': { buttonColor: '#4c4c4c', title: 'GitHub' }, + 'github_enterprise': { buttonColor: '#4c4c4c', title: 'GitHub Enterprise' }, + 'gitlab': { buttonColor: '#373d47', title: 'GitLab' }, + 'trello': { buttonColor: '#026aa7' }, + 'meteor-developer': { buttonColor: '#de4f4f', title: 'Meteor' }, + 'wordpress': { buttonColor: '#1e8cbe', title: 'WordPress' }, + 'linkedin': { buttonColor: '#1b86bc' }, +}; const logout = (): Promise => new Promise((resolve, reject) => { @@ -40,15 +43,36 @@ const logout = (): Promise => }); }); +type LoginMethods = keyof typeof Meteor; + const UserProvider: FC = ({ children }) => { + const isLdapEnabled = Boolean(useSetting('LDAP_Enable')); + const isCrowdEnabled = Boolean(useSetting('CROWD_Enable')); + const userId = useReactiveValue(getUserId); const user = useReactiveValue(getUser); + + const loginMethod: LoginMethods = (isLdapEnabled && 'loginWithLDAP') || (isCrowdEnabled && 'loginWithCrowd') || 'loginWithPassword'; + + useEffect(() => { + if (isLdapEnabled && isCrowdEnabled) { + if (process.env.NODE_ENV === 'development') { + throw new Error('You can not use both LDAP and Crowd at the same time'); + } + console.log('Both LDAP and Crowd are enabled. Please disable one of them.'); + } + if (!Meteor[loginMethod]) { + if (process.env.NODE_ENV === 'development') { + throw new Error(`Meteor.${loginMethod} is not defined`); + } + console.log(`Meteor.${loginMethod} is not defined`); + } + }, [isLdapEnabled, isCrowdEnabled, loginMethod]); + const contextValue = useMemo( () => ({ userId, user, - loginWithPassword, - logout, queryPreference: createReactiveSubscriptionFactory( (key: string, defaultValue?: T) => getUserPreference(userId, key, defaultValue) as T, ), @@ -59,8 +83,75 @@ const UserProvider: FC = ({ children }) => { querySubscriptions: createReactiveSubscriptionFactory | []>((query, options) => (userId ? Subscriptions : Rooms).find(query, options).fetch(), ), + loginWithToken: (token: string): Promise => + new Promise((resolve, reject) => + Meteor.loginWithToken(token, (err) => { + if (err) { + return reject(err); + } + resolve(undefined); + }), + ), + loginWithPassword: (user: string | object, password: string): Promise => + new Promise((resolve, reject) => { + Meteor[loginMethod](user, password, (error: Error | Meteor.Error | Meteor.TypedError | undefined) => { + if (error) { + reject(error); + return; + } + + resolve(); + }); + }), + logout, + loginWithService: ({ service, clientConfig }: T): (() => Promise) => { + const loginMethods = { + 'meteor-developer': 'MeteorDeveloperAccount', + }; + + const loginWithService = `loginWith${(loginMethods as any)[service] || capitalize(String(service || ''))}`; + + const method: (config: unknown, cb: (error: any) => void) => Promise = (Meteor as any)[loginWithService] as any; + + if (!method) { + return () => Promise.reject(new Error('Login method not found')); + } + + return () => + new Promise((resolve, reject) => { + method(clientConfig, (error: any): void => { + if (!error) { + resolve(true); + return; + } + reject(error); + }); + }); + }, + queryAllServices: createReactiveSubscriptionFactory(() => + ServiceConfiguration.configurations + .find( + { + showButton: { $ne: false }, + }, + { + sort: { + service: 1, + }, + }, + ) + .fetch() + .map( + ({ appId: _, ...service }) => + ({ + title: capitalize(String((service as any).service || '')), + ...service, + ...(config[(service as any).service] ?? {}), + } as any), + ), + ), }), - [userId, user], + [userId, user, loginMethod], ); return ; diff --git a/apps/meteor/client/sidebar/RoomList/SideBarItemTemplateWithData.tsx b/apps/meteor/client/sidebar/RoomList/SideBarItemTemplateWithData.tsx index 263d4d75dd54..29f3f2fb69d2 100644 --- a/apps/meteor/client/sidebar/RoomList/SideBarItemTemplateWithData.tsx +++ b/apps/meteor/client/sidebar/RoomList/SideBarItemTemplateWithData.tsx @@ -61,7 +61,6 @@ type RoomListRowProps = { isAnonymous?: boolean; room: ISubscription & IRoom; - lastMessage?: IMessage; id?: string; /* @deprecated */ style?: AllHTMLAttributes['style']; @@ -218,7 +217,7 @@ export default memo(SideBarItemTemplateWithData, (prevProps, nextProps) => { if (prevProps.room._updatedAt?.toISOString() !== nextProps.room._updatedAt?.toISOString()) { return false; } - if (safeDateNotEqualCheck(prevProps.lastMessage?._updatedAt, nextProps.lastMessage?._updatedAt)) { + if (safeDateNotEqualCheck(prevProps.room.lastMessage?._updatedAt, nextProps.room.lastMessage?._updatedAt)) { return false; } if (prevProps.room.alert !== nextProps.room.alert) { diff --git a/apps/meteor/client/sidebar/Sidebar.stories.tsx b/apps/meteor/client/sidebar/Sidebar.stories.tsx index 023b852f0d46..bf2e7f7579b1 100644 --- a/apps/meteor/client/sidebar/Sidebar.stories.tsx +++ b/apps/meteor/client/sidebar/Sidebar.stories.tsx @@ -1,5 +1,5 @@ import type { ISetting, ISubscription } from '@rocket.chat/core-typings'; -import { UserContext, SettingsContext } from '@rocket.chat/ui-contexts'; +import { UserContext, SettingsContext, LoginService } from '@rocket.chat/ui-contexts'; import { Meta, Story } from '@storybook/react'; import type { ObjectId } from 'mongodb'; import React, { ContextType } from 'react'; @@ -86,9 +86,13 @@ const userContextValue: ContextType = { ], querySubscriptions: () => [() => () => undefined, () => subscriptions], querySubscription: () => [() => () => undefined, () => undefined], - loginWithPassword: () => Promise.resolve(undefined), - logout: () => Promise.resolve(undefined), queryRoom: () => [() => () => undefined, () => undefined], + + queryAllServices: () => [() => (): void => undefined, (): LoginService[] => []], + loginWithService: () => () => Promise.reject('loginWithService not implemented'), + loginWithPassword: async () => Promise.reject('loginWithPassword not implemented'), + loginWithToken: async () => Promise.reject('loginWithToken not implemented'), + logout: () => Promise.resolve(), }; export const Sidebar: Story = () => ( diff --git a/apps/meteor/client/sidebar/header/CreateDirectMessage.tsx b/apps/meteor/client/sidebar/header/CreateDirectMessage.tsx index 00ed4895690f..20babcb7ce7d 100644 --- a/apps/meteor/client/sidebar/header/CreateDirectMessage.tsx +++ b/apps/meteor/client/sidebar/header/CreateDirectMessage.tsx @@ -34,7 +34,7 @@ const CreateDirectMessage: FC = ({ onClose }) => { }); return ( - + {t('Direct_Messages')} diff --git a/apps/meteor/client/sidebar/sections/actions/OmnichannelLivechatToggle.tsx b/apps/meteor/client/sidebar/sections/actions/OmnichannelLivechatToggle.tsx index b45f0771fd09..e0417ef2d472 100644 --- a/apps/meteor/client/sidebar/sections/actions/OmnichannelLivechatToggle.tsx +++ b/apps/meteor/client/sidebar/sections/actions/OmnichannelLivechatToggle.tsx @@ -21,6 +21,7 @@ export const OmnichannelLivechatToggle = (): ReactElement => { return ( { Tracker.autorun(() => { @@ -11,7 +11,7 @@ Meteor.startup(() => { if (!userId) { return; } - + Session.set('force_logout', false); Notifications.onUser('force_logout', () => { Session.set('force_logout', true); }); diff --git a/apps/meteor/client/startup/index.ts b/apps/meteor/client/startup/index.ts index 86212df4ecee..8a203ecca545 100644 --- a/apps/meteor/client/startup/index.ts +++ b/apps/meteor/client/startup/index.ts @@ -9,6 +9,7 @@ import './customTranslations'; import './e2e'; import './emailVerification'; import './enterRoom'; +import './forceLogout'; import './i18n'; import './incomingMessages'; import './ldap'; diff --git a/apps/meteor/client/startup/routes.tsx b/apps/meteor/client/startup/routes.tsx index 2d10424951ff..4396a5dd60c8 100644 --- a/apps/meteor/client/startup/routes.tsx +++ b/apps/meteor/client/startup/routes.tsx @@ -17,12 +17,16 @@ const PageLoading = lazy(() => import('../views/root/PageLoading')); const HomePage = lazy(() => import('../views/home/HomePage')); const InvitePage = lazy(() => import('../views/invite/InvitePage')); const SecretURLPage = lazy(() => import('../views/invite/SecretURLPage')); -const CMSPage = lazy(() => import('../views/root/CMSPage')); -const ResetPasswordPage = lazy(() => import('../views/login/ResetPassword/ResetPassword')); -const SetupWizardRoute = lazy(() => import('../views/setupWizard/SetupWizardRoute')); +const CMSPage = lazy(() => import('@rocket.chat/web-ui-registration').then(({ CMSPage }) => ({ default: CMSPage }))); +const ResetPasswordPage = lazy(() => + import('@rocket.chat/web-ui-registration').then(({ ResetPasswordPage }) => ({ default: ResetPasswordPage })), +); + const MailerUnsubscriptionPage = lazy(() => import('../views/mailer/MailerUnsubscriptionPage')); +const SetupWizardRoute = lazy(() => import('../views/setupWizard/SetupWizardRoute')); const NotFoundPage = lazy(() => import('../views/notFound/NotFoundPage')); const MeetPage = lazy(() => import('../views/meet/MeetPage')); + const DirectoryPage = lazy(() => import('../views/directory/DirectoryPage')); const OmnichannelDirectoryPage = lazy(() => import('../views/omnichannel/directory/OmnichannelDirectoryPage')); const OmnichannelQueueList = lazy(() => import('../views/omnichannel/queueList')); diff --git a/apps/meteor/client/templates.ts b/apps/meteor/client/templates.ts index 7e7c3ceb0213..e17e66e94dbd 100644 --- a/apps/meteor/client/templates.ts +++ b/apps/meteor/client/templates.ts @@ -56,10 +56,6 @@ createTemplateForComponent('omnichannelFlex', () => import('./views/omnichannel/ renderContainerView: () => HTML.DIV({ style: 'height: 100%; position: relative;' }), }); -createTemplateForComponent('loginLayoutHeader', () => import('./views/login/LoginLayout/Header')); - -createTemplateForComponent('loginLayoutFooter', () => import('./views/login/LoginLayout/Footer')); - createTemplateForComponent('ModalBlock', () => import('./views/blocks/ConnectedModalBlock'), { renderContainerView: () => HTML.DIV({ style: 'display: flex; width: 100%; height: 100%;' }), }); diff --git a/apps/meteor/client/views/account/preferences/PreferencesMessagesSection.tsx b/apps/meteor/client/views/account/preferences/PreferencesMessagesSection.tsx index fb4750790778..c7f70d353b6e 100644 --- a/apps/meteor/client/views/account/preferences/PreferencesMessagesSection.tsx +++ b/apps/meteor/client/views/account/preferences/PreferencesMessagesSection.tsx @@ -1,5 +1,5 @@ import { Accordion, Field, Select, FieldGroup, ToggleSwitch, SelectOption } from '@rocket.chat/fuselage'; -import { useUserPreference, useSetting, useTranslation, TranslationKey } from '@rocket.chat/ui-contexts'; +import { useUserPreference, useSetting, useTranslation } from '@rocket.chat/ui-contexts'; import React, { ReactElement, useMemo } from 'react'; import { useForm } from '../../../hooks/useForm'; @@ -82,9 +82,9 @@ const PreferencesMessagesSection = ({ onChange, commitRef, ...props }: FormSecti const alsoSendThreadMessageToChannelOptions = useMemo( (): SelectOption[] => [ - ['default', t('Default')], - ['always', t('Always' as TranslationKey)], // TO DO: add "Always" to en.json ui-contexts - ['never', t('Never')], + ['default', t('Selected_first_reply_unselected_following_replies')], + ['always', t('Selected_by_default')], + ['never', t('Unselected_by_default')], ], [t], ); diff --git a/apps/meteor/client/views/account/profile/AccountProfileForm.tsx b/apps/meteor/client/views/account/profile/AccountProfileForm.tsx index a081b9f53e5d..4adfdd65ac78 100644 --- a/apps/meteor/client/views/account/profile/AccountProfileForm.tsx +++ b/apps/meteor/client/views/account/profile/AccountProfileForm.tsx @@ -360,7 +360,7 @@ const AccountProfileForm = ({ values, handlers, user, settings, onSaveStateChang - + ); }; diff --git a/apps/meteor/client/views/admin/apps/AppDetailsPage/AppDetailsPage.tsx b/apps/meteor/client/views/admin/apps/AppDetailsPage/AppDetailsPage.tsx index 84f3ead9d165..9997c0f542c8 100644 --- a/apps/meteor/client/views/admin/apps/AppDetailsPage/AppDetailsPage.tsx +++ b/apps/meteor/client/views/admin/apps/AppDetailsPage/AppDetailsPage.tsx @@ -27,18 +27,19 @@ const AppDetailsPage = ({ id }: { id: App['id'] }): ReactElement => { const settingsRef = useRef>({}); const appData = useAppInfo(id); - const [routeName, urlParams] = useCurrentRoute(); - const appsRoute = useRoute('admin-apps'); - const marketplaceRoute = useRoute('admin-marketplace'); - const tab = useRouteParameter('tab'); - const [currentRouteName] = useCurrentRoute(); if (!currentRouteName) { throw new Error('No current route name'); } - const router = useRoute(currentRouteName); - const handleReturn = useMutableCallback((): void => router.push({})); + + const [, urlParams] = useCurrentRoute(); + const tab = useRouteParameter('tab'); + const context = useRouteParameter('context'); + + const handleReturn = useMutableCallback((): void => { + context && router.push({ context, page: 'list' }); + }); const { installed, settings, privacyPolicySummary, permissions, tosLink, privacyLink, marketplace } = appData || {}; const isSecurityVisible = privacyPolicySummary || permissions || tosLink || privacyLink; @@ -61,13 +62,7 @@ const AppDetailsPage = ({ id }: { id: App['id'] }): ReactElement => { }, [id, settings]); const handleTabClick = (tab: 'details' | 'security' | 'releases' | 'settings' | 'logs'): void => { - if (routeName === 'admin-marketplace') { - marketplaceRoute.replace({ ...urlParams, tab }); - } - - if (routeName === 'admin-apps') { - appsRoute.replace({ ...urlParams, tab }); - } + router.replace({ ...urlParams, tab }); }; return ( diff --git a/apps/meteor/client/views/admin/apps/AppInstallPage.js b/apps/meteor/client/views/admin/apps/AppInstallPage.js index 43f424ebe073..ef1c07d345c0 100644 --- a/apps/meteor/client/views/admin/apps/AppInstallPage.js +++ b/apps/meteor/client/views/admin/apps/AppInstallPage.js @@ -6,7 +6,8 @@ import { useEndpoint, useUpload, useTranslation, - useToastMessageDispatch, + useCurrentRoute, + useRouteParameter, } from '@rocket.chat/ui-contexts'; import React, { useCallback, useEffect, useState } from 'react'; @@ -16,16 +17,25 @@ import { useFileInput } from '../../../hooks/useFileInput'; import { useForm } from '../../../hooks/useForm'; import AppPermissionsReviewModal from './AppPermissionsReviewModal'; import AppUpdateModal from './AppUpdateModal'; -import { handleInstallError } from './helpers'; +import { useAppsReload } from './AppsContext'; +import { handleAPIError, handleInstallError } from './helpers'; import { getManifestFromZippedApp } from './lib/getManifestFromZippedApp'; const placeholderUrl = 'https://rocket.chat/apps/package.zip'; function AppInstallPage() { const t = useTranslation(); - const dispatchToastMessage = useToastMessageDispatch(); - const appsRoute = useRoute('admin-apps'); + const reload = useAppsReload(); + + const [currentRouteName] = useCurrentRoute(); + if (!currentRouteName) { + throw new Error('No current route name'); + } + const router = useRoute(currentRouteName); + + const context = useRouteParameter('context'); + const setModal = useSetModal(); const appId = useQueryStringParameter('id'); @@ -61,25 +71,20 @@ function AppInstallPage() { fileData.append('app', appFile, appFile.name); fileData.append('permissions', JSON.stringify(permissionsGranted)); - if (appId) { - try { + try { + if (appId) { await uploadUpdateApp(fileData); - } catch (error) { - dispatchToastMessage({ type: 'error', message: error }); - } finally { - setModal(null); - } - } else { - try { + } else { app = await uploadApp(fileData); - } catch (error) { - dispatchToastMessage({ type: 'error', message: error }); - } finally { - setModal(null); } + } catch (e) { + handleAPIError(e); } - appsRoute.push({ context: 'details', id: appId || app.app.id }); + router.push({ context: 'installed', page: 'info', id: appId || app.app.id }); + + reload(); + setModal(null); }; @@ -140,7 +145,7 @@ function AppInstallPage() { }; const handleCancel = () => { - appsRoute.push(); + router.push({ context, page: 'list' }); }; return ( diff --git a/apps/meteor/client/views/admin/apps/AppMenu.js b/apps/meteor/client/views/admin/apps/AppMenu.js index 44ee181a99c1..7c5314c54da2 100644 --- a/apps/meteor/client/views/admin/apps/AppMenu.js +++ b/apps/meteor/client/views/admin/apps/AppMenu.js @@ -1,5 +1,13 @@ import { Box, Icon, Menu, Throbber } from '@rocket.chat/fuselage'; -import { useSetModal, useMethod, useEndpoint, useTranslation, useRoute, useRouteParameter } from '@rocket.chat/ui-contexts'; +import { + useSetModal, + useMethod, + useEndpoint, + useTranslation, + useRoute, + useRouteParameter, + useCurrentRoute, +} from '@rocket.chat/ui-contexts'; import React, { useMemo, useCallback, useState } from 'react'; import { Apps } from '../../../../app/apps/client/orchestrator'; @@ -35,7 +43,13 @@ function AppMenu({ app, ...props }) { const t = useTranslation(); const setModal = useSetModal(); const checkUserLoggedIn = useMethod('cloud:checkUserLoggedIn'); - const appsRoute = useRoute('admin-apps'); + + const [currentRouteName] = useCurrentRoute(); + if (!currentRouteName) { + throw new Error('No current route name'); + } + const router = useRoute(currentRouteName); + const context = useRouteParameter('context'); const setAppStatus = useEndpoint('POST', `/apps/${app.id}/status`); @@ -97,8 +111,8 @@ function AppMenu({ app, ...props }) { }, [checkUserLoggedIn, setModal, closeModal, buildExternalUrl, app.id, app.purchaseType, syncApp]); const handleViewLogs = useCallback(() => { - appsRoute.push({ context: 'details', id: app.id, version: app.version, tab: 'logs' }); - }, [app.id, app.version, appsRoute]); + router.push({ context, page: 'info', id: app.id, version: app.version, tab: 'logs' }); + }, [app.id, app.version, context, router]); const handleDisable = useCallback(() => { const confirm = async () => { @@ -261,6 +275,11 @@ function AppMenu({ app, ...props }) { action: handleEnable, }, }), + ...(app.installed && { + divider: { + type: 'divider', + }, + }), ...(app.installed && { uninstall: { label: ( diff --git a/apps/meteor/client/views/admin/apps/AppsList/AppRow.tsx b/apps/meteor/client/views/admin/apps/AppsList/AppRow.tsx index aca58478dc98..1d07aeafbb88 100644 --- a/apps/meteor/client/views/admin/apps/AppsList/AppRow.tsx +++ b/apps/meteor/client/views/admin/apps/AppsList/AppRow.tsx @@ -3,8 +3,8 @@ import { css } from '@rocket.chat/css-in-js'; import { Box } from '@rocket.chat/fuselage'; import { useBreakpoints } from '@rocket.chat/fuselage-hooks'; import colors from '@rocket.chat/fuselage-tokens/colors'; -import { useRoute } from '@rocket.chat/ui-contexts'; -import React, { FC, memo, KeyboardEvent, MouseEvent } from 'react'; +import { useCurrentRoute, useRoute, useRouteParameter } from '@rocket.chat/ui-contexts'; +import React, { memo, KeyboardEvent, MouseEvent, ReactElement } from 'react'; import AppAvatar from '../../../../components/avatar/AppAvatar'; import AppStatus from '../AppDetailsPage/tabs/AppStatus/AppStatus'; @@ -13,32 +13,28 @@ import BundleChips from '../BundleChips'; type AppRowProps = App & { isMarketplace: boolean }; -// TODO: org props -const AppRow: FC = (props) => { - const { name, id, description, iconFileData, marketplaceVersion, iconFileContent, installed, isSubscribed, isMarketplace, bundledIn } = - props; +const AppRow = (props: AppRowProps): ReactElement => { + const { name, id, description, iconFileData, marketplaceVersion, iconFileContent, installed, isSubscribed, bundledIn } = props; const breakpoints = useBreakpoints(); const isDescriptionVisible = breakpoints.includes('xl'); - const appsRoute = useRoute('admin-apps'); - const marketplaceRoute = useRoute('admin-marketplace'); + const [currentRouteName] = useCurrentRoute(); + if (!currentRouteName) { + throw new Error('No current route name'); + } + const router = useRoute(currentRouteName); - const handleClick = (): void => { - if (isMarketplace) { - marketplaceRoute.push({ - context: 'details', + const context = useRouteParameter('context'); + + const handleNavigateToAppInfo = (): void => { + context && + router.push({ + context, + page: 'info', version: marketplaceVersion, id, }); - return; - } - - appsRoute.push({ - context: 'details', - version: marketplaceVersion, - id, - }); }; const handleKeyDown = (e: KeyboardEvent): void => { @@ -46,7 +42,7 @@ const AppRow: FC = (props) => { return; } - handleClick(); + handleNavigateToAppInfo(); }; const preventClickPropagation = (e: MouseEvent): void => { @@ -67,7 +63,7 @@ const AppRow: FC = (props) => { key={id} role='link' tabIndex={0} - onClick={handleClick} + onClick={handleNavigateToAppInfo} onKeyDown={handleKeyDown} display='flex' flexDirection='row' diff --git a/apps/meteor/client/views/admin/apps/AppsPage/AppsPage.tsx b/apps/meteor/client/views/admin/apps/AppsPage/AppsPage.tsx index 400ec4c3bcf1..23485b04dc87 100644 --- a/apps/meteor/client/views/admin/apps/AppsPage/AppsPage.tsx +++ b/apps/meteor/client/views/admin/apps/AppsPage/AppsPage.tsx @@ -1,5 +1,5 @@ import { Button, ButtonGroup, Icon, Skeleton, Tabs } from '@rocket.chat/fuselage'; -import { useRoute, useSetting, useMethod, useTranslation } from '@rocket.chat/ui-contexts'; +import { useRoute, useSetting, useMethod, useTranslation, useCurrentRoute, useRouteParameter } from '@rocket.chat/ui-contexts'; import React, { useEffect, useState, ReactElement } from 'react'; import Page from '../../../../components/Page'; @@ -13,11 +13,17 @@ const AppsPage = ({ isMarketplace }: AppsPageProps): ReactElement => { const t = useTranslation(); const isDevelopmentMode = useSetting('Apps_Framework_Development_Mode'); - const marketplaceRoute = useRoute('admin-marketplace'); - const appsRoute = useRoute('admin-apps'); const cloudRoute = useRoute('cloud'); const checkUserLoggedIn = useMethod('cloud:checkUserLoggedIn'); + const [currentRouteName] = useCurrentRoute(); + if (!currentRouteName) { + throw new Error('No current route name'); + } + const router = useRoute(currentRouteName); + + const context = useRouteParameter('context'); + const [isLoggedInCloud, setIsLoggedInCloud] = useState(); useEffect(() => { @@ -32,9 +38,13 @@ const AppsPage = ({ isMarketplace }: AppsPageProps): ReactElement => { }; const handleUploadButtonClick = (): void => { - appsRoute.push({ context: 'install' }); + context && router.push({ context, page: 'install' }); }; + const handleMarketplaceTabClick = (): void => router.push({ context: 'all', page: 'list' }); + + const handleInstalledTabClick = (): void => router.push({ context: 'installed', page: 'list' }); + return ( @@ -58,21 +68,15 @@ const AppsPage = ({ isMarketplace }: AppsPageProps): ReactElement => { - marketplaceRoute.push({ context: '' })} selected={isMarketplace}> + {t('Marketplace')} - marketplaceRoute.push({ context: 'installed' })} - selected={!isMarketplace} - mbe='neg-x4' - borderWidth='0' - borderBlockWidth='x4' - > + {t('Installed')} - + ); diff --git a/apps/meteor/client/views/admin/apps/AppsPage/AppsPageContent.tsx b/apps/meteor/client/views/admin/apps/AppsPage/AppsPageContent.tsx index ea6be4c0422b..bc3491504250 100644 --- a/apps/meteor/client/views/admin/apps/AppsPage/AppsPageContent.tsx +++ b/apps/meteor/client/views/admin/apps/AppsPage/AppsPageContent.tsx @@ -1,6 +1,6 @@ import { Pagination, Divider } from '@rocket.chat/fuselage'; import { useDebouncedState } from '@rocket.chat/fuselage-hooks'; -import { useRoute, useTranslation } from '@rocket.chat/ui-contexts'; +import { useCurrentRoute, useRoute, useRouteParameter, useTranslation } from '@rocket.chat/ui-contexts'; import React, { ReactElement, useMemo, useState } from 'react'; import { usePagination } from '../../../../components/GenericTable/hooks/usePagination'; @@ -19,14 +19,21 @@ import NoInstalledAppMatchesEmptyState from './NoInstalledAppMatchesEmptyState'; import NoInstalledAppsFoundEmptyState from './NoInstalledAppsFoundEmptyState'; import NoMarketplaceOrInstalledAppMatchesEmptyState from './NoMarketplaceOrInstalledAppMatchesEmptyState'; -const AppsPageContent = ({ isMarketplace }: { isMarketplace: boolean }): ReactElement => { +const AppsPageContent = (): ReactElement => { const t = useTranslation(); const { marketplaceApps, installedApps } = useAppsResult(); const [text, setText] = useDebouncedState('', 500); const reload = useAppsReload(); const { current, itemsPerPage, setItemsPerPage: onSetItemsPerPage, setCurrent: onSetCurrent, ...paginationProps } = usePagination(); - const marketplaceRoute = useRoute('admin-marketplace'); + const [currentRouteName] = useCurrentRoute(); + if (!currentRouteName) { + throw new Error('No current route name'); + } + const router = useRoute(currentRouteName); + + const context = useRouteParameter('context'); + const isMarketplace = context === 'all'; const [freePaidFilterStructure, setFreePaidFilterStructure] = useState({ label: t('Filter_By_Price'), @@ -85,6 +92,10 @@ const AppsPageContent = ({ isMarketplace }: { isMarketplace: boolean }): ReactEl sortFilterStructure.items.find((item) => item.checked)?.id !== 'mru' || selectedCategories.length > 0; + const handleReturn = (): void => { + router.push({ context: 'all', page: 'list' }); + }; + return ( <> marketplaceRoute.push({ context: '' })} + onButtonClick={handleReturn} /> )} - {noInstalledAppsFound && marketplaceRoute.push({ context: '' })} />} + {noInstalledAppsFound && } {appsResult.phase === AsyncStatePhase.REJECTED && } ); diff --git a/apps/meteor/client/views/admin/apps/AppsRoute.tsx b/apps/meteor/client/views/admin/apps/AppsRoute.tsx index 9480e86fd1a9..a014a94c0cc6 100644 --- a/apps/meteor/client/views/admin/apps/AppsRoute.tsx +++ b/apps/meteor/client/views/admin/apps/AppsRoute.tsx @@ -1,5 +1,5 @@ import { useRouteParameter, useRoute, usePermission, useMethod } from '@rocket.chat/ui-contexts'; -import React, { useState, useEffect, FC } from 'react'; +import React, { useState, useEffect, ReactElement } from 'react'; import PageSkeleton from '../../../components/PageSkeleton'; import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; @@ -8,17 +8,18 @@ import AppInstallPage from './AppInstallPage'; import AppsPage from './AppsPage/AppsPage'; import AppsProvider from './AppsProvider'; -const AppsRoute: FC = () => { +const AppsRoute = (): ReactElement => { const [isLoading, setLoading] = useState(true); - const canViewAppsAndMarketplace = usePermission('manage-apps'); + const canManageApps = usePermission('manage-apps'); const isAppsEngineEnabled = useMethod('apps/is-enabled'); const appsWhatIsItRoute = useRoute('admin-apps-disabled'); + const marketplaceRoute = useRoute('admin-marketplace'); useEffect(() => { let mounted = true; const initialize = async (): Promise => { - if (!canViewAppsAndMarketplace) { + if (!canManageApps) { return; } @@ -39,15 +40,16 @@ const AppsRoute: FC = () => { return (): void => { mounted = false; }; - }, [canViewAppsAndMarketplace, isAppsEngineEnabled, appsWhatIsItRoute]); + }, [canManageApps, isAppsEngineEnabled, appsWhatIsItRoute, marketplaceRoute]); const context = useRouteParameter('context'); const isMarketplace = !context; const id = useRouteParameter('id'); + const page = useRouteParameter('page'); - if (!canViewAppsAndMarketplace) { + if (!canManageApps) { return ; } @@ -57,9 +59,9 @@ const AppsRoute: FC = () => { return ( - {((!context || context === 'installed') && ) || - (id && context === 'details' && ) || - (context === 'install' && )} + {(page === 'list' && ) || + (id && page === 'info' && ) || + (page === 'install' && )} ); }; diff --git a/apps/meteor/client/views/admin/emailInbox/EmailInboxForm.tsx b/apps/meteor/client/views/admin/emailInbox/EmailInboxForm.tsx index 074e3031ca88..0419bda84731 100644 --- a/apps/meteor/client/views/admin/emailInbox/EmailInboxForm.tsx +++ b/apps/meteor/client/views/admin/emailInbox/EmailInboxForm.tsx @@ -47,7 +47,7 @@ const EmailInboxForm = ({ inboxData }: { inboxData?: IEmailInboxPayload }): Reac email: inboxData?.email, description: inboxData?.description, senderInfo: inboxData?.senderInfo, - department: inboxData?.department, + department: inboxData?.department || '', // SMTP smtpServer: inboxData?.smtp.server, smtpPort: inboxData?.smtp.port ?? 587, diff --git a/apps/meteor/client/views/admin/info/FederationCard/components/CardHeader.tsx b/apps/meteor/client/views/admin/info/FederationCard/components/CardHeader.tsx index 9b989576f022..8eca8586e2ca 100644 --- a/apps/meteor/client/views/admin/info/FederationCard/components/CardHeader.tsx +++ b/apps/meteor/client/views/admin/info/FederationCard/components/CardHeader.tsx @@ -2,7 +2,7 @@ import { Box } from '@rocket.chat/fuselage'; import React, { FC } from 'react'; const CardHeader: FC = ({ children }) => ( - + {children} ); diff --git a/apps/meteor/client/views/admin/info/InformationPage.tsx b/apps/meteor/client/views/admin/info/InformationPage.tsx index 5048fa4732ad..981f540a4f58 100644 --- a/apps/meteor/client/views/admin/info/InformationPage.tsx +++ b/apps/meteor/client/views/admin/info/InformationPage.tsx @@ -5,6 +5,7 @@ import React, { memo } from 'react'; import SeatsCard from '../../../../ee/client/views/admin/info/SeatsCard'; import Page from '../../../components/Page'; +import { useIsEnterprise } from '../../../hooks/useIsEnterprise'; import DeploymentCard from './DeploymentCard'; import FederationCard from './FederationCard'; import LicenseCard from './LicenseCard'; @@ -29,11 +30,13 @@ const InformationPage = memo(function InformationPage({ }: InformationPageProps) { const t = useTranslation(); + const isEnterprise = useIsEnterprise(); + if (!info) { return null; } - const warningMultipleInstances = !statistics?.msEnabled && statistics?.instanceCount > 1; + const warningMultipleInstances = !isEnterprise && !statistics?.msEnabled && statistics?.instanceCount > 1; const alertOplogForMultipleInstances = warningMultipleInstances && !statistics.oplogEnabled; return ( @@ -54,7 +57,7 @@ const InformationPage = memo(function InformationPage({ {warningMultipleInstances && ( - + )} {alertOplogForMultipleInstances && ( import('./apps/AppsWhatIsIt')), }); -registerAdminRoute('/marketplace/:context?/:id?/:version?/:tab?', { +registerAdminRoute('/marketplace/:context?/:page?/:id?/:version?/:tab?', { name: 'admin-marketplace', component: lazy(() => import('./apps/AppsRoute')), -}); - -registerAdminRoute('/apps/:context?/:id?/:version?/:tab?', { - name: 'admin-apps', - component: lazy(() => import('./apps/AppsRoute')), + triggersEnter: [ + (context, redirect): void => { + if (!context.params.context) { + redirect('/admin/marketplace/all/list'); + } + }, + ], }); registerAdminRoute('/info', { diff --git a/apps/meteor/client/views/home/DefaultHomePage.tsx b/apps/meteor/client/views/home/DefaultHomePage.tsx index 1f48c7f1e565..634621b50d31 100644 --- a/apps/meteor/client/views/home/DefaultHomePage.tsx +++ b/apps/meteor/client/views/home/DefaultHomePage.tsx @@ -24,10 +24,10 @@ const DefaultHomePage = (): ReactElement => { const displayCustomBody = Boolean(useSetting('Layout_Home_Body')); return ( - + - + {t('Welcome_to', { Site_Name: workspaceName || 'Rocket.Chat' })} diff --git a/apps/meteor/client/views/invite/SecretURLPage.tsx b/apps/meteor/client/views/invite/SecretURLPage.tsx index 2fd28cf18fce..c788eed05e8f 100644 --- a/apps/meteor/client/views/invite/SecretURLPage.tsx +++ b/apps/meteor/client/views/invite/SecretURLPage.tsx @@ -1,65 +1,22 @@ -import { useSessionDispatch, useRouteParameter, useSetting, useTranslation } from '@rocket.chat/ui-contexts'; -import { useQuery } from '@tanstack/react-query'; -import React, { ReactElement } from 'react'; - -import { KonchatNotification } from '../../../app/ui/client'; -import { call } from '../../lib/utils/call'; -import LoginPage from '../root/MainLayout/LoginPage'; -import PageLoading from '../root/PageLoading'; - -const SecretURLPage = (): ReactElement => { - const t = useTranslation(); - - const hash = useRouteParameter('hash'); - const registrationForm = useSetting('Accounts_RegistrationForm'); - const setLoginDefaultState = useSessionDispatch('loginDefaultState'); - - const { isLoading, data } = useQuery( - ['secretURL', hash], - async () => { - if (registrationForm !== 'Secret URL' || !hash) { - return false; - } - - return call('checkRegistrationSecretURL', hash); - }, - { - onSuccess: (valid) => { - if (!valid) { - return; - } - - setLoginDefaultState('register'); - KonchatNotification.getDesktopPermission(); - }, - }, - ); - - if (isLoading) { - return ; - } - - if (data) { - return ; +import { useUserId, useRoute } from '@rocket.chat/ui-contexts'; +import RegistrationPageRouter from '@rocket.chat/web-ui-registration'; +import React, { ReactElement, useEffect } from 'react'; + +const SecretURLPage = (): ReactElement | null => { + const uid = useUserId(); + const homeRouter = useRoute('home'); + + useEffect(() => { + if (uid) { + homeRouter.replace(); + } + }, [uid, homeRouter]); + + if (uid) { + return null; } - return ( -
-
-
- - - -
-
-
- - {t('Invalid_secret_URL_message')} -
-
-
-
- ); + return ; }; export default SecretURLPage; diff --git a/apps/meteor/client/views/login/LoginLayout/Footer.tsx b/apps/meteor/client/views/login/LoginLayout/Footer.tsx deleted file mode 100644 index 56ce68bfc29e..000000000000 --- a/apps/meteor/client/views/login/LoginLayout/Footer.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { useSetting, useLoadLanguage, useTranslation } from '@rocket.chat/ui-contexts'; -import { Meteor } from 'meteor/meteor'; -import React, { ReactElement, useCallback, useEffect, useState } from 'react'; - -import { filterLanguage } from '../../../lib/utils/filterLanguage'; - -const Footer = (): ReactElement => { - const t = useTranslation(); - const loadLanguage = useLoadLanguage(); - - const serverLanguage = filterLanguage((useSetting('Language') as string | undefined) ?? 'en'); - - const getSuggestedLanguage = useCallback( - (loadedLanguage) => { - if (serverLanguage !== loadedLanguage) { - return serverLanguage; - } - - if (serverLanguage !== 'en') { - return 'en'; - } - - return undefined; - }, - [serverLanguage], - ); - - const [currentLanguage, setCurrentLanguage] = useState(() => filterLanguage(Meteor._localStorage.getItem('userLanguage') ?? 'en')); - - useEffect(() => { - loadLanguage(currentLanguage).then(() => { - window?.setLanguage?.(currentLanguage); - }); - }, [currentLanguage, loadLanguage]); - - const [suggestedLanguage, setSuggestedLanguage] = useState(() => { - const currentLanguage = filterLanguage(Meteor._localStorage.getItem('userLanguage') ?? 'en'); - return getSuggestedLanguage(currentLanguage); - }); - - const handleSwitchLanguageClick = (): void => { - const language = suggestedLanguage; - - if (!language) { - return; - } - - setCurrentLanguage(language); - setSuggestedLanguage(getSuggestedLanguage(language)); - }; - - return ( -
- {suggestedLanguage ? ( -
- -
- ) : null} -
- ); -}; - -export default Footer; diff --git a/apps/meteor/client/views/login/LoginLayout/Header.tsx b/apps/meteor/client/views/login/LoginLayout/Header.tsx deleted file mode 100644 index ce929bf5fc85..000000000000 --- a/apps/meteor/client/views/login/LoginLayout/Header.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React, { ReactElement } from 'react'; - -import { useAssetPath } from './useAssetPath'; - -const Header = (): ReactElement => { - const logoUrl = useAssetPath('Assets_logo'); - - return ( -
- - - -
- ); -}; - -export default Header; diff --git a/apps/meteor/client/views/login/LoginLayout/LoginLayout.tsx b/apps/meteor/client/views/login/LoginLayout/LoginLayout.tsx deleted file mode 100644 index 09d1f843e6fb..000000000000 --- a/apps/meteor/client/views/login/LoginLayout/LoginLayout.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React, { ReactElement, ReactNode } from 'react'; - -import Footer from './Footer'; -import Header from './Header'; -import { useAssetPath } from './useAssetPath'; - -type LoginLayoutProps = { - children?: ReactNode; -}; - -const LoginLayout = ({ children }: LoginLayoutProps): ReactElement => { - const backgroundUrl = useAssetPath('Assets_background'); - - return ( -
-
-
- {children} -
-
-
- ); -}; - -export default LoginLayout; diff --git a/apps/meteor/client/views/login/LoginLayout/index.ts b/apps/meteor/client/views/login/LoginLayout/index.ts deleted file mode 100644 index 6f3a5a1e9a53..000000000000 --- a/apps/meteor/client/views/login/LoginLayout/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './LoginLayout'; diff --git a/apps/meteor/client/views/login/LoginLayout/useAssetPath.ts b/apps/meteor/client/views/login/LoginLayout/useAssetPath.ts deleted file mode 100644 index e4beea7ee728..000000000000 --- a/apps/meteor/client/views/login/LoginLayout/useAssetPath.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { ISetting } from '@rocket.chat/core-typings'; -import { useSetting } from '@rocket.chat/ui-contexts'; - -type Asset = { url?: string; defaultUrl?: string }; - -export const useAssetPath = (assetId: ISetting['_id']): string | undefined => { - const asset = useSetting(assetId) as Asset | undefined; - - const prefix = window.__meteor_runtime_config__.ROOT_URL_PATH_PREFIX ?? ''; - const url = asset?.url ?? asset?.defaultUrl; - - return url ? `${prefix}/${url}` : undefined; -}; diff --git a/apps/meteor/client/views/login/ResetPassword/ResetPassword.stories.tsx b/apps/meteor/client/views/login/ResetPassword/ResetPassword.stories.tsx deleted file mode 100644 index b72ccc7bb5b6..000000000000 --- a/apps/meteor/client/views/login/ResetPassword/ResetPassword.stories.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react'; - -import ResetPassword from './ResetPassword'; - -export default { - title: 'Login/ResetPassword', - component: ResetPassword, -}; - -export const Basic = () => ; diff --git a/apps/meteor/client/views/login/ResetPassword/ResetPassword.tsx b/apps/meteor/client/views/login/ResetPassword/ResetPassword.tsx deleted file mode 100644 index 437a0cbf39dd..000000000000 --- a/apps/meteor/client/views/login/ResetPassword/ResetPassword.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import { Button, Field, Modal, Box, Throbber, PasswordInput } from '@rocket.chat/fuselage'; -import { useSafely } from '@rocket.chat/fuselage-hooks'; -import { - useRouteParameter, - useRoute, - useUser, - useMethod, - useTranslation, - TranslationKey, - useToastMessageDispatch, - useEndpoint, -} from '@rocket.chat/ui-contexts'; -import { useQuery } from '@tanstack/react-query'; -import { Meteor } from 'meteor/meteor'; -import React, { useState, useCallback, ReactElement } from 'react'; - -import LoginLayout from '../LoginLayout'; - -const getChangePasswordReason = ({ - requirePasswordChange, - requirePasswordChangeReason = requirePasswordChange ? 'You_need_to_change_your_password' : 'Please_enter_your_new_password_below', -}: { requirePasswordChange?: boolean; requirePasswordChangeReason?: TranslationKey } = {}): TranslationKey => requirePasswordChangeReason; - -const ResetPassword = (): ReactElement => { - const user = useUser(); - const t = useTranslation(); - const setUserPassword = useMethod('setUserPassword'); - const resetPassword = useMethod('resetPassword'); - const token = useRouteParameter('token'); - - const getPasswordPolicy = useEndpoint('GET', '/v1/pw.getPolicy'); - const getPasswordPolicyRest = useEndpoint('GET', '/v1/pw.getPolicyReset'); - - const dispatchToastMessage = useToastMessageDispatch(); - - const { data: { enabled: policyEnabled, policy: policies } = {} } = useQuery( - ['login/password-policy', token], - async () => (user || !token ? getPasswordPolicy() : getPasswordPolicyRest({ token })), - { - onError: (error: any) => { - dispatchToastMessage({ type: 'error', message: error }); - }, - }, - ); - - const router = useRoute('home'); - - const changePasswordReason = getChangePasswordReason(user || {}); - - const [newPassword, setNewPassword] = useState(''); - const [isLoading, setIsLoading] = useSafely(useState(false)); - const [error, setError] = useSafely(useState()); - - const handleOnChange = useCallback((event) => setNewPassword(event.currentTarget.value), [setNewPassword]); - - const isSubmitDisabled = !newPassword.trim() || isLoading; - - const handleSubmit = useCallback( - async (e) => { - e.preventDefault(); - if (isSubmitDisabled) { - return; - } - setIsLoading(true); - try { - if (token && resetPassword) { - const result = await resetPassword(token, newPassword); - await Meteor.loginWithToken(result.token); - router.push({}); - } else { - await setUserPassword(newPassword); - } - } catch ({ error, reason }) { - const _error = reason ?? error; - setError(_error ? String(_error) : undefined); - } finally { - setIsLoading(false); - } - }, - [isSubmitDisabled, setIsLoading, token, resetPassword, newPassword, router, setUserPassword, setError], - ); - - return ( - - - - {t('Password')} - - - - {t(changePasswordReason)} - - - - {error && {error}} - {policyEnabled && ( - - {policies?.map((policy, index) => ( - - {t(...(policy as unknown as [name: TranslationKey, options?: Record]))} - - ))} - - )} - - - - - - - - - - ); -}; - -export default ResetPassword; diff --git a/apps/meteor/client/views/omnichannel/currentChats/FilterByText.tsx b/apps/meteor/client/views/omnichannel/currentChats/FilterByText.tsx index ba5f94b65603..11a5df1f42df 100644 --- a/apps/meteor/client/views/omnichannel/currentChats/FilterByText.tsx +++ b/apps/meteor/client/views/omnichannel/currentChats/FilterByText.tsx @@ -74,7 +74,7 @@ const FilterByText: FilterByTextType = ({ setFilter, reload, customFields, setCu guest, servedBy, status, - ...(department?.value && department.value !== 'all' && { department: department.value }), + department: department?.value && department.value !== 'all' ? department.value : '', from: from && moment(new Date(from)).utc().format('YYYY-MM-DDTHH:mm:ss'), to: to && moment(new Date(to)).utc().format('YYYY-MM-DDTHH:mm:ss'), tags: tags.map((tag) => tag.label), @@ -149,7 +149,7 @@ const FilterByText: FilterByTextType = ({ setFilter, reload, customFields, setCu - + {EETagsComponent && ( diff --git a/apps/meteor/client/views/omnichannel/queueList/QueueListFilter.tsx b/apps/meteor/client/views/omnichannel/queueList/QueueListFilter.tsx index 00c49783d23a..a47663dc1a86 100644 --- a/apps/meteor/client/views/omnichannel/queueList/QueueListFilter.tsx +++ b/apps/meteor/client/views/omnichannel/queueList/QueueListFilter.tsx @@ -58,7 +58,7 @@ export const QueueListFilter: QueueListFilterPropsType = ({ setFilter, ...props
- +
diff --git a/apps/meteor/client/views/room/MessageList/components/UrlPreview/PreviewList.tsx b/apps/meteor/client/views/room/MessageList/components/UrlPreview/PreviewList.tsx index 8b1b23a5c11d..d6c4d6b8cf73 100644 --- a/apps/meteor/client/views/room/MessageList/components/UrlPreview/PreviewList.tsx +++ b/apps/meteor/client/views/room/MessageList/components/UrlPreview/PreviewList.tsx @@ -57,7 +57,7 @@ export const buildImageURL = (url: string, imageUrl: string): string => { const { origin } = new URL(url); const imgURL = `${origin}/${imageUrl}`; - const normalizedUrl = imgURL.replace(/(? { - const homeRoute = useRoute('/'); - const pageContent = useSetting(page) as string; - - const handlePageCloseClick = (): void => { - homeRoute.push(); - }; - - return ( -
-
-
-
- -
-
-
-
-
- ); -}; - -export default CMSPage; diff --git a/apps/meteor/client/views/root/MainLayout/LayoutWithSidebar.tsx b/apps/meteor/client/views/root/MainLayout/LayoutWithSidebar.tsx index 86f950d7f581..9fed698e6f1f 100644 --- a/apps/meteor/client/views/root/MainLayout/LayoutWithSidebar.tsx +++ b/apps/meteor/client/views/root/MainLayout/LayoutWithSidebar.tsx @@ -18,7 +18,7 @@ const LayoutWithSidebar = ({ children }: { children: ReactNode }): ReactElement return ( { + const showForcedLogoutBanner = useSession('force_logout'); const iframeLoginUrl = useIframeLogin(); if (iframeLoginUrl) { return