Merge pull request #30772 from RocketChat/release-6.4.4

pull/30828/head
Douglas Gubert 2 years ago committed by GitHub
commit 5b3021afba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      .changeset/bump-patch-1698348780628.md
  2. 5
      .changeset/cuddly-animal-eat.md
  3. 5
      .changeset/cuddly-ties-run.md
  4. 5
      .changeset/serious-cats-fetch.md
  5. 5
      .changeset/serious-cats-run.md
  6. 4
      apps/meteor/.docker/Dockerfile
  7. 2
      apps/meteor/app/ui-master/server/index.js
  8. 2
      apps/meteor/client/components/AutoCompleteAgent.tsx
  9. 2
      apps/meteor/client/components/Omnichannel/hooks/useAgentsList.ts
  10. 18
      apps/meteor/client/providers/AppsProvider.tsx
  11. 43
      apps/meteor/client/views/marketplace/AppsPage/AppsPageContent.tsx
  12. 13
      apps/meteor/client/views/marketplace/AppsPage/NoAppRequestsEmptyState.tsx
  13. 2
      apps/meteor/client/views/marketplace/AppsPage/PrivateEmptyState.tsx
  14. 4
      apps/meteor/client/views/marketplace/helpers/handleAPIError.ts
  15. 3
      apps/meteor/ee/server/apps/communication/rest.ts
  16. 7
      apps/meteor/tests/data/users.helper.js
  17. 66
      apps/meteor/tests/end-to-end/api/livechat/01-agents.ts
  18. 8
      packages/livechat/src/lib/hooks.js
  19. 2
      packages/livechat/src/lib/triggers.js
  20. 2
      packages/rest-typings/src/v1/omnichannel.ts

@ -0,0 +1,5 @@
---
'@rocket.chat/meteor': patch
---
Bump @rocket.chat/meteor version.

@ -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

@ -0,0 +1,5 @@
---
'@rocket.chat/meteor': patch
---
fix: custom-css injection

@ -0,0 +1,5 @@
---
'@rocket.chat/meteor': patch
---
Fixed a problem that would prevent private apps from being shown on air-gapped environments

@ -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

@ -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 '{}' ';' \

@ -120,8 +120,6 @@ Meteor.startup(() => {
})(__meteor_runtime_config__.ROOT_URL_PATH_PREFIX);
injectIntoHead('base', `<base href="${baseUrl}">`);
injectIntoHead('css-theme', '');
});
const renderDynamicCssList = withDebouncing({ wait: 500 })(async () => {

@ -27,7 +27,7 @@ const AutoCompleteAgent = ({
haveAll = false,
haveNoAgentsSelectedOption = false,
excludeId,
showIdleAgents = false,
showIdleAgents = true,
onlyAvailable = false,
withTitle = false,
onChange,

@ -29,7 +29,7 @@ export const useAgentsList = (
const reload = useCallback(() => setItemsList(new RecordList<AgentOption>()), []);
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();

@ -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 }) => {
<AppsContext.Provider
children={children}
value={{
installedApps: { phase: AsyncStatePhase.RESOLVED, value: { apps: store.data[1] } },
marketplaceApps: { phase: AsyncStatePhase.RESOLVED, value: { apps: store.data[0] } },
privateApps: { phase: AsyncStatePhase.RESOLVED, value: { apps: store.data[2] } },
installedApps: { phase: AsyncStatePhase.RESOLVED, value: { apps: store.data?.[1] || [] } },
marketplaceApps: { phase: AsyncStatePhase.RESOLVED, value: { apps: store.data?.[0] || [] } },
privateApps: { phase: AsyncStatePhase.RESOLVED, value: { apps: store.data?.[2] || [] } },
reload: async () => {
await Promise.all([queryClient.invalidateQueries(['marketplace'])]);
},

@ -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 <NoAppRequestsEmptyState />;
}
if (noMarketplaceOrInstalledAppMatches) {
return <NoMarketplaceOrInstalledAppMatchesEmptyState shouldShowSearchText={appsResult.value.shouldShowSearchText} text={text} />;
}
if (noInstalledAppMatches) {
return (
<NoInstalledAppMatchesEmptyState
shouldShowSearchText={appsResult.value.shouldShowSearchText}
text={text}
onButtonClick={handleReturn}
/>
);
}
if (noInstalledApps) {
return context === 'private' ? <PrivateEmptyState /> : <NoInstalledAppsEmptyState onButtonClick={handleReturn} />;
}
};
return (
<>
<AppsFilters
@ -198,18 +224,7 @@ const AppsPageContent = (): ReactElement => {
noErrorsOcurred={noErrorsOcurred}
/>
)}
{noAppRequests && <NoAppRequestsEmptyState />}
{noMarketplaceOrInstalledAppMatches && (
<NoMarketplaceOrInstalledAppMatchesEmptyState shouldShowSearchText={appsResult.value.shouldShowSearchText} text={text} />
)}
{noInstalledAppMatches && (
<NoInstalledAppMatchesEmptyState
shouldShowSearchText={appsResult.value.shouldShowSearchText}
text={text}
onButtonClick={handleReturn}
/>
)}
{noInstalledApps && <>{context === 'private' ? <PrivateEmptyState /> : <NoInstalledAppsEmptyState onButtonClick={handleReturn} />}</>}
{getEmptyState()}
{appsResult.phase === AsyncStatePhase.REJECTED && <AppsPageConnectionError onButtonClick={reload} />}
</>
);

@ -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 (
<States>
<StatesTitle>{t('No_requested_apps')}</StatesTitle>
<StatesSubtitle>{t('Requested_apps_will_appear_here')}</StatesSubtitle>
</States>
<Box mbs='24px'>
<States>
<StatesIcon name='cube' />
<StatesTitle>{t('No_requested_apps')}</StatesTitle>
<StatesSubtitle>{t('Requested_apps_will_appear_here')}</StatesSubtitle>
</States>
</Box>
);
};

@ -8,7 +8,7 @@ const PrivateEmptyState = () => {
return (
<Box mbs='24px'>
<States>
<StatesIcon name='cube' />
<StatesIcon name='lock' />
<StatesTitle>{t('No_private_apps_installed')}</StatesTitle>
<StatesSubtitle>{t('Private_apps_are_side-loaded')}</StatesSubtitle>
</States>

@ -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}`) });

@ -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);

@ -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,
});

@ -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']);

@ -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();
},

@ -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);
}

@ -765,7 +765,7 @@ const LivechatUsersManagerGETSchema = {
nullable: true,
},
onlyAvailable: {
type: 'string',
type: 'boolean',
nullable: true,
},
excludeId: {

Loading…
Cancel
Save