regression: show roles on user info view (#5154)

* create usersRoles reducer

* add usersRoles test

* fix usersRoles reducer

* fetchUsersRoles on login

* use new roles

* add test

* fix roles when the user has permission to see other user roles

* use role name

---------

Co-authored-by: Reinaldo Neto <47038980+reinaldonetof@users.noreply.github.com>
This commit is contained in:
Gleidson Daniel Silva 2023-08-11 17:35:24 -03:00 committed by GitHub
parent cbbea73374
commit b23df26d42
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 134 additions and 41 deletions

View File

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

13
app/actions/usersRoles.ts Normal file
View File

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

View File

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

View File

@ -14,7 +14,7 @@ export async function setRoles(): Promise<void> {
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));
}

View File

@ -973,3 +973,5 @@ export const postMessage = (roomId: string, text: string) => sdk.post('chat.post
export const notifyUser = (type: string, params: Record<string, any>): Promise<boolean> =>
sdk.methodCall('stream-notify-user', type, params);
export const getUsersRoles = (): Promise<boolean> => sdk.methodCall('getUserRoles');

View File

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

View File

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

View File

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

View File

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

View File

@ -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) {
<View style={styles.item}>
<Text style={[styles.itemLabel, { color: themes[theme].titleText }]}>{I18n.t('Roles')}</Text>
<View style={styles.rolesContainer}>
{roles.map(role =>
role ? (
<View style={[styles.roleBadge, { backgroundColor: themes[theme].chatComponentBackground }]} key={role}>
<Text style={[styles.role, { color: themes[theme].titleText }]}>{role}</Text>
</View>
) : null
)}
if (roles?.length) {
return (
<View style={styles.item}>
<Text testID='user-roles' style={[styles.itemLabel, { color: colors.titleText }]}>
{I18n.t('Roles')}
</Text>
<View style={styles.rolesContainer}>
{roles.map(role =>
role ? (
<View
testID={`user-role-${role.replace(/ /g, '-')}`}
style={[styles.roleBadge, { backgroundColor: colors.chatComponentBackground }]}
key={role}
>
<Text style={[styles.role, { color: colors.titleText }]}>{role}</Text>
</View>
) : null
)}
</View>
</View>
</View>;
);
}
return null;
};
const Direct = ({ roomUser }: { roomUser: IUserParsed }) => (
const Direct = ({ roomUser }: { roomUser: IUserParsed }): React.ReactElement => (
<>
<Roles roles={roomUser.parsedRoles} />
<Timezone utcOffset={roomUser.utcOffset} />

View File

@ -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<IRoomInfoViewProps, IRoomInfoViewStat
})
);
setUser = async (user: IUser) => {
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<IRoomInfoViewProps, IRoomInfoViewStat
const result = await Services.getUserInfo(roomUserId);
if (result.success) {
const { user } = result;
const { roles } = user;
const parsedRoles: { parsedRoles?: string[] } = {};
if (roles && roles.length) {
parsedRoles.parsedRoles = await this.parseRoles(roles);
}
this.setState({ roomUser: { ...user, ...parsedRoles } as IUserParsed });
this.setUser(user as IUser);
}
} catch {
// do nothing
}
} else {
try {
const { roles } = roomUser as IUserParsed;
if (roles && roles.length) {
const parsedRoles = await this.parseRoles(roles);
this.setState({ roomUser: { ...roomUser, parsedRoles } });
} else {
this.setState({ roomUser });
}
} catch (e) {
// do nothing
}
this.setUser(roomUser as IUser);
}
};
@ -523,7 +527,8 @@ const mapStateToProps = (state: IApplicationState) => ({
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));

View File

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