diff --git a/app/actions/actionsTypes.ts b/app/actions/actionsTypes.ts index 84689f5ab..e05090434 100644 --- a/app/actions/actionsTypes.ts +++ b/app/actions/actionsTypes.ts @@ -85,6 +85,7 @@ export const ENCRYPTION = createRequestTypes('ENCRYPTION', ['INIT', 'STOP', 'DEC export const PERMISSIONS = createRequestTypes('PERMISSIONS', ['SET', 'UPDATE']); export const ROLES = createRequestTypes('ROLES', ['SET', 'UPDATE', 'REMOVE']); +export const USERS_ROLES = createRequestTypes('USERS_ROLES', ['SET']); export const VIDEO_CONF = createRequestTypes('VIDEO_CONF', [ 'HANDLE_INCOMING_WEBSOCKET_MESSAGES', 'SET', diff --git a/app/actions/usersRoles.ts b/app/actions/usersRoles.ts new file mode 100644 index 000000000..a17ec9595 --- /dev/null +++ b/app/actions/usersRoles.ts @@ -0,0 +1,13 @@ +import { Action } from 'redux'; + +import { TUsersRoles } from '../reducers/usersRoles'; +import { USERS_ROLES } from './actionsTypes'; + +export type TActionUsersRoles = Action & { usersRoles: TUsersRoles }; + +export function setUsersRoles(usersRoles: TUsersRoles): Action & { usersRoles: TUsersRoles } { + return { + type: USERS_ROLES.SET, + usersRoles + }; +} diff --git a/app/definitions/redux/index.ts b/app/definitions/redux/index.ts index b7105032e..ec8dea5da 100644 --- a/app/definitions/redux/index.ts +++ b/app/definitions/redux/index.ts @@ -36,6 +36,8 @@ import { IInquiry } from '../../ee/omnichannel/reducers/inquiry'; import { IPermissionsState } from '../../reducers/permissions'; import { IEnterpriseModules } from '../../reducers/enterpriseModules'; import { IVideoConf } from '../../reducers/videoConf'; +import { TActionUsersRoles } from '../../actions/usersRoles'; +import { TUsersRoles } from '../../reducers/usersRoles'; export interface IApplicationState { settings: TSettingsState; @@ -60,6 +62,7 @@ export interface IApplicationState { permissions: IPermissionsState; roles: IRoles; videoConf: IVideoConf; + usersRoles: TUsersRoles; } export type TApplicationActions = TActionActiveUsers & @@ -79,4 +82,5 @@ export type TApplicationActions = TActionActiveUsers & TActionInquiry & TActionPermissions & TActionEnterpriseModules & - TActionVideoConf; + TActionVideoConf & + TActionUsersRoles; diff --git a/app/lib/methods/getRoles.ts b/app/lib/methods/getRoles.ts index 83195855c..54f25d71b 100644 --- a/app/lib/methods/getRoles.ts +++ b/app/lib/methods/getRoles.ts @@ -14,7 +14,7 @@ export async function setRoles(): Promise { const db = database.active; const rolesCollection = db.get('roles'); const allRoles = await rolesCollection.query().fetch(); - const parsed = allRoles.reduce((acc, item) => ({ ...acc, [item.id]: item.description || item.id }), {}); + const parsed = allRoles.reduce((acc, item) => ({ ...acc, [item.id]: item.description || item.name || item.id }), {}); reduxStore.dispatch(setRolesAction(parsed)); } diff --git a/app/lib/services/restApi.ts b/app/lib/services/restApi.ts index 63253a60b..509ddf8d3 100644 --- a/app/lib/services/restApi.ts +++ b/app/lib/services/restApi.ts @@ -973,3 +973,5 @@ export const postMessage = (roomId: string, text: string) => sdk.post('chat.post export const notifyUser = (type: string, params: Record): Promise => sdk.methodCall('stream-notify-user', type, params); + +export const getUsersRoles = (): Promise => sdk.methodCall('getUserRoles'); diff --git a/app/reducers/index.js b/app/reducers/index.js index 1e05aa24c..a612e6f3f 100644 --- a/app/reducers/index.js +++ b/app/reducers/index.js @@ -22,6 +22,7 @@ import encryption from './encryption'; import permissions from './permissions'; import roles from './roles'; import videoConf from './videoConf'; +import usersRoles from './usersRoles'; export default combineReducers({ settings, @@ -45,5 +46,6 @@ export default combineReducers({ encryption, permissions, roles, - videoConf + videoConf, + usersRoles }); diff --git a/app/reducers/usersRoles.test.ts b/app/reducers/usersRoles.test.ts new file mode 100644 index 000000000..41d3ce93f --- /dev/null +++ b/app/reducers/usersRoles.test.ts @@ -0,0 +1,17 @@ +import { setUsersRoles } from '../actions/usersRoles'; +import { mockedStore } from './mockedStore'; +import { TUsersRoles, initialState } from './usersRoles'; + +describe('test userRoles reducer', () => { + it('should return initial state', () => { + const state = mockedStore.getState().usersRoles; + expect(state).toEqual(initialState); + }); + + it('should return correctly value after call setUserRoles action', () => { + const usersRoles: TUsersRoles = [{ _id: '1', roles: ['admin'], username: 'admin' }]; + mockedStore.dispatch(setUsersRoles(usersRoles)); + const state = mockedStore.getState().usersRoles; + expect(state).toEqual(usersRoles); + }); +}); diff --git a/app/reducers/usersRoles.ts b/app/reducers/usersRoles.ts new file mode 100644 index 000000000..969a50d5d --- /dev/null +++ b/app/reducers/usersRoles.ts @@ -0,0 +1,21 @@ +import { USERS_ROLES } from '../actions/actionsTypes'; +import { TApplicationActions } from '../definitions'; + +type TUserRole = { + _id: string; + roles: string[]; + username: string; +}; + +export type TUsersRoles = TUserRole[]; + +export const initialState: TUsersRoles = []; + +export default (state = initialState, action: TApplicationActions): TUsersRoles => { + switch (action.type) { + case USERS_ROLES.SET: + return action.usersRoles; + default: + return state; + } +}; diff --git a/app/sagas/login.js b/app/sagas/login.js index 94138f451..06318e3ee 100644 --- a/app/sagas/login.js +++ b/app/sagas/login.js @@ -36,6 +36,7 @@ import { subscribeUsersPresence } from '../lib/methods'; import { Services } from '../lib/services'; +import { setUsersRoles } from '../actions/usersRoles'; const getServer = state => state.server.server; const loginWithPasswordCall = args => Services.loginWithPassword(args); @@ -141,6 +142,13 @@ const fetchRoomsFork = function* fetchRoomsFork() { yield put(roomsRequest()); }; +const fetchUsersRoles = function* fetchRoomsFork() { + const roles = yield Services.getUsersRoles(); + if (roles.length) { + yield put(setUsersRoles(roles)); + } +}; + const handleLoginSuccess = function* handleLoginSuccess({ user }) { try { getUserPresence(user.id); @@ -156,6 +164,7 @@ const handleLoginSuccess = function* handleLoginSuccess({ user }) { yield fork(fetchEnterpriseModulesFork, { user }); yield fork(subscribeSettingsFork); yield put(encryptionInit()); + yield fork(fetchUsersRoles); setLanguage(user?.language); diff --git a/app/views/RoomInfoView/Direct.tsx b/app/views/RoomInfoView/Direct.tsx index c5fee5d02..506827ff1 100644 --- a/app/views/RoomInfoView/Direct.tsx +++ b/app/views/RoomInfoView/Direct.tsx @@ -1,36 +1,43 @@ import React from 'react'; import { Text, View } from 'react-native'; -import { themes } from '../../lib/constants'; +import { IUserParsed } from '.'; import I18n from '../../i18n'; import { useTheme } from '../../theme'; -import Timezone from './Timezone'; import CustomFields from './CustomFields'; +import Timezone from './Timezone'; import styles from './styles'; -import { IUserParsed } from '.'; const Roles = ({ roles }: { roles?: string[] }) => { - const { theme } = useTheme(); + const { colors } = useTheme(); - if (roles && roles.length) { - - {I18n.t('Roles')} - - {roles.map(role => - role ? ( - - {role} - - ) : null - )} + if (roles?.length) { + return ( + + + {I18n.t('Roles')} + + + {roles.map(role => + role ? ( + + {role} + + ) : null + )} + - ; + ); } return null; }; -const Direct = ({ roomUser }: { roomUser: IUserParsed }) => ( +const Direct = ({ roomUser }: { roomUser: IUserParsed }): React.ReactElement => ( <> diff --git a/app/views/RoomInfoView/index.tsx b/app/views/RoomInfoView/index.tsx index c477ca01b..a81b72294 100644 --- a/app/views/RoomInfoView/index.tsx +++ b/app/views/RoomInfoView/index.tsx @@ -1,5 +1,6 @@ import { CompositeNavigationProp, RouteProp } from '@react-navigation/native'; import { StackNavigationProp } from '@react-navigation/stack'; +import { uniq } from 'lodash'; import isEmpty from 'lodash/isEmpty'; import React from 'react'; import { ScrollView, Text, View } from 'react-native'; @@ -11,12 +12,12 @@ import UAParser from 'ua-parser-js'; import { AvatarWithEdit } from '../../containers/Avatar'; import { CustomIcon, TIconsName } from '../../containers/CustomIcon'; import * as HeaderButton from '../../containers/HeaderButton'; -import { MarkdownPreview } from '../../containers/markdown'; import RoomTypeIcon from '../../containers/RoomTypeIcon'; import SafeAreaView from '../../containers/SafeAreaView'; import Status from '../../containers/Status'; import StatusBar from '../../containers/StatusBar'; import { LISTENER } from '../../containers/Toast'; +import { MarkdownPreview } from '../../containers/markdown'; import { IApplicationState, ISubscription, IUser, SubscriptionType, TSubscriptionModel } from '../../definitions'; import { ILivechatVisitor } from '../../definitions/ILivechatVisitor'; import I18n from '../../i18n'; @@ -29,14 +30,15 @@ import { handleIgnore } from '../../lib/methods/helpers/handleIgnore'; import log, { events, logEvent } from '../../lib/methods/helpers/log'; import Navigation from '../../lib/navigation/appNavigation'; import { Services } from '../../lib/services'; +import { TUsersRoles } from '../../reducers/usersRoles'; import { MasterDetailInsideStackParamList } from '../../stacks/MasterDetailStack/types'; import { ChatsStackParamList } from '../../stacks/types'; import { TSupportedThemes, withTheme } from '../../theme'; import sharedStyles from '../Styles'; import Channel from './Channel'; -import { CallButton } from './components/UserInfoButton'; import Direct from './Direct'; import Livechat from './Livechat'; +import { CallButton } from './components/UserInfoButton'; import styles from './styles'; interface IGetRoomTitle { @@ -95,6 +97,7 @@ interface IRoomInfoViewProps { editOmnichannelContact?: string[]; editLivechatRoomCustomfields?: string[]; roles: { [key: string]: string }; + usersRoles: TUsersRoles; } export interface IUserParsed extends IUser { @@ -245,6 +248,23 @@ class RoomInfoView extends React.Component { + const roles = (() => { + const { usersRoles } = this.props; + const userRoles = usersRoles.find(u => u?.username === user.username); + let r: string[] = []; + if (userRoles?.roles?.length) r = userRoles.roles; + if (user.roles?.length) r = [...r, ...user.roles]; + return uniq(r); + })(); + if (roles.length) { + const parsedRoles = await this.parseRoles(roles); + this.setState({ roomUser: { ...user, parsedRoles } }); + } else { + this.setState({ roomUser: user }); + } + }; + loadUser = async () => { const { room, roomUser } = this.state; @@ -254,29 +274,13 @@ class RoomInfoView extends React.Component ({ editRoomPermission: state.permissions['edit-room'], editOmnichannelContact: state.permissions['edit-omnichannel-contact'], editLivechatRoomCustomfields: state.permissions['edit-livechat-room-customfields'], - roles: state.roles + roles: state.roles, + usersRoles: state.usersRoles }); export default connect(mapStateToProps)(withTheme(RoomInfoView)); diff --git a/e2e/tests/room/08-roominfo.spec.ts b/e2e/tests/room/08-roominfo.spec.ts index b5df534eb..f48dcec46 100644 --- a/e2e/tests/room/08-roominfo.spec.ts +++ b/e2e/tests/room/08-roominfo.spec.ts @@ -256,5 +256,17 @@ describe('Room info screen', () => { .withTimeout(60000); }); }); + + describe('Navigate to random user', () => { + it('should see user role correctly', async () => { + await navigateToRoomInfo('roles-test'); + await waitFor(element(by.id(`user-roles`))) + .toBeVisible() + .withTimeout(10000); + await waitFor(element(by.id(`user-role-Livechat-Agent`))) + .toBeVisible() + .withTimeout(10000); + }); + }); }); });