diff --git a/.changeset/bump-patch-1698348780628.md b/.changeset/bump-patch-1698348780628.md new file mode 100644 index 00000000000..e1eaa7980af --- /dev/null +++ b/.changeset/bump-patch-1698348780628.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Bump @rocket.chat/meteor version. diff --git a/.changeset/cuddly-animal-eat.md b/.changeset/cuddly-animal-eat.md new file mode 100644 index 00000000000..41fa17b9cb3 --- /dev/null +++ b/.changeset/cuddly-animal-eat.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixed widget's `nextAgent` API sending an array of chars instead of an object for `departmentId` parameter diff --git a/.changeset/cuddly-ties-run.md b/.changeset/cuddly-ties-run.md new file mode 100644 index 00000000000..cb387389984 --- /dev/null +++ b/.changeset/cuddly-ties-run.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +fix: custom-css injection diff --git a/.changeset/serious-cats-fetch.md b/.changeset/serious-cats-fetch.md new file mode 100644 index 00000000000..4718d3597e5 --- /dev/null +++ b/.changeset/serious-cats-fetch.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixed a problem that would prevent private apps from being shown on air-gapped environments diff --git a/.changeset/serious-cats-run.md b/.changeset/serious-cats-run.md new file mode 100644 index 00000000000..8f9e6618c85 --- /dev/null +++ b/.changeset/serious-cats-run.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixed a problem caused by `onlyAvailable` property of `livechat/users/agent` endpoint not having the proper type validation \ No newline at end of file diff --git a/apps/meteor/.docker/Dockerfile b/apps/meteor/.docker/Dockerfile index 650d3fc5fee..456ed4becaf 100644 --- a/apps/meteor/.docker/Dockerfile +++ b/apps/meteor/.docker/Dockerfile @@ -14,9 +14,11 @@ RUN groupadd -g 65533 -r rocketchat \ ADD --chown=rocketchat:rocketchat . /app RUN aptMark="$(apt-mark showmanual)" \ - && apt-get install -y --no-install-recommends g++ make python ca-certificates \ + && apt-get install -y --no-install-recommends g++ make python3 ca-certificates \ && cd /app/bundle/programs/server \ && npm install \ + && cd npm/node_modules/isolated-vm \ + && npm install \ && apt-mark auto '.*' > /dev/null \ && apt-mark manual $aptMark > /dev/null \ && find /usr/local -type f -executable -exec ldd '{}' ';' \ diff --git a/apps/meteor/app/ui-master/server/index.js b/apps/meteor/app/ui-master/server/index.js index f9e335451d7..34a9618d2db 100644 --- a/apps/meteor/app/ui-master/server/index.js +++ b/apps/meteor/app/ui-master/server/index.js @@ -120,8 +120,6 @@ Meteor.startup(() => { })(__meteor_runtime_config__.ROOT_URL_PATH_PREFIX); injectIntoHead('base', ``); - - injectIntoHead('css-theme', ''); }); const renderDynamicCssList = withDebouncing({ wait: 500 })(async () => { diff --git a/apps/meteor/client/components/AutoCompleteAgent.tsx b/apps/meteor/client/components/AutoCompleteAgent.tsx index f2cbebe4692..059ac4251cc 100644 --- a/apps/meteor/client/components/AutoCompleteAgent.tsx +++ b/apps/meteor/client/components/AutoCompleteAgent.tsx @@ -27,7 +27,7 @@ const AutoCompleteAgent = ({ haveAll = false, haveNoAgentsSelectedOption = false, excludeId, - showIdleAgents = false, + showIdleAgents = true, onlyAvailable = false, withTitle = false, onChange, diff --git a/apps/meteor/client/components/Omnichannel/hooks/useAgentsList.ts b/apps/meteor/client/components/Omnichannel/hooks/useAgentsList.ts index e2f6f80f235..ef00a6c0b81 100644 --- a/apps/meteor/client/components/Omnichannel/hooks/useAgentsList.ts +++ b/apps/meteor/client/components/Omnichannel/hooks/useAgentsList.ts @@ -29,7 +29,7 @@ export const useAgentsList = ( const reload = useCallback(() => setItemsList(new RecordList()), []); const getAgents = useEndpoint('GET', '/v1/livechat/users/agent'); - const { text, onlyAvailable = false, showIdleAgents = false, excludeId, haveAll, haveNoAgentsSelectedOption } = options; + const { text, onlyAvailable = false, showIdleAgents = true, excludeId, haveAll, haveNoAgentsSelectedOption } = options; useComponentDidUpdate(() => { options && reload(); diff --git a/apps/meteor/client/providers/AppsProvider.tsx b/apps/meteor/client/providers/AppsProvider.tsx index 042ce836547..15512fad24c 100644 --- a/apps/meteor/client/providers/AppsProvider.tsx +++ b/apps/meteor/client/providers/AppsProvider.tsx @@ -66,24 +66,23 @@ const AppsProvider: FC = ({ children }) => { }, { staleTime: Infinity, - keepPreviousData: true, onSettled: () => queryClient.invalidateQueries(['marketplace', 'apps-stored']), }, ); const store = useQuery( - ['marketplace', 'apps-stored', isAdminUser], + ['marketplace', 'apps-stored', instance.data, marketplace.data], () => { - if (!marketplace.isSuccess || !instance.isSuccess) { + if (!marketplace.isFetched && !instance.isFetched) { throw new Error('Apps not loaded'); } const marketplaceApps: App[] = []; const installedApps: App[] = []; const privateApps: App[] = []; - const clonedData = [...instance.data]; + const clonedData = [...(instance.data || [])]; - sortByName(marketplace.data).forEach((app) => { + sortByName(marketplace.data || []).forEach((app) => { const appIndex = clonedData.findIndex(({ id }) => id === app.id); const [installedApp] = appIndex > -1 ? clonedData.splice(appIndex, 1) : []; @@ -117,8 +116,7 @@ const AppsProvider: FC = ({ children }) => { return [marketplaceApps, installedApps, privateApps]; }, { - enabled: marketplace.isSuccess && instance.isSuccess && !instance.isRefetching, - keepPreviousData: true, + enabled: marketplace.isFetched && instance.isFetched, }, ); @@ -130,9 +128,9 @@ const AppsProvider: FC = ({ children }) => { { await Promise.all([queryClient.invalidateQueries(['marketplace'])]); }, diff --git a/apps/meteor/client/views/marketplace/AppsPage/AppsPageContent.tsx b/apps/meteor/client/views/marketplace/AppsPage/AppsPageContent.tsx index 40d90b56e04..5e564fca59b 100644 --- a/apps/meteor/client/views/marketplace/AppsPage/AppsPageContent.tsx +++ b/apps/meteor/client/views/marketplace/AppsPage/AppsPageContent.tsx @@ -32,6 +32,7 @@ const AppsPageContent = (): ReactElement => { const context = useRouteParameter('context'); const isMarketplace = context === 'explore'; + const isPremium = context === 'premium'; const isRequested = context === 'requested'; const [freePaidFilterStructure, setFreePaidFilterStructure] = useState({ @@ -115,7 +116,8 @@ const AppsPageContent = (): ReactElement => { const noInstalledApps = appsResult.phase === AsyncStatePhase.RESOLVED && !isMarketplace && appsResult.value.totalAppsLength === 0; - const noMarketplaceOrInstalledAppMatches = appsResult.phase === AsyncStatePhase.RESOLVED && isMarketplace && appsResult.value.count === 0; + const noMarketplaceOrInstalledAppMatches = + appsResult.phase === AsyncStatePhase.RESOLVED && (isMarketplace || isPremium) && appsResult.value.count === 0; const noInstalledAppMatches = appsResult.phase === AsyncStatePhase.RESOLVED && @@ -123,7 +125,7 @@ const AppsPageContent = (): ReactElement => { appsResult.value.totalAppsLength !== 0 && appsResult.value.count === 0; - const noAppRequests = context === 'requested' && appsResult?.value?.totalAppsLength !== 0 && appsResult?.value?.count === 0; + const noAppRequests = context === 'requested' && appsResult?.value?.count === 0; const noErrorsOcurred = !noMarketplaceOrInstalledAppMatches && !noInstalledAppMatches && !noInstalledApps && !noAppRequests; @@ -168,6 +170,30 @@ const AppsPageContent = (): ReactElement => { toggleInitialSortOption(isRequested); }, [isMarketplace, isRequested, sortFilterOnSelected, t, toggleInitialSortOption]); + const getEmptyState = () => { + if (noAppRequests) { + return ; + } + + if (noMarketplaceOrInstalledAppMatches) { + return ; + } + + if (noInstalledAppMatches) { + return ( + + ); + } + + if (noInstalledApps) { + return context === 'private' ? : ; + } + }; + return ( <> { noErrorsOcurred={noErrorsOcurred} /> )} - {noAppRequests && } - {noMarketplaceOrInstalledAppMatches && ( - - )} - {noInstalledAppMatches && ( - - )} - {noInstalledApps && <>{context === 'private' ? : }} + {getEmptyState()} {appsResult.phase === AsyncStatePhase.REJECTED && } ); diff --git a/apps/meteor/client/views/marketplace/AppsPage/NoAppRequestsEmptyState.tsx b/apps/meteor/client/views/marketplace/AppsPage/NoAppRequestsEmptyState.tsx index 230c52a7a9a..dc7d77e64a7 100644 --- a/apps/meteor/client/views/marketplace/AppsPage/NoAppRequestsEmptyState.tsx +++ b/apps/meteor/client/views/marketplace/AppsPage/NoAppRequestsEmptyState.tsx @@ -1,4 +1,4 @@ -import { States, StatesSubtitle, StatesTitle } from '@rocket.chat/fuselage'; +import { Box, States, StatesIcon, StatesSubtitle, StatesTitle } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; @@ -6,10 +6,13 @@ const NoAppRequestsEmptyState = () => { const t = useTranslation(); return ( - - {t('No_requested_apps')} - {t('Requested_apps_will_appear_here')} - + + + + {t('No_requested_apps')} + {t('Requested_apps_will_appear_here')} + + ); }; diff --git a/apps/meteor/client/views/marketplace/AppsPage/PrivateEmptyState.tsx b/apps/meteor/client/views/marketplace/AppsPage/PrivateEmptyState.tsx index d52d7b92a77..b7fec778401 100644 --- a/apps/meteor/client/views/marketplace/AppsPage/PrivateEmptyState.tsx +++ b/apps/meteor/client/views/marketplace/AppsPage/PrivateEmptyState.tsx @@ -8,7 +8,7 @@ const PrivateEmptyState = () => { return ( - + {t('No_private_apps_installed')} {t('Private_apps_are_side-loaded')} diff --git a/apps/meteor/client/views/marketplace/helpers/handleAPIError.ts b/apps/meteor/client/views/marketplace/helpers/handleAPIError.ts index 61ec512bd6e..108e695b6bd 100644 --- a/apps/meteor/client/views/marketplace/helpers/handleAPIError.ts +++ b/apps/meteor/client/views/marketplace/helpers/handleAPIError.ts @@ -8,10 +8,10 @@ const shouldHandleErrorAsWarning = (message: string): boolean => { }; export const handleAPIError = (errorObject: unknown): void => { - const { message = '', error = '' } = errorObject as { message?: string; error?: string }; + const { error = '', message = error } = errorObject as { message?: string; error?: string }; if (shouldHandleErrorAsWarning(message)) { - return dispatchToastMessage({ type: 'warning', message: t(message) }); + return dispatchToastMessage({ type: 'error', message: t(message) }); } dispatchToastMessage({ type: 'error', message: t(`Apps_Error_${error}`) }); diff --git a/apps/meteor/ee/server/apps/communication/rest.ts b/apps/meteor/ee/server/apps/communication/rest.ts index 1203d0d8c91..4f8bd0026a1 100644 --- a/apps/meteor/ee/server/apps/communication/rest.ts +++ b/apps/meteor/ee/server/apps/communication/rest.ts @@ -164,8 +164,7 @@ export class AppsRestApi { } result = await request.json(); } catch (e: any) { - orchestrator.getRocketChatLogger().error('Error getting the categories from the Marketplace:', e.response.data); - return API.v1.internalError(); + return handleError('Unable to access Marketplace. Does the server has access to the internet?', e); } return API.v1.success(result); diff --git a/apps/meteor/tests/data/users.helper.js b/apps/meteor/tests/data/users.helper.js index 92425902cb5..47d26f83c5b 100644 --- a/apps/meteor/tests/data/users.helper.js +++ b/apps/meteor/tests/data/users.helper.js @@ -1,3 +1,4 @@ +import { UserStatus } from '@rocket.chat/core-typings'; import { api, credentials, request } from './api-data'; import { password } from './user'; @@ -89,3 +90,9 @@ export const setUserActiveStatus = (userId, activeStatus = true) => }) .end(resolve); }); + +export const setUserStatus = (overrideCredentials = credentials, status = UserStatus.ONLINE) => + request.post(api('users.setStatus')).set(overrideCredentials).send({ + message: '', + status, + }); diff --git a/apps/meteor/tests/end-to-end/api/livechat/01-agents.ts b/apps/meteor/tests/end-to-end/api/livechat/01-agents.ts index 6f9120ea909..044cdf498a8 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/01-agents.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/01-agents.ts @@ -1,4 +1,4 @@ -import type { ILivechatAgent, ILivechatDepartment, IUser } from '@rocket.chat/core-typings'; +import { UserStatus, type ILivechatAgent, type ILivechatDepartment, type IUser } from '@rocket.chat/core-typings'; import { expect } from 'chai'; import { after, before, describe, it } from 'mocha'; import type { Response } from 'supertest'; @@ -16,7 +16,7 @@ import { } from '../../../data/livechat/rooms'; import { updatePermission, updateSetting } from '../../../data/permissions.helper'; import { password } from '../../../data/user'; -import { createUser, deleteUser, getMe, login } from '../../../data/users.helper'; +import { createUser, deleteUser, getMe, login, setUserStatus } from '../../../data/users.helper'; describe('LIVECHAT - Agents', function () { this.retries(0); @@ -114,6 +114,68 @@ describe('LIVECHAT - Agents', function () { expect(res.body.users.every((u: { statusLivechat: string }) => u.statusLivechat === 'available')).to.be.true; }); }); + it('should return an array of available/unavailable agents when onlyAvailable is false', async () => { + await request + .get(api('livechat/users/agent')) + .set(credentials) + .expect('Content-Type', 'application/json') + .query({ onlyAvailable: false }) + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(res.body.users).to.be.an('array'); + expect(res.body).to.have.property('offset'); + expect(res.body).to.have.property('total'); + expect(res.body).to.have.property('count'); + expect( + res.body.users.every( + (u: { statusLivechat: string }) => !u.statusLivechat || ['available', 'not-available'].includes(u.statusLivechat), + ), + ).to.be.true; + }); + }); + + it('should return offline agents when showIdleAgents is true', async () => { + await setUserStatus(agent2.credentials, UserStatus.OFFLINE); + await request + .get(api('livechat/users/agent')) + .set(credentials) + .expect('Content-Type', 'application/json') + .query({ showIdleAgents: true }) + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(res.body.users).to.be.an('array'); + expect(res.body).to.have.property('offset'); + expect(res.body).to.have.property('total'); + expect(res.body).to.have.property('count'); + expect( + res.body.users.every( + (u: { status: UserStatus }) => + !u.status || [UserStatus.ONLINE, UserStatus.OFFLINE, UserStatus.AWAY, UserStatus.BUSY].includes(u.status), + ), + ).to.be.true; + }); + }); + + it('should return only online agents when showIdleAgents is false', async () => { + await setUserStatus(agent2.credentials, UserStatus.ONLINE); + await request + .get(api('livechat/users/agent')) + .set(credentials) + .expect('Content-Type', 'application/json') + .query({ showIdleAgents: false }) + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(res.body.users).to.be.an('array'); + expect(res.body).to.have.property('offset'); + expect(res.body).to.have.property('total'); + expect(res.body).to.have.property('count'); + expect(res.body.users.every((u: { status: UserStatus }) => u.status !== UserStatus.OFFLINE)).to.be.true; + }); + }); + it('should return an array of managers', async () => { await updatePermission('view-livechat-manager', ['admin']); await updatePermission('manage-livechat-agents', ['admin']); diff --git a/packages/livechat/src/lib/hooks.js b/packages/livechat/src/lib/hooks.js index b988579acfa..79535831217 100644 --- a/packages/livechat/src/lib/hooks.js +++ b/packages/livechat/src/lib/hooks.js @@ -137,15 +137,11 @@ const api = { }, async setGuestToken(token) { - const { - token: localToken, - iframe, - iframe: { guest }, - } = store.state; + const { token: localToken } = store.state; if (token === localToken) { return; } - store.setState({ token, iframe: { ...iframe, guest: { ...guest, token } } }); + createOrUpdateGuest({ token }); await loadConfig(); }, diff --git a/packages/livechat/src/lib/triggers.js b/packages/livechat/src/lib/triggers.js index 4cf01090587..efa9e9bdc65 100644 --- a/packages/livechat/src/lib/triggers.js +++ b/packages/livechat/src/lib/triggers.js @@ -34,7 +34,7 @@ const getAgent = (triggerAction) => { let agent; try { - agent = await Livechat.nextAgent(department); + agent = await Livechat.nextAgent({ department }); } catch (error) { return reject(error); } diff --git a/packages/rest-typings/src/v1/omnichannel.ts b/packages/rest-typings/src/v1/omnichannel.ts index 4f9317fa3d0..c86b87c2b59 100644 --- a/packages/rest-typings/src/v1/omnichannel.ts +++ b/packages/rest-typings/src/v1/omnichannel.ts @@ -765,7 +765,7 @@ const LivechatUsersManagerGETSchema = { nullable: true, }, onlyAvailable: { - type: 'string', + type: 'boolean', nullable: true, }, excludeId: {