From d2b334623a527b513ff74a6e8daf5737f4f5aeaf Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Fri, 22 Sep 2023 12:20:23 -0300 Subject: [PATCH 01/11] Empty commit From 9a6a4dd8d459713030cee109d20f208fe4524aa2 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Mon, 25 Sep 2023 13:11:18 -0300 Subject: [PATCH 02/11] feat(cloud): Base for announcements (#30471) * Empty commit * Add type for synchronization payload * Splitting fetching and consumption of sync data * Normalize registration data * Expand `Cloud.Announcement` * Add model for cloud announcements * Validate the payload * Use `suretype` * Refactor a bunch of stuff * Handle announcements on sync * Fix typo --- .../server/functions/buildRegistrationData.ts | 24 +- .../server/functions/connectWorkspace.ts | 85 ++++-- .../functions/finishOAuthAuthorization.ts | 22 +- .../server/functions/getConfirmationPoll.ts | 18 +- .../functions/getUserCloudAccessToken.ts | 12 +- .../functions/getWorkspaceAccessToken.ts | 8 +- .../getWorkspaceAccessTokenWithScope.ts | 12 +- .../server/functions/getWorkspaceLicense.ts | 104 ++++--- .../server/functions/reconnectWorkspace.ts | 2 +- .../registerPreIntentWorkspaceWizard.ts | 10 +- .../server/functions/saveRegistrationData.ts | 2 +- .../functions/startRegisterWorkspace.ts | 19 +- .../startRegisterWorkspaceSetupWizard.ts | 16 +- .../cloud/server/functions/syncWorkspace.ts | 274 +++++++++++++----- .../app/cloud/server/functions/userLogout.ts | 3 +- .../app/statistics/server/lib/statistics.ts | 34 ++- .../lib/errors/CloudWorkspaceAccessError.ts | 8 + .../errors/CloudWorkspaceConnectionError.ts | 8 + apps/meteor/lib/errors/CloudWorkspaceError.ts | 6 + .../lib/errors/CloudWorkspaceLicenseError.ts | 8 + .../errors/CloudWorkspaceRegistrationError.ts | 8 + apps/meteor/package.json | 3 +- .../server/models/CloudAnnouncements.ts | 6 + .../server/models/raw/CloudAnnouncements.ts | 15 + apps/meteor/server/models/startup.ts | 1 + apps/meteor/server/services/banner/service.ts | 2 +- package.json | 2 +- .../core-services/src/types/IBannerService.ts | 2 +- packages/core-typings/.eslintrc.json | 5 +- packages/core-typings/package.json | 15 +- packages/core-typings/src/IStats.ts | 10 +- .../core-typings/src/cloud/Announcement.ts | 28 ++ .../src/cloud/NpsSurveyAnnouncement.ts | 7 + .../src/cloud/WorkspaceLicensePayload.ts | 10 + .../src/cloud/WorkspaceSyncPayload.ts | 33 +++ packages/core-typings/src/cloud/index.ts | 4 + packages/core-typings/src/index.ts | 2 + packages/model-typings/src/index.ts | 1 + .../model-typings/src/models/IBannersModel.ts | 4 +- .../src/models/ICloudAnnouncementsModel.ts | 6 + packages/models/src/index.ts | 2 + .../AutotranslateSaveSettingsParamsPOST.ts | 2 +- .../FederationVerifyMatrixIdProps.ts | 2 +- .../src/v1/moderation/ReportHistoryProps.ts | 1 + packages/rest-typings/src/v1/omnichannel.ts | 18 +- yarn.lock | 191 ++++++++---- 46 files changed, 755 insertions(+), 300 deletions(-) create mode 100644 apps/meteor/lib/errors/CloudWorkspaceAccessError.ts create mode 100644 apps/meteor/lib/errors/CloudWorkspaceConnectionError.ts create mode 100644 apps/meteor/lib/errors/CloudWorkspaceError.ts create mode 100644 apps/meteor/lib/errors/CloudWorkspaceLicenseError.ts create mode 100644 apps/meteor/lib/errors/CloudWorkspaceRegistrationError.ts create mode 100644 apps/meteor/server/models/CloudAnnouncements.ts create mode 100644 apps/meteor/server/models/raw/CloudAnnouncements.ts create mode 100644 packages/core-typings/src/cloud/Announcement.ts create mode 100644 packages/core-typings/src/cloud/NpsSurveyAnnouncement.ts create mode 100644 packages/core-typings/src/cloud/WorkspaceLicensePayload.ts create mode 100644 packages/core-typings/src/cloud/WorkspaceSyncPayload.ts create mode 100644 packages/core-typings/src/cloud/index.ts create mode 100644 packages/model-typings/src/models/ICloudAnnouncementsModel.ts diff --git a/apps/meteor/app/cloud/server/functions/buildRegistrationData.ts b/apps/meteor/app/cloud/server/functions/buildRegistrationData.ts index d65897b72094..10e0d7f7f7ee 100644 --- a/apps/meteor/app/cloud/server/functions/buildRegistrationData.ts +++ b/apps/meteor/app/cloud/server/functions/buildRegistrationData.ts @@ -5,7 +5,7 @@ import { settings } from '../../../settings/server'; import { statistics } from '../../../statistics/server'; import { LICENSE_VERSION } from '../license'; -type WorkspaceRegistrationData = { +export type WorkspaceRegistrationData = { uniqueId: string; workspaceId: SettingValue; address: SettingValue; @@ -14,11 +14,11 @@ type WorkspaceRegistrationData = { seats: number; allowMarketing: SettingValue; accountName: SettingValue; - organizationType: unknown; - industry: unknown; - orgSize: unknown; - country: unknown; - language: unknown; + organizationType: string; + industry: string; + orgSize: string; + country: string; + language: string; agreePrivacyTerms: SettingValue; website: SettingValue; siteName: SettingValue; @@ -61,15 +61,15 @@ export async function buildWorkspaceRegistrationData { + const cloudUrl = settings.get('Cloud_Url'); + const response = await fetch(`${cloudUrl}/api/oauth/clients`, { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + }, + body, + }); + + if (!response.ok) { + try { + const { error } = await response.json(); + throw new CloudWorkspaceConnectionError(`Failed to connect to Rocket.Chat Cloud: ${error}`); + } catch (error) { + throw new CloudWorkspaceConnectionError(`Failed to connect to Rocket.Chat Cloud: ${response.statusText}`); + } + } + + const payload = await response.json(); + + if (!payload) { + return undefined; + } + + return payload; +}; + export async function connectWorkspace(token: string) { - // shouldn't get here due to checking this on the method - // but this is just to double check if (!token) { - return new Error('Invalid token; the registration token is required.'); + throw new CloudWorkspaceConnectionError('Invalid registration token'); } - const redirectUri = getRedirectUri(); + try { + const redirectUri = getRedirectUri(); - const regInfo = { - email: settings.get('Organization_Email'), - client_name: settings.get('Site_Name'), - redirect_uris: [redirectUri], - }; + const body = { + email: settings.get('Organization_Email'), + client_name: settings.get('Site_Name'), + redirect_uris: [redirectUri], + }; - const cloudUrl = settings.get('Cloud_Url'); - let result; - try { - const request = await fetch(`${cloudUrl}/api/oauth/clients`, { - method: 'POST', - headers: { - Authorization: `Bearer ${token}`, - }, - body: regInfo, - }); + const payload = await fetchRegistrationDataPayload({ token, body }); - if (!request.ok) { - throw new Error((await request.json()).error); + if (!payload) { + return false; } - result = await request.json(); - } catch (err: any) { + await saveRegistrationData(payload); + + return true; + } catch (err) { SystemLogger.error({ msg: 'Failed to Connect with Rocket.Chat Cloud', url: '/api/oauth/clients', @@ -45,12 +76,4 @@ export async function connectWorkspace(token: string) { return false; } - - if (!result) { - return false; - } - - await saveRegistrationData(result); - - return true; } diff --git a/apps/meteor/app/cloud/server/functions/finishOAuthAuthorization.ts b/apps/meteor/app/cloud/server/functions/finishOAuthAuthorization.ts index 780aa5c67a99..61b3a77966e7 100644 --- a/apps/meteor/app/cloud/server/functions/finishOAuthAuthorization.ts +++ b/apps/meteor/app/cloud/server/functions/finishOAuthAuthorization.ts @@ -14,15 +14,15 @@ export async function finishOAuthAuthorization(code: string, state: string) { }); } - const cloudUrl = settings.get('Cloud_Url'); const clientId = settings.get('Cloud_Workspace_Client_Id'); const clientSecret = settings.get('Cloud_Workspace_Client_Secret'); const scope = userScopes.join(' '); - let result; + let payload; try { - const request = await fetch(`${cloudUrl}/api/oauth/token`, { + const cloudUrl = settings.get('Cloud_Url'); + const response = await fetch(`${cloudUrl}/api/oauth/token`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, params: new URLSearchParams({ @@ -35,11 +35,11 @@ export async function finishOAuthAuthorization(code: string, state: string) { }), }); - if (!request.ok) { - throw new Error((await request.json()).error); + if (!response.ok) { + throw new Error((await response.json()).error); } - result = await request.json(); + payload = await response.json(); } catch (err) { SystemLogger.error({ msg: 'Failed to finish OAuth authorization with Rocket.Chat Cloud', @@ -51,7 +51,7 @@ export async function finishOAuthAuthorization(code: string, state: string) { } const expiresAt = new Date(); - expiresAt.setSeconds(expiresAt.getSeconds() + result.expires_in); + expiresAt.setSeconds(expiresAt.getSeconds() + payload.expires_in); const uid = Meteor.userId(); if (!uid) { @@ -65,11 +65,11 @@ export async function finishOAuthAuthorization(code: string, state: string) { { $set: { 'services.cloud': { - accessToken: result.access_token, + accessToken: payload.access_token, expiresAt, - scope: result.scope, - tokenType: result.token_type, - refreshToken: result.refresh_token, + scope: payload.scope, + tokenType: payload.token_type, + refreshToken: payload.refresh_token, }, }, }, diff --git a/apps/meteor/app/cloud/server/functions/getConfirmationPoll.ts b/apps/meteor/app/cloud/server/functions/getConfirmationPoll.ts index 4a35c9834ba5..2c5d9dec77dc 100644 --- a/apps/meteor/app/cloud/server/functions/getConfirmationPoll.ts +++ b/apps/meteor/app/cloud/server/functions/getConfirmationPoll.ts @@ -5,16 +5,16 @@ import { SystemLogger } from '../../../../server/lib/logger/system'; import { settings } from '../../../settings/server'; export async function getConfirmationPoll(deviceCode: string): Promise { - const cloudUrl = settings.get('Cloud_Url'); - - let result; + let payload; try { - const request = await fetch(`${cloudUrl}/api/v2/register/workspace/poll`, { params: { token: deviceCode } }); - if (!request.ok) { - throw new Error((await request.json()).error); + const cloudUrl = settings.get('Cloud_Url'); + const response = await fetch(`${cloudUrl}/api/v2/register/workspace/poll`, { params: { token: deviceCode } }); + + if (!response.ok) { + throw new Error((await response.json()).error); } - result = await request.json(); + payload = await response.json(); } catch (err: any) { SystemLogger.error({ msg: 'Failed to get confirmation poll from Rocket.Chat Cloud', @@ -25,9 +25,9 @@ export async function getConfirmationPoll(deviceCode: string): Promise('Cloud_Url'); const redirectUri = getRedirectUri(); if (scope === '') { @@ -53,9 +53,9 @@ export async function getUserCloudAccessToken(userId: string, forceNew = false, let authTokenResult; try { - const request = await fetch(`${cloudUrl}/api/oauth/token`, { - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + const response = await fetch(`${cloudUrl}/api/oauth/token`, { method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, params: new URLSearchParams({ client_id: clientId, client_secret: clientSecret, @@ -66,11 +66,11 @@ export async function getUserCloudAccessToken(userId: string, forceNew = false, }), }); - if (!request.ok) { - throw new Error((await request.json()).error); + if (!response.ok) { + throw new Error((await response.json()).error); } - authTokenResult = await request.json(); + authTokenResult = await response.json(); } catch (err: any) { SystemLogger.error({ msg: 'Failed to get User AccessToken from Rocket.Chat Cloud', diff --git a/apps/meteor/app/cloud/server/functions/getWorkspaceAccessToken.ts b/apps/meteor/app/cloud/server/functions/getWorkspaceAccessToken.ts index 2b731ef82757..491c0379b3e9 100644 --- a/apps/meteor/app/cloud/server/functions/getWorkspaceAccessToken.ts +++ b/apps/meteor/app/cloud/server/functions/getWorkspaceAccessToken.ts @@ -4,12 +4,6 @@ import { settings } from '../../../settings/server'; import { getWorkspaceAccessTokenWithScope } from './getWorkspaceAccessTokenWithScope'; import { retrieveRegistrationStatus } from './retrieveRegistrationStatus'; -/** - * @param {boolean} forceNew - * @param {string} scope - * @param {boolean} save - * @returns string - */ export async function getWorkspaceAccessToken(forceNew = false, scope = '', save = true) { const { workspaceRegistered } = await retrieveRegistrationStatus(); @@ -25,7 +19,7 @@ export async function getWorkspaceAccessToken(forceNew = false, scope = '', save const now = new Date(); if (expires.value && now < expires.value && !forceNew) { - return settings.get('Cloud_Workspace_Access_Token'); + return settings.get('Cloud_Workspace_Access_Token'); } const accessToken = await getWorkspaceAccessTokenWithScope(scope); diff --git a/apps/meteor/app/cloud/server/functions/getWorkspaceAccessTokenWithScope.ts b/apps/meteor/app/cloud/server/functions/getWorkspaceAccessTokenWithScope.ts index 351b4cba20e5..88509902cb6d 100644 --- a/apps/meteor/app/cloud/server/functions/getWorkspaceAccessTokenWithScope.ts +++ b/apps/meteor/app/cloud/server/functions/getWorkspaceAccessTokenWithScope.ts @@ -26,12 +26,11 @@ export async function getWorkspaceAccessTokenWithScope(scope = '') { scope = workspaceScopes.join(' '); } - const cloudUrl = settings.get('Cloud_Url'); // eslint-disable-next-line @typescript-eslint/naming-convention const client_secret = settings.get('Cloud_Workspace_Client_Secret'); const redirectUri = getRedirectUri(); - let authTokenResult; + let payload; try { const body = new URLSearchParams(); body.append('client_id', client_id); @@ -40,12 +39,13 @@ export async function getWorkspaceAccessTokenWithScope(scope = '') { body.append('grant_type', 'client_credentials'); body.append('redirect_uri', redirectUri); - const result = await fetch(`${cloudUrl}/api/oauth/token`, { + const cloudUrl = settings.get('Cloud_Url'); + const response = await fetch(`${cloudUrl}/api/oauth/token`, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, method: 'POST', body, }); - authTokenResult = await result.json(); + payload = await response.json(); } catch (err: any) { SystemLogger.error({ msg: 'Failed to get Workspace AccessToken from Rocket.Chat Cloud', @@ -64,10 +64,10 @@ export async function getWorkspaceAccessTokenWithScope(scope = '') { } const expiresAt = new Date(); - expiresAt.setSeconds(expiresAt.getSeconds() + authTokenResult.expires_in); + expiresAt.setSeconds(expiresAt.getSeconds() + payload.expires_in); tokenResponse.expiresAt = expiresAt; - tokenResponse.token = authTokenResult.access_token; + tokenResponse.token = payload.access_token; return tokenResponse; } diff --git a/apps/meteor/app/cloud/server/functions/getWorkspaceLicense.ts b/apps/meteor/app/cloud/server/functions/getWorkspaceLicense.ts index 275e646e5343..8a99f1af34b7 100644 --- a/apps/meteor/app/cloud/server/functions/getWorkspaceLicense.ts +++ b/apps/meteor/app/cloud/server/functions/getWorkspaceLicense.ts @@ -1,68 +1,94 @@ +import type { Cloud, Serialized } from '@rocket.chat/core-typings'; import { Settings } from '@rocket.chat/models'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; +import { v, compile } from 'suretype'; import { callbacks } from '../../../../lib/callbacks'; +import { CloudWorkspaceConnectionError } from '../../../../lib/errors/CloudWorkspaceConnectionError'; +import { CloudWorkspaceLicenseError } from '../../../../lib/errors/CloudWorkspaceLicenseError'; import { SystemLogger } from '../../../../server/lib/logger/system'; import { settings } from '../../../settings/server'; import { LICENSE_VERSION } from '../license'; import { getWorkspaceAccessToken } from './getWorkspaceAccessToken'; +const workspaceLicensePayloadSchema = v.object({ + version: v.number().required(), + address: v.string().required(), + license: v.string().required(), + updatedAt: v.string().format('date-time').required(), + modules: v.string().required(), + expireAt: v.string().format('date-time').required(), +}); + +const assertWorkspaceLicensePayload = compile(workspaceLicensePayloadSchema); + +const fetchCloudWorkspaceLicensePayload = async ({ token }: { token: string }): Promise> => { + const workspaceRegistrationClientUri = settings.get('Cloud_Workspace_Registration_Client_Uri'); + const response = await fetch(`${workspaceRegistrationClientUri}/license`, { + headers: { + Authorization: `Bearer ${token}`, + }, + params: { + version: LICENSE_VERSION, + }, + }); + + if (!response.ok) { + try { + const { error } = await response.json(); + throw new CloudWorkspaceConnectionError(`Failed to connect to Rocket.Chat Cloud: ${error}`); + } catch (error) { + throw new CloudWorkspaceConnectionError(`Failed to connect to Rocket.Chat Cloud: ${response.statusText}`); + } + } + + const payload = await response.json(); + + assertWorkspaceLicensePayload(payload); + + return payload; +}; + export async function getWorkspaceLicense(): Promise<{ updated: boolean; license: string }> { const currentLicense = await Settings.findOne('Cloud_Workspace_License'); - const cachedLicenseReturn = async () => { - const license = currentLicense?.value as string; + const fromCurrentLicense = async () => { + const license = currentLicense?.value as string | undefined; if (license) { - await callbacks.run('workspaceLicenseChanged', license); + callbacks.run('workspaceLicenseChanged', license); } - return { updated: false, license }; + return { updated: false, license: license ?? '' }; }; - const token = await getWorkspaceAccessToken(); - if (!token) { - return cachedLicenseReturn(); - } - - let licenseResult; try { - const request = await fetch(`${settings.get('Cloud_Workspace_Registration_Client_Uri')}/license`, { - headers: { - Authorization: `Bearer ${token}`, - }, - params: { - version: LICENSE_VERSION, - }, - }); + const token = await getWorkspaceAccessToken(); + if (!token) { + return fromCurrentLicense(); + } - if (!request.ok) { - throw new Error((await request.json()).error); + if (!currentLicense?._updatedAt) { + throw new CloudWorkspaceLicenseError('Failed to retrieve current license'); } - licenseResult = await request.json(); - } catch (err: any) { + const payload = await fetchCloudWorkspaceLicensePayload({ token }); + + if (Date.parse(payload.updatedAt) <= currentLicense._updatedAt.getTime()) { + return fromCurrentLicense(); + } + + await Settings.updateValueById('Cloud_Workspace_License', payload.license); + + callbacks.run('workspaceLicenseChanged', payload.license); + + return { updated: true, license: payload.license }; + } catch (err) { SystemLogger.error({ msg: 'Failed to update license from Rocket.Chat Cloud', url: '/license', err, }); - return cachedLicenseReturn(); + return fromCurrentLicense(); } - - const remoteLicense = licenseResult; - - if (!currentLicense || !currentLicense._updatedAt) { - throw new Error('Failed to retrieve current license'); - } - - if (remoteLicense.updatedAt <= currentLicense._updatedAt) { - return cachedLicenseReturn(); - } - - await Settings.updateValueById('Cloud_Workspace_License', remoteLicense.license); - - await callbacks.run('workspaceLicenseChanged', remoteLicense.license); - - return { updated: true, license: remoteLicense.license }; } diff --git a/apps/meteor/app/cloud/server/functions/reconnectWorkspace.ts b/apps/meteor/app/cloud/server/functions/reconnectWorkspace.ts index db425d2e8a30..7ee02a5e5de4 100644 --- a/apps/meteor/app/cloud/server/functions/reconnectWorkspace.ts +++ b/apps/meteor/app/cloud/server/functions/reconnectWorkspace.ts @@ -11,7 +11,7 @@ export async function reconnectWorkspace() { await Settings.updateValueById('Register_Server', true); - await syncWorkspace(true); + await syncWorkspace(); return true; } diff --git a/apps/meteor/app/cloud/server/functions/registerPreIntentWorkspaceWizard.ts b/apps/meteor/app/cloud/server/functions/registerPreIntentWorkspaceWizard.ts index 2a04aa54cfe7..ce415d2aa983 100644 --- a/apps/meteor/app/cloud/server/functions/registerPreIntentWorkspaceWizard.ts +++ b/apps/meteor/app/cloud/server/functions/registerPreIntentWorkspaceWizard.ts @@ -15,16 +15,16 @@ export async function registerPreIntentWorkspaceWizard(): Promise { } const regInfo = await buildWorkspaceRegistrationData(email); - const cloudUrl = settings.get('Cloud_Url'); try { - const request = await fetch(`${cloudUrl}/api/v2/register/workspace/pre-intent`, { + const cloudUrl = settings.get('Cloud_Url'); + const response = await fetch(`${cloudUrl}/api/v2/register/workspace/pre-intent`, { + method: 'POST', body: regInfo, timeout: 10 * 1000, - method: 'POST', }); - if (!request.ok) { - throw new Error((await request.json()).error); + if (!response.ok) { + throw new Error((await response.json()).error); } return true; diff --git a/apps/meteor/app/cloud/server/functions/saveRegistrationData.ts b/apps/meteor/app/cloud/server/functions/saveRegistrationData.ts index 8e1e03113af4..154fe7ad5901 100644 --- a/apps/meteor/app/cloud/server/functions/saveRegistrationData.ts +++ b/apps/meteor/app/cloud/server/functions/saveRegistrationData.ts @@ -34,7 +34,7 @@ export function saveRegistrationData({ Settings.updateValueById('Cloud_Workspace_Registration_Client_Uri', registration_client_uri), Settings.updateValueById('Cloud_Workspace_License', licenseData.license || ''), ]).then(async (...results) => { - await callbacks.run('workspaceLicenseChanged', licenseData.license); + callbacks.run('workspaceLicenseChanged', licenseData.license); return results; }); } diff --git a/apps/meteor/app/cloud/server/functions/startRegisterWorkspace.ts b/apps/meteor/app/cloud/server/functions/startRegisterWorkspace.ts index af74fcd7d211..5f5df80d0d3d 100644 --- a/apps/meteor/app/cloud/server/functions/startRegisterWorkspace.ts +++ b/apps/meteor/app/cloud/server/functions/startRegisterWorkspace.ts @@ -10,7 +10,7 @@ import { syncWorkspace } from './syncWorkspace'; export async function startRegisterWorkspace(resend = false) { const { workspaceRegistered } = await retrieveRegistrationStatus(); if (workspaceRegistered || process.env.TEST_MODE) { - await syncWorkspace(true); + await syncWorkspace(); return true; } @@ -19,22 +19,21 @@ export async function startRegisterWorkspace(resend = false) { const regInfo = await buildWorkspaceRegistrationData(undefined); - const cloudUrl = settings.get('Cloud_Url'); - - let result; + let payload; try { - const request = await fetch(`${cloudUrl}/api/v2/register/workspace`, { + const cloudUrl = settings.get('Cloud_Url'); + const response = await fetch(`${cloudUrl}/api/v2/register/workspace`, { method: 'POST', body: regInfo, params: { resend, }, }); - if (!request.ok) { - throw new Error((await request.json()).error); + if (!response.ok) { + throw new Error((await response.json()).error); } - result = await request.json(); + payload = await response.json(); } catch (err: any) { SystemLogger.error({ msg: 'Failed to register with Rocket.Chat Cloud', @@ -44,11 +43,11 @@ export async function startRegisterWorkspace(resend = false) { return false; } - if (!result) { + if (!payload) { return false; } - await Settings.updateValueById('Cloud_Workspace_Id', result.id); + await Settings.updateValueById('Cloud_Workspace_Id', payload.id); return true; } diff --git a/apps/meteor/app/cloud/server/functions/startRegisterWorkspaceSetupWizard.ts b/apps/meteor/app/cloud/server/functions/startRegisterWorkspaceSetupWizard.ts index 3afe84c409ec..382478db61c7 100644 --- a/apps/meteor/app/cloud/server/functions/startRegisterWorkspaceSetupWizard.ts +++ b/apps/meteor/app/cloud/server/functions/startRegisterWorkspaceSetupWizard.ts @@ -7,22 +7,22 @@ import { buildWorkspaceRegistrationData } from './buildRegistrationData'; export async function startRegisterWorkspaceSetupWizard(resend = false, email: string): Promise { const regInfo = await buildWorkspaceRegistrationData(email); - const cloudUrl = settings.get('Cloud_Url'); - let result; + let payload; try { - const request = await fetch(`${cloudUrl}/api/v2/register/workspace/intent`, { + const cloudUrl = settings.get('Cloud_Url'); + const response = await fetch(`${cloudUrl}/api/v2/register/workspace/intent`, { body: regInfo, method: 'POST', params: { resent: resend, }, }); - if (!request.ok) { - throw new Error((await request.json()).error); + if (!response.ok) { + throw new Error((await response.json()).error); } - result = await request.json(); + payload = await response.json(); } catch (err: any) { SystemLogger.error({ msg: 'Failed to register workspace intent with Rocket.Chat Cloud', @@ -33,9 +33,9 @@ export async function startRegisterWorkspaceSetupWizard(resend = false, email: s throw err; } - if (!result) { + if (!payload) { throw new Error('Failed to fetch registration intent endpoint'); } - return result; + return payload; } diff --git a/apps/meteor/app/cloud/server/functions/syncWorkspace.ts b/apps/meteor/app/cloud/server/functions/syncWorkspace.ts index c8a323e40f95..65ce5dc2e474 100644 --- a/apps/meteor/app/cloud/server/functions/syncWorkspace.ts +++ b/apps/meteor/app/cloud/server/functions/syncWorkspace.ts @@ -1,108 +1,242 @@ import { NPS, Banner } from '@rocket.chat/core-services'; -import { Settings } from '@rocket.chat/models'; +import { type Cloud, type Serialized } from '@rocket.chat/core-typings'; +import { CloudAnnouncents, Settings } from '@rocket.chat/models'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; +import { v, compile } from 'suretype'; +import { CloudWorkspaceAccessError } from '../../../../lib/errors/CloudWorkspaceAccessError'; +import { CloudWorkspaceConnectionError } from '../../../../lib/errors/CloudWorkspaceConnectionError'; +import { CloudWorkspaceRegistrationError } from '../../../../lib/errors/CloudWorkspaceRegistrationError'; import { SystemLogger } from '../../../../server/lib/logger/system'; import { getAndCreateNpsSurvey } from '../../../../server/services/nps/getAndCreateNpsSurvey'; import { settings } from '../../../settings/server'; +import type { WorkspaceRegistrationData } from './buildRegistrationData'; import { buildWorkspaceRegistrationData } from './buildRegistrationData'; import { getWorkspaceAccessToken } from './getWorkspaceAccessToken'; import { getWorkspaceLicense } from './getWorkspaceLicense'; import { retrieveRegistrationStatus } from './retrieveRegistrationStatus'; -export async function syncWorkspace(_reconnectCheck = false) { - const { workspaceRegistered } = await retrieveRegistrationStatus(); - if (!workspaceRegistered) { - return false; +const workspaceSyncPayloadSchema = v.object({ + workspaceId: v.string().required(), + publicKey: v.string(), + trial: v.object({ + trialing: v.boolean().required(), + trialID: v.string().required(), + endDate: v.string().format('date-time').required(), + marketing: v + .object({ + utmContent: v.string().required(), + utmMedium: v.string().required(), + utmSource: v.string().required(), + utmCampaign: v.string().required(), + }) + .required(), + DowngradesToPlan: v + .object({ + id: v.string().required(), + }) + .required(), + trialRequested: v.boolean().required(), + }), + nps: v.object({ + id: v.string().required(), + startAt: v.string().format('date-time').required(), + expireAt: v.string().format('date-time').required(), + }), + banners: v.array( + v.object({ + _id: v.string().required(), + _updatedAt: v.string().format('date-time').required(), + platform: v.array(v.string()).required(), + expireAt: v.string().format('date-time').required(), + startAt: v.string().format('date-time').required(), + roles: v.array(v.string()), + createdBy: v.object({ + _id: v.string().required(), + username: v.string(), + }), + createdAt: v.string().format('date-time').required(), + view: v.any(), + active: v.boolean(), + inactivedAt: v.string().format('date-time'), + snapshot: v.string(), + }), + ), + announcements: v.object({ + create: v.array( + v.object({ + _id: v.string().required(), + _updatedAt: v.string().format('date-time').required(), + selector: v.object({ + roles: v.array(v.string()), + }), + platform: v.array(v.string().enum('web', 'mobile')).required(), + expireAt: v.string().format('date-time').required(), + startAt: v.string().format('date-time').required(), + createdBy: v.string().enum('cloud', 'system').required(), + createdAt: v.string().format('date-time').required(), + dictionary: v.object({}).additional(v.object({}).additional(v.string())), + view: v.any(), + surface: v.string().enum('banner', 'modal').required(), + }), + ), + delete: v.array(v.string()), + }), +}); + +const assertWorkspaceSyncPayload = compile(workspaceSyncPayloadSchema); + +const fetchWorkspaceSyncPayload = async ({ + token, + workspaceRegistrationData, +}: { + token: string; + workspaceRegistrationData: WorkspaceRegistrationData; +}): Promise | undefined> => { + const workspaceRegistrationClientUri = settings.get('Cloud_Workspace_Registration_Client_Uri'); + const response = await fetch(`${workspaceRegistrationClientUri}/client`, { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + }, + body: workspaceRegistrationData, + }); + + if (!response.ok) { + try { + const { error } = await response.json(); + throw new CloudWorkspaceConnectionError(`Failed to connect to Rocket.Chat Cloud: ${error}`); + } catch (error) { + throw new CloudWorkspaceConnectionError(`Failed to connect to Rocket.Chat Cloud: ${response.statusText}`); + } } - const info = await buildWorkspaceRegistrationData(undefined); + const payload = await response.json(); - const workspaceUrl = settings.get('Cloud_Workspace_Registration_Client_Uri'); + if (!payload) { + return undefined; + } - let result; - try { - const headers: Record = {}; - const token = await getWorkspaceAccessToken(true); + assertWorkspaceSyncPayload(payload); - if (token) { - headers.Authorization = `Bearer ${token}`; - } else { - return false; - } + return payload; +}; - const request = await fetch(`${workspaceUrl}/client`, { - headers, - body: info, - method: 'POST', - }); +const handleNpsOnWorkspaceSync = async (nps: Exclude['nps'], undefined>) => { + const { id: npsId, expireAt } = nps; - if (!request.ok) { - throw new Error((await request.json()).error); - } + const startAt = new Date(nps.startAt); - result = await request.json(); - } catch (err: any) { - SystemLogger.error({ - msg: 'Failed to sync with Rocket.Chat Cloud', - url: '/client', - err, - }); + await NPS.create({ + npsId, + startAt, + expireAt: new Date(expireAt), + createdBy: { + _id: 'rocket.cat', + username: 'rocket.cat', + }, + }); - return false; - } finally { - // aways fetch the license - await getWorkspaceLicense(); + const now = new Date(); + + if (startAt.getFullYear() === now.getFullYear() && startAt.getMonth() === now.getMonth() && startAt.getDate() === now.getDate()) { + await getAndCreateNpsSurvey(npsId); } +}; - const data = result; - if (!data) { - return true; +const handleBannerOnWorkspaceSync = async (banners: Exclude['banners'], undefined>) => { + for await (const banner of banners) { + const { createdAt, expireAt, startAt, inactivedAt, _updatedAt, ...rest } = banner; + + await Banner.create({ + ...rest, + createdAt: new Date(createdAt), + expireAt: new Date(expireAt), + startAt: new Date(startAt), + ...(inactivedAt && { inactivedAt: new Date(inactivedAt) }), + }); + } +}; + +const deserializeAnnouncement = (announcement: Serialized): Cloud.Announcement => ({ + ...announcement, + _updatedAt: new Date(announcement._updatedAt), + expireAt: new Date(announcement.expireAt), + startAt: new Date(announcement.startAt), + createdAt: new Date(announcement.createdAt), +}); + +const handleAnnouncementsOnWorkspaceSync = async ( + announcements: Exclude['announcements'], undefined>, +) => { + const { create, delete: deleteIds } = announcements; + + if (deleteIds) { + await CloudAnnouncents.deleteMany({ _id: { $in: deleteIds } }); + } + + for await (const announcement of create.map(deserializeAnnouncement)) { + const { _id, ...rest } = announcement; + + await CloudAnnouncents.updateOne({ _id }, { $set: rest }, { upsert: true }); } +}; - if (data.publicKey) { - await Settings.updateValueById('Cloud_Workspace_PublicKey', data.publicKey); +const consumeWorkspaceSyncPayload = async (result: Serialized) => { + if (result.publicKey) { + await Settings.updateValueById('Cloud_Workspace_PublicKey', result.publicKey); } - if (data.trial?.trialId) { + if (result.trial?.trialID) { await Settings.updateValueById('Cloud_Workspace_Had_Trial', true); } - if (data.nps) { - const { id: npsId, expireAt } = data.nps; + if (result.nps) { + await handleNpsOnWorkspaceSync(result.nps); + } - const startAt = new Date(data.nps.startAt); + // add banners + if (result.banners) { + await handleBannerOnWorkspaceSync(result.banners); + } - await NPS.create({ - npsId, - startAt, - expireAt: new Date(expireAt), - createdBy: { - _id: 'rocket.cat', - username: 'rocket.cat', - }, - }); + if (result.announcements) { + await handleAnnouncementsOnWorkspaceSync(result.announcements); + } +}; - const now = new Date(); +export async function syncWorkspace() { + try { + const { workspaceRegistered } = await retrieveRegistrationStatus(); + if (!workspaceRegistered) { + throw new CloudWorkspaceRegistrationError('Workspace is not registered'); + } - if (startAt.getFullYear() === now.getFullYear() && startAt.getMonth() === now.getMonth() && startAt.getDate() === now.getDate()) { - await getAndCreateNpsSurvey(npsId); + const token = await getWorkspaceAccessToken(true); + if (!token) { + throw new CloudWorkspaceAccessError('Workspace does not have a valid access token'); } - } - // add banners - if (data.banners) { - for await (const banner of data.banners) { - const { createdAt, expireAt, startAt } = banner; - - await Banner.create({ - ...banner, - createdAt: new Date(createdAt), - expireAt: new Date(expireAt), - startAt: new Date(startAt), - }); + const workspaceRegistrationData = await buildWorkspaceRegistrationData(undefined); + + const payload = await fetchWorkspaceSyncPayload({ token, workspaceRegistrationData }); + + if (!payload) { + return true; } - } - return true; + await consumeWorkspaceSyncPayload(payload); + + return true; + } catch (err) { + SystemLogger.error({ + msg: 'Failed to sync with Rocket.Chat Cloud', + url: '/client', + err, + }); + + return false; + } finally { + await getWorkspaceLicense(); + } } diff --git a/apps/meteor/app/cloud/server/functions/userLogout.ts b/apps/meteor/app/cloud/server/functions/userLogout.ts index 7dd4aa094535..386137ced604 100644 --- a/apps/meteor/app/cloud/server/functions/userLogout.ts +++ b/apps/meteor/app/cloud/server/functions/userLogout.ts @@ -26,10 +26,11 @@ export async function userLogout(userId: string): Promise { return ''; } - const cloudUrl = settings.get('Cloud_Url'); const clientSecret = settings.get('Cloud_Workspace_Client_Secret'); const { refreshToken } = user.services.cloud; + + const cloudUrl = settings.get('Cloud_Url'); await fetch(`${cloudUrl}/api/oauth/revoke`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, diff --git a/apps/meteor/app/statistics/server/lib/statistics.ts b/apps/meteor/app/statistics/server/lib/statistics.ts index 8cfe45b42232..91c4f7141145 100644 --- a/apps/meteor/app/statistics/server/lib/statistics.ts +++ b/apps/meteor/app/statistics/server/lib/statistics.ts @@ -41,8 +41,6 @@ import { getAppsStatistics } from './getAppsStatistics'; import { getImporterStatistics } from './getImporterStatistics'; import { getServicesStatistics } from './getServicesStatistics'; -const wizardFields = ['Organization_Type', 'Industry', 'Size', 'Country', 'Language', 'Server_Type', 'Register_Server']; - const getUserLanguages = async (totalUsers: number): Promise<{ [key: string]: number }> => { const result = await Users.getUserLanguages(); @@ -70,17 +68,29 @@ export const statistics = { const statistics = {} as IStats; const statsPms = []; + const fetchWizardSettingValue = async (settingName: string): Promise => { + return ((await Settings.findOne(settingName))?.value as T | undefined) ?? undefined; + }; + // Setup Wizard - statistics.wizard = {}; - await Promise.all( - wizardFields.map(async (field) => { - const record = await Settings.findOne(field); - if (record) { - const wizardField = field.replace(/_/g, '').replace(field[0], field[0].toLowerCase()); - statistics.wizard[wizardField] = record.value; - } - }), - ); + const [organizationType, industry, size, country, language, serverType, registerServer] = await Promise.all([ + fetchWizardSettingValue('Organization_Type'), + fetchWizardSettingValue('Industry'), + fetchWizardSettingValue('Size'), + fetchWizardSettingValue('Country'), + fetchWizardSettingValue('Language'), + fetchWizardSettingValue('Server_Type'), + fetchWizardSettingValue('Register_Server'), + ]); + statistics.wizard = { + organizationType, + industry, + size, + country, + language, + serverType, + registerServer, + }; // Version const uniqueID = await Settings.findOne('uniqueID'); diff --git a/apps/meteor/lib/errors/CloudWorkspaceAccessError.ts b/apps/meteor/lib/errors/CloudWorkspaceAccessError.ts new file mode 100644 index 000000000000..4cea63a01f09 --- /dev/null +++ b/apps/meteor/lib/errors/CloudWorkspaceAccessError.ts @@ -0,0 +1,8 @@ +import { CloudWorkspaceError } from './CloudWorkspaceError'; + +export class CloudWorkspaceAccessError extends CloudWorkspaceError { + constructor(message: string) { + super(message); + this.name = CloudWorkspaceAccessError.name; + } +} diff --git a/apps/meteor/lib/errors/CloudWorkspaceConnectionError.ts b/apps/meteor/lib/errors/CloudWorkspaceConnectionError.ts new file mode 100644 index 000000000000..8b4edcf8f588 --- /dev/null +++ b/apps/meteor/lib/errors/CloudWorkspaceConnectionError.ts @@ -0,0 +1,8 @@ +import { CloudWorkspaceError } from './CloudWorkspaceError'; + +export class CloudWorkspaceConnectionError extends CloudWorkspaceError { + constructor(message: string) { + super(message); + this.name = CloudWorkspaceConnectionError.name; + } +} diff --git a/apps/meteor/lib/errors/CloudWorkspaceError.ts b/apps/meteor/lib/errors/CloudWorkspaceError.ts new file mode 100644 index 000000000000..d843c42ea520 --- /dev/null +++ b/apps/meteor/lib/errors/CloudWorkspaceError.ts @@ -0,0 +1,6 @@ +export class CloudWorkspaceError extends Error { + constructor(message: string) { + super(message); + this.name = CloudWorkspaceError.name; + } +} diff --git a/apps/meteor/lib/errors/CloudWorkspaceLicenseError.ts b/apps/meteor/lib/errors/CloudWorkspaceLicenseError.ts new file mode 100644 index 000000000000..96c9a28be82c --- /dev/null +++ b/apps/meteor/lib/errors/CloudWorkspaceLicenseError.ts @@ -0,0 +1,8 @@ +import { CloudWorkspaceError } from './CloudWorkspaceError'; + +export class CloudWorkspaceLicenseError extends CloudWorkspaceError { + constructor(message: string) { + super(message); + this.name = CloudWorkspaceLicenseError.name; + } +} diff --git a/apps/meteor/lib/errors/CloudWorkspaceRegistrationError.ts b/apps/meteor/lib/errors/CloudWorkspaceRegistrationError.ts new file mode 100644 index 000000000000..aecec757acee --- /dev/null +++ b/apps/meteor/lib/errors/CloudWorkspaceRegistrationError.ts @@ -0,0 +1,8 @@ +import { CloudWorkspaceError } from './CloudWorkspaceError'; + +export class CloudWorkspaceRegistrationError extends CloudWorkspaceError { + constructor(message: string) { + super(message); + this.name = CloudWorkspaceRegistrationError.name; + } +} diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 9e68456a78c8..ae3000b7f6b9 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -286,7 +286,7 @@ "@xmldom/xmldom": "^0.8.8", "adm-zip": "0.5.10", "ajv": "^8.11.0", - "ajv-formats": "^2.1.1", + "ajv-formats": "~2.1.1", "apn": "2.2.0", "archiver": "^3.1.1", "asterisk-manager": "^0.2.0", @@ -413,6 +413,7 @@ "stream-buffers": "^3.0.2", "strict-uri-encode": "^2.0.0", "string-strip-html": "^7.0.3", + "suretype": "~2.4.1", "tar-stream": "^1.6.2", "textarea-caret": "^3.1.0", "tinykeys": "^1.4.0", diff --git a/apps/meteor/server/models/CloudAnnouncements.ts b/apps/meteor/server/models/CloudAnnouncements.ts new file mode 100644 index 000000000000..4f6692d67fc9 --- /dev/null +++ b/apps/meteor/server/models/CloudAnnouncements.ts @@ -0,0 +1,6 @@ +import { registerModel } from '@rocket.chat/models'; + +import { db } from '../database/utils'; +import { CloudAnnouncementsRaw } from './raw/CloudAnnouncements'; + +registerModel('ICloudAnnouncementsModel', new CloudAnnouncementsRaw(db)); diff --git a/apps/meteor/server/models/raw/CloudAnnouncements.ts b/apps/meteor/server/models/raw/CloudAnnouncements.ts new file mode 100644 index 000000000000..d48082b0c33c --- /dev/null +++ b/apps/meteor/server/models/raw/CloudAnnouncements.ts @@ -0,0 +1,15 @@ +import type { Cloud, RocketChatRecordDeleted } from '@rocket.chat/core-typings'; +import type { ICloudAnnouncementsModel } from '@rocket.chat/model-typings'; +import type { Collection, Db, IndexDescription } from 'mongodb'; + +import { BaseRaw } from './BaseRaw'; + +export class CloudAnnouncementsRaw extends BaseRaw implements ICloudAnnouncementsModel { + constructor(db: Db, trash?: Collection>) { + super(db, 'cloud_announcements', trash); + } + + modelIndexes(): IndexDescription[] { + return [{ key: { status: 1, expireAt: 1 } }]; + } +} diff --git a/apps/meteor/server/models/startup.ts b/apps/meteor/server/models/startup.ts index 14b26e0f188f..d355d1febd16 100644 --- a/apps/meteor/server/models/startup.ts +++ b/apps/meteor/server/models/startup.ts @@ -68,3 +68,4 @@ import './Imports'; import './AppsTokens'; import './CronHistory'; import './Migrations'; +import './CloudAnnouncements'; diff --git a/apps/meteor/server/services/banner/service.ts b/apps/meteor/server/services/banner/service.ts index 4dc0dbbec494..d20b9e780875 100644 --- a/apps/meteor/server/services/banner/service.ts +++ b/apps/meteor/server/services/banner/service.ts @@ -26,7 +26,7 @@ export class BannerService extends ServiceClassInternal implements IBannerServic return true; } - async create(doc: Optional): Promise { + async create(doc: Optional): Promise { const bannerId = doc._id || uuidv4(); doc.view.appId = 'banner-core'; diff --git a/package.json b/package.json index f9029efb8b66..962e42d48c6e 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "@types/chart.js": "^2.9.37", "@types/js-yaml": "^4.0.5", "husky": "^7.0.4", - "turbo": "^1.10.13" + "turbo": "~1.10.14" }, "workspaces": [ "apps/*", diff --git a/packages/core-services/src/types/IBannerService.ts b/packages/core-services/src/types/IBannerService.ts index 1035bdd59510..50b8ab08275c 100644 --- a/packages/core-services/src/types/IBannerService.ts +++ b/packages/core-services/src/types/IBannerService.ts @@ -2,7 +2,7 @@ import type { BannerPlatform, IBanner, Optional } from '@rocket.chat/core-typing export interface IBannerService { getBannersForUser(userId: string, platform: BannerPlatform, bannerId?: string): Promise; - create(banner: Optional): Promise; + create(banner: Optional): Promise; dismiss(userId: string, bannerId: string): Promise; discardDismissal(bannerId: string): Promise; getById(bannerId: string): Promise; diff --git a/packages/core-typings/.eslintrc.json b/packages/core-typings/.eslintrc.json index 56a6f6602e33..44d74e043bc4 100644 --- a/packages/core-typings/.eslintrc.json +++ b/packages/core-typings/.eslintrc.json @@ -8,5 +8,8 @@ } } ], - "ignorePatterns": ["**/dist"] + "ignorePatterns": ["**/dist"], + "rules": { + "@typescript-eslint/no-empty-interface": "off" + } } diff --git a/packages/core-typings/package.json b/packages/core-typings/package.json index 2d0b0d734897..63beff9e41db 100644 --- a/packages/core-typings/package.json +++ b/packages/core-typings/package.json @@ -1,13 +1,7 @@ { + "$schema": "https://json.schemastore.org/package", "name": "@rocket.chat/core-typings", "version": "6.4.0-rc.4", - "devDependencies": { - "@rocket.chat/eslint-config": "workspace:^", - "eslint": "~8.45.0", - "mongodb": "^4.17.1", - "prettier": "~2.8.8", - "typescript": "~5.2.2" - }, "scripts": { "lint": "eslint --ext .js,.jsx,.ts,.tsx .", "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", @@ -26,6 +20,13 @@ "@rocket.chat/message-parser": "next", "@rocket.chat/ui-kit": "^0.32.1" }, + "devDependencies": { + "@rocket.chat/eslint-config": "workspace:^", + "eslint": "~8.45.0", + "mongodb": "^4.12.1", + "prettier": "~2.8.8", + "typescript": "~5.2.2" + }, "volta": { "extends": "../../package.json" } diff --git a/packages/core-typings/src/IStats.ts b/packages/core-typings/src/IStats.ts index 2ea8115a727c..cd8aeb9f1762 100644 --- a/packages/core-typings/src/IStats.ts +++ b/packages/core-typings/src/IStats.ts @@ -6,7 +6,15 @@ import type { ITeamStats } from './ITeam'; export interface IStats { _id: string; - wizard: Record; + wizard: { + organizationType?: string; + industry?: string; + size?: string; + country?: string; + language?: string; + serverType?: string; + registerServer?: boolean; + }; uniqueId: string; installedAt?: string; version?: string; diff --git a/packages/core-typings/src/cloud/Announcement.ts b/packages/core-typings/src/cloud/Announcement.ts new file mode 100644 index 000000000000..3d891daf132f --- /dev/null +++ b/packages/core-typings/src/cloud/Announcement.ts @@ -0,0 +1,28 @@ +/* eslint-disable @typescript-eslint/naming-convention */ + +import type { IRocketChatRecord } from '../IRocketChatRecord'; +import { type UiKitPayload } from '../UIKit'; + +type TargetPlatform = 'web' | 'mobile'; + +type Dictionary = { + [lng: string]: { + [key: string]: string; + }; +}; + +type Creator = 'cloud' | 'system'; + +export interface Announcement extends IRocketChatRecord { + selector?: { + roles?: string[]; + }; + platform: TargetPlatform[]; + expireAt: Date; + startAt: Date; + createdBy: Creator; + createdAt: Date; + dictionary?: Dictionary; + view: UiKitPayload; + surface: 'banner' | 'modal'; +} diff --git a/packages/core-typings/src/cloud/NpsSurveyAnnouncement.ts b/packages/core-typings/src/cloud/NpsSurveyAnnouncement.ts new file mode 100644 index 000000000000..fff1db8f1b99 --- /dev/null +++ b/packages/core-typings/src/cloud/NpsSurveyAnnouncement.ts @@ -0,0 +1,7 @@ +/* eslint-disable @typescript-eslint/naming-convention */ + +export interface NpsSurveyAnnouncement { + id: string; + startAt: Date; + expireAt: Date; +} diff --git a/packages/core-typings/src/cloud/WorkspaceLicensePayload.ts b/packages/core-typings/src/cloud/WorkspaceLicensePayload.ts new file mode 100644 index 000000000000..7e81e1b47599 --- /dev/null +++ b/packages/core-typings/src/cloud/WorkspaceLicensePayload.ts @@ -0,0 +1,10 @@ +/* eslint-disable @typescript-eslint/naming-convention */ + +export interface WorkspaceLicensePayload { + version: number; + address: string; + license: string; + updatedAt: Date; + modules: string; + expireAt: Date; +} diff --git a/packages/core-typings/src/cloud/WorkspaceSyncPayload.ts b/packages/core-typings/src/cloud/WorkspaceSyncPayload.ts new file mode 100644 index 000000000000..964cb42571b2 --- /dev/null +++ b/packages/core-typings/src/cloud/WorkspaceSyncPayload.ts @@ -0,0 +1,33 @@ +/* eslint-disable @typescript-eslint/naming-convention */ + +import type { IBanner } from '../IBanner'; +import type { Announcement } from './Announcement'; +import type { NpsSurveyAnnouncement } from './NpsSurveyAnnouncement'; + +export interface WorkspaceSyncPayload { + workspaceId: string; + publicKey?: string; + announcements?: { + create: Announcement[]; + delete: Announcement['_id'][]; + }; + trial?: { + trialing: boolean; + trialID: string; + endDate: Date; + marketing: { + utmContent: string; + utmMedium: string; + utmSource: string; + utmCampaign: string; + }; + DowngradesToPlan: { + id: string; + }; + trialRequested: boolean; + }; + /** @deprecated */ + nps?: NpsSurveyAnnouncement; + /** @deprecated */ + banners?: IBanner[]; +} diff --git a/packages/core-typings/src/cloud/index.ts b/packages/core-typings/src/cloud/index.ts new file mode 100644 index 000000000000..b9c044b054e3 --- /dev/null +++ b/packages/core-typings/src/cloud/index.ts @@ -0,0 +1,4 @@ +export { Announcement } from './Announcement'; +export { NpsSurveyAnnouncement } from './NpsSurveyAnnouncement'; +export { WorkspaceLicensePayload } from './WorkspaceLicensePayload'; +export { WorkspaceSyncPayload } from './WorkspaceSyncPayload'; diff --git a/packages/core-typings/src/index.ts b/packages/core-typings/src/index.ts index 8cd004dd09f1..cd1267f743e7 100644 --- a/packages/core-typings/src/index.ts +++ b/packages/core-typings/src/index.ts @@ -135,3 +135,5 @@ export * from './ICustomOAuthConfig'; export * from './IModerationReport'; export * from './CustomFieldMetadata'; + +export * as Cloud from './cloud'; diff --git a/packages/model-typings/src/index.ts b/packages/model-typings/src/index.ts index 23e77ff1de29..a1874b144347 100644 --- a/packages/model-typings/src/index.ts +++ b/packages/model-typings/src/index.ts @@ -79,3 +79,4 @@ export * from './models/IAuditLogModel'; export * from './models/ICronHistoryModel'; export * from './models/IMigrationsModel'; export * from './models/IModerationReportsModel'; +export * from './models/ICloudAnnouncementsModel'; diff --git a/packages/model-typings/src/models/IBannersModel.ts b/packages/model-typings/src/models/IBannersModel.ts index 62f33ef5d3b2..4fe496bb954c 100644 --- a/packages/model-typings/src/models/IBannersModel.ts +++ b/packages/model-typings/src/models/IBannersModel.ts @@ -1,10 +1,10 @@ -import type { BannerPlatform, IBanner } from '@rocket.chat/core-typings'; +import type { BannerPlatform, IBanner, Optional } from '@rocket.chat/core-typings'; import type { Document, FindCursor, FindOptions, UpdateResult, InsertOneResult } from 'mongodb'; import type { IBaseModel } from './IBaseModel'; export interface IBannersModel extends IBaseModel { - create(doc: IBanner): Promise>; + create(doc: Optional): Promise>; findActiveByRoleOrId(roles: string[], platform: BannerPlatform, bannerId?: string, options?: FindOptions): FindCursor; diff --git a/packages/model-typings/src/models/ICloudAnnouncementsModel.ts b/packages/model-typings/src/models/ICloudAnnouncementsModel.ts new file mode 100644 index 000000000000..672ff8c316a0 --- /dev/null +++ b/packages/model-typings/src/models/ICloudAnnouncementsModel.ts @@ -0,0 +1,6 @@ +import type { Cloud } from '@rocket.chat/core-typings'; + +import type { IBaseModel } from './IBaseModel'; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface ICloudAnnouncementsModel extends IBaseModel {} diff --git a/packages/models/src/index.ts b/packages/models/src/index.ts index e1cf91f1b0ee..ecb25cae71c2 100644 --- a/packages/models/src/index.ts +++ b/packages/models/src/index.ts @@ -78,6 +78,7 @@ import type { ICronHistoryModel, IMigrationsModel, IModerationReportsModel, + ICloudAnnouncementsModel, } from '@rocket.chat/model-typings'; import { proxify } from './proxify'; @@ -170,3 +171,4 @@ export const AuditLog = proxify('IAuditLogModel'); export const CronHistory = proxify('ICronHistoryModel'); export const Migrations = proxify('IMigrationsModel'); export const ModerationReports = proxify('IModerationReportsModel'); +export const CloudAnnouncents = proxify('ICloudAnnouncementsModel'); diff --git a/packages/rest-typings/src/v1/autotranslate/AutotranslateSaveSettingsParamsPOST.ts b/packages/rest-typings/src/v1/autotranslate/AutotranslateSaveSettingsParamsPOST.ts index 3690d0672ce2..914739d000a0 100644 --- a/packages/rest-typings/src/v1/autotranslate/AutotranslateSaveSettingsParamsPOST.ts +++ b/packages/rest-typings/src/v1/autotranslate/AutotranslateSaveSettingsParamsPOST.ts @@ -21,7 +21,7 @@ const AutotranslateSaveSettingsParamsPostSchema = { enum: ['autoTranslate', 'autoTranslateLanguage'], }, value: { - type: ['boolean', 'string'], + anyOf: [{ type: 'boolean' }, { type: 'string' }], }, defaultLanguage: { type: 'string', diff --git a/packages/rest-typings/src/v1/federation/FederationVerifyMatrixIdProps.ts b/packages/rest-typings/src/v1/federation/FederationVerifyMatrixIdProps.ts index a63d37da07ba..a6009fe20d85 100644 --- a/packages/rest-typings/src/v1/federation/FederationVerifyMatrixIdProps.ts +++ b/packages/rest-typings/src/v1/federation/FederationVerifyMatrixIdProps.ts @@ -11,7 +11,7 @@ const FederationVerifyMatrixIdPropsSchema = { properties: { matrixIds: { type: 'array', - items: [{ type: 'string' }], + items: { type: 'string' }, uniqueItems: true, }, }, diff --git a/packages/rest-typings/src/v1/moderation/ReportHistoryProps.ts b/packages/rest-typings/src/v1/moderation/ReportHistoryProps.ts index 69b1d85f22a5..48b859a7899b 100644 --- a/packages/rest-typings/src/v1/moderation/ReportHistoryProps.ts +++ b/packages/rest-typings/src/v1/moderation/ReportHistoryProps.ts @@ -10,6 +10,7 @@ type ReportHistoryProps = { export type ReportHistoryPropsGET = PaginatedRequest; const reportHistoryPropsSchema = { + type: 'object', properties: { latest: { type: 'string', diff --git a/packages/rest-typings/src/v1/omnichannel.ts b/packages/rest-typings/src/v1/omnichannel.ts index 60f6ed7ace08..31d004ba39c4 100644 --- a/packages/rest-typings/src/v1/omnichannel.ts +++ b/packages/rest-typings/src/v1/omnichannel.ts @@ -2550,6 +2550,10 @@ const GETLivechatRoomsParamsSchema = { type: 'string', nullable: true, }, + query: { + type: 'string', + nullable: true, + }, fields: { type: 'string', nullable: true, @@ -2582,12 +2586,16 @@ const GETLivechatRoomsParamsSchema = { nullable: true, }, open: { - type: ['string', 'boolean'], - nullable: true, + anyOf: [ + { type: 'string', nullable: true }, + { type: 'boolean', nullable: true }, + ], }, onhold: { - type: ['string', 'boolean'], - nullable: true, + anyOf: [ + { type: 'string', nullable: true }, + { type: 'boolean', nullable: true }, + ], }, tags: { type: 'array', @@ -3112,7 +3120,7 @@ const POSTLivechatAppearanceParamsSchema = { type: 'string', }, value: { - type: ['string', 'boolean', 'number'], + anyOf: [{ type: 'string' }, { type: 'boolean' }, { type: 'number' }], }, }, required: ['_id', 'value'], diff --git a/yarn.lock b/yarn.lock index 7d7b23691683..6aa03f27ba79 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7901,7 +7901,7 @@ __metadata: "@rocket.chat/message-parser": next "@rocket.chat/ui-kit": ^0.32.1 eslint: ~8.45.0 - mongodb: ^4.17.1 + mongodb: ^4.12.1 prettier: ~2.8.8 typescript: ~5.2.2 languageName: unknown @@ -7937,7 +7937,7 @@ __metadata: languageName: node linkType: hard -"@rocket.chat/css-in-js@npm:next, @rocket.chat/css-in-js@npm:~0.31.26-dev.19": +"@rocket.chat/css-in-js@npm:next": version: 0.31.26-dev.19 resolution: "@rocket.chat/css-in-js@npm:0.31.26-dev.19" dependencies: @@ -7950,6 +7950,15 @@ __metadata: languageName: node linkType: hard +"@rocket.chat/css-in-js@npm:~0.31.26-dev.19": + version: 0.31.26-dev.23 + resolution: "@rocket.chat/css-in-js@npm:0.31.26-dev.23" + dependencies: + "@rocket.chat/memo": ^0.31.25 + checksum: 6d71bd0f232c8ea3fc2711347064ddd14925b1c2b8713f6d7649b98679455029a53ee41d08b98d010da3ea4789afa21a15901a92efef61dee7b32d6965157445 + languageName: node + linkType: hard + "@rocket.chat/css-supports@npm:^0.31.25": version: 0.31.25 resolution: "@rocket.chat/css-supports@npm:0.31.25" @@ -7959,12 +7968,12 @@ __metadata: languageName: node linkType: hard -"@rocket.chat/css-supports@npm:~0.31.26-dev.19": - version: 0.31.26-dev.19 - resolution: "@rocket.chat/css-supports@npm:0.31.26-dev.19" +"@rocket.chat/css-supports@npm:~0.31.26-dev.19, @rocket.chat/css-supports@npm:~0.31.26-dev.23": + version: 0.31.26-dev.23 + resolution: "@rocket.chat/css-supports@npm:0.31.26-dev.23" dependencies: - "@rocket.chat/memo": ~0.31.26-dev.19 - checksum: c689ccca04901b128c8993e7475d89ca1e49d01efac9bb9641a0a35bba4237d36da48204cd26b39e92b8d98f24ff85df40e516fd0e421beaaf7c10a8308536ea + "@rocket.chat/memo": ~0.31.26-dev.23 + checksum: a4f25562df67214b1c92c85a1cd16eb03fc2aea385f48cdde42ad0053b9e03a92ca9e3486d1387c7a31cf68f47fa888825f31acae8f4700ee2b9f03495286a12 languageName: node linkType: hard @@ -8520,13 +8529,20 @@ __metadata: languageName: node linkType: hard -"@rocket.chat/memo@npm:next, @rocket.chat/memo@npm:~0.31.26-dev.19": +"@rocket.chat/memo@npm:next": version: 0.31.26-dev.19 resolution: "@rocket.chat/memo@npm:0.31.26-dev.19" checksum: 387c29643c0d725b2e2d3b79eeebf2ed3ac2fa518178d2836913dddf48f2aa72e80b277d54c77ac0498c144324cdfd3449bae883895c316fbb43c7dbbfcb3993 languageName: node linkType: hard +"@rocket.chat/memo@npm:~0.31.26-dev.19, @rocket.chat/memo@npm:~0.31.26-dev.23": + version: 0.31.26-dev.23 + resolution: "@rocket.chat/memo@npm:0.31.26-dev.23" + checksum: 68301161d87ba25347f1d2ab85c139ba86c5fdd1101f41678808c19ba461772814f4bff048a30e4aefd08978fe2feb952c541bddc0beb6bc3cd190bd7852393b + languageName: node + linkType: hard + "@rocket.chat/message-parser@npm:next": version: 0.32.0-dev.377 resolution: "@rocket.chat/message-parser@npm:0.32.0-dev.377" @@ -8711,7 +8727,7 @@ __metadata: "@xmldom/xmldom": ^0.8.8 adm-zip: 0.5.10 ajv: ^8.11.0 - ajv-formats: ^2.1.1 + ajv-formats: ~2.1.1 apn: 2.2.0 archiver: ^3.1.1 asterisk-manager: ^0.2.0 @@ -8885,6 +8901,7 @@ __metadata: stylelint: ^14.9.1 stylelint-order: ^5.0.0 supertest: ^6.2.3 + suretype: ~2.4.1 tar-stream: ^1.6.2 template-file: ^6.0.1 textarea-caret: ^3.1.0 @@ -9392,13 +9409,13 @@ __metadata: linkType: hard "@rocket.chat/stylis-logical-props-middleware@npm:~0.31.26-dev.19": - version: 0.31.26-dev.19 - resolution: "@rocket.chat/stylis-logical-props-middleware@npm:0.31.26-dev.19" + version: 0.31.26-dev.23 + resolution: "@rocket.chat/stylis-logical-props-middleware@npm:0.31.26-dev.23" dependencies: - "@rocket.chat/css-supports": ~0.31.26-dev.19 + "@rocket.chat/css-supports": ~0.31.26-dev.23 peerDependencies: stylis: 4.0.10 - checksum: 893bd48b6cc320ee7a970cda019e08b00c299c51562cf74f14e925bd4f613fc0c9448de876c3aa6b651bfc060a42097ccdd5a2dee0769a9a05cfe32eaff684f3 + checksum: b2fbfad3b2f4dedd9023b30d4cdc51e76ae76faeeca5819cf697e896c02fd4bb2dde5bbc428b377d77f32011fd8cc82c6d98a84d66b93056ef981c13aee1dc67 languageName: node linkType: hard @@ -14112,7 +14129,7 @@ __metadata: languageName: node linkType: hard -"ajv-formats@npm:^2.1.1": +"ajv-formats@npm:^2.1.1, ajv-formats@npm:~2.1.1": version: 2.1.1 resolution: "ajv-formats@npm:2.1.1" dependencies: @@ -14158,7 +14175,7 @@ __metadata: languageName: node linkType: hard -"ajv@npm:^6.1.0, ajv@npm:^6.10.0, ajv@npm:^6.10.2, ajv@npm:^6.12.2, ajv@npm:^6.12.3, ajv@npm:^6.12.4, ajv@npm:^6.12.5": +"ajv@npm:^6.1.0, ajv@npm:^6.10.0, ajv@npm:^6.10.2, ajv@npm:^6.11.0, ajv@npm:^6.12.2, ajv@npm:^6.12.3, ajv@npm:^6.12.4, ajv@npm:^6.12.5": version: 6.12.6 resolution: "ajv@npm:6.12.6" dependencies: @@ -15001,6 +15018,21 @@ __metadata: languageName: node linkType: hard +"awesome-ajv-errors@npm:^1.0.1": + version: 1.0.1 + resolution: "awesome-ajv-errors@npm:1.0.1" + dependencies: + chalk: ^4.1.0 + jsonpointer: ^4.1.0 + jsonpos: ^1.1.0 + leven: ^3.1.0 + terminal-link: ^2.1.1 + peerDependencies: + ajv: ^6 || ^7 + checksum: 1653f6dcebaf4913341e9ad5722aaa772bc1eddd623c11c58434d958c11bddc8f06f470c8ce6f04f269b45e296c4328455151e90cd0bb6892c6f1629753730d8 + languageName: node + linkType: hard + "aws-sdk@npm:^2.1363.0": version: 2.1363.0 resolution: "aws-sdk@npm:2.1363.0" @@ -17144,6 +17176,13 @@ __metadata: languageName: node linkType: hard +"code-error-fragment@npm:0.0.230": + version: 0.0.230 + resolution: "code-error-fragment@npm:0.0.230" + checksum: 6c5e800d6d70b30938cc85a2fc2c6069f028eadb58bceb65716b995ce6228c99906302f2c438ba50115fd81a1ee15dd95dc7d317b16a6c590e311ac7e50613f3 + languageName: node + linkType: hard + "code-point-at@npm:^1.0.0": version: 1.1.0 resolution: "code-point-at@npm:1.1.0" @@ -26280,6 +26319,16 @@ __metadata: languageName: node linkType: hard +"json-to-ast@npm:^2.1.0": + version: 2.1.0 + resolution: "json-to-ast@npm:2.1.0" + dependencies: + code-error-fragment: 0.0.230 + grapheme-splitter: ^1.0.4 + checksum: 1e9b051505b218573b39f3fec9054d75772413aefc2fee3e763d9033276664faa7eec26b945a71f70b9ce29685b2f13259df7dd3243e15eacf4672c62d5ba7ce + languageName: node + linkType: hard + "json5@npm:^0.5.0": version: 0.5.1 resolution: "json5@npm:0.5.1" @@ -26353,6 +26402,13 @@ __metadata: languageName: node linkType: hard +"jsonpointer@npm:^4.1.0": + version: 4.1.0 + resolution: "jsonpointer@npm:4.1.0" + checksum: ffc3e8937380989934676b339718d3213ecf5f6b7ce637b1ce5669a22f45dc61a86463e28abbe8c743d62f87ae790253c50cce0f586cb8e7623a21a7f811a444 + languageName: node + linkType: hard + "jsonpointer@npm:^5.0.0": version: 5.0.0 resolution: "jsonpointer@npm:5.0.0" @@ -26360,6 +26416,15 @@ __metadata: languageName: node linkType: hard +"jsonpos@npm:^1.1.0": + version: 1.1.0 + resolution: "jsonpos@npm:1.1.0" + dependencies: + json-to-ast: ^2.1.0 + checksum: 00a11fff623e74e1b14d10dcda2846e25ccdf0a12ade911fcbc8a75b82f4d33429c22dd57b6f7d2fd8a8eb07bc6435f2c3e412cb7ec09f2c8f63f19381742483 + languageName: node + linkType: hard + "jsonwebtoken@npm:^8.1.0, jsonwebtoken@npm:^8.5.1": version: 8.5.1 resolution: "jsonwebtoken@npm:8.5.1" @@ -27850,6 +27915,13 @@ __metadata: languageName: node linkType: hard +"meta-types@npm:^1.1.0": + version: 1.1.1 + resolution: "meta-types@npm:1.1.1" + checksum: 4dc31cf2eca16529ea8fc317e7d21cf8e88d85a64bc7894c8a00cf7395c1ac2d56d6655767c0a8ec02c80b8916555cf2968fad14629b06c5fefd00cb9b731b40 + languageName: node + linkType: hard + "meteor-blaze-tools@npm:^1.2.0, meteor-blaze-tools@npm:^1.2.4": version: 1.5.0 resolution: "meteor-blaze-tools@npm:1.5.0" @@ -28562,7 +28634,7 @@ __metadata: languageName: node linkType: hard -"mongodb@npm:4.17.1, mongodb@npm:^4.3.1": +"mongodb@npm:4.17.1, mongodb@npm:^4.12.1, mongodb@npm:^4.3.1": version: 4.17.1 resolution: "mongodb@npm:4.17.1" dependencies: @@ -34092,7 +34164,7 @@ __metadata: "@types/chart.js": ^2.9.37 "@types/js-yaml": ^4.0.5 husky: ^7.0.4 - turbo: ^1.10.13 + turbo: ~1.10.14 languageName: unknown linkType: soft @@ -36371,13 +36443,13 @@ __metadata: languageName: node linkType: hard -"supports-hyperlinks@npm:^2.2.0": - version: 2.2.0 - resolution: "supports-hyperlinks@npm:2.2.0" +"supports-hyperlinks@npm:^2.0.0, supports-hyperlinks@npm:^2.2.0": + version: 2.3.0 + resolution: "supports-hyperlinks@npm:2.3.0" dependencies: has-flag: ^4.0.0 supports-color: ^7.0.0 - checksum: aef04fb41f4a67f1bc128f7c3e88a81b6cf2794c800fccf137006efe5bafde281da3e42e72bf9206c2fcf42e6438f37e3a820a389214d0a88613ca1f2d36076a + checksum: 9ee0de3c8ce919d453511b2b1588a8205bd429d98af94a01df87411391010fe22ca463f268c84b2ce2abad019dfff8452aa02806eeb5c905a8d7ad5c4f4c52b8 languageName: node linkType: hard @@ -36388,6 +36460,17 @@ __metadata: languageName: node linkType: hard +"suretype@npm:~2.4.1": + version: 2.4.1 + resolution: "suretype@npm:2.4.1" + dependencies: + ajv: ^6.11.0 + awesome-ajv-errors: ^1.0.1 + meta-types: ^1.1.0 + checksum: f7562a8c1faa68e8daa3969e4488948eb72397eb5f9b4dbaed28c5ce1eb58e0701cca6352166834cde947662b2e30aeefb6f24d31904f773ee9bf65dd98810d1 + languageName: node + linkType: hard + "svg-arc-to-cubic-bezier@npm:^3.0.0, svg-arc-to-cubic-bezier@npm:^3.2.0": version: 3.2.0 resolution: "svg-arc-to-cubic-bezier@npm:3.2.0" @@ -36674,6 +36757,16 @@ __metadata: languageName: node linkType: hard +"terminal-link@npm:^2.1.1": + version: 2.1.1 + resolution: "terminal-link@npm:2.1.1" + dependencies: + ansi-escapes: ^4.2.1 + supports-hyperlinks: ^2.0.0 + checksum: ce3d2cd3a438c4a9453947aa664581519173ea40e77e2534d08c088ee6dda449eabdbe0a76d2a516b8b73c33262fedd10d5270ccf7576ae316e3db170ce6562f + languageName: node + linkType: hard + "terser-webpack-plugin@npm:^1.4.3": version: 1.4.5 resolution: "terser-webpack-plugin@npm:1.4.5" @@ -37453,58 +37546,58 @@ __metadata: languageName: node linkType: hard -"turbo-darwin-64@npm:1.10.13": - version: 1.10.13 - resolution: "turbo-darwin-64@npm:1.10.13" +"turbo-darwin-64@npm:1.10.14": + version: 1.10.14 + resolution: "turbo-darwin-64@npm:1.10.14" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"turbo-darwin-arm64@npm:1.10.13": - version: 1.10.13 - resolution: "turbo-darwin-arm64@npm:1.10.13" +"turbo-darwin-arm64@npm:1.10.14": + version: 1.10.14 + resolution: "turbo-darwin-arm64@npm:1.10.14" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"turbo-linux-64@npm:1.10.13": - version: 1.10.13 - resolution: "turbo-linux-64@npm:1.10.13" +"turbo-linux-64@npm:1.10.14": + version: 1.10.14 + resolution: "turbo-linux-64@npm:1.10.14" conditions: os=linux & cpu=x64 languageName: node linkType: hard -"turbo-linux-arm64@npm:1.10.13": - version: 1.10.13 - resolution: "turbo-linux-arm64@npm:1.10.13" +"turbo-linux-arm64@npm:1.10.14": + version: 1.10.14 + resolution: "turbo-linux-arm64@npm:1.10.14" conditions: os=linux & cpu=arm64 languageName: node linkType: hard -"turbo-windows-64@npm:1.10.13": - version: 1.10.13 - resolution: "turbo-windows-64@npm:1.10.13" +"turbo-windows-64@npm:1.10.14": + version: 1.10.14 + resolution: "turbo-windows-64@npm:1.10.14" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"turbo-windows-arm64@npm:1.10.13": - version: 1.10.13 - resolution: "turbo-windows-arm64@npm:1.10.13" +"turbo-windows-arm64@npm:1.10.14": + version: 1.10.14 + resolution: "turbo-windows-arm64@npm:1.10.14" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"turbo@npm:^1.10.13": - version: 1.10.13 - resolution: "turbo@npm:1.10.13" +"turbo@npm:~1.10.14": + version: 1.10.14 + resolution: "turbo@npm:1.10.14" dependencies: - turbo-darwin-64: 1.10.13 - turbo-darwin-arm64: 1.10.13 - turbo-linux-64: 1.10.13 - turbo-linux-arm64: 1.10.13 - turbo-windows-64: 1.10.13 - turbo-windows-arm64: 1.10.13 + turbo-darwin-64: 1.10.14 + turbo-darwin-arm64: 1.10.14 + turbo-linux-64: 1.10.14 + turbo-linux-arm64: 1.10.14 + turbo-windows-64: 1.10.14 + turbo-windows-arm64: 1.10.14 dependenciesMeta: turbo-darwin-64: optional: true @@ -37520,7 +37613,7 @@ __metadata: optional: true bin: turbo: bin/turbo - checksum: 0c000c671534c8c80270c6d1fc77646df0e44164c0db561a85b3fefadd4bda6d5920626d067abb09af38613024e3984fb8d8bc5be922dae6236eda6aab9447a2 + checksum: 219d245bb5cc32a9f76b136b81e86e179228d93a44cab4df3e3d487a55dd2688b5b85f4d585b66568ac53166145352399dd2d7ed0cd47f1aae63d08beb814ebb languageName: node linkType: hard From 6a575534a1e309f5db20912dccc35e84f3b09f02 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Fri, 22 Sep 2023 17:26:03 -0300 Subject: [PATCH 03/11] Fix dependencies --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 6aa03f27ba79..2d7159db0031 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16514,9 +16514,9 @@ __metadata: linkType: hard "caniuse-lite@npm:^1.0.0, caniuse-lite@npm:^1.0.30001109, caniuse-lite@npm:^1.0.30001503": - version: 1.0.30001503 - resolution: "caniuse-lite@npm:1.0.30001503" - checksum: cd5f0af37655ff71ec4ab3c49124d75e0b8b68de625d07ea80e9a82329e616b5203d5dad6865192653be9da50081c06878f081ab069dac0be35adf29aa1599cd + version: 1.0.30001538 + resolution: "caniuse-lite@npm:1.0.30001538" + checksum: 94c5d55757a339c7cc175f08a024671e2b4e7c04f130b1015793303d637061347efb6ad84447c3b8137333e742d150b8ad9672716bbf2482646c2e63a56f6c55 languageName: node linkType: hard From ed35dfbe4af87229a98c90f55b4fb01c3704d058 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Mon, 25 Sep 2023 12:19:59 -0300 Subject: [PATCH 04/11] Declare new endpoint --- apps/meteor/app/api/server/v1/cloud.ts | 19 +++++++++++++++++++ .../core-typings/src/cloud/Announcement.ts | 5 ++--- .../core-typings/src/cloud/TargetPlatform.ts | 1 + packages/core-typings/src/cloud/index.ts | 1 + packages/rest-typings/src/v1/cloud.ts | 11 ++++++++++- 5 files changed, 33 insertions(+), 4 deletions(-) create mode 100644 packages/core-typings/src/cloud/TargetPlatform.ts diff --git a/apps/meteor/app/api/server/v1/cloud.ts b/apps/meteor/app/api/server/v1/cloud.ts index 55b3f8588275..f4118ba765f8 100644 --- a/apps/meteor/app/api/server/v1/cloud.ts +++ b/apps/meteor/app/api/server/v1/cloud.ts @@ -1,3 +1,4 @@ +import { CloudAnnouncents } from '@rocket.chat/models'; import { check } from 'meteor/check'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; @@ -122,3 +123,21 @@ API.v1.addRoute( }, }, ); + +API.v1.addRoute( + 'cloud.announcements', + { authRequired: true }, + { + async get() { + if (!(await hasRoleAsync(this.userId, 'admin'))) { + return API.v1.unauthorized(); + } + + const announcements = await CloudAnnouncents.find({ + $or: [{ 'selector.roles': { $in: ['admin'] } }, { 'selector.roles': { $exists: false } }], + }).toArray(); + + return API.v1.success({ announcements }); + }, + }, +); diff --git a/packages/core-typings/src/cloud/Announcement.ts b/packages/core-typings/src/cloud/Announcement.ts index 3d891daf132f..acfbca9e78cc 100644 --- a/packages/core-typings/src/cloud/Announcement.ts +++ b/packages/core-typings/src/cloud/Announcement.ts @@ -1,9 +1,8 @@ /* eslint-disable @typescript-eslint/naming-convention */ import type { IRocketChatRecord } from '../IRocketChatRecord'; -import { type UiKitPayload } from '../UIKit'; - -type TargetPlatform = 'web' | 'mobile'; +import type { UiKitPayload } from '../UIKit'; +import type { TargetPlatform } from './TargetPlatform'; type Dictionary = { [lng: string]: { diff --git a/packages/core-typings/src/cloud/TargetPlatform.ts b/packages/core-typings/src/cloud/TargetPlatform.ts new file mode 100644 index 000000000000..2c31c98b3203 --- /dev/null +++ b/packages/core-typings/src/cloud/TargetPlatform.ts @@ -0,0 +1 @@ +export type TargetPlatform = 'web' | 'mobile'; diff --git a/packages/core-typings/src/cloud/index.ts b/packages/core-typings/src/cloud/index.ts index b9c044b054e3..2204f6df3bb8 100644 --- a/packages/core-typings/src/cloud/index.ts +++ b/packages/core-typings/src/cloud/index.ts @@ -1,4 +1,5 @@ export { Announcement } from './Announcement'; export { NpsSurveyAnnouncement } from './NpsSurveyAnnouncement'; +export { TargetPlatform } from './TargetPlatform'; export { WorkspaceLicensePayload } from './WorkspaceLicensePayload'; export { WorkspaceSyncPayload } from './WorkspaceSyncPayload'; diff --git a/packages/rest-typings/src/v1/cloud.ts b/packages/rest-typings/src/v1/cloud.ts index 90664dcc243b..ae99203ea376 100644 --- a/packages/rest-typings/src/v1/cloud.ts +++ b/packages/rest-typings/src/v1/cloud.ts @@ -1,4 +1,4 @@ -import type { CloudRegistrationIntentData, CloudConfirmationPollData, CloudRegistrationStatus } from '@rocket.chat/core-typings'; +import type { CloudRegistrationIntentData, CloudConfirmationPollData, CloudRegistrationStatus, Cloud } from '@rocket.chat/core-typings'; import Ajv from 'ajv'; const ajv = new Ajv({ @@ -66,6 +66,12 @@ const CloudConfirmationPollSchema = { export const isCloudConfirmationPollProps = ajv.compile(CloudConfirmationPollSchema); +type CloudAnnouncementsParams = { + platform: Cloud.TargetPlatform; +}; + +type CloudAnnouncementsResponse = Cloud.Announcement[]; + export type CloudEndpoints = { '/v1/cloud.manualRegister': { POST: (params: CloudManualRegister) => void; @@ -88,4 +94,7 @@ export type CloudEndpoints = { '/v1/cloud.registrationStatus': { GET: () => { registrationStatus: CloudRegistrationStatus }; }; + '/v1/cloud.announcements': { + GET: (params: CloudAnnouncementsParams) => CloudAnnouncementsResponse; + }; }; From 6187c8b538ef4d872b8d3c5ddd29cecdaec955aa Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Wed, 27 Sep 2023 04:06:42 -0300 Subject: [PATCH 05/11] Prototype announcement retrieval --- apps/meteor/app/api/server/v1/cloud.ts | 13 +++++-- .../hooks/useCloudAnnouncementsQuery.ts | 19 ++++++++++ .../client/views/banners/BannerRegion.tsx | 2 + .../hooks/useCloudAnnouncementBanners.ts | 31 +++++++++++++++ .../meteor/client/views/modal/ModalRegion.tsx | 2 + .../hooks/useCloudAnnouncementModals.tsx | 38 +++++++++++++++++++ .../client/views/modal/uikit/UiKitModal.tsx | 15 ++++++-- packages/rest-typings/src/v1/cloud.ts | 4 +- 8 files changed, 117 insertions(+), 7 deletions(-) create mode 100644 apps/meteor/client/hooks/useCloudAnnouncementsQuery.ts create mode 100644 apps/meteor/client/views/banners/hooks/useCloudAnnouncementBanners.ts create mode 100644 apps/meteor/client/views/modal/hooks/useCloudAnnouncementModals.tsx diff --git a/apps/meteor/app/api/server/v1/cloud.ts b/apps/meteor/app/api/server/v1/cloud.ts index f4118ba765f8..0d23af895440 100644 --- a/apps/meteor/app/api/server/v1/cloud.ts +++ b/apps/meteor/app/api/server/v1/cloud.ts @@ -133,9 +133,16 @@ API.v1.addRoute( return API.v1.unauthorized(); } - const announcements = await CloudAnnouncents.find({ - $or: [{ 'selector.roles': { $in: ['admin'] } }, { 'selector.roles': { $exists: false } }], - }).toArray(); + const announcements = await CloudAnnouncents.find( + { + $or: [{ 'selector.roles': { $in: ['admin'] } }, { 'selector.roles': { $exists: false } }], + }, + { + sort: { + createdAt: -1, + }, + }, + ).toArray(); return API.v1.success({ announcements }); }, diff --git a/apps/meteor/client/hooks/useCloudAnnouncementsQuery.ts b/apps/meteor/client/hooks/useCloudAnnouncementsQuery.ts new file mode 100644 index 000000000000..6f78f145e862 --- /dev/null +++ b/apps/meteor/client/hooks/useCloudAnnouncementsQuery.ts @@ -0,0 +1,19 @@ +import type { Cloud, Serialized } from '@rocket.chat/core-typings'; +import { useEndpoint } from '@rocket.chat/ui-contexts'; +import type { UseQueryOptions } from '@tanstack/react-query'; +import { useQuery } from '@tanstack/react-query'; + +export const useCloudAnnouncementsQuery = []>( + options?: UseQueryOptions[], TError, TData, readonly ['cloud', 'announcements']>, +) => { + const getCloudAnnouncements = useEndpoint('GET', '/v1/cloud.announcements'); + + return useQuery( + ['cloud', 'announcements'] as const, + async () => { + const { announcements } = await getCloudAnnouncements({ platform: 'web' }); + return announcements; + }, + options, + ); +}; diff --git a/apps/meteor/client/views/banners/BannerRegion.tsx b/apps/meteor/client/views/banners/BannerRegion.tsx index c5394f787229..f457d2b38e8f 100644 --- a/apps/meteor/client/views/banners/BannerRegion.tsx +++ b/apps/meteor/client/views/banners/BannerRegion.tsx @@ -5,6 +5,7 @@ import { useSyncExternalStore } from 'use-sync-external-store/shim'; import * as banners from '../../lib/banners'; import LegacyBanner from './LegacyBanner'; import UiKitBanner from './UiKitBanner'; +import { useCloudAnnouncementBanners } from './hooks/useCloudAnnouncementBanners'; import { useRemoteBanners } from './hooks/useRemoteBanners'; import { useUserBanners } from './hooks/useUserBanners'; @@ -13,6 +14,7 @@ const BannerRegion = (): ReactElement | null => { useRemoteBanners(); useUserBanners(); + useCloudAnnouncementBanners(); if (!payload) { return null; diff --git a/apps/meteor/client/views/banners/hooks/useCloudAnnouncementBanners.ts b/apps/meteor/client/views/banners/hooks/useCloudAnnouncementBanners.ts new file mode 100644 index 000000000000..c0800735ad10 --- /dev/null +++ b/apps/meteor/client/views/banners/hooks/useCloudAnnouncementBanners.ts @@ -0,0 +1,31 @@ +import type { Serialized, Cloud, UiKitBannerPayload } from '@rocket.chat/core-typings'; +import { useEffect } from 'react'; + +import { useCloudAnnouncementsQuery } from '../../../hooks/useCloudAnnouncementsQuery'; +import * as banners from '../../../lib/banners'; + +const isBannerCarryingAnnouncement = ( + announcement: Serialized, +): announcement is Serialized & { surface: 'banner'; view: UiKitBannerPayload } => announcement.surface === 'banner'; + +export const useCloudAnnouncementBanners = () => { + const queryResult = useCloudAnnouncementsQuery({ + select: (announcements) => announcements.filter(isBannerCarryingAnnouncement), + }); + + useEffect(() => { + if (!queryResult.isSuccess) { + return; + } + + for (const announcement of queryResult.data) { + banners.open(announcement.view); + } + + return () => { + for (const announcement of queryResult.data) { + banners.closeById(announcement.view.viewId); + } + }; + }, [queryResult.data, queryResult.isSuccess]); +}; diff --git a/apps/meteor/client/views/modal/ModalRegion.tsx b/apps/meteor/client/views/modal/ModalRegion.tsx index b014a5be4f7c..b6c73d7cadbe 100644 --- a/apps/meteor/client/views/modal/ModalRegion.tsx +++ b/apps/meteor/client/views/modal/ModalRegion.tsx @@ -4,6 +4,7 @@ import React, { lazy, useCallback } from 'react'; import ModalBackdrop from '../../components/modal/ModalBackdrop'; import ModalPortal from '../../components/modal/ModalPortal'; +import { useCloudAnnouncementModals } from './hooks/useCloudAnnouncementModals'; const FocusScope = lazy(() => import('react-aria').then((module) => ({ default: module.FocusScope }))); @@ -11,6 +12,7 @@ const ModalRegion = (): ReactElement | null => { const currentModal = useCurrentModal(); const { setModal } = useModal(); const handleDismiss = useCallback(() => setModal(null), [setModal]); + useCloudAnnouncementModals(); if (!currentModal) { return null; diff --git a/apps/meteor/client/views/modal/hooks/useCloudAnnouncementModals.tsx b/apps/meteor/client/views/modal/hooks/useCloudAnnouncementModals.tsx new file mode 100644 index 000000000000..1177c14e08a0 --- /dev/null +++ b/apps/meteor/client/views/modal/hooks/useCloudAnnouncementModals.tsx @@ -0,0 +1,38 @@ +import type { IUIKitSurface } from '@rocket.chat/apps-engine/definition/uikit'; +import type { Cloud, Serialized } from '@rocket.chat/core-typings'; +import { useSetModal } from '@rocket.chat/ui-contexts'; +import React, { useEffect } from 'react'; + +import { useCloudAnnouncementsQuery } from '../../../hooks/useCloudAnnouncementsQuery'; +import UiKitModal from '../uikit/UiKitModal'; + +const isModalCarryingAnnouncement = ( + announcement: Serialized, +): announcement is Serialized & { surface: 'modal'; view: IUIKitSurface } => announcement.surface === 'modal'; + +export const useCloudAnnouncementModals = () => { + const queryResult = useCloudAnnouncementsQuery({ + select: (announcements) => announcements.filter(isModalCarryingAnnouncement), + }); + + const setModal = useSetModal(); + + useEffect(() => { + if (!queryResult.isSuccess) { + return; + } + + for (const announcement of queryResult.data) { + setModal( + , + ); + } + }, [queryResult.data, queryResult.isSuccess, setModal]); +}; diff --git a/apps/meteor/client/views/modal/uikit/UiKitModal.tsx b/apps/meteor/client/views/modal/uikit/UiKitModal.tsx index b985f94b09b9..4909c1e1bea2 100644 --- a/apps/meteor/client/views/modal/uikit/UiKitModal.tsx +++ b/apps/meteor/client/views/modal/uikit/UiKitModal.tsx @@ -1,19 +1,28 @@ +import type { IUIKitSurface } from '@rocket.chat/apps-engine/definition/uikit'; import { UIKitIncomingInteractionContainerType } from '@rocket.chat/apps-engine/definition/uikit/UIKitIncomingInteractionContainer'; import { useDebouncedCallback, useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { UiKitContext } from '@rocket.chat/fuselage-ui-kit'; import { MarkupInteractionContext } from '@rocket.chat/gazzodown'; import type { LayoutBlock } from '@rocket.chat/ui-kit'; -import type { ContextType, ReactElement, ReactEventHandler } from 'react'; +import type { ContextType, ReactEventHandler } from 'react'; import React from 'react'; import { useUiKitActionManager } from '../../../hooks/useUiKitActionManager'; import { detectEmoji } from '../../../lib/utils/detectEmoji'; import ModalBlock from './ModalBlock'; -import type { ActionManagerState } from './hooks/useActionManagerState'; import { useActionManagerState } from './hooks/useActionManagerState'; import { useValues } from './hooks/useValues'; -const UiKitModal = (props: ActionManagerState): ReactElement => { +type UiKitModalProps = { + viewId: string; + type: 'errors' | string; + appId: string; + mid: string; + errors: Record; + view: IUIKitSurface; +}; + +const UiKitModal = (props: UiKitModalProps) => { const actionManager = useUiKitActionManager(); const state = useActionManagerState(props); diff --git a/packages/rest-typings/src/v1/cloud.ts b/packages/rest-typings/src/v1/cloud.ts index ae99203ea376..843c15ee936e 100644 --- a/packages/rest-typings/src/v1/cloud.ts +++ b/packages/rest-typings/src/v1/cloud.ts @@ -70,7 +70,9 @@ type CloudAnnouncementsParams = { platform: Cloud.TargetPlatform; }; -type CloudAnnouncementsResponse = Cloud.Announcement[]; +type CloudAnnouncementsResponse = { + announcements: Cloud.Announcement[]; +}; export type CloudEndpoints = { '/v1/cloud.manualRegister': { From 15a16fd8e74d92332ac2c7a02a12d02275a798b6 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Wed, 27 Sep 2023 23:21:55 -0300 Subject: [PATCH 06/11] Keep tuples' structure when using `Serialized` --- packages/core-typings/src/Serialized.ts | 31 +++++++++++++++++++------ 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/packages/core-typings/src/Serialized.ts b/packages/core-typings/src/Serialized.ts index c84077610ee8..94f79cb64d06 100644 --- a/packages/core-typings/src/Serialized.ts +++ b/packages/core-typings/src/Serialized.ts @@ -1,9 +1,26 @@ -export type Serialized = T extends Date - ? Exclude | string - : T extends boolean | number | string | null | undefined +/* eslint-disable @typescript-eslint/ban-types */ + +type SerializablePrimitive = boolean | number | string | null; + +type UnserializablePrimitive = Function | bigint | symbol | undefined; + +type CustomSerializable = { + toJSON(key: string): T; +}; + +/** + * The type of a value that was serialized via `JSON.stringify` and then deserialized via `JSON.parse`. + */ +export type Serialized = T extends CustomSerializable + ? Serialized + : T extends [any, ...any] // is T a tuple? + ? { [K in keyof T]: T extends UnserializablePrimitive ? null : Serialized } + : T extends any[] + ? Serialized[] + : T extends object + ? { [K in keyof T]: Serialized } + : T extends SerializablePrimitive ? T - : T extends {} - ? { - [K in keyof T]: Serialized; - } + : T extends UnserializablePrimitive + ? undefined : null; From de5e179f2ad33296e07a17aa97ac6fa7f78c0823 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Thu, 28 Sep 2023 00:15:12 -0300 Subject: [PATCH 07/11] Namespace UiKit banner payload type --- .../UIKit/hooks/useUIKitHandleAction.tsx | 4 +- .../UIKit/hooks/useUIKitHandleClose.tsx | 7 +++- .../UIKit/hooks/useUIKitStateManager.tsx | 6 +-- apps/meteor/client/lib/banners.ts | 6 +-- .../client/views/banners/UiKitBanner.tsx | 10 +++-- .../hooks/useCloudAnnouncementBanners.ts | 4 +- .../views/banners/hooks/useRemoteBanners.ts | 4 +- .../client/views/modal/uikit/UiKitModal.tsx | 4 +- .../uikit/hooks/useActionManagerState.ts | 39 ------------------- .../ee/app/license/server/maxSeatsBanners.ts | 12 ++---- .../services/nps/getAndCreateNpsSurvey.ts | 4 +- packages/core-typings/src/IBanner.ts | 4 +- .../core-typings/src/cloud/Announcement.ts | 4 +- packages/core-typings/src/index.ts | 3 +- .../core-typings/src/uikit/BannerPayload.ts | 12 ++++++ packages/core-typings/src/uikit/Payload.ts | 7 ++++ .../src/{UIKit.ts => uikit/deprecations.ts} | 15 ++----- packages/core-typings/src/uikit/index.ts | 2 + 18 files changed, 63 insertions(+), 84 deletions(-) delete mode 100644 apps/meteor/client/views/modal/uikit/hooks/useActionManagerState.ts create mode 100644 packages/core-typings/src/uikit/BannerPayload.ts create mode 100644 packages/core-typings/src/uikit/Payload.ts rename packages/core-typings/src/{UIKit.ts => uikit/deprecations.ts} (85%) create mode 100644 packages/core-typings/src/uikit/index.ts diff --git a/apps/meteor/client/UIKit/hooks/useUIKitHandleAction.tsx b/apps/meteor/client/UIKit/hooks/useUIKitHandleAction.tsx index 6a97f18a7936..c5a265203c8a 100644 --- a/apps/meteor/client/UIKit/hooks/useUIKitHandleAction.tsx +++ b/apps/meteor/client/UIKit/hooks/useUIKitHandleAction.tsx @@ -1,10 +1,10 @@ import { UIKitIncomingInteractionContainerType } from '@rocket.chat/apps-engine/definition/uikit/UIKitIncomingInteractionContainer'; -import type { UiKitPayload, UIKitActionEvent } from '@rocket.chat/core-typings'; +import type { UiKitPayload, UIKitActionEvent, UiKit } from '@rocket.chat/core-typings'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useUiKitActionManager } from '../../hooks/useUiKitActionManager'; -const useUIKitHandleAction = (state: S): ((event: UIKitActionEvent) => Promise) => { +const useUIKitHandleAction = (state: S): ((event: UIKitActionEvent) => Promise) => { const actionManager = useUiKitActionManager(); return useMutableCallback(async ({ blockId, value, appId, actionId }) => { if (!appId) { diff --git a/apps/meteor/client/UIKit/hooks/useUIKitHandleClose.tsx b/apps/meteor/client/UIKit/hooks/useUIKitHandleClose.tsx index 672e1b311b5d..561852a06527 100644 --- a/apps/meteor/client/UIKit/hooks/useUIKitHandleClose.tsx +++ b/apps/meteor/client/UIKit/hooks/useUIKitHandleClose.tsx @@ -1,5 +1,5 @@ import type { UIKitInteractionType } from '@rocket.chat/apps-engine/definition/uikit'; -import type { UiKitPayload } from '@rocket.chat/core-typings'; +import type { UiKit, UiKitPayload } from '@rocket.chat/core-typings'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useToastMessageDispatch } from '@rocket.chat/ui-contexts'; @@ -8,7 +8,10 @@ import { useUiKitActionManager } from '../../hooks/useUiKitActionManager'; // eslint-disable-next-line @typescript-eslint/no-unused-vars const emptyFn = (_error: any, _result: UIKitInteractionType | void): void => undefined; -const useUIKitHandleClose = (state: S, fn = emptyFn): (() => Promise) => { +const useUIKitHandleClose = ( + state: S, + fn = emptyFn, +): (() => Promise) => { const actionManager = useUiKitActionManager(); const dispatchToastMessage = useToastMessageDispatch(); return useMutableCallback(() => diff --git a/apps/meteor/client/UIKit/hooks/useUIKitStateManager.tsx b/apps/meteor/client/UIKit/hooks/useUIKitStateManager.tsx index 26b329f2ea60..738b337e3c91 100644 --- a/apps/meteor/client/UIKit/hooks/useUIKitStateManager.tsx +++ b/apps/meteor/client/UIKit/hooks/useUIKitStateManager.tsx @@ -1,11 +1,11 @@ -import type { UIKitUserInteractionResult, UiKitPayload } from '@rocket.chat/core-typings'; +import type { UIKitUserInteractionResult, UiKit, UiKitPayload } from '@rocket.chat/core-typings'; import { isErrorType } from '@rocket.chat/core-typings'; import { useSafely } from '@rocket.chat/fuselage-hooks'; import { useEffect, useState } from 'react'; import { useUiKitActionManager } from '../../hooks/useUiKitActionManager'; -const useUIKitStateManager = (initialState: S): S => { +const useUIKitStateManager = (initialState: S): S => { const actionManager = useUiKitActionManager(); const [state, setState] = useSafely(useState(initialState)); @@ -28,7 +28,7 @@ const useUIKitStateManager = (initialState: S): S => { return (): void => { actionManager.off(viewId, handleUpdate); }; - }, [setState, viewId]); + }, [actionManager, setState, viewId]); return state; }; diff --git a/apps/meteor/client/lib/banners.ts b/apps/meteor/client/lib/banners.ts index 89310da2e3c7..df38e7f16dcd 100644 --- a/apps/meteor/client/lib/banners.ts +++ b/apps/meteor/client/lib/banners.ts @@ -1,4 +1,4 @@ -import type { UiKitBannerPayload } from '@rocket.chat/core-typings'; +import type { UiKit } from '@rocket.chat/core-typings'; import { Emitter } from '@rocket.chat/emitter'; import type { Keys as IconName } from '@rocket.chat/icons'; @@ -15,7 +15,7 @@ export type LegacyBannerPayload = { onClose?: () => Promise | void; }; -type BannerPayload = LegacyBannerPayload | UiKitBannerPayload; +type BannerPayload = LegacyBannerPayload | UiKit.BannerPayload; export const isLegacyPayload = (payload: BannerPayload): payload is LegacyBannerPayload => !('blocks' in payload); @@ -35,7 +35,7 @@ export const open = (payload: BannerPayload): void => { if (isLegacyPayload(_payload)) { return _payload.id === (payload as LegacyBannerPayload).id; } - return (_payload as UiKitBannerPayload).viewId === (payload as UiKitBannerPayload).viewId; + return _payload.viewId === (payload as UiKit.BannerPayload).viewId; }); if (index === -1) { diff --git a/apps/meteor/client/views/banners/UiKitBanner.tsx b/apps/meteor/client/views/banners/UiKitBanner.tsx index 7cb52dd8d3c9..8e306c98e633 100644 --- a/apps/meteor/client/views/banners/UiKitBanner.tsx +++ b/apps/meteor/client/views/banners/UiKitBanner.tsx @@ -1,9 +1,9 @@ -import type { UIKitActionEvent, UiKitBannerProps } from '@rocket.chat/core-typings'; +import type { UIKitActionEvent, UiKit } from '@rocket.chat/core-typings'; import { Banner, Icon } from '@rocket.chat/fuselage'; import { UiKitContext, bannerParser, UiKitBanner as UiKitBannerSurfaceRender, UiKitComponent } from '@rocket.chat/fuselage-ui-kit'; import type { Keys as IconName } from '@rocket.chat/icons'; import type { LayoutBlock } from '@rocket.chat/ui-kit'; -import type { FC, ReactElement, ContextType } from 'react'; +import type { ReactElement, ContextType } from 'react'; import React, { useMemo } from 'react'; import { useUIKitHandleAction } from '../../UIKit/hooks/useUIKitHandleAction'; @@ -15,7 +15,11 @@ import * as banners from '../../lib/banners'; // TODO: move this to fuselage-ui-kit itself bannerParser.mrkdwn = ({ text }): ReactElement => ; -const UiKitBanner: FC = ({ payload }) => { +type UiKitBannerProps = { + payload: UiKit.BannerPayload; +}; + +const UiKitBanner = ({ payload }: UiKitBannerProps) => { const state = useUIKitStateManager(payload); const icon = useMemo(() => { diff --git a/apps/meteor/client/views/banners/hooks/useCloudAnnouncementBanners.ts b/apps/meteor/client/views/banners/hooks/useCloudAnnouncementBanners.ts index c0800735ad10..0fc675284b61 100644 --- a/apps/meteor/client/views/banners/hooks/useCloudAnnouncementBanners.ts +++ b/apps/meteor/client/views/banners/hooks/useCloudAnnouncementBanners.ts @@ -1,4 +1,4 @@ -import type { Serialized, Cloud, UiKitBannerPayload } from '@rocket.chat/core-typings'; +import type { Serialized, Cloud, UiKit } from '@rocket.chat/core-typings'; import { useEffect } from 'react'; import { useCloudAnnouncementsQuery } from '../../../hooks/useCloudAnnouncementsQuery'; @@ -6,7 +6,7 @@ import * as banners from '../../../lib/banners'; const isBannerCarryingAnnouncement = ( announcement: Serialized, -): announcement is Serialized & { surface: 'banner'; view: UiKitBannerPayload } => announcement.surface === 'banner'; +): announcement is Serialized => announcement.surface === 'banner'; export const useCloudAnnouncementBanners = () => { const queryResult = useCloudAnnouncementsQuery({ diff --git a/apps/meteor/client/views/banners/hooks/useRemoteBanners.ts b/apps/meteor/client/views/banners/hooks/useRemoteBanners.ts index ff42d4ae9ace..ddd2b5ab651a 100644 --- a/apps/meteor/client/views/banners/hooks/useRemoteBanners.ts +++ b/apps/meteor/client/views/banners/hooks/useRemoteBanners.ts @@ -1,5 +1,5 @@ import { BannerPlatform } from '@rocket.chat/core-typings'; -import type { IBanner, Serialized, UiKitBannerPayload } from '@rocket.chat/core-typings'; +import type { IBanner, Serialized, UiKit } from '@rocket.chat/core-typings'; import { useEndpoint, useStream, useUserId, ServerContext } from '@rocket.chat/ui-contexts'; import { useContext, useEffect } from 'react'; @@ -22,7 +22,7 @@ export const useRemoteBanners = () => { const { signal } = controller; - const mapBanner = (banner: Serialized): UiKitBannerPayload => ({ + const mapBanner = (banner: Serialized): UiKit.BannerPayload => ({ ...banner.view, viewId: banner.view.viewId || banner._id, }); diff --git a/apps/meteor/client/views/modal/uikit/UiKitModal.tsx b/apps/meteor/client/views/modal/uikit/UiKitModal.tsx index 4909c1e1bea2..5baad8543769 100644 --- a/apps/meteor/client/views/modal/uikit/UiKitModal.tsx +++ b/apps/meteor/client/views/modal/uikit/UiKitModal.tsx @@ -7,10 +7,10 @@ import type { LayoutBlock } from '@rocket.chat/ui-kit'; import type { ContextType, ReactEventHandler } from 'react'; import React from 'react'; +import { useUIKitStateManager } from '../../../UIKit/hooks/useUIKitStateManager'; import { useUiKitActionManager } from '../../../hooks/useUiKitActionManager'; import { detectEmoji } from '../../../lib/utils/detectEmoji'; import ModalBlock from './ModalBlock'; -import { useActionManagerState } from './hooks/useActionManagerState'; import { useValues } from './hooks/useValues'; type UiKitModalProps = { @@ -24,7 +24,7 @@ type UiKitModalProps = { const UiKitModal = (props: UiKitModalProps) => { const actionManager = useUiKitActionManager(); - const state = useActionManagerState(props); + const state = useUIKitStateManager(props as any); const { appId, viewId, mid: _mid, errors, view } = state; diff --git a/apps/meteor/client/views/modal/uikit/hooks/useActionManagerState.ts b/apps/meteor/client/views/modal/uikit/hooks/useActionManagerState.ts deleted file mode 100644 index fb1da19010e3..000000000000 --- a/apps/meteor/client/views/modal/uikit/hooks/useActionManagerState.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { IUIKitSurface } from '@rocket.chat/apps-engine/definition/uikit'; -import { useEffect, useState } from 'react'; - -import { useUiKitActionManager } from '../../../../hooks/useUiKitActionManager'; - -export type ActionManagerState = { - viewId: string; - type: 'errors' | string; - appId: string; - mid: string; - errors: Record; - view: IUIKitSurface; -}; - -export const useActionManagerState = (initialState: ActionManagerState) => { - const actionManager = useUiKitActionManager(); - const [state, setState] = useState(initialState); - - const { viewId } = state; - - useEffect(() => { - const handleUpdate = ({ type, errors, ...data }: ActionManagerState) => { - if (type === 'errors') { - setState((state) => ({ ...state, errors, type })); - return; - } - - setState({ ...data, type, errors }); - }; - - actionManager.on(viewId, handleUpdate); - - return () => { - actionManager.off(viewId, handleUpdate); - }; - }, [actionManager, viewId]); - - return state; -}; diff --git a/apps/meteor/ee/app/license/server/maxSeatsBanners.ts b/apps/meteor/ee/app/license/server/maxSeatsBanners.ts index 1aefb7848a42..b5aba719f29d 100644 --- a/apps/meteor/ee/app/license/server/maxSeatsBanners.ts +++ b/apps/meteor/ee/app/license/server/maxSeatsBanners.ts @@ -1,5 +1,3 @@ -import { BlockType } from '@rocket.chat/apps-engine/definition/uikit/blocks/Blocks'; -import { TextObjectType } from '@rocket.chat/apps-engine/definition/uikit/blocks/Objects'; import { Banner } from '@rocket.chat/core-services'; import type { IBanner } from '@rocket.chat/core-typings'; import { BannerPlatform } from '@rocket.chat/core-typings'; @@ -21,15 +19,14 @@ const makeWarningBanner = (seats: number): IBanner => ({ appId: 'banner-core', blocks: [ { - type: BlockType.SECTION, + type: 'section', blockId: 'attention', text: { - type: TextObjectType.MARKDOWN, + type: 'mrkdwn', text: i18n.t('Close_to_seat_limit_banner_warning', { seats, url: Meteor.absoluteUrl('/requestSeats'), }), - emoji: false, }, }, ], @@ -56,14 +53,13 @@ const makeDangerBanner = (): IBanner => ({ appId: 'banner-core', blocks: [ { - type: BlockType.SECTION, + type: 'section', blockId: 'attention', text: { - type: TextObjectType.MARKDOWN, + type: 'mrkdwn', text: i18n.t('Reached_seat_limit_banner_warning', { url: Meteor.absoluteUrl('/requestSeats'), }), - emoji: false, }, }, ], diff --git a/apps/meteor/server/services/nps/getAndCreateNpsSurvey.ts b/apps/meteor/server/services/nps/getAndCreateNpsSurvey.ts index 02a3c29eedf3..dce5f7eac4ad 100644 --- a/apps/meteor/server/services/nps/getAndCreateNpsSurvey.ts +++ b/apps/meteor/server/services/nps/getAndCreateNpsSurvey.ts @@ -1,5 +1,5 @@ import { Banner } from '@rocket.chat/core-services'; -import type { UiKitBannerPayload, IBanner, BannerPlatform } from '@rocket.chat/core-typings'; +import type { UiKit, IBanner, BannerPlatform } from '@rocket.chat/core-typings'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; import { getWorkspaceAccessToken } from '../../../app/cloud/server'; @@ -10,7 +10,7 @@ type NpsSurveyData = { id: string; platform: BannerPlatform[]; roles: string[]; - survey: UiKitBannerPayload; + survey: UiKit.BannerPayload; createdAt: Date; startAt: Date; expireAt: Date; diff --git a/packages/core-typings/src/IBanner.ts b/packages/core-typings/src/IBanner.ts index 29867cdfb6c8..da40f3b0fad2 100644 --- a/packages/core-typings/src/IBanner.ts +++ b/packages/core-typings/src/IBanner.ts @@ -1,6 +1,6 @@ import type { IRocketChatRecord } from './IRocketChatRecord'; import type { IUser } from './IUser'; -import type { UiKitBannerPayload } from './UIKit'; +import type * as UiKit from './uikit'; export enum BannerPlatform { Web = 'web', @@ -13,7 +13,7 @@ export interface IBanner extends IRocketChatRecord { roles?: string[]; // only show the banner to this roles createdBy: Pick; createdAt: Date; - view: UiKitBannerPayload; + view: UiKit.BannerPayload; active?: boolean; inactivedAt?: Date; snapshot?: string; diff --git a/packages/core-typings/src/cloud/Announcement.ts b/packages/core-typings/src/cloud/Announcement.ts index acfbca9e78cc..22cb3cac2d41 100644 --- a/packages/core-typings/src/cloud/Announcement.ts +++ b/packages/core-typings/src/cloud/Announcement.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/naming-convention */ import type { IRocketChatRecord } from '../IRocketChatRecord'; -import type { UiKitPayload } from '../UIKit'; +import type * as UiKit from '../uikit'; import type { TargetPlatform } from './TargetPlatform'; type Dictionary = { @@ -22,6 +22,6 @@ export interface Announcement extends IRocketChatRecord { createdBy: Creator; createdAt: Date; dictionary?: Dictionary; - view: UiKitPayload; + view: UiKit.Payload; surface: 'banner' | 'modal'; } diff --git a/packages/core-typings/src/index.ts b/packages/core-typings/src/index.ts index cd1267f743e7..da812702d473 100644 --- a/packages/core-typings/src/index.ts +++ b/packages/core-typings/src/index.ts @@ -4,7 +4,6 @@ export * from './FeaturedApps'; export * from './AppRequests'; export * from './MarketplaceRest'; export * from './IRoom'; -export * from './UIKit'; export * from './IMessage'; export * from './federation'; export * from './Serialized'; @@ -137,3 +136,5 @@ export * from './IModerationReport'; export * from './CustomFieldMetadata'; export * as Cloud from './cloud'; +export * as UiKit from './uikit'; +export * from './uikit/deprecations'; diff --git a/packages/core-typings/src/uikit/BannerPayload.ts b/packages/core-typings/src/uikit/BannerPayload.ts new file mode 100644 index 000000000000..63c37176449a --- /dev/null +++ b/packages/core-typings/src/uikit/BannerPayload.ts @@ -0,0 +1,12 @@ +import type { Keys as IconName } from '@rocket.chat/icons'; +import type { BannerSurfaceLayout } from '@rocket.chat/ui-kit'; + +import type { Payload } from './Payload'; + +export type BannerPayload = { + inline?: boolean; + variant?: 'neutral' | 'info' | 'success' | 'warning' | 'danger'; + icon?: IconName; + title?: string; // TODO: change to plain_text block in the future + blocks: BannerSurfaceLayout; +} & Payload; diff --git a/packages/core-typings/src/uikit/Payload.ts b/packages/core-typings/src/uikit/Payload.ts new file mode 100644 index 000000000000..48b3b4e4bd01 --- /dev/null +++ b/packages/core-typings/src/uikit/Payload.ts @@ -0,0 +1,7 @@ +import type { LayoutBlock } from '@rocket.chat/ui-kit'; + +export type Payload = { + viewId: string; + appId: string; + blocks: LayoutBlock[]; +}; diff --git a/packages/core-typings/src/UIKit.ts b/packages/core-typings/src/uikit/deprecations.ts similarity index 85% rename from packages/core-typings/src/UIKit.ts rename to packages/core-typings/src/uikit/deprecations.ts index 19cf46f82b92..f2c9df775f16 100644 --- a/packages/core-typings/src/UIKit.ts +++ b/packages/core-typings/src/uikit/deprecations.ts @@ -7,6 +7,9 @@ import type { IInputBlock, } from '@rocket.chat/apps-engine/definition/uikit/blocks/Blocks'; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import type * as UiKit from '.'; + enum UIKitInteractionTypeExtended { BANNER_OPEN = 'banner.open', BANNER_UPDATE = 'banner.update', @@ -20,27 +23,17 @@ export const UIKitInteractionTypes = { ...UIKitInteractionTypeExtended, }; +/** @deprecated use {@link UiKit.Payload} instead */ export type UiKitPayload = { viewId: string; appId: string; blocks: (IDividerBlock | ISectionBlock | IActionsBlock | IContextBlock | IInputBlock)[]; }; -export type UiKitBannerPayload = UiKitPayload & { - inline?: boolean; - variant?: 'neutral' | 'info' | 'success' | 'warning' | 'danger'; - icon?: string; - title?: string; -}; - export type UIKitUserInteraction = { type: UIKitInteractionType; } & UiKitPayload; -export type UiKitBannerProps = { - payload: UiKitBannerPayload; -}; - export type UIKitUserInteractionResult = UIKitUserInteractionResultError | UIKitUserInteraction; type UIKitUserInteractionResultError = UIKitUserInteraction & { diff --git a/packages/core-typings/src/uikit/index.ts b/packages/core-typings/src/uikit/index.ts new file mode 100644 index 000000000000..420e80ea02c3 --- /dev/null +++ b/packages/core-typings/src/uikit/index.ts @@ -0,0 +1,2 @@ +export { Payload } from './Payload'; +export { BannerPayload } from './BannerPayload'; From 0f383a3dbdd2c2957fa6acff0e32f619fca6ee2f Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Thu, 28 Sep 2023 04:23:55 -0300 Subject: [PATCH 08/11] Rename a bunch of types --- .../app/ui-message/client/ActionManager.js | 37 ++++++++++++++----- .../UIKit/hooks/useUIKitHandleAction.tsx | 2 +- .../UIKit/hooks/useUIKitHandleClose.tsx | 5 +-- .../UIKit/hooks/useUIKitStateManager.tsx | 8 ++-- apps/meteor/client/lib/banners.ts | 4 +- .../client/views/banners/BannerRegion.tsx | 2 +- .../client/views/banners/UiKitBanner.tsx | 6 +-- .../hooks/useCloudAnnouncementBanners.ts | 2 +- .../views/banners/hooks/useRemoteBanners.ts | 2 +- .../hooks/useCloudAnnouncementModals.tsx | 16 ++------ .../client/views/modal/uikit/UiKitModal.tsx | 28 ++++++-------- .../services/nps/getAndCreateNpsSurvey.ts | 2 +- 12 files changed, 57 insertions(+), 57 deletions(-) diff --git a/apps/meteor/app/ui-message/client/ActionManager.js b/apps/meteor/app/ui-message/client/ActionManager.js index ebe9d1aed093..220b786c6540 100644 --- a/apps/meteor/app/ui-message/client/ActionManager.js +++ b/apps/meteor/app/ui-message/client/ActionManager.js @@ -75,9 +75,7 @@ export const handlePayloadUserInteraction = (type, { /* appId,*/ triggerId, ...d return UIKitInteractionTypes.ERRORS; } - if ( - [UIKitInteractionTypes.BANNER_UPDATE, UIKitInteractionTypes.MODAL_UPDATE, UIKitInteractionTypes.CONTEXTUAL_BAR_UPDATE].includes(type) - ) { + if ([UIKitInteractionTypes.BANNER_UPDATE, UIKitInteractionTypes.CONTEXTUAL_BAR_UPDATE].includes(type)) { events.emit(viewId, { type, triggerId, @@ -88,14 +86,19 @@ export const handlePayloadUserInteraction = (type, { /* appId,*/ triggerId, ...d return type; } - if ([UIKitInteractionTypes.MODAL_OPEN].includes(type)) { + if (type === UIKitInteractionTypes.MODAL_OPEN) { const instance = imperativeModal.open({ component: UiKitModal, props: { - triggerId, - viewId, - appId, - ...data, + view: { + viewId: data.view.id, + appId: data.view.appId, + blocks: data.view.blocks, + title: data.view.title, + close: data.view.close, + showIcon: data.view.showIcon, + submit: data.view.submit, + }, }, }); @@ -106,7 +109,23 @@ export const handlePayloadUserInteraction = (type, { /* appId,*/ triggerId, ...d }, }); - return UIKitInteractionTypes.MODAL_OPEN; + return type; + } + + if (type === UIKitInteractionTypes.MODAL_UPDATE) { + events.emit(viewId, { + type, + triggerId, + viewId: data.view.id, + appId: data.view.appId, + blocks: data.view.blocks, + title: data.view.title, + close: data.view.close, + showIcon: data.view.showIcon, + submit: data.view.submit, + }); + + return type; } if ([UIKitInteractionTypes.CONTEXTUAL_BAR_OPEN].includes(type)) { diff --git a/apps/meteor/client/UIKit/hooks/useUIKitHandleAction.tsx b/apps/meteor/client/UIKit/hooks/useUIKitHandleAction.tsx index c5a265203c8a..d2c3493f4695 100644 --- a/apps/meteor/client/UIKit/hooks/useUIKitHandleAction.tsx +++ b/apps/meteor/client/UIKit/hooks/useUIKitHandleAction.tsx @@ -4,7 +4,7 @@ import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useUiKitActionManager } from '../../hooks/useUiKitActionManager'; -const useUIKitHandleAction = (state: S): ((event: UIKitActionEvent) => Promise) => { +const useUIKitHandleAction = (state: S): ((event: UIKitActionEvent) => Promise) => { const actionManager = useUiKitActionManager(); return useMutableCallback(async ({ blockId, value, appId, actionId }) => { if (!appId) { diff --git a/apps/meteor/client/UIKit/hooks/useUIKitHandleClose.tsx b/apps/meteor/client/UIKit/hooks/useUIKitHandleClose.tsx index 561852a06527..2573d2868607 100644 --- a/apps/meteor/client/UIKit/hooks/useUIKitHandleClose.tsx +++ b/apps/meteor/client/UIKit/hooks/useUIKitHandleClose.tsx @@ -8,10 +8,7 @@ import { useUiKitActionManager } from '../../hooks/useUiKitActionManager'; // eslint-disable-next-line @typescript-eslint/no-unused-vars const emptyFn = (_error: any, _result: UIKitInteractionType | void): void => undefined; -const useUIKitHandleClose = ( - state: S, - fn = emptyFn, -): (() => Promise) => { +const useUIKitHandleClose = (state: S, fn = emptyFn): (() => Promise) => { const actionManager = useUiKitActionManager(); const dispatchToastMessage = useToastMessageDispatch(); return useMutableCallback(() => diff --git a/apps/meteor/client/UIKit/hooks/useUIKitStateManager.tsx b/apps/meteor/client/UIKit/hooks/useUIKitStateManager.tsx index 738b337e3c91..7c582800f40d 100644 --- a/apps/meteor/client/UIKit/hooks/useUIKitStateManager.tsx +++ b/apps/meteor/client/UIKit/hooks/useUIKitStateManager.tsx @@ -5,7 +5,9 @@ import { useEffect, useState } from 'react'; import { useUiKitActionManager } from '../../hooks/useUiKitActionManager'; -const useUIKitStateManager = (initialState: S): S => { +export function useUIKitStateManager(initialState: UiKit.ModalView): any; +export function useUIKitStateManager(initialState: S): S; +export function useUIKitStateManager(initialState: S): S { const actionManager = useUiKitActionManager(); const [state, setState] = useSafely(useState(initialState)); @@ -31,6 +33,4 @@ const useUIKitStateManager = (initialSta }, [actionManager, setState, viewId]); return state; -}; - -export { useUIKitStateManager }; +} diff --git a/apps/meteor/client/lib/banners.ts b/apps/meteor/client/lib/banners.ts index df38e7f16dcd..91185450a21a 100644 --- a/apps/meteor/client/lib/banners.ts +++ b/apps/meteor/client/lib/banners.ts @@ -15,7 +15,7 @@ export type LegacyBannerPayload = { onClose?: () => Promise | void; }; -type BannerPayload = LegacyBannerPayload | UiKit.BannerPayload; +type BannerPayload = LegacyBannerPayload | UiKit.BannerView; export const isLegacyPayload = (payload: BannerPayload): payload is LegacyBannerPayload => !('blocks' in payload); @@ -35,7 +35,7 @@ export const open = (payload: BannerPayload): void => { if (isLegacyPayload(_payload)) { return _payload.id === (payload as LegacyBannerPayload).id; } - return _payload.viewId === (payload as UiKit.BannerPayload).viewId; + return _payload.viewId === (payload as UiKit.BannerView).viewId; }); if (index === -1) { diff --git a/apps/meteor/client/views/banners/BannerRegion.tsx b/apps/meteor/client/views/banners/BannerRegion.tsx index f457d2b38e8f..91a1737f1ca8 100644 --- a/apps/meteor/client/views/banners/BannerRegion.tsx +++ b/apps/meteor/client/views/banners/BannerRegion.tsx @@ -24,7 +24,7 @@ const BannerRegion = (): ReactElement | null => { return ; } - return ; + return ; }; export default BannerRegion; diff --git a/apps/meteor/client/views/banners/UiKitBanner.tsx b/apps/meteor/client/views/banners/UiKitBanner.tsx index 8e306c98e633..4c54ac0abb8a 100644 --- a/apps/meteor/client/views/banners/UiKitBanner.tsx +++ b/apps/meteor/client/views/banners/UiKitBanner.tsx @@ -16,11 +16,11 @@ import * as banners from '../../lib/banners'; bannerParser.mrkdwn = ({ text }): ReactElement => ; type UiKitBannerProps = { - payload: UiKit.BannerPayload; + view: UiKit.BannerView; }; -const UiKitBanner = ({ payload }: UiKitBannerProps) => { - const state = useUIKitStateManager(payload); +const UiKitBanner = ({ view }: UiKitBannerProps) => { + const state = useUIKitStateManager(view); const icon = useMemo(() => { if (state.icon) { diff --git a/apps/meteor/client/views/banners/hooks/useCloudAnnouncementBanners.ts b/apps/meteor/client/views/banners/hooks/useCloudAnnouncementBanners.ts index 0fc675284b61..9828766a47ef 100644 --- a/apps/meteor/client/views/banners/hooks/useCloudAnnouncementBanners.ts +++ b/apps/meteor/client/views/banners/hooks/useCloudAnnouncementBanners.ts @@ -6,7 +6,7 @@ import * as banners from '../../../lib/banners'; const isBannerCarryingAnnouncement = ( announcement: Serialized, -): announcement is Serialized => announcement.surface === 'banner'; +): announcement is Serialized => announcement.surface === 'banner'; export const useCloudAnnouncementBanners = () => { const queryResult = useCloudAnnouncementsQuery({ diff --git a/apps/meteor/client/views/banners/hooks/useRemoteBanners.ts b/apps/meteor/client/views/banners/hooks/useRemoteBanners.ts index ddd2b5ab651a..ebed89e06037 100644 --- a/apps/meteor/client/views/banners/hooks/useRemoteBanners.ts +++ b/apps/meteor/client/views/banners/hooks/useRemoteBanners.ts @@ -22,7 +22,7 @@ export const useRemoteBanners = () => { const { signal } = controller; - const mapBanner = (banner: Serialized): UiKit.BannerPayload => ({ + const mapBanner = (banner: Serialized): UiKit.BannerView => ({ ...banner.view, viewId: banner.view.viewId || banner._id, }); diff --git a/apps/meteor/client/views/modal/hooks/useCloudAnnouncementModals.tsx b/apps/meteor/client/views/modal/hooks/useCloudAnnouncementModals.tsx index 1177c14e08a0..cb0247b95d0c 100644 --- a/apps/meteor/client/views/modal/hooks/useCloudAnnouncementModals.tsx +++ b/apps/meteor/client/views/modal/hooks/useCloudAnnouncementModals.tsx @@ -1,5 +1,4 @@ -import type { IUIKitSurface } from '@rocket.chat/apps-engine/definition/uikit'; -import type { Cloud, Serialized } from '@rocket.chat/core-typings'; +import type { Cloud, Serialized, UiKit } from '@rocket.chat/core-typings'; import { useSetModal } from '@rocket.chat/ui-contexts'; import React, { useEffect } from 'react'; @@ -8,7 +7,7 @@ import UiKitModal from '../uikit/UiKitModal'; const isModalCarryingAnnouncement = ( announcement: Serialized, -): announcement is Serialized & { surface: 'modal'; view: IUIKitSurface } => announcement.surface === 'modal'; +): announcement is Serialized => announcement.surface === 'modal'; export const useCloudAnnouncementModals = () => { const queryResult = useCloudAnnouncementsQuery({ @@ -23,16 +22,7 @@ export const useCloudAnnouncementModals = () => { } for (const announcement of queryResult.data) { - setModal( - , - ); + setModal(); } }, [queryResult.data, queryResult.isSuccess, setModal]); }; diff --git a/apps/meteor/client/views/modal/uikit/UiKitModal.tsx b/apps/meteor/client/views/modal/uikit/UiKitModal.tsx index 5baad8543769..5319f85bb1a5 100644 --- a/apps/meteor/client/views/modal/uikit/UiKitModal.tsx +++ b/apps/meteor/client/views/modal/uikit/UiKitModal.tsx @@ -1,9 +1,8 @@ -import type { IUIKitSurface } from '@rocket.chat/apps-engine/definition/uikit'; import { UIKitIncomingInteractionContainerType } from '@rocket.chat/apps-engine/definition/uikit/UIKitIncomingInteractionContainer'; +import type { UiKit } from '@rocket.chat/core-typings'; import { useDebouncedCallback, useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { UiKitContext } from '@rocket.chat/fuselage-ui-kit'; import { MarkupInteractionContext } from '@rocket.chat/gazzodown'; -import type { LayoutBlock } from '@rocket.chat/ui-kit'; import type { ContextType, ReactEventHandler } from 'react'; import React from 'react'; @@ -14,21 +13,16 @@ import ModalBlock from './ModalBlock'; import { useValues } from './hooks/useValues'; type UiKitModalProps = { - viewId: string; - type: 'errors' | string; - appId: string; - mid: string; - errors: Record; - view: IUIKitSurface; + view: UiKit.ModalView; }; -const UiKitModal = (props: UiKitModalProps) => { +const UiKitModal = ({ view }: UiKitModalProps) => { const actionManager = useUiKitActionManager(); - const state = useUIKitStateManager(props as any); + const state = useUIKitStateManager(view); - const { appId, viewId, mid: _mid, errors, view } = state; + const { appId, errors, viewId, blocks } = state; - const [values, updateValues] = useValues(view.blocks as LayoutBlock[]); + const [values, updateValues] = useValues(blocks); const groupStateByBlockId = (values: { value: unknown; blockId: string }[]) => Object.entries(values).reduce((obj, [key, { blockId, value }]) => { @@ -63,7 +57,7 @@ const UiKitModal = (props: UiKitModalProps) => { // TODO: this structure is atrociously wrong; we should revisit this const context: ContextType = { // @ts-expect-error Property 'mid' does not exist on type 'ActionParams'. - action: ({ actionId, appId, value, blockId, mid = _mid, dispatchActionConfig }) => { + action: ({ actionId, appId, value, blockId, mid, dispatchActionConfig }) => { if (Array.isArray(dispatchActionConfig) && dispatchActionConfig.includes('on_character_entered')) { debouncedBlockAction(actionId, appId, value, blockId, mid); } else { @@ -101,7 +95,7 @@ const UiKitModal = (props: UiKitModalProps) => { appId, payload: { view: { - ...view, + ...state, id: viewId, state: groupStateByBlockId(values), }, @@ -115,7 +109,7 @@ const UiKitModal = (props: UiKitModalProps) => { viewId, appId, view: { - ...view, + ...state, id: viewId, state: groupStateByBlockId(values), }, @@ -127,7 +121,7 @@ const UiKitModal = (props: UiKitModalProps) => { viewId, appId, view: { - ...view, + ...state, id: viewId, state: groupStateByBlockId(values), }, @@ -142,7 +136,7 @@ const UiKitModal = (props: UiKitModalProps) => { detectEmoji, }} > - + ); diff --git a/apps/meteor/server/services/nps/getAndCreateNpsSurvey.ts b/apps/meteor/server/services/nps/getAndCreateNpsSurvey.ts index dce5f7eac4ad..8e4c06941c81 100644 --- a/apps/meteor/server/services/nps/getAndCreateNpsSurvey.ts +++ b/apps/meteor/server/services/nps/getAndCreateNpsSurvey.ts @@ -10,7 +10,7 @@ type NpsSurveyData = { id: string; platform: BannerPlatform[]; roles: string[]; - survey: UiKit.BannerPayload; + survey: UiKit.BannerView; createdAt: Date; startAt: Date; expireAt: Date; From 644426c32dd352b66226362c3d9e94c6210b7688 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Fri, 29 Sep 2023 01:03:31 -0300 Subject: [PATCH 09/11] Apply TSDoc on `ActionManager` --- .../app/ui-message/client/ActionManager.js | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/apps/meteor/app/ui-message/client/ActionManager.js b/apps/meteor/app/ui-message/client/ActionManager.js index 220b786c6540..34a7738a0e79 100644 --- a/apps/meteor/app/ui-message/client/ActionManager.js +++ b/apps/meteor/app/ui-message/client/ActionManager.js @@ -13,14 +13,18 @@ import { t } from '../../utils/lib/i18n'; const UiKitModal = lazy(() => import('../../../client/views/modal/uikit/UiKitModal')); +/** @typedef {Exclude, undefined>} ActionManager */ + export const events = new Emitter(); -export const on = (...args) => { - events.on(...args); +/** @type {ActionManager['on']} */ +export const on = (viewId, listener) => { + events.on(viewId, listener); }; -export const off = (...args) => { - events.off(...args); +/** @type {ActionManager['off']} */ +export const off = (viewId, listener) => { + events.off(viewId, listener); }; const TRIGGER_TIMEOUT = 5000; @@ -29,6 +33,7 @@ const TRIGGER_TIMEOUT_ERROR = 'TRIGGER_TIMEOUT_ERROR'; const triggersId = new Map(); +/** @type {Map void }>} */ const instances = new Map(); const invalidateTriggerId = (id) => { @@ -44,6 +49,7 @@ export const generateTriggerId = (appId) => { return triggerId; }; +/** @type {ActionManager['handlePayloadUserInteraction']} */ export const handlePayloadUserInteraction = (type, { /* appId,*/ triggerId, ...data }) => { if (!triggersId.has(triggerId)) { return; @@ -154,13 +160,14 @@ export const handlePayloadUserInteraction = (type, { /* appId,*/ triggerId, ...d return UIKitInteractionTypes.CONTEXTUAL_BAR_OPEN; } - if ([UIKitInteractionTypes.BANNER_OPEN].includes(type)) { + if (type === UIKitInteractionTypes.BANNER_OPEN) { banners.open(data); instances.set(viewId, { close() { banners.closeById(viewId); }, }); + return UIKitInteractionTypes.BANNER_OPEN; } @@ -185,6 +192,7 @@ export const handlePayloadUserInteraction = (type, { /* appId,*/ triggerId, ...d return UIKitInteractionTypes.MODAL_ClOSE; }; +/** @type {ActionManager['triggerAction']} */ export const triggerAction = async ({ type, actionId, appId, rid, mid, viewId, container, tmid, ...rest }) => new Promise(async (resolve, reject) => { events.emit('busy', { busy: true }); @@ -219,8 +227,10 @@ export const triggerAction = async ({ type, actionId, appId, rid, mid, viewId, c return resolve(handlePayloadUserInteraction(interactionType, data)); }); +/** @type {ActionManager['triggerBlockAction']} */ export const triggerBlockAction = (options) => triggerAction({ type: UIKitIncomingInteractionType.BLOCK, ...options }); +/** @type {ActionManager['triggerActionButtonAction']} */ export const triggerActionButtonAction = (options) => triggerAction({ type: UIKitIncomingInteractionType.ACTION_BUTTON, ...options }).catch(async (reason) => { if (Array.isArray(reason) && reason[0] === TRIGGER_TIMEOUT_ERROR) { @@ -231,6 +241,7 @@ export const triggerActionButtonAction = (options) => } }); +/** @type {ActionManager['triggerSubmitView']} */ export const triggerSubmitView = async ({ viewId, ...options }) => { const close = () => { const instance = instances.get(viewId); @@ -254,6 +265,7 @@ export const triggerSubmitView = async ({ viewId, ...options }) => { } }; +/** @type {ActionManager['triggerCancel']} */ export const triggerCancel = async ({ view, ...options }) => { const instance = instances.get(view.id); try { @@ -265,6 +277,7 @@ export const triggerCancel = async ({ view, ...options }) => { } }; +/** @type {ActionManager['getUserInteractionPayloadByViewId']} */ export const getUserInteractionPayloadByViewId = (viewId) => { if (!viewId) { throw new Error('No viewId provided when checking for `user interaction payload`'); From bf2fc562b42d6cbd59b0531388bdaaf530df5bd7 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Fri, 29 Sep 2023 01:05:24 -0300 Subject: [PATCH 10/11] Review types related to view state management --- .../app/ui-message/client/ActionManager.js | 3 +- .../UIKit/hooks/useUIKitHandleAction.tsx | 26 --- .../UIKit/hooks/useUIKitStateManager.tsx | 36 --- .../client/UIKit/hooks/useUiKitView.tsx | 47 ++++ .../message/uikit/UiKitMessageBlock.tsx | 22 +- .../variants/room/RoomMessageContent.tsx | 2 +- .../variants/thread/ThreadMessageContent.tsx | 2 +- .../moderation/helpers/ContextMessage.tsx | 2 +- .../client/views/banners/BannerRegion.tsx | 2 +- .../client/views/banners/UiKitBanner.tsx | 55 +++-- .../banners}/hooks/useUIKitHandleClose.tsx | 2 +- .../hooks/useCloudAnnouncementModals.tsx | 2 +- .../client/views/modal/uikit/ModalBlock.tsx | 10 +- .../client/views/modal/uikit/UiKitModal.tsx | 113 ++++----- .../views/modal/uikit/getButtonStyle.ts | 7 +- .../MessageList/ContactHistoryMessage.tsx | 2 +- apps/meteor/client/views/room/Room.tsx | 11 +- .../uikit/UiKitContextualBar.tsx | 220 +++++------------- .../contextualBar/uikit/hooks/useValues.tsx | 64 +++++ .../views/room/hooks/useAppsContextualBar.ts | 46 +--- packages/core-typings/src/IBanner.ts | 2 +- .../core-typings/src/cloud/Announcement.ts | 2 +- .../uikit/{BannerPayload.ts => BannerView.ts} | 9 +- .../src/uikit/ContextualBarView.ts | 13 ++ packages/core-typings/src/uikit/ModalView.ts | 14 ++ .../core-typings/src/uikit/UserInteraction.ts | 1 + .../src/uikit/{Payload.ts => View.ts} | 5 +- .../core-typings/src/uikit/deprecations.ts | 2 +- packages/core-typings/src/uikit/index.ts | 7 +- .../ui-contexts/src/ActionManagerContext.ts | 66 +++++- 30 files changed, 392 insertions(+), 403 deletions(-) delete mode 100644 apps/meteor/client/UIKit/hooks/useUIKitHandleAction.tsx delete mode 100644 apps/meteor/client/UIKit/hooks/useUIKitStateManager.tsx create mode 100644 apps/meteor/client/UIKit/hooks/useUiKitView.tsx rename apps/meteor/client/{UIKit => views/banners}/hooks/useUIKitHandleClose.tsx (93%) create mode 100644 apps/meteor/client/views/room/contextualBar/uikit/hooks/useValues.tsx rename packages/core-typings/src/uikit/{BannerPayload.ts => BannerView.ts} (72%) create mode 100644 packages/core-typings/src/uikit/ContextualBarView.ts create mode 100644 packages/core-typings/src/uikit/ModalView.ts create mode 100644 packages/core-typings/src/uikit/UserInteraction.ts rename packages/core-typings/src/uikit/{Payload.ts => View.ts} (59%) diff --git a/apps/meteor/app/ui-message/client/ActionManager.js b/apps/meteor/app/ui-message/client/ActionManager.js index 34a7738a0e79..e37ac63517f0 100644 --- a/apps/meteor/app/ui-message/client/ActionManager.js +++ b/apps/meteor/app/ui-message/client/ActionManager.js @@ -96,7 +96,8 @@ export const handlePayloadUserInteraction = (type, { /* appId,*/ triggerId, ...d const instance = imperativeModal.open({ component: UiKitModal, props: { - view: { + key: data.view.id, + initialView: { viewId: data.view.id, appId: data.view.appId, blocks: data.view.blocks, diff --git a/apps/meteor/client/UIKit/hooks/useUIKitHandleAction.tsx b/apps/meteor/client/UIKit/hooks/useUIKitHandleAction.tsx deleted file mode 100644 index d2c3493f4695..000000000000 --- a/apps/meteor/client/UIKit/hooks/useUIKitHandleAction.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { UIKitIncomingInteractionContainerType } from '@rocket.chat/apps-engine/definition/uikit/UIKitIncomingInteractionContainer'; -import type { UiKitPayload, UIKitActionEvent, UiKit } from '@rocket.chat/core-typings'; -import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; - -import { useUiKitActionManager } from '../../hooks/useUiKitActionManager'; - -const useUIKitHandleAction = (state: S): ((event: UIKitActionEvent) => Promise) => { - const actionManager = useUiKitActionManager(); - return useMutableCallback(async ({ blockId, value, appId, actionId }) => { - if (!appId) { - throw new Error('useUIKitHandleAction - invalid appId'); - } - return actionManager.triggerBlockAction({ - container: { - type: UIKitIncomingInteractionContainerType.VIEW, - id: state.viewId || state.appId, - }, - actionId, - appId, - value, - blockId, - }); - }); -}; - -export { useUIKitHandleAction }; diff --git a/apps/meteor/client/UIKit/hooks/useUIKitStateManager.tsx b/apps/meteor/client/UIKit/hooks/useUIKitStateManager.tsx deleted file mode 100644 index 7c582800f40d..000000000000 --- a/apps/meteor/client/UIKit/hooks/useUIKitStateManager.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import type { UIKitUserInteractionResult, UiKit, UiKitPayload } from '@rocket.chat/core-typings'; -import { isErrorType } from '@rocket.chat/core-typings'; -import { useSafely } from '@rocket.chat/fuselage-hooks'; -import { useEffect, useState } from 'react'; - -import { useUiKitActionManager } from '../../hooks/useUiKitActionManager'; - -export function useUIKitStateManager(initialState: UiKit.ModalView): any; -export function useUIKitStateManager(initialState: S): S; -export function useUIKitStateManager(initialState: S): S { - const actionManager = useUiKitActionManager(); - const [state, setState] = useSafely(useState(initialState)); - - const { viewId } = state; - - useEffect(() => { - const handleUpdate = ({ ...data }: UIKitUserInteractionResult): void => { - if (isErrorType(data)) { - const { errors } = data; - setState((state) => ({ ...state, errors })); - return; - } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { type, ...rest } = data; - setState(rest as any); - }; - - actionManager.on(viewId, handleUpdate); - - return (): void => { - actionManager.off(viewId, handleUpdate); - }; - }, [actionManager, setState, viewId]); - - return state; -} diff --git a/apps/meteor/client/UIKit/hooks/useUiKitView.tsx b/apps/meteor/client/UIKit/hooks/useUiKitView.tsx new file mode 100644 index 000000000000..7fe230c8e96b --- /dev/null +++ b/apps/meteor/client/UIKit/hooks/useUiKitView.tsx @@ -0,0 +1,47 @@ +import type { UIKitUserInteractionResult, UiKit, UiKitPayload } from '@rocket.chat/core-typings'; +import { isErrorType } from '@rocket.chat/core-typings'; +import { useSafely } from '@rocket.chat/fuselage-hooks'; +import { useEffect, useState } from 'react'; + +import { useUiKitActionManager } from '../../hooks/useUiKitActionManager'; + +type ViewState = { + view: TView; + errors?: { [field: string]: string }[]; +}; + +export function useUiKitView(initialView: UiKit.ModalView): ViewState; +export function useUiKitView(initialView: UiKit.ContextualBarView): ViewState; +export function useUiKitView(initialView: S): ViewState; +export function useUiKitView(initialView: S): ViewState { + const [state, setState] = useSafely(useState>({ view: initialView })); + const actionManager = useUiKitActionManager(); + + const { viewId } = state.view; + + useEffect(() => { + const handleUpdate = (data: UIKitUserInteractionResult): void => { + if (isErrorType(data)) { + const { errors } = data; + setState((state) => ({ ...state, errors })); + return; + } + + setState((state) => { + const { type, ...rest } = data; + return { + ...state, + view: { ...state.view, ...rest }, + }; + }); + }; + + actionManager.on(viewId, handleUpdate); + + return (): void => { + actionManager.off(viewId, handleUpdate); + }; + }, [actionManager, setState, viewId]); + + return state; +} diff --git a/apps/meteor/client/components/message/uikit/UiKitMessageBlock.tsx b/apps/meteor/client/components/message/uikit/UiKitMessageBlock.tsx index d59314ae8198..fbebfd380da3 100644 --- a/apps/meteor/client/components/message/uikit/UiKitMessageBlock.tsx +++ b/apps/meteor/client/components/message/uikit/UiKitMessageBlock.tsx @@ -1,4 +1,3 @@ -import { UIKitIncomingInteractionContainerType } from '@rocket.chat/apps-engine/definition/uikit/UIKitIncomingInteractionContainer'; import type { IMessage, IRoom } from '@rocket.chat/core-typings'; import { MessageBlock } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; @@ -29,13 +28,12 @@ const patchMessageParser = () => { }; type UiKitMessageBlockProps = { + rid: IRoom['_id']; mid: IMessage['_id']; blocks: MessageSurfaceLayout; - rid: IRoom['_id']; - appId?: string | boolean; // TODO: this is a hack while the context value is not properly typed }; -const UiKitMessageBlock = ({ mid: _mid, blocks, rid, appId }: UiKitMessageBlockProps): ReactElement => { +const UiKitMessageBlock = ({ rid, mid, blocks }: UiKitMessageBlockProps): ReactElement => { const joinCall = useVideoConfJoinCall(); const setPreferences = useVideoConfSetPreferences(); const isCalling = useVideoConfIsCalling(); @@ -62,8 +60,7 @@ const UiKitMessageBlock = ({ mid: _mid, blocks, rid, appId }: UiKitMessageBlockP // TODO: this structure is attrociously wrong; we should revisit this const context: ContextType = { - // @ts-ignore Property 'mid' does not exist on type 'ActionParams'. - action: ({ actionId, value, blockId, mid = _mid, appId }, event) => { + action: ({ actionId, blockId, appId }, event) => { if (appId === 'videoconf-core') { event.preventDefault(); setPreferences({ mic: true, cam: false }); @@ -77,21 +74,20 @@ const UiKitMessageBlock = ({ mid: _mid, blocks, rid, appId }: UiKitMessageBlockP } actionManager?.triggerBlockAction({ - blockId, actionId, - value, - mid, - rid, appId, container: { - type: UIKitIncomingInteractionContainerType.MESSAGE, + type: 'message', id: mid, }, + rid, + mid, }); }, - // @ts-ignore Type 'string | boolean | undefined' is not assignable to type 'string'. - appId, + appId: '', // TODO: this is a hack rid, + state: () => undefined, // TODO: this is a hack + values: {}, // TODO: this is a hack }; patchMessageParser(); // TODO: this is a hack diff --git a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx index 2b54588c6263..b22627bea8d2 100644 --- a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx +++ b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx @@ -61,7 +61,7 @@ const RoomMessageContent = ({ message, unread, all, mention, searchText }: RoomM )} {normalizedMessage.blocks && ( - + )} {!!normalizedMessage?.attachments?.length && } diff --git a/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx b/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx index 655f96639929..57835ec75e0c 100644 --- a/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx +++ b/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx @@ -49,7 +49,7 @@ const ThreadMessageContent = ({ message }: ThreadMessageContentProps): ReactElem )} {normalizedMessage.blocks && ( - + )} {normalizedMessage.attachments && } diff --git a/apps/meteor/client/views/admin/moderation/helpers/ContextMessage.tsx b/apps/meteor/client/views/admin/moderation/helpers/ContextMessage.tsx index 114cde52c1d2..500ae78a6d26 100644 --- a/apps/meteor/client/views/admin/moderation/helpers/ContextMessage.tsx +++ b/apps/meteor/client/views/admin/moderation/helpers/ContextMessage.tsx @@ -75,7 +75,7 @@ const ContextMessage = ({ ) : ( message.msg )} - {message.blocks && } + {message.blocks && } {message.attachments && } diff --git a/apps/meteor/client/views/banners/BannerRegion.tsx b/apps/meteor/client/views/banners/BannerRegion.tsx index 91a1737f1ca8..c388452d332f 100644 --- a/apps/meteor/client/views/banners/BannerRegion.tsx +++ b/apps/meteor/client/views/banners/BannerRegion.tsx @@ -24,7 +24,7 @@ const BannerRegion = (): ReactElement | null => { return ; } - return ; + return ; }; export default BannerRegion; diff --git a/apps/meteor/client/views/banners/UiKitBanner.tsx b/apps/meteor/client/views/banners/UiKitBanner.tsx index 4c54ac0abb8a..8cc7820dfa20 100644 --- a/apps/meteor/client/views/banners/UiKitBanner.tsx +++ b/apps/meteor/client/views/banners/UiKitBanner.tsx @@ -1,59 +1,66 @@ -import type { UIKitActionEvent, UiKit } from '@rocket.chat/core-typings'; +import type { UiKit } from '@rocket.chat/core-typings'; import { Banner, Icon } from '@rocket.chat/fuselage'; import { UiKitContext, bannerParser, UiKitBanner as UiKitBannerSurfaceRender, UiKitComponent } from '@rocket.chat/fuselage-ui-kit'; -import type { Keys as IconName } from '@rocket.chat/icons'; -import type { LayoutBlock } from '@rocket.chat/ui-kit'; import type { ReactElement, ContextType } from 'react'; import React, { useMemo } from 'react'; -import { useUIKitHandleAction } from '../../UIKit/hooks/useUIKitHandleAction'; -import { useUIKitHandleClose } from '../../UIKit/hooks/useUIKitHandleClose'; -import { useUIKitStateManager } from '../../UIKit/hooks/useUIKitStateManager'; +import { useUiKitView } from '../../UIKit/hooks/useUiKitView'; import MarkdownText from '../../components/MarkdownText'; +import { useUiKitActionManager } from '../../hooks/useUiKitActionManager'; import * as banners from '../../lib/banners'; +import { useUIKitHandleClose } from './hooks/useUIKitHandleClose'; // TODO: move this to fuselage-ui-kit itself bannerParser.mrkdwn = ({ text }): ReactElement => ; type UiKitBannerProps = { - view: UiKit.BannerView; + key: UiKit.BannerView['viewId']; // force re-mount when viewId changes + initialView: UiKit.BannerView; }; -const UiKitBanner = ({ view }: UiKitBannerProps) => { - const state = useUIKitStateManager(view); +const UiKitBanner = ({ initialView }: UiKitBannerProps) => { + const { view } = useUiKitView(initialView); const icon = useMemo(() => { - if (state.icon) { - return ; + if (view.icon) { + return ; } return null; - }, [state.icon]); + }, [view.icon]); - const handleClose = useUIKitHandleClose(state, () => banners.close()); + const handleClose = useUIKitHandleClose(view, () => banners.close()); - const action = useUIKitHandleAction(state); + const actionManager = useUiKitActionManager(); - const contextValue = useMemo>( - () => ({ - action: async (event): Promise => { - if (!event.viewId) { + const contextValue = useMemo( + (): ContextType => ({ + action: async ({ appId, viewId, actionId }): Promise => { + if (!appId || !viewId) { return; } - await action(event as UIKitActionEvent); - banners.closeById(state.viewId); + + await actionManager.triggerBlockAction({ + container: { + type: 'view', + id: viewId, + }, + actionId, + appId, + }); + banners.closeById(view.viewId); }, state: (): void => undefined, - appId: state.appId, + appId: view.appId, values: {}, }), - [action, state.appId, state.viewId], + [actionManager, view.appId, view.viewId], ); return ( - + - + ); diff --git a/apps/meteor/client/UIKit/hooks/useUIKitHandleClose.tsx b/apps/meteor/client/views/banners/hooks/useUIKitHandleClose.tsx similarity index 93% rename from apps/meteor/client/UIKit/hooks/useUIKitHandleClose.tsx rename to apps/meteor/client/views/banners/hooks/useUIKitHandleClose.tsx index 2573d2868607..3b8bf52fe754 100644 --- a/apps/meteor/client/UIKit/hooks/useUIKitHandleClose.tsx +++ b/apps/meteor/client/views/banners/hooks/useUIKitHandleClose.tsx @@ -3,7 +3,7 @@ import type { UiKit, UiKitPayload } from '@rocket.chat/core-typings'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useToastMessageDispatch } from '@rocket.chat/ui-contexts'; -import { useUiKitActionManager } from '../../hooks/useUiKitActionManager'; +import { useUiKitActionManager } from '../../../hooks/useUiKitActionManager'; // eslint-disable-next-line @typescript-eslint/no-unused-vars const emptyFn = (_error: any, _result: UIKitInteractionType | void): void => undefined; diff --git a/apps/meteor/client/views/modal/hooks/useCloudAnnouncementModals.tsx b/apps/meteor/client/views/modal/hooks/useCloudAnnouncementModals.tsx index cb0247b95d0c..940b93d5434a 100644 --- a/apps/meteor/client/views/modal/hooks/useCloudAnnouncementModals.tsx +++ b/apps/meteor/client/views/modal/hooks/useCloudAnnouncementModals.tsx @@ -22,7 +22,7 @@ export const useCloudAnnouncementModals = () => { } for (const announcement of queryResult.data) { - setModal(); + setModal(); } }, [queryResult.data, queryResult.isSuccess, setModal]); }; diff --git a/apps/meteor/client/views/modal/uikit/ModalBlock.tsx b/apps/meteor/client/views/modal/uikit/ModalBlock.tsx index 7993355206e7..bd0876fc49ad 100644 --- a/apps/meteor/client/views/modal/uikit/ModalBlock.tsx +++ b/apps/meteor/client/views/modal/uikit/ModalBlock.tsx @@ -1,4 +1,4 @@ -import type { IUIKitSurface } from '@rocket.chat/apps-engine/definition/uikit'; +import type { UiKit } from '@rocket.chat/core-typings'; import { Modal, AnimatedVisibility, Button, Box } from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import { UiKitComponent, UiKitModal, modalParser } from '@rocket.chat/fuselage-ui-kit'; @@ -38,7 +38,7 @@ const focusableElementsStringInvalid = ` [contenteditable]:invalid`; type ModalBlockParams = { - view: IUIKitSurface & { showIcon?: boolean }; + view: UiKit.ModalView; errors: any; appId: string; onSubmit: FormEventHandler; @@ -55,7 +55,7 @@ const KeyboardCode = new Map([ ['TAB', 9], ]); -const ModalBlock = ({ view, errors, appId, onSubmit, onClose, onCancel }: ModalBlockParams): ReactElement => { +const ModalBlock = ({ view, errors, onSubmit, onClose, onCancel }: ModalBlockParams): ReactElement => { const id = `modal_id_${useUniqueId()}`; const ref = useRef(null); @@ -165,7 +165,7 @@ const ModalBlock = ({ view, errors, appId, onSubmit, onClose, onCancel }: ModalB - {view.showIcon ? : null} + {view.showIcon ? : null} {modalParser.text(view.title, BlockContext.NONE, 0)} @@ -182,7 +182,7 @@ const ModalBlock = ({ view, errors, appId, onSubmit, onClose, onCancel }: ModalB )} {view.submit && ( - )} diff --git a/apps/meteor/client/views/modal/uikit/UiKitModal.tsx b/apps/meteor/client/views/modal/uikit/UiKitModal.tsx index 5319f85bb1a5..11adbf64a19f 100644 --- a/apps/meteor/client/views/modal/uikit/UiKitModal.tsx +++ b/apps/meteor/client/views/modal/uikit/UiKitModal.tsx @@ -1,28 +1,25 @@ -import { UIKitIncomingInteractionContainerType } from '@rocket.chat/apps-engine/definition/uikit/UIKitIncomingInteractionContainer'; import type { UiKit } from '@rocket.chat/core-typings'; import { useDebouncedCallback, useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { UiKitContext } from '@rocket.chat/fuselage-ui-kit'; import { MarkupInteractionContext } from '@rocket.chat/gazzodown'; import type { ContextType, ReactEventHandler } from 'react'; -import React from 'react'; +import React, { useMemo } from 'react'; -import { useUIKitStateManager } from '../../../UIKit/hooks/useUIKitStateManager'; +import { useUiKitView } from '../../../UIKit/hooks/useUiKitView'; import { useUiKitActionManager } from '../../../hooks/useUiKitActionManager'; import { detectEmoji } from '../../../lib/utils/detectEmoji'; import ModalBlock from './ModalBlock'; import { useValues } from './hooks/useValues'; type UiKitModalProps = { - view: UiKit.ModalView; + key: UiKit.ModalView['viewId']; // force re-mount when viewId changes + initialView: UiKit.ModalView; }; -const UiKitModal = ({ view }: UiKitModalProps) => { +const UiKitModal = ({ initialView }: UiKitModalProps) => { const actionManager = useUiKitActionManager(); - const state = useUIKitStateManager(view); - - const { appId, errors, viewId, blocks } = state; - - const [values, updateValues] = useValues(blocks); + const { view, errors } = useUiKitView(initialView); + const [values, updateValues] = useValues(view.blocks); const groupStateByBlockId = (values: { value: unknown; blockId: string }[]) => Object.entries(values).reduce((obj, [key, { blockId, value }]) => { @@ -40,63 +37,53 @@ const UiKitModal = ({ view }: UiKitModalProps) => { } }; - const debouncedBlockAction = useDebouncedCallback((actionId, appId, value, blockId, mid) => { - actionManager.triggerBlockAction({ - container: { - type: UIKitIncomingInteractionContainerType.VIEW, - id: viewId, - }, - actionId, - appId, - value, - blockId, - mid, - }); - }, 700); + const triggerBlockAction = useMemo(() => actionManager.triggerBlockAction.bind(actionManager), [actionManager]); + const debouncedTriggerBlockAction = useDebouncedCallback(triggerBlockAction, 700); // TODO: this structure is atrociously wrong; we should revisit this - const context: ContextType = { - // @ts-expect-error Property 'mid' does not exist on type 'ActionParams'. - action: ({ actionId, appId, value, blockId, mid, dispatchActionConfig }) => { - if (Array.isArray(dispatchActionConfig) && dispatchActionConfig.includes('on_character_entered')) { - debouncedBlockAction(actionId, appId, value, blockId, mid); - } else { - actionManager.triggerBlockAction({ + const contextValue = useMemo( + (): ContextType => ({ + action: async ({ actionId, viewId, appId, dispatchActionConfig }) => { + if (!appId || !viewId) { + return; + } + + const trigger = dispatchActionConfig?.includes('on_character_entered') ? debouncedTriggerBlockAction : triggerBlockAction; + + await trigger({ container: { - type: UIKitIncomingInteractionContainerType.VIEW, + type: 'view', id: viewId, }, actionId, appId, - value, - blockId, - mid, }); - } - }, - - state: ({ actionId, value, /* ,appId, */ blockId = 'default' }) => { - updateValues({ - actionId, - payload: { - blockId, - value, - }, - }); - }, - ...state, - values, - }; + }, + + state: ({ actionId, value, /* ,appId, */ blockId = 'default' }) => { + updateValues({ + actionId, + payload: { + blockId, + value, + }, + }); + }, + ...view, + values, + }), + [debouncedTriggerBlockAction, triggerBlockAction, updateValues, values, view], + ); const handleSubmit = useMutableCallback((e) => { prevent(e); actionManager.triggerSubmitView({ - viewId, - appId, + viewId: view.viewId, + appId: view.appId, payload: { view: { - ...state, - id: viewId, + ...view, + id: view.viewId, state: groupStateByBlockId(values), }, }, @@ -106,11 +93,11 @@ const UiKitModal = ({ view }: UiKitModalProps) => { const handleCancel = useMutableCallback((e) => { prevent(e); actionManager.triggerCancel({ - viewId, - appId, + viewId: view.viewId, + appId: view.appId, view: { - ...state, - id: viewId, + ...view, + id: view.viewId, state: groupStateByBlockId(values), }, }); @@ -118,11 +105,11 @@ const UiKitModal = ({ view }: UiKitModalProps) => { const handleClose = useMutableCallback(() => { actionManager.triggerCancel({ - viewId, - appId, + viewId: view.viewId, + appId: view.appId, view: { - ...state, - id: viewId, + ...view, + id: view.viewId, state: groupStateByBlockId(values), }, isCleared: true, @@ -130,13 +117,13 @@ const UiKitModal = ({ view }: UiKitModalProps) => { }); return ( - + - + ); diff --git a/apps/meteor/client/views/modal/uikit/getButtonStyle.ts b/apps/meteor/client/views/modal/uikit/getButtonStyle.ts index 4a78cb5e250a..54e05b183fd6 100644 --- a/apps/meteor/client/views/modal/uikit/getButtonStyle.ts +++ b/apps/meteor/client/views/modal/uikit/getButtonStyle.ts @@ -1,6 +1,7 @@ -import type { IUIKitSurface } from '@rocket.chat/apps-engine/definition/uikit'; +import type { IButtonElement } from '@rocket.chat/apps-engine/definition/uikit'; +import type { ButtonElement } from '@rocket.chat/ui-kit'; // TODO: Move to fuselage-ui-kit -export const getButtonStyle = (view: IUIKitSurface): { danger: boolean } | { primary: boolean } => { - return view.submit?.style === 'danger' ? { danger: true } : { primary: true }; +export const getButtonStyle = (buttonElement: ButtonElement | IButtonElement): { danger: boolean } | { primary: boolean } => { + return buttonElement?.style === 'danger' ? { danger: true } : { primary: true }; }; diff --git a/apps/meteor/client/views/omnichannel/contactHistory/MessageList/ContactHistoryMessage.tsx b/apps/meteor/client/views/omnichannel/contactHistory/MessageList/ContactHistoryMessage.tsx index 12342a6258a3..dab42e58e300 100644 --- a/apps/meteor/client/views/omnichannel/contactHistory/MessageList/ContactHistoryMessage.tsx +++ b/apps/meteor/client/views/omnichannel/contactHistory/MessageList/ContactHistoryMessage.tsx @@ -105,7 +105,7 @@ const ContactHistoryMessage: FC<{ )} - {message.blocks && } + {message.blocks && } {message.attachments && } diff --git a/apps/meteor/client/views/room/Room.tsx b/apps/meteor/client/views/room/Room.tsx index d53254647483..4f5355426038 100644 --- a/apps/meteor/client/views/room/Room.tsx +++ b/apps/meteor/client/views/room/Room.tsx @@ -23,7 +23,7 @@ const Room = (): ReactElement => { const toolbox = useRoomToolbox(); - const appsContextualBarContext = useAppsContextualBar(); + const contextualBarView = useAppsContextualBar(); return ( @@ -41,16 +41,11 @@ const Room = (): ReactElement => { )) || - (appsContextualBarContext && ( + (contextualBarView && ( }> - + diff --git a/apps/meteor/client/views/room/contextualBar/uikit/UiKitContextualBar.tsx b/apps/meteor/client/views/room/contextualBar/uikit/UiKitContextualBar.tsx index 6f3b803ebf84..bbc922f050e0 100644 --- a/apps/meteor/client/views/room/contextualBar/uikit/UiKitContextualBar.tsx +++ b/apps/meteor/client/views/room/contextualBar/uikit/UiKitContextualBar.tsx @@ -1,15 +1,4 @@ -import type { - IUIKitContextualBarInteraction, - IUIKitErrorInteraction, - IUIKitSurface, - IInputElement, - IInputBlock, - IBlock, - IBlockElement, - IActionsBlock, -} from '@rocket.chat/apps-engine/definition/uikit'; -import { InputElementDispatchAction } from '@rocket.chat/apps-engine/definition/uikit'; -import { UIKitIncomingInteractionContainerType } from '@rocket.chat/apps-engine/definition/uikit/UIKitIncomingInteractionContainer'; +import type { UiKit } from '@rocket.chat/core-typings'; import { Avatar, Box, Button, ButtonGroup, ContextualbarFooter, ContextualbarHeader, ContextualbarTitle } from '@rocket.chat/fuselage'; import { useDebouncedCallback, useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { @@ -18,122 +7,31 @@ import { contextualBarParser, UiKitContext, } from '@rocket.chat/fuselage-ui-kit'; -import type { LayoutBlock } from '@rocket.chat/ui-kit'; -import { BlockContext, type Block } from '@rocket.chat/ui-kit'; -import type { Dispatch, SyntheticEvent, ContextType } from 'react'; -import React, { memo, useState, useEffect, useReducer } from 'react'; +import { BlockContext } from '@rocket.chat/ui-kit'; +import type { SyntheticEvent, ContextType } from 'react'; +import React, { memo, useMemo } from 'react'; import { getURL } from '../../../../../app/utils/client'; +import { useUiKitView } from '../../../../UIKit/hooks/useUiKitView'; import { ContextualbarClose, ContextualbarScrollableContent } from '../../../../components/Contextualbar'; import { useUiKitActionManager } from '../../../../hooks/useUiKitActionManager'; import { getButtonStyle } from '../../../modal/uikit/getButtonStyle'; import { useRoomToolbox } from '../../contexts/RoomToolboxContext'; +import type { InputFieldStateByBlockId, InputFieldStateObject, InputFieldStateTuple } from './hooks/useValues'; +import { useValues } from './hooks/useValues'; -type FieldStateValue = string | Array | undefined; -type FieldState = { value: FieldStateValue; blockId: string }; -type InputFieldStateTuple = [string, FieldState]; -type InputFieldStateObject = { [key: string]: FieldState }; -type InputFieldStateByBlockId = { [blockId: string]: { [actionId: string]: FieldStateValue } }; -type ActionParams = { - blockId: string; - appId: string; - actionId: string; - value: unknown; - viewId?: string; - dispatchActionConfig?: InputElementDispatchAction[]; +type UiKitContextualBarProps = { + key: UiKit.ContextualBarView['viewId']; // force re-mount when viewId changes + initialView: UiKit.ContextualBarView; }; -type ViewState = IUIKitContextualBarInteraction & { - errors?: { [field: string]: string }; -}; - -const isInputBlock = (block: any): block is IInputBlock => block?.element?.initialValue; - -const useValues = (view: IUIKitSurface): [any, Dispatch] => { - const reducer = useMutableCallback((values, { actionId, payload }) => ({ - ...values, - [actionId]: payload, - })); - - const initializer = useMutableCallback(() => { - const filterInputFields = (block: IBlock | Block): boolean => { - if (isInputBlock(block)) { - return true; - } - - if ( - ((block as IActionsBlock).elements as IInputElement[])?.filter((element) => filterInputFields({ element } as IInputBlock)).length - ) { - return true; - } - - return false; - }; - - const mapElementToState = (block: IBlock | Block): InputFieldStateTuple | InputFieldStateTuple[] => { - if (isInputBlock(block)) { - const { element, blockId } = block; - return [element.actionId, { value: element.initialValue, blockId } as FieldState]; - } - - const { elements, blockId }: { elements: IBlockElement[]; blockId?: string } = block as IActionsBlock; - - return elements - .filter((element) => filterInputFields({ element } as IInputBlock)) - .map((element) => mapElementToState({ element, blockId } as IInputBlock)) as InputFieldStateTuple[]; - }; - - return view.blocks - .filter(filterInputFields) - .map(mapElementToState) - .reduce((obj: InputFieldStateObject, el: InputFieldStateTuple | InputFieldStateTuple[]) => { - if (Array.isArray(el[0])) { - return { ...obj, ...Object.fromEntries(el as InputFieldStateTuple[]) }; - } - - const [key, value] = el as InputFieldStateTuple; - return { ...obj, [key]: value }; - }, {} as InputFieldStateObject); - }); - - return useReducer(reducer, null, initializer); -}; - -const UiKitContextualBar = ({ - viewId, - roomId, - payload, - appId, -}: { - viewId: string; - roomId: string; - payload: IUIKitContextualBarInteraction; - appId: string; -}): JSX.Element => { - const actionManager = useUiKitActionManager(); +const UiKitContextualBar = ({ initialView }: UiKitContextualBarProps): JSX.Element => { const { closeTab } = useRoomToolbox(); + const actionManager = useUiKitActionManager(); - const [state, setState] = useState(payload); - const { view } = state; - const [values, updateValues] = useValues(view); - - useEffect(() => { - const handleUpdate = ({ type, ...data }: IUIKitContextualBarInteraction | IUIKitErrorInteraction): void => { - if (type === 'errors') { - const { errors } = data as Omit; - setState((state: ViewState) => ({ ...state, errors })); - return; - } - - setState(data as IUIKitContextualBarInteraction); - }; - - actionManager.on(viewId, handleUpdate); + const { view } = useUiKitView(initialView); - return (): void => { - actionManager.off(viewId, handleUpdate); - }; - }, [actionManager, state, viewId]); + const [values, updateValues] = useValues(view.blocks); const groupStateByBlockId = (obj: InputFieldStateObject): InputFieldStateByBlockId => Object.entries(obj).reduce((obj: InputFieldStateByBlockId, [key, { blockId, value }]: InputFieldStateTuple) => { @@ -150,60 +48,52 @@ const UiKitContextualBar = ({ } }; - const debouncedBlockAction = useDebouncedCallback(({ actionId, appId, value, blockId }: ActionParams) => { - actionManager.triggerBlockAction({ - container: { - type: UIKitIncomingInteractionContainerType.VIEW, - id: viewId, - }, - actionId, - appId, - value, - blockId, - }); - }, 700); + const triggerBlockAction = useMemo(() => actionManager.triggerBlockAction.bind(actionManager), [actionManager]); + const debouncedTriggerBlockAction = useDebouncedCallback(triggerBlockAction, 700); + + const contextValue = useMemo( + (): ContextType => ({ + action: async ({ appId, viewId, actionId, dispatchActionConfig }): Promise => { + if (!appId || !viewId) { + return; + } - const context: ContextType = { - action: async ({ actionId, appId, value, blockId, dispatchActionConfig }: ActionParams): Promise => { - if (Array.isArray(dispatchActionConfig) && dispatchActionConfig.includes(InputElementDispatchAction.ON_CHARACTER_ENTERED)) { - await debouncedBlockAction({ actionId, appId, value, blockId }); - } else { - await actionManager.triggerBlockAction({ + const trigger = dispatchActionConfig?.includes('on_character_entered') ? debouncedTriggerBlockAction : triggerBlockAction; + + await trigger({ container: { - type: UIKitIncomingInteractionContainerType.VIEW, + type: 'view', id: viewId, }, actionId, appId, - rid: roomId, - value, - blockId, }); - } - }, - state: ({ actionId, value, blockId = 'default' }: ActionParams): void => { - updateValues({ - actionId, - payload: { - blockId, - value, - }, - }); - }, - ...state, - values, - } as ContextType; + }, + state: ({ actionId, value, blockId = 'default' }) => { + updateValues({ + actionId, + payload: { + blockId, + value, + }, + }); + }, + ...view, + values, + }), + [debouncedTriggerBlockAction, triggerBlockAction, updateValues, values, view], + ); const handleSubmit = useMutableCallback((e) => { prevent(e); closeTab(); actionManager.triggerSubmitView({ - viewId, - appId, + viewId: view.viewId, + appId: view.appId, payload: { view: { ...view, - id: viewId, + id: view.viewId, state: groupStateByBlockId(values), }, }, @@ -214,11 +104,11 @@ const UiKitContextualBar = ({ prevent(e); closeTab(); return actionManager.triggerCancel({ - appId, - viewId, + appId: view.appId, + viewId: view.viewId, view: { ...view, - id: viewId, + id: view.viewId, state: groupStateByBlockId(values), }, }); @@ -228,11 +118,11 @@ const UiKitContextualBar = ({ prevent(e); closeTab(); return actionManager.triggerCancel({ - appId, - viewId, + appId: view.appId, + viewId: view.viewId, view: { ...view, - id: viewId, + id: view.viewId, state: groupStateByBlockId(values), }, isCleared: true, @@ -240,15 +130,15 @@ const UiKitContextualBar = ({ }); return ( - + - + {contextualBarParser.text(view.title, BlockContext.NONE, 0)} {handleClose && } - + @@ -259,7 +149,7 @@ const UiKitContextualBar = ({ )} {view.submit && ( - )} diff --git a/apps/meteor/client/views/room/contextualBar/uikit/hooks/useValues.tsx b/apps/meteor/client/views/room/contextualBar/uikit/hooks/useValues.tsx new file mode 100644 index 000000000000..26ff16b8e6a6 --- /dev/null +++ b/apps/meteor/client/views/room/contextualBar/uikit/hooks/useValues.tsx @@ -0,0 +1,64 @@ +import type { IInputElement, IInputBlock, IBlock, IBlockElement, IActionsBlock } from '@rocket.chat/apps-engine/definition/uikit'; +import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import type { LayoutBlock } from '@rocket.chat/ui-kit'; +import { type Block } from '@rocket.chat/ui-kit'; +import type { Dispatch } from 'react'; +import { useReducer } from 'react'; + +type FieldStateValue = string | Array | undefined; +export type FieldState = { value: FieldStateValue; blockId: string }; +export type InputFieldStateTuple = [string, FieldState]; +export type InputFieldStateObject = { [key: string]: FieldState }; +export type InputFieldStateByBlockId = { [blockId: string]: { [actionId: string]: FieldStateValue } }; + +const isInputBlock = (block: any): block is IInputBlock => block?.element?.initialValue; + +export const useValues = (blocks: LayoutBlock[]): [any, Dispatch] => { + const reducer = useMutableCallback((values, { actionId, payload }) => ({ + ...values, + [actionId]: payload, + })); + + const initializer = useMutableCallback(() => { + const filterInputFields = (block: IBlock | Block): boolean => { + if (isInputBlock(block)) { + return true; + } + + if ( + ((block as IActionsBlock).elements as IInputElement[])?.filter((element) => filterInputFields({ element } as IInputBlock)).length + ) { + return true; + } + + return false; + }; + + const mapElementToState = (block: IBlock | Block): InputFieldStateTuple | InputFieldStateTuple[] => { + if (isInputBlock(block)) { + const { element, blockId } = block; + return [element.actionId, { value: element.initialValue, blockId } as FieldState]; + } + + const { elements, blockId }: { elements: IBlockElement[]; blockId?: string } = block as IActionsBlock; + + return elements + .filter((element) => filterInputFields({ element } as IInputBlock)) + .map((element) => mapElementToState({ element, blockId } as IInputBlock)) as InputFieldStateTuple[]; + }; + + return blocks + .filter(filterInputFields) + .map(mapElementToState) + .reduce((obj: InputFieldStateObject, el: InputFieldStateTuple | InputFieldStateTuple[]) => { + if (Array.isArray(el[0])) { + return { ...obj, ...Object.fromEntries(el as InputFieldStateTuple[]) }; + } + + const [key, value] = el as InputFieldStateTuple; + return { ...obj, [key]: value }; + }, {} as InputFieldStateObject); + }); + + return useReducer(reducer, null, initializer); +}; diff --git a/apps/meteor/client/views/room/hooks/useAppsContextualBar.ts b/apps/meteor/client/views/room/hooks/useAppsContextualBar.ts index 6afa6c3a6f84..63cd8322f2bd 100644 --- a/apps/meteor/client/views/room/hooks/useAppsContextualBar.ts +++ b/apps/meteor/client/views/room/hooks/useAppsContextualBar.ts @@ -1,49 +1,19 @@ -import type { IUIKitContextualBarInteraction } from '@rocket.chat/apps-engine/definition/uikit'; import { useRouteParameter } from '@rocket.chat/ui-contexts'; -import { useEffect, useState } from 'react'; +import { useMemo } from 'react'; import { useUiKitActionManager } from '../../../hooks/useUiKitActionManager'; -import { useRoom } from '../contexts/RoomContext'; - -type AppsContextualBarData = { - viewId: string; - roomId: string; - payload: IUIKitContextualBarInteraction; - appId: string; -}; - -export const useAppsContextualBar = (): AppsContextualBarData | undefined => { - const [payload, setPayload] = useState(); - const actionManager = useUiKitActionManager(); - const [appId, setAppId] = useState(); - - const { _id: roomId } = useRoom(); +export const useAppsContextualBar = () => { const viewId = useRouteParameter('context'); + const actionManager = useUiKitActionManager(); - useEffect(() => { + const view = useMemo(() => { if (viewId) { - setPayload(actionManager.getUserInteractionPayloadByViewId(viewId) as IUIKitContextualBarInteraction); - } - - if (payload?.appId) { - setAppId(payload.appId); + return actionManager.getUserInteractionPayloadByViewId(viewId); } - return (): void => { - setPayload(undefined); - setAppId(undefined); - }; - }, [viewId, payload?.appId, actionManager]); - - if (viewId && payload && appId) { - return { - viewId, - roomId, - payload, - appId, - }; - } + return undefined; + }, [actionManager, viewId]); - return undefined; + return view; }; diff --git a/packages/core-typings/src/IBanner.ts b/packages/core-typings/src/IBanner.ts index da40f3b0fad2..275c3353aa1f 100644 --- a/packages/core-typings/src/IBanner.ts +++ b/packages/core-typings/src/IBanner.ts @@ -13,7 +13,7 @@ export interface IBanner extends IRocketChatRecord { roles?: string[]; // only show the banner to this roles createdBy: Pick; createdAt: Date; - view: UiKit.BannerPayload; + view: UiKit.BannerView; active?: boolean; inactivedAt?: Date; snapshot?: string; diff --git a/packages/core-typings/src/cloud/Announcement.ts b/packages/core-typings/src/cloud/Announcement.ts index 22cb3cac2d41..88a670d06688 100644 --- a/packages/core-typings/src/cloud/Announcement.ts +++ b/packages/core-typings/src/cloud/Announcement.ts @@ -22,6 +22,6 @@ export interface Announcement extends IRocketChatRecord { createdBy: Creator; createdAt: Date; dictionary?: Dictionary; - view: UiKit.Payload; + view: UiKit.View; surface: 'banner' | 'modal'; } diff --git a/packages/core-typings/src/uikit/BannerPayload.ts b/packages/core-typings/src/uikit/BannerView.ts similarity index 72% rename from packages/core-typings/src/uikit/BannerPayload.ts rename to packages/core-typings/src/uikit/BannerView.ts index 63c37176449a..747141a280d0 100644 --- a/packages/core-typings/src/uikit/BannerPayload.ts +++ b/packages/core-typings/src/uikit/BannerView.ts @@ -1,12 +1,15 @@ import type { Keys as IconName } from '@rocket.chat/icons'; import type { BannerSurfaceLayout } from '@rocket.chat/ui-kit'; -import type { Payload } from './Payload'; +import type { View } from './View'; -export type BannerPayload = { +/** + * A view that is displayed as a banner. + */ +export type BannerView = View & { inline?: boolean; variant?: 'neutral' | 'info' | 'success' | 'warning' | 'danger'; icon?: IconName; title?: string; // TODO: change to plain_text block in the future blocks: BannerSurfaceLayout; -} & Payload; +}; diff --git a/packages/core-typings/src/uikit/ContextualBarView.ts b/packages/core-typings/src/uikit/ContextualBarView.ts new file mode 100644 index 000000000000..bdaba2cb179d --- /dev/null +++ b/packages/core-typings/src/uikit/ContextualBarView.ts @@ -0,0 +1,13 @@ +import type { ButtonElement, ContextualBarSurfaceLayout, TextObject } from '@rocket.chat/ui-kit'; + +import type { View } from './View'; + +/** + * A view that is displayed as a contextual bar. + */ +export type ContextualBarView = View & { + title: TextObject; + close?: ButtonElement; + submit?: ButtonElement; + blocks: ContextualBarSurfaceLayout; +}; diff --git a/packages/core-typings/src/uikit/ModalView.ts b/packages/core-typings/src/uikit/ModalView.ts new file mode 100644 index 000000000000..f377a3c4fde0 --- /dev/null +++ b/packages/core-typings/src/uikit/ModalView.ts @@ -0,0 +1,14 @@ +import type { ButtonElement, ModalSurfaceLayout, TextObject } from '@rocket.chat/ui-kit'; + +import type { View } from './View'; + +/** + * A view that is displayed as a modal dialog. + */ +export type ModalView = View & { + showIcon?: boolean; + title: TextObject; + close?: ButtonElement; + submit?: ButtonElement; + blocks: ModalSurfaceLayout; +}; diff --git a/packages/core-typings/src/uikit/UserInteraction.ts b/packages/core-typings/src/uikit/UserInteraction.ts new file mode 100644 index 000000000000..5eba97ca0bb4 --- /dev/null +++ b/packages/core-typings/src/uikit/UserInteraction.ts @@ -0,0 +1 @@ +export type UserInteraction = {}; diff --git a/packages/core-typings/src/uikit/Payload.ts b/packages/core-typings/src/uikit/View.ts similarity index 59% rename from packages/core-typings/src/uikit/Payload.ts rename to packages/core-typings/src/uikit/View.ts index 48b3b4e4bd01..3b7219f15aea 100644 --- a/packages/core-typings/src/uikit/Payload.ts +++ b/packages/core-typings/src/uikit/View.ts @@ -1,6 +1,9 @@ import type { LayoutBlock } from '@rocket.chat/ui-kit'; -export type Payload = { +/** + * An instance of a UiKit surface and its metadata. + */ +export type View = { viewId: string; appId: string; blocks: LayoutBlock[]; diff --git a/packages/core-typings/src/uikit/deprecations.ts b/packages/core-typings/src/uikit/deprecations.ts index f2c9df775f16..fc643e23111e 100644 --- a/packages/core-typings/src/uikit/deprecations.ts +++ b/packages/core-typings/src/uikit/deprecations.ts @@ -23,7 +23,7 @@ export const UIKitInteractionTypes = { ...UIKitInteractionTypeExtended, }; -/** @deprecated use {@link UiKit.Payload} instead */ +/** @deprecated use {@link UiKit.View} instead */ export type UiKitPayload = { viewId: string; appId: string; diff --git a/packages/core-typings/src/uikit/index.ts b/packages/core-typings/src/uikit/index.ts index 420e80ea02c3..bfa5d6c740ab 100644 --- a/packages/core-typings/src/uikit/index.ts +++ b/packages/core-typings/src/uikit/index.ts @@ -1,2 +1,5 @@ -export { Payload } from './Payload'; -export { BannerPayload } from './BannerPayload'; +export { View } from './View'; +export { BannerView } from './BannerView'; +export { ContextualBarView } from './ContextualBarView'; +export { ModalView } from './ModalView'; +export { UserInteraction } from './UserInteraction'; diff --git a/packages/ui-contexts/src/ActionManagerContext.ts b/packages/ui-contexts/src/ActionManagerContext.ts index d4dcdb61bfb9..3d2e37c66c5e 100644 --- a/packages/ui-contexts/src/ActionManagerContext.ts +++ b/packages/ui-contexts/src/ActionManagerContext.ts @@ -1,8 +1,62 @@ +import type { IMessage, IRoom, UiKit } from '@rocket.chat/core-typings'; import { createContext } from 'react'; +// type BlockActionUserInteractionPayload = { +// type: 'blockAction'; +// actionId: string; +// triggerId: string; +// mid?: IMessage['_id']; +// rid?: IRoom['_id']; +// payload: UiKit.View; +// container: +// | { +// type: 'view'; +// id: UiKit.View['viewId']; +// } +// | { +// type: 'message'; +// id: IMessage['_id']; +// }; +// }; + +type BlockActionTriggerOptions = + | { + type: 'blockAction'; + actionId: string; + appId: string; + container: { + type: 'view'; + id: UiKit.View['viewId']; + }; + visitor?: unknown; + } + | { + type: 'blockAction'; + actionId: string; + appId: string; + container: { + type: 'message'; + id: UiKit.View['viewId']; + }; + rid: IRoom['_id']; + mid: IMessage['_id']; + visitor?: unknown; + }; + +/** + * Utility type to remove the `type` property from an **union** of objects. + */ +type WithoutType = X extends { type: any } ? Omit : X; + type ActionManagerContextValue = { - on: (...args: any[]) => void; - off: (...args: any[]) => void; + on: { + (viewId: string, listener: (...args: any[]) => any): void; + (eventName: 'busy', listener: ({ busy }: { busy: boolean }) => void): void; + }; + off: { + (viewId: string, listener: (...args: any[]) => any): void; + (eventName: 'busy', listener?: ({ busy }: { busy: boolean }) => void): void; + }; generateTriggerId: (appId: any) => string; handlePayloadUserInteraction: ( type: any, @@ -14,7 +68,8 @@ type ActionManagerContextValue = { triggerId: any; }, ) => any; - triggerAction: ({ + triggerAction(action: BlockActionTriggerOptions): Promise; + triggerAction({ type, actionId, appId, @@ -34,8 +89,9 @@ type ActionManagerContextValue = { viewId: any; container: any; tmid: any; - }) => Promise; - triggerBlockAction: (options: any) => Promise; + }): Promise; + triggerBlockAction(options: WithoutType): Promise; + // triggerBlockAction(options: any): Promise; triggerActionButtonAction: (options: any) => Promise; triggerSubmitView: ({ viewId, ...options }: { [x: string]: any; viewId: any }) => Promise; triggerCancel: ({ view, ...options }: { [x: string]: any; view: any }) => Promise; From fb72892eb6ed60a0062791d9561699a7c589d7a4 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Fri, 29 Sep 2023 01:10:30 -0300 Subject: [PATCH 11/11] Remove obsolete code --- .../message/uikit/UiKitMessageBlock.tsx | 72 +++++++++---------- 1 file changed, 32 insertions(+), 40 deletions(-) diff --git a/apps/meteor/client/components/message/uikit/UiKitMessageBlock.tsx b/apps/meteor/client/components/message/uikit/UiKitMessageBlock.tsx index fbebfd380da3..0dda866311c0 100644 --- a/apps/meteor/client/components/message/uikit/UiKitMessageBlock.tsx +++ b/apps/meteor/client/components/message/uikit/UiKitMessageBlock.tsx @@ -4,7 +4,7 @@ import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { UiKitComponent, UiKitMessage as UiKitMessageSurfaceRender, UiKitContext } from '@rocket.chat/fuselage-ui-kit'; import type { MessageSurfaceLayout } from '@rocket.chat/ui-kit'; import type { ContextType, ReactElement } from 'react'; -import React from 'react'; +import React, { useMemo } from 'react'; import { useVideoConfDispatchOutgoing, @@ -18,15 +18,6 @@ import { useUiKitActionManager } from '../../../hooks/useUiKitActionManager'; import { useVideoConfWarning } from '../../../views/room/contextualBar/VideoConference/hooks/useVideoConfWarning'; import GazzodownText from '../../GazzodownText'; -let patched = false; -const patchMessageParser = () => { - if (patched) { - return; - } - - patched = true; -}; - type UiKitMessageBlockProps = { rid: IRoom['_id']; mid: IMessage['_id']; @@ -59,42 +50,43 @@ const UiKitMessageBlock = ({ rid, mid, blocks }: UiKitMessageBlockProps): ReactE const actionManager = useUiKitActionManager(); // TODO: this structure is attrociously wrong; we should revisit this - const context: ContextType = { - action: ({ actionId, blockId, appId }, event) => { - if (appId === 'videoconf-core') { - event.preventDefault(); - setPreferences({ mic: true, cam: false }); - if (actionId === 'join') { - return joinCall(blockId); - } + const contextValue = useMemo( + (): ContextType => ({ + action: ({ actionId, blockId, appId }, event) => { + if (appId === 'videoconf-core') { + event.preventDefault(); + setPreferences({ mic: true, cam: false }); + if (actionId === 'join') { + return joinCall(blockId); + } - if (actionId === 'callBack') { - return handleOpenVideoConf(blockId); + if (actionId === 'callBack') { + return handleOpenVideoConf(blockId); + } } - } - - actionManager?.triggerBlockAction({ - actionId, - appId, - container: { - type: 'message', - id: mid, - }, - rid, - mid, - }); - }, - appId: '', // TODO: this is a hack - rid, - state: () => undefined, // TODO: this is a hack - values: {}, // TODO: this is a hack - }; - patchMessageParser(); // TODO: this is a hack + actionManager?.triggerBlockAction({ + actionId, + appId, + container: { + type: 'message', + id: mid, + }, + rid, + mid, + }); + }, + appId: '', // TODO: this is a hack + rid, + state: () => undefined, // TODO: this is a hack + values: {}, // TODO: this is a hack + }), + [actionManager, handleOpenVideoConf, joinCall, mid, rid, setPreferences], + ); return ( - +